@@ -27,55 +27,67 @@ interface ProducerState {
2727export async function * RenderQueueConsumer ( signal ?: AbortSignal ) {
2828 const mqttClient = await connectMqtt ( ) ;
2929
30- const producerStates = new Map < string , ProducerState > ( ) ;
31-
32- mqttClient . on ( 'message' , ( _topic , payload ) => {
33- const queueState = JSON . parse ( payload . toString ( ) ) ;
34- producerStates . set ( queueState . hostname , queueState ) ;
35- } ) ;
36-
37- await mqttClient . subscribeAsync ( Topic . queueState , { qos : 1 , rh : 0 } ) ;
38-
39- const pickAvailableQueueHost : ( ) => string | null = ( ) => {
40- const eligibleHosts : string [ ] = [ ] ;
41-
42- producerStates . forEach ( ( producer ) => {
43- if ( producer . status === ProducerStatus . queued ) {
44- eligibleHosts . push ( producer . hostname ) ;
30+ // try wraps ALL setup after connectMqtt (message handler, subscribe, loop) so a failure
31+ // during setup — e.g. subscribeAsync throwing — still closes the MQTT client in `finally`
32+ // instead of leaking the connection.
33+ try {
34+ const producerStates = new Map < string , ProducerState > ( ) ;
35+
36+ mqttClient . on ( 'message' , ( _topic , payload ) => {
37+ // This runs async in the event loop, so a JSON.parse throw would bypass the
38+ // surrounding try/finally and crash the worker — guard it.
39+ try {
40+ const queueState = JSON . parse ( payload . toString ( ) ) ;
41+ producerStates . set ( queueState . hostname , queueState ) ;
42+ } catch ( err ) {
43+ logger . error ( { err } , 'failed to parse queue_status message' ) ;
4544 }
4645 } ) ;
4746
48- if ( eligibleHosts . length === 0 ) return null ;
49-
50- return eligibleHosts [ Math . floor ( Math . random ( ) * eligibleHosts . length ) ] ;
51- } ;
52-
53- const claimJobs = async ( host : string , limit : number ) : Promise < RenderJob [ ] > => {
54- try {
55- const res = await request ( `http://${ host } :${ settings . queuePort } ` , {
56- method : 'POST' ,
57- path : '/render_queue/claim' ,
58- body : JSON . stringify ( {
59- limit,
60- } ) ,
61- headers : {
62- 'content-type' : 'application/json' ,
63- } ,
47+ await mqttClient . subscribeAsync ( Topic . queueState , { qos : 1 , rh : 0 } ) ;
48+
49+ const pickAvailableQueueHost : ( ) => string | null = ( ) => {
50+ const eligibleHosts : string [ ] = [ ] ;
51+
52+ producerStates . forEach ( ( producer ) => {
53+ if ( producer . status === ProducerStatus . queued ) {
54+ eligibleHosts . push ( producer . hostname ) ;
55+ }
6456 } ) ;
65- if ( res . statusCode === 200 ) {
66- const jobs : any = await res . body . json ( ) ;
67- return jobs . map ( ( job : any ) => new RenderJob ( job ) ) ;
68- } else {
69- logger . error ( res . body . json ( ) ) ;
57+
58+ if ( eligibleHosts . length === 0 ) return null ;
59+
60+ return eligibleHosts [ Math . floor ( Math . random ( ) * eligibleHosts . length ) ] ;
61+ } ;
62+
63+ const claimJobs = async ( host : string , limit : number ) : Promise < RenderJob [ ] > => {
64+ try {
65+ const res = await request ( `http://${ host } :${ settings . queuePort } ` , {
66+ method : 'POST' ,
67+ path : '/render_queue/claim' ,
68+ body : JSON . stringify ( {
69+ limit,
70+ } ) ,
71+ headers : {
72+ 'content-type' : 'application/json' ,
73+ } ,
74+ } ) ;
75+ if ( res . statusCode === 200 ) {
76+ const jobs : any = await res . body . json ( ) ;
77+ return jobs . map ( ( job : any ) => new RenderJob ( job ) ) ;
78+ } else {
79+ // res.body.json() is a Promise (and can reject on a non-JSON error body) — await
80+ // text() so we log the actual response, not a pending Promise / unhandled rejection.
81+ const body = await res . body . text ( ) . catch ( ( ) => '' ) ;
82+ logger . error ( { statusCode : res . statusCode , body } , 'failed to claim jobs' ) ;
83+ return [ ] ;
84+ }
85+ } catch ( e ) {
86+ logger . error ( e ) ;
7087 return [ ] ;
7188 }
72- } catch ( e ) {
73- logger . error ( e ) ;
74- return [ ] ;
75- }
76- } ;
89+ } ;
7790
78- try {
7991 while ( ! signal ?. aborted ) {
8092 const host = pickAvailableQueueHost ( ) ;
8193
0 commit comments