@@ -175,7 +175,7 @@ export class RunLocker {
175
175
routine ?: ( signal : redlock . RedlockAbortSignal ) => Promise < T >
176
176
) : Promise < T > {
177
177
const currentContext = this . asyncLocalStorage . getStore ( ) ;
178
- const joinedResources = resources . sort ( ) . join ( "," ) ;
178
+ const joinedResources = [ ... resources ] . sort ( ) . join ( "," ) ;
179
179
180
180
// Handle overloaded parameters
181
181
let actualDuration : number ;
@@ -251,7 +251,9 @@ export class RunLocker {
251
251
lockId : string ,
252
252
lockStartTime : number
253
253
) : Promise < T > {
254
- const joinedResources = resources . sort ( ) . join ( "," ) ;
254
+ // Sort resources to ensure consistent lock acquisition order and prevent deadlocks
255
+ const sortedResources = [ ...resources ] . sort ( ) ;
256
+ const joinedResources = sortedResources . join ( "," ) ;
255
257
256
258
// Use configured retry settings with exponential backoff
257
259
const { maxRetries, baseDelay, maxDelay, backoffMultiplier, jitterFactor, maxTotalWaitTime } =
@@ -266,14 +268,14 @@ export class RunLocker {
266
268
let lastError : Error | undefined ;
267
269
268
270
for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
269
- const [ error , acquiredLock ] = await tryCatch ( this . redlock . acquire ( resources , duration ) ) ;
271
+ const [ error , acquiredLock ] = await tryCatch ( this . redlock . acquire ( sortedResources , duration ) ) ;
270
272
271
273
if ( ! error && acquiredLock ) {
272
274
lock = acquiredLock ;
273
275
if ( attempt > 0 ) {
274
276
this . logger . debug ( "[RunLocker] Lock acquired after retries" , {
275
277
name,
276
- resources,
278
+ resources : sortedResources ,
277
279
attempts : attempt + 1 ,
278
280
totalWaitTime : Math . round ( totalWaitTime ) ,
279
281
} ) ;
@@ -287,16 +289,16 @@ export class RunLocker {
287
289
if ( totalWaitTime >= maxTotalWaitTime ) {
288
290
this . logger . warn ( "[RunLocker] Lock acquisition exceeded total wait time limit" , {
289
291
name,
290
- resources,
292
+ resources : sortedResources ,
291
293
attempts : attempt + 1 ,
292
294
totalWaitTime : Math . round ( totalWaitTime ) ,
293
295
maxTotalWaitTime,
294
296
} ) ;
295
297
throw new LockAcquisitionTimeoutError (
296
- resources ,
298
+ sortedResources ,
297
299
Math . round ( totalWaitTime ) ,
298
300
attempt + 1 ,
299
- `Lock acquisition on resources [${ resources . join (
301
+ `Lock acquisition on resources [${ sortedResources . join (
300
302
", "
301
303
) } ] exceeded total wait time limit of ${ maxTotalWaitTime } ms`
302
304
) ;
@@ -306,16 +308,16 @@ export class RunLocker {
306
308
if ( attempt === maxRetries ) {
307
309
this . logger . warn ( "[RunLocker] Lock acquisition exhausted all retries" , {
308
310
name,
309
- resources,
311
+ resources : sortedResources ,
310
312
attempts : attempt + 1 ,
311
313
totalWaitTime : Math . round ( totalWaitTime ) ,
312
314
lastError : lastError . message ,
313
315
} ) ;
314
316
throw new LockAcquisitionTimeoutError (
315
- resources ,
317
+ sortedResources ,
316
318
Math . round ( totalWaitTime ) ,
317
319
attempt + 1 ,
318
- `Lock acquisition on resources [${ resources . join ( ", " ) } ] failed after ${
320
+ `Lock acquisition on resources [${ sortedResources . join ( ", " ) } ] failed after ${
319
321
attempt + 1
320
322
} attempts`
321
323
) ;
@@ -334,14 +336,14 @@ export class RunLocker {
334
336
maxDelay
335
337
) ;
336
338
const jitter = exponentialDelay * jitterFactor * ( Math . random ( ) * 2 - 1 ) ; // ±jitterFactor% jitter
337
- const delay = Math . max ( 0 , Math . round ( exponentialDelay + jitter ) ) ;
339
+ const delay = Math . min ( maxDelay , Math . max ( 0 , Math . round ( exponentialDelay + jitter ) ) ) ;
338
340
339
341
// Update total wait time before delay
340
342
totalWaitTime += delay ;
341
343
342
344
this . logger . debug ( "[RunLocker] Lock acquisition failed, retrying with backoff" , {
343
345
name,
344
- resources,
346
+ resources : sortedResources ,
345
347
attempt : attempt + 1 ,
346
348
delay,
347
349
totalWaitTime : Math . round ( totalWaitTime ) ,
@@ -356,7 +358,7 @@ export class RunLocker {
356
358
// For other errors (non-retryable), throw immediately
357
359
this . logger . error ( "[RunLocker] Lock acquisition failed with non-retryable error" , {
358
360
name,
359
- resources,
361
+ resources : sortedResources ,
360
362
attempt : attempt + 1 ,
361
363
error : lastError . message ,
362
364
errorName : lastError . name ,
@@ -390,7 +392,7 @@ export class RunLocker {
390
392
// Track active lock
391
393
this . activeLocks . set ( lockId , {
392
394
lockType : name ,
393
- resources : resources ,
395
+ resources : sortedResources ,
394
396
} ) ;
395
397
396
398
let lockSuccess = true ;
@@ -426,7 +428,7 @@ export class RunLocker {
426
428
if ( releaseError ) {
427
429
this . logger . warn ( "[RunLocker] Error releasing lock" , {
428
430
error : releaseError ,
429
- resources,
431
+ resources : sortedResources ,
430
432
lockValue : lock ! . value ,
431
433
} ) ;
432
434
}
0 commit comments