88 type ObservableGauge ,
99} from "@internal/tracing" ;
1010import { logger } from "~/services/logger.server" ;
11+ import { signalsEmitter } from "~/services/signals.server" ;
1112
1213export type TransformKind = "traces" | "logs" | "metrics" ;
1314
@@ -37,6 +38,7 @@ const TASK_TIMEOUT_MS = 30_000;
3738const MAX_QUEUE_DEPTH = 2_000 ;
3839const RESPAWN_BASE_MS = 500 ;
3940const RESPAWN_MAX_MS = 30_000 ;
41+ const SHUTDOWN_DRAIN_MS = 5_000 ;
4042
4143// Hand-rolled worker_threads pool: one in-flight task per worker so CPU-bound transforms run
4244// fully in parallel. The main thread stays the only DB reader and broadcasts pricing to workers.
@@ -48,6 +50,7 @@ export class OtlpWorkerPool {
4850 private readonly busyByWorker = new Map < Worker , number > ( ) ;
4951 private nextId = 1 ;
5052 private consecutiveFailures = 0 ;
53+ private isShuttingDown = false ;
5154 private latestPricingModels : unknown [ ] ;
5255
5356 // Pre-allocated per-kind {kind} attribute objects so the per-task record path never allocates.
@@ -199,10 +202,12 @@ export class OtlpWorkerPool {
199202 }
200203
201204 private scheduleRespawn ( ) {
205+ if ( this . isShuttingDown ) return ;
202206 if ( this . workers . length >= this . size ) return ;
203207 const delay = Math . min ( RESPAWN_BASE_MS * 2 ** this . consecutiveFailures , RESPAWN_MAX_MS ) ;
204208 this . consecutiveFailures ++ ;
205209 setTimeout ( ( ) => {
210+ if ( this . isShuttingDown ) return ;
206211 if ( this . workers . length < this . size ) this . spawn ( ) ;
207212 this . drain ( ) ;
208213 } , delay ) ;
@@ -247,6 +252,9 @@ export class OtlpWorkerPool {
247252 payload : Uint8Array ,
248253 config : { spanAttributeValueLengthLimit : number ; defaultEventStore : string }
249254 ) : Promise < any > {
255+ if ( this . isShuttingDown ) {
256+ return Promise . reject ( new Error ( "otlp worker pool is shutting down" ) ) ;
257+ }
250258 if ( this . queue . length >= MAX_QUEUE_DEPTH ) {
251259 this . _tasksCounter ?. add ( 1 , { kind, outcome : "rejected" } ) ;
252260 return Promise . reject ( new Error ( "otlp worker pool queue is full" ) ) ;
@@ -289,13 +297,28 @@ export class OtlpWorkerPool {
289297 return this . queue . length ;
290298 }
291299
292- // Terminate all workers and reject anything in flight. Terminated workers fire "exit", but reap()
293- // no-ops on an already-removed worker, so no respawn is scheduled.
300+ // Stop taking new work, let in-flight tasks finish (bounded), then terminate every worker.
301+ // Terminated workers fire "exit", but reap() no-ops on an already-removed worker, and the
302+ // isShuttingDown guard stops any pending respawn, so shutdown is quiet.
294303 async shutdown ( ) : Promise < void > {
304+ if ( this . isShuttingDown ) return ;
305+ this . isShuttingDown = true ;
306+
307+ logger . info ( "OtlpWorkerPool shutting down" , {
308+ workers : this . workers . length ,
309+ inFlight : this . tasks . size ,
310+ } ) ;
311+
312+ const deadline = Date . now ( ) + SHUTDOWN_DRAIN_MS ;
313+ while ( this . tasks . size > 0 && Date . now ( ) < deadline ) {
314+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
315+ }
316+
295317 const workers = this . workers . splice ( 0 ) ;
296318 this . idle . length = 0 ;
297319 this . queue . length = 0 ;
298320 this . busyByWorker . clear ( ) ;
321+ // Reject anything that didn't drain within the deadline.
299322 for ( const [ , task ] of this . tasks ) {
300323 clearTimeout ( task . timer ) ;
301324 task . reject ( new Error ( "otlp worker pool shutting down" ) ) ;
@@ -316,6 +339,10 @@ export function getOtlpWorkerPool(
316339 if ( ! pool ) {
317340 const resolvedPath = workerPath ?? path . join ( process . cwd ( ) , "build" , "otlpTransformWorker.cjs" ) ;
318341 pool = new OtlpWorkerPool ( size , resolvedPath , pricingModels , meter ) ;
342+ // Drain + terminate workers on shutdown so they aren't force-killed mid-task (which would
343+ // churn respawns). The main thread stays the only DB writer, so inserts are unaffected.
344+ signalsEmitter . on ( "SIGTERM" , ( ) => void pool ! . shutdown ( ) ) ;
345+ signalsEmitter . on ( "SIGINT" , ( ) => void pool ! . shutdown ( ) ) ;
319346 }
320347 return pool ;
321348}
0 commit comments