@@ -22,6 +22,7 @@ const notLockAcquisitionError = e => e.name !== 'LockAcquisitionError';
2222const isBoolean = filter ( Boolean ) ;
2323const toFlattenedTruthyArray = compose ( isBoolean , flatten ) ;
2424const couldNotAcquireLockError = new LockAcquisitionError ( 'job is already running' ) ;
25+ const TimeoutError = new Promise . TimeoutError ( 'queue-no-response' ) ;
2526
2627/**
2728 * @class DistributedCallbackQueue
@@ -32,7 +33,7 @@ const couldNotAcquireLockError = new LockAcquisitionError('job is already runnin
3233 * @param {redisClient } pubsub: redis connection that will be used for notifications
3334 * @param {String } pubsubChannel - will be used to pass notifications
3435 * @param {Object } lock - configuration for redislock:
35- * @param {Number } timeout - defaults to 1000
36+ * @param {Number } timeout - defaults to 10000
3637 * @param {Number } retries - defaults to 0
3738 * @param {Number } delay - defaults to 100
3839 * @param {Object|Boolean } log: sets up logger. If set to false supresses all warnings
@@ -55,7 +56,7 @@ class DistributedCallbackQueue {
5556
5657 const lockOptions = defaults ( options . lock || { } , {
5758 timeout : 10000 ,
58- retries : 1 ,
59+ retries : 2 ,
5960 delay : 100 ,
6061 } ) ;
6162
@@ -124,10 +125,11 @@ class DistributedCallbackQueue {
124125 * Adds callback to distributed queue
125126 * @param {String } suffix - queue identifier
126127 * @param {Function } next - callback to be called when request is finished
128+ * @param {number } [timeout=this.lockOptions.timeout * 2] - fail after <timeout>, set to 0 to disable
127129 * @returns {Promise } if promise is resolved then we must act, if it's rejected - then
128130 * somebody else is working on the same task right now
129131 */
130- push ( suffix , next ) {
132+ push ( suffix , next , timeout = this . lockOptions . timeout * 2 ) {
131133 assert ( suffix , 'must be a truthy string' ) ;
132134
133135 // first queue locally to make use of pending requests
@@ -140,6 +142,13 @@ class DistributedCallbackQueue {
140142 return Promise . reject ( couldNotAcquireLockError ) ;
141143 }
142144
145+ if ( timeout ) {
146+ /* we are first in the local queue */
147+ const onTimeout = setTimeout ( callbackQueue . _call , timeout , lockRedisKey , [ TimeoutError ] , this . logger ) ;
148+ /* if we have no response from dlock -> without timeout, clean local queue */
149+ callbackQueue . add ( lockRedisKey , ( ) => clearTimeout ( onTimeout ) ) ;
150+ }
151+
143152 // create lock
144153 const lock = this . getLock ( ) ;
145154
@@ -254,33 +263,45 @@ class DistributedCallbackQueue {
254263 * all queued callbacks
255264 */
256265 createWorker ( lockRedisKey , lock ) {
257- const dlock = this ;
258266 /**
259267 * This function must be called when job has been completed
260268 * @param {Error } err
261269 * @param {Array } ...args
262270 */
263- return ( err , ...args ) => {
271+ const broadcastJobStatus = async ( err , ...args ) => {
272+ /* clen ref */
273+ broadcastJobStatus . lock = null ;
274+
264275 // must release lock now. Technically there could be an event
265276 // where lock had not been released, notification already emitted
266277 // and callback is stuck in the queue, to avoid that we can add retry
267278 // to lock acquisition. Desicion and constraints are up to you. Ideally
268279 // you would want to cache result of the function for some time - and then
269280 // this race is completed. Multi() command is not possible to use here
270- return lock
271- . release ( )
272- . then ( ( ) => {
273- // emit event
274- // at this point we are sure that this job still belongs to us,
275- // if it doesn't - we can't publish response, because this task may be acquired
276- // by someone else
277- return dlock . publish ( lockRedisKey , err , ...args ) ;
278- } )
279- . catch ( ( error ) => {
280- // because a job may take too much time, other listeners must implement timeout/retry strategy
281- dlock . logger . warn ( 'failed to release lock and publish results' , error ) ;
282- } ) ;
281+ try {
282+ // ensure lock still belongs to us
283+ await lock . extend ( ) ;
284+ } catch ( error ) {
285+ // because a job may take too much time, other listeners must implement timeout/retry strategy
286+ this . logger . warn ( 'failed to release lock and publish results' , error ) ;
287+ return null ;
288+ }
289+
290+ // emit event
291+ // at this point we are sure that this job still belongs to us,
292+ // if it doesn't - we can't publish response, because this task may be acquired
293+ // by someone else
294+ return this
295+ . publish ( lockRedisKey , err , ...args )
296+ /* ensure we release the lock once publish is completed */
297+ /* during race conditions we rely on _retry_ setting to re-acquire lock */
298+ . finally ( ( ) => lock . release ( ) . reflect ( ) ) ;
283299 } ;
300+
301+ // set associated lock -> lengthy jobs must extend this
302+ broadcastJobStatus . lock = lock ;
303+
304+ return broadcastJobStatus ;
284305 }
285306}
286307
0 commit comments