@@ -4,29 +4,38 @@ import { logger } from "~/services/logger.server";
44
55export type TransformKind = "traces" | "logs" | "metrics" ;
66
7- type Pending = { resolve : ( r : any ) => void ; reject : ( e : Error ) => void } ;
8-
9- type QueueItem = {
10- message : {
11- id : number ;
12- kind : TransformKind ;
13- payload : Uint8Array ;
14- spanAttributeValueLengthLimit : number ;
15- defaultEventStore : string ;
16- } ;
7+ type TaskMessage = {
8+ id : number ;
9+ kind : TransformKind ;
10+ payload : Uint8Array ;
11+ spanAttributeValueLengthLimit : number ;
12+ defaultEventStore : string ;
13+ } ;
14+
15+ type Task = {
16+ message : TaskMessage ;
1717 transfer : ArrayBuffer [ ] ;
18- pending : Pending ;
18+ resolve : ( r : any ) => void ;
19+ reject : ( e : Error ) => void ;
20+ timer : NodeJS . Timeout ;
21+ worker ?: Worker ;
1922} ;
2023
24+ const TASK_TIMEOUT_MS = 30_000 ;
25+ const MAX_QUEUE_DEPTH = 2_000 ;
26+ const RESPAWN_BASE_MS = 500 ;
27+ const RESPAWN_MAX_MS = 30_000 ;
28+
2129// Hand-rolled worker_threads pool: one in-flight task per worker so CPU-bound transforms run
2230// fully in parallel. The main thread stays the only DB reader and broadcasts pricing to workers.
2331export class OtlpWorkerPool {
2432 private readonly workers : Worker [ ] = [ ] ;
2533 private readonly idle : Worker [ ] = [ ] ;
26- private readonly queue : QueueItem [ ] = [ ] ;
27- private readonly pending = new Map < number , Pending > ( ) ;
34+ private readonly queue : number [ ] = [ ] ;
35+ private readonly tasks = new Map < number , Task > ( ) ;
2836 private readonly busyByWorker = new Map < Worker , number > ( ) ;
2937 private nextId = 1 ;
38+ private consecutiveFailures = 0 ;
3039 private latestPricingModels : unknown [ ] ;
3140
3241 constructor (
@@ -35,9 +44,7 @@ export class OtlpWorkerPool {
3544 pricingModels : unknown [ ]
3645 ) {
3746 this . latestPricingModels = pricingModels ;
38- for ( let i = 0 ; i < size ; i ++ ) {
39- this . spawn ( ) ;
40- }
47+ for ( let i = 0 ; i < size ; i ++ ) this . spawn ( ) ;
4148 logger . info ( "OtlpWorkerPool started" , { size, workerPath } ) ;
4249 }
4350
@@ -47,12 +54,14 @@ export class OtlpWorkerPool {
4754 } ) ;
4855
4956 worker . on ( "message" , ( msg : { id : number ; ok : boolean ; result ?: any ; error ?: string } ) => {
50- const pending = this . pending . get ( msg . id ) ;
51- this . pending . delete ( msg . id ) ;
57+ this . consecutiveFailures = 0 ;
5258 this . busyByWorker . delete ( worker ) ;
53- if ( pending ) {
54- if ( msg . ok ) pending . resolve ( msg . result ) ;
55- else pending . reject ( new Error ( msg . error ?? "otlp worker error" ) ) ;
59+ const task = this . tasks . get ( msg . id ) ;
60+ if ( task ) {
61+ clearTimeout ( task . timer ) ;
62+ this . tasks . delete ( msg . id ) ;
63+ if ( msg . ok ) task . resolve ( msg . result ) ;
64+ else task . reject ( new Error ( msg . error ?? "otlp worker error" ) ) ;
5665 }
5766 this . release ( worker ) ;
5867 } ) ;
@@ -72,22 +81,35 @@ export class OtlpWorkerPool {
7281 this . idle . push ( worker ) ;
7382 }
7483
75- // On crash: fail its in-flight task, drop it, and spawn a replacement.
84+ // On crash/timeout: fail the worker's in-flight task (if still pending), drop the worker, and
85+ // respawn with exponential backoff so a persistently failing worker can't tight-loop.
7686 private reap ( worker : Worker , error : Error ) {
7787 const inFlightId = this . busyByWorker . get ( worker ) ;
88+ this . busyByWorker . delete ( worker ) ;
7889 if ( inFlightId !== undefined ) {
79- const pending = this . pending . get ( inFlightId ) ;
80- this . pending . delete ( inFlightId ) ;
81- this . busyByWorker . delete ( worker ) ;
82- pending ?. reject ( error ) ;
90+ const task = this . tasks . get ( inFlightId ) ;
91+ if ( task ) {
92+ clearTimeout ( task . timer ) ;
93+ this . tasks . delete ( inFlightId ) ;
94+ task . reject ( error ) ;
95+ }
8396 }
8497 const wi = this . workers . indexOf ( worker ) ;
8598 if ( wi !== - 1 ) this . workers . splice ( wi , 1 ) ;
8699 const ii = this . idle . indexOf ( worker ) ;
87100 if ( ii !== - 1 ) this . idle . splice ( ii , 1 ) ;
88101 void worker . terminate ( ) . catch ( ( ) => { } ) ;
89- if ( this . workers . length < this . size ) this . spawn ( ) ;
90- this . drain ( ) ;
102+ this . scheduleRespawn ( ) ;
103+ }
104+
105+ private scheduleRespawn ( ) {
106+ if ( this . workers . length >= this . size ) return ;
107+ const delay = Math . min ( RESPAWN_BASE_MS * 2 ** this . consecutiveFailures , RESPAWN_MAX_MS ) ;
108+ this . consecutiveFailures ++ ;
109+ setTimeout ( ( ) => {
110+ if ( this . workers . length < this . size ) this . spawn ( ) ;
111+ this . drain ( ) ;
112+ } , delay ) ;
91113 }
92114
93115 private release ( worker : Worker ) {
@@ -98,21 +120,43 @@ export class OtlpWorkerPool {
98120 private drain ( ) {
99121 while ( this . queue . length > 0 && this . idle . length > 0 ) {
100122 const worker = this . idle . pop ( ) ! ;
101- const item = this . queue . shift ( ) ! ;
102- this . pending . set ( item . message . id , item . pending ) ;
103- this . busyByWorker . set ( worker , item . message . id ) ;
104- worker . postMessage ( item . message , item . transfer ) ;
123+ const id = this . queue . shift ( ) ! ;
124+ const task = this . tasks . get ( id ) ;
125+ if ( ! task ) continue ;
126+ task . worker = worker ;
127+ this . busyByWorker . set ( worker , id ) ;
128+ worker . postMessage ( task . message , task . transfer ) ;
105129 }
106130 }
107131
132+ private onTimeout ( id : number ) {
133+ const task = this . tasks . get ( id ) ;
134+ if ( ! task ) return ;
135+ this . tasks . delete ( id ) ;
136+ const err = new Error ( `otlp worker task timed out after ${ TASK_TIMEOUT_MS } ms` ) ;
137+ if ( task . worker ) {
138+ // Dispatched to a stuck worker: reap it. The task is already removed, so reap won't
139+ // double-reject.
140+ this . reap ( task . worker , err ) ;
141+ } else {
142+ const qi = this . queue . indexOf ( id ) ;
143+ if ( qi !== - 1 ) this . queue . splice ( qi , 1 ) ;
144+ }
145+ task . reject ( err ) ;
146+ }
147+
108148 runTransform (
109149 kind : TransformKind ,
110150 payload : Uint8Array ,
111151 config : { spanAttributeValueLengthLimit : number ; defaultEventStore : string }
112152 ) : Promise < any > {
153+ if ( this . queue . length >= MAX_QUEUE_DEPTH ) {
154+ return Promise . reject ( new Error ( "otlp worker pool queue is full" ) ) ;
155+ }
113156 const id = this . nextId ++ ;
114157 return new Promise ( ( resolve , reject ) => {
115- this . queue . push ( {
158+ const timer = setTimeout ( ( ) => this . onTimeout ( id ) , TASK_TIMEOUT_MS ) ;
159+ this . tasks . set ( id , {
116160 message : {
117161 id,
118162 kind,
@@ -122,8 +166,11 @@ export class OtlpWorkerPool {
122166 } ,
123167 // Zero-copy the payload into the worker; the request owns a fresh ArrayBuffer.
124168 transfer : [ payload . buffer as ArrayBuffer ] ,
125- pending : { resolve, reject } ,
169+ resolve,
170+ reject,
171+ timer,
126172 } ) ;
173+ this . queue . push ( id ) ;
127174 this . drain ( ) ;
128175 } ) ;
129176 }
@@ -146,12 +193,14 @@ export class OtlpWorkerPool {
146193
147194let pool : OtlpWorkerPool | undefined ;
148195
149- export function getOtlpWorkerPool ( size : number , pricingModels : unknown [ ] ) : OtlpWorkerPool {
196+ export function getOtlpWorkerPool (
197+ size : number ,
198+ pricingModels : unknown [ ] ,
199+ workerPath ?: string
200+ ) : OtlpWorkerPool {
150201 if ( ! pool ) {
151- const workerPath =
152- process . env . OTEL_TRANSFORM_WORKER_PATH ??
153- path . join ( process . cwd ( ) , "build" , "otlpTransformWorker.cjs" ) ;
154- pool = new OtlpWorkerPool ( size , workerPath , pricingModels ) ;
202+ const resolvedPath = workerPath ?? path . join ( process . cwd ( ) , "build" , "otlpTransformWorker.cjs" ) ;
203+ pool = new OtlpWorkerPool ( size , resolvedPath , pricingModels ) ;
155204 }
156205 return pool ;
157206}
0 commit comments