@@ -26,13 +26,16 @@ const CIRCUIT_BREAKER_WINDOW_MS = 10 * 60 * 1000
2626const CIRCUIT_BREAKER_COOLDOWN_MS = 10 * 60 * 1000
2727
2828/**
29- * Upper bound on how long shutdown() will wait for in-flight capture calls to drain.
30- * deactivate() awaits shutdown() before terminal cleanup, so an unbounded wait here
31- * (e.g. a capture stuck on network I/O that never resolves/rejects) would block the
32- * extension host from ever finishing deactivation. Losing an in-flight capture on
33- * timeout is an acceptable tradeoff against blocking shutdown indefinitely.
29+ * Upper bound applied separately to each phase of shutdown(): draining in-flight capture
30+ * calls, then awaiting client.shutdown(). deactivate() awaits shutdown() before terminal
31+ * cleanup, so an unbounded wait in either phase (e.g. a capture stuck on network I/O, or
32+ * a client's own shutdown() -- posthog-node defaults to a 30s internal timeout -- never
33+ * settling) would block the extension host from ever finishing deactivation. Losing an
34+ * in-flight capture, or a client's graceful flush, on timeout is an acceptable tradeoff
35+ * against blocking shutdown indefinitely. Worst case, shutdown() takes up to roughly
36+ * 2 * SHUTDOWN_PHASE_TIMEOUT_MS.
3437 */
35- const SHUTDOWN_DRAIN_TIMEOUT_MS = 3000
38+ const SHUTDOWN_PHASE_TIMEOUT_MS = 3000
3639
3740/**
3841 * TelemetryService wrapper class that defers initialization.
@@ -368,15 +371,23 @@ export class TelemetryService {
368371 // stuck on network I/O that never resolves/rejects can't block deactivate() forever --
369372 // losing that one capture is an acceptable tradeoff against hanging terminal cleanup.
370373 const drainStart = Date . now ( )
371- while ( this . pendingClientCalls . size > 0 && Date . now ( ) - drainStart < SHUTDOWN_DRAIN_TIMEOUT_MS ) {
374+ while ( this . pendingClientCalls . size > 0 && Date . now ( ) - drainStart < SHUTDOWN_PHASE_TIMEOUT_MS ) {
372375 await Promise . race ( [
373376 Promise . all ( this . pendingClientCalls ) ,
374- new Promise ( ( resolve ) => setTimeout ( resolve , SHUTDOWN_DRAIN_TIMEOUT_MS - ( Date . now ( ) - drainStart ) ) ) ,
377+ new Promise ( ( resolve ) => setTimeout ( resolve , SHUTDOWN_PHASE_TIMEOUT_MS - ( Date . now ( ) - drainStart ) ) ) ,
375378 ] )
376379 }
377380
381+ // Bound client shutdown the same way as the drain above: posthog-node's own shutdown()
382+ // defaults to a 30s internal timeout when called with no argument (as PostHogTelemetryClient
383+ // does), and TelemetryClient#shutdown() takes no timeout parameter to pass one through. Racing
384+ // against our own timer here, instead of just awaiting client.shutdown() directly, keeps
385+ // deactivate() from blocking for up to 30s on a client that never settles.
378386 // allSettled, not all: one client rejecting must not stop us from awaiting the others.
379- await Promise . allSettled ( this . clients . map ( ( client ) => client . shutdown ( ) ) )
387+ await Promise . race ( [
388+ Promise . allSettled ( this . clients . map ( ( client ) => client . shutdown ( ) ) ) ,
389+ new Promise ( ( resolve ) => setTimeout ( resolve , SHUTDOWN_PHASE_TIMEOUT_MS ) ) ,
390+ ] )
380391 }
381392
382393 private static _instance : TelemetryService | null = null
0 commit comments