1717import fetch from 'node-fetch' ;
1818import IClientContext from '../contracts/IClientContext' ;
1919import { LogLevel } from '../contracts/IDBSQLLogger' ;
20+ import { buildTelemetryUrl } from './telemetryUtils' ;
2021import driverVersion from '../version' ;
2122
2223/**
@@ -37,8 +38,15 @@ export interface FeatureFlagContext {
3738export default class FeatureFlagCache {
3839 private contexts : Map < string , FeatureFlagContext > ;
3940
41+ /** In-flight fetch promises for deduplication (prevents thundering herd) */
42+ private fetchPromises : Map < string , Promise < boolean > > = new Map ( ) ;
43+
4044 private readonly CACHE_DURATION_MS = 15 * 60 * 1000 ; // 15 minutes
4145
46+ private readonly MIN_CACHE_DURATION_S = 60 ; // 1 minute minimum TTL
47+
48+ private readonly MAX_CACHE_DURATION_S = 3600 ; // 1 hour maximum TTL
49+
4250 private readonly FEATURE_FLAG_NAME = 'databricks.partnerplatform.clientConfigsFeatureFlags.enableTelemetryForNodeJs' ;
4351
4452 constructor ( private context : IClientContext ) {
@@ -72,6 +80,7 @@ export default class FeatureFlagCache {
7280 ctx . refCount -= 1 ;
7381 if ( ctx . refCount <= 0 ) {
7482 this . contexts . delete ( host ) ;
83+ this . fetchPromises . delete ( host ) ; // Invalidate stale in-flight fetch
7584 }
7685 }
7786 }
@@ -91,22 +100,34 @@ export default class FeatureFlagCache {
91100 const isExpired = ! ctx . lastFetched || Date . now ( ) - ctx . lastFetched . getTime ( ) > ctx . cacheDuration ;
92101
93102 if ( isExpired ) {
94- try {
95- // Fetch feature flag from server
96- ctx . telemetryEnabled = await this . fetchFeatureFlag ( host ) ;
97- ctx . lastFetched = new Date ( ) ;
98- } catch ( error : any ) {
99- // Log at debug level only, never propagate exceptions
100- logger . log ( LogLevel . debug , `Error fetching feature flag: ${ error . message } ` ) ;
103+ // Deduplicate concurrent fetches for the same host (prevents thundering herd)
104+ if ( ! this . fetchPromises . has ( host ) ) {
105+ const fetchPromise = this . fetchFeatureFlag ( host )
106+ . then ( ( enabled ) => {
107+ ctx . telemetryEnabled = enabled ;
108+ ctx . lastFetched = new Date ( ) ;
109+ return enabled ;
110+ } )
111+ . catch ( ( error : any ) => {
112+ logger . log ( LogLevel . debug , `Error fetching feature flag: ${ error . message } ` ) ;
113+ return ctx . telemetryEnabled ?? false ;
114+ } )
115+ . finally ( ( ) => {
116+ this . fetchPromises . delete ( host ) ;
117+ } ) ;
118+ this . fetchPromises . set ( host , fetchPromise ) ;
101119 }
120+
121+ // Promise is guaranteed to resolve (never rejects) due to .catch() in the chain above
122+ await this . fetchPromises . get ( host ) ;
102123 }
103124
104125 return ctx . telemetryEnabled ?? false ;
105126 }
106127
107128 /**
108129 * Fetches feature flag from server using connector-service API.
109- * Calls GET /api/2.0/connector-service/feature-flags/OSS_NODEJS /{version}
130+ * Calls GET /api/2.0/connector-service/feature-flags/NODEJS /{version}
110131 *
111132 * @param host The host to fetch feature flag for
112133 * @returns true if feature flag is enabled, false otherwise
@@ -119,7 +140,7 @@ export default class FeatureFlagCache {
119140 const version = this . getDriverVersion ( ) ;
120141
121142 // Build feature flags endpoint for Node.js driver
122- const endpoint = this . buildUrl ( host , `/api/2.0/connector-service/feature-flags/NODEJS/${ version } ` ) ;
143+ const endpoint = buildTelemetryUrl ( host , `/api/2.0/connector-service/feature-flags/NODEJS/${ version } ` ) ;
123144
124145 // Get authentication headers
125146 const authHeaders = await this . context . getAuthHeaders ( ) ;
@@ -139,9 +160,12 @@ export default class FeatureFlagCache {
139160 'User-Agent' : `databricks-sql-nodejs/${ driverVersion } ` ,
140161 } ,
141162 agent, // Include agent for proxy support
163+ timeout : 10000 , // 10 second timeout to prevent indefinite hangs
142164 } ) ;
143165
144166 if ( ! response . ok ) {
167+ // Consume response body to release socket back to connection pool
168+ await response . text ( ) . catch ( ( ) => { } ) ;
145169 logger . log ( LogLevel . debug , `Feature flag fetch failed: ${ response . status } ${ response . statusText } ` ) ;
146170 return false ;
147171 }
@@ -151,11 +175,12 @@ export default class FeatureFlagCache {
151175
152176 // Response format: { flags: [{ name: string, value: string }], ttl_seconds?: number }
153177 if ( data && data . flags && Array . isArray ( data . flags ) ) {
154- // Update cache duration if TTL provided
178+ // Update cache duration if TTL provided, clamped to safe bounds
155179 const ctx = this . contexts . get ( host ) ;
156- if ( ctx && data . ttl_seconds ) {
157- ctx . cacheDuration = data . ttl_seconds * 1000 ; // Convert to milliseconds
158- logger . log ( LogLevel . debug , `Updated cache duration to ${ data . ttl_seconds } seconds` ) ;
180+ if ( ctx && typeof data . ttl_seconds === 'number' && data . ttl_seconds > 0 ) {
181+ const clampedTtl = Math . max ( this . MIN_CACHE_DURATION_S , Math . min ( this . MAX_CACHE_DURATION_S , data . ttl_seconds ) ) ;
182+ ctx . cacheDuration = clampedTtl * 1000 ; // Convert to milliseconds
183+ logger . log ( LogLevel . debug , `Updated cache duration to ${ clampedTtl } seconds` ) ;
159184 }
160185
161186 // Look for our specific feature flag
@@ -180,16 +205,6 @@ export default class FeatureFlagCache {
180205 }
181206 }
182207
183- /**
184- * Build full URL from host and path, handling protocol correctly.
185- */
186- private buildUrl ( host : string , path : string ) : string {
187- if ( host . startsWith ( 'http://' ) || host . startsWith ( 'https://' ) ) {
188- return `${ host } ${ path } ` ;
189- }
190- return `https://${ host } ${ path } ` ;
191- }
192-
193208 /**
194209 * Gets the driver version without -oss suffix for API calls.
195210 * Format: "1.12.0" from "1.12.0-oss"
0 commit comments