@@ -378,6 +378,104 @@ describe('BackbeatConsumer rebalance tests', () => {
378378 }
379379 } , 1000 ) ;
380380 } ) . timeout ( 60000 ) ;
381+
382+ } ) ;
383+
384+ describe ( 'BackbeatConsumer deferred commit after rebalance' , ( ) => {
385+ const topic = 'backbeat-consumer-spec-ERR-STATE' ;
386+ const groupId = `replication-group-${ Math . random ( ) } ` ;
387+ let producer ;
388+ let consumer1 ;
389+ let consumer2 ;
390+
391+ before ( function before ( done ) {
392+ this . timeout ( 60000 ) ;
393+
394+ producer = new BackbeatProducer ( {
395+ kafka : producerKafkaConf , topic,
396+ pollIntervalMs : 100 ,
397+ compressionType : 'none' ,
398+ } ) ;
399+ consumer1 = new BackbeatConsumer ( {
400+ clientId : 'BackbeatConsumer-ERR-STATE-1' ,
401+ zookeeper : zookeeperConf ,
402+ kafka : { ...consumerKafkaConf , compressionType : 'none' } ,
403+ groupId, topic,
404+ queueProcessor : ( _msg , cb ) => cb ( ) ,
405+ bootstrap : true ,
406+ } ) ;
407+ async . parallel ( [
408+ innerDone => producer . on ( 'ready' , innerDone ) ,
409+ innerDone => consumer1 . on ( 'ready' , innerDone ) ,
410+ ] , err => {
411+ if ( err ) return done ( err ) ;
412+ consumer2 = new BackbeatConsumer ( {
413+ clientId : 'BackbeatConsumer-ERR-STATE-2' ,
414+ zookeeper : zookeeperConf ,
415+ kafka : { ...consumerKafkaConf , compressionType : 'none' } ,
416+ groupId, topic,
417+ queueProcessor : ( _msg , cb ) => cb ( ) ,
418+ } ) ;
419+ consumer2 . on ( 'ready' , done ) ;
420+ } ) ;
421+ } ) ;
422+
423+ after ( function after ( done ) {
424+ this . timeout ( 10000 ) ;
425+ async . parallel ( [
426+ innerDone => producer . close ( innerDone ) ,
427+ innerDone => consumer1 . close ( innerDone ) ,
428+ innerDone => ( consumer2 ? consumer2 . close ( innerDone ) : innerDone ( ) ) ,
429+ ] , done ) ;
430+ } ) ;
431+
432+ it ( 'should not crash when onEntryCommittable is called after partition revoke' , done => {
433+ let deferredEntry = null ;
434+
435+ // Setup: when consumer1 receives a message, complete with
436+ // { committable: false }. This frees the processing queue
437+ // slot but does NOT commit the offset.
438+ consumer1 . _queueProcessor = ( message , cb ) => {
439+ deferredEntry = message ;
440+ process . nextTick ( ( ) => cb ( null , { committable : false } ) ) ;
441+ } ;
442+
443+ consumer2 . _queueProcessor = ( _message , cb ) => {
444+ process . nextTick ( cb ) ;
445+ } ;
446+
447+ // 1 : consumer1 subscribes and consumes the message.
448+ consumer1 . subscribe ( ) ;
449+ producer . send ( [ { key : 'foo' , message : '{"hello":"foo"}' } ] , err => {
450+ assert . ifError ( err ) ;
451+ } ) ;
452+
453+ // 2 : wait until consumer1 has processed the message.
454+ // The processing queue is now idle but the
455+ // deferred commit is still pending.
456+ const waitForDeferred = setInterval ( ( ) => {
457+ if ( ! deferredEntry ) {
458+ return ;
459+ }
460+ clearInterval ( waitForDeferred ) ;
461+
462+ // 3 : consumer2 joins the same group, triggering a
463+ // rebalance. consumer1's revoke handler sees an idle
464+ // queue and immediately unassigns the partition.
465+ consumer1 . once ( 'unassign' , ( ) => {
466+ // 4 : the external caller finishes its work and calls
467+ // onEntryCommittable() for the now-revoked partition.
468+ // It would crash without the try catch in the method, as
469+ // an error ERR__STATE is returned by librdkafka when trying to commit
470+ assert . doesNotThrow ( ( ) => {
471+ consumer1 . onEntryCommittable ( deferredEntry ) ;
472+ } ) ;
473+ done ( ) ;
474+ } ) ;
475+
476+ consumer2 . subscribe ( ) ;
477+ } , 100 ) ;
478+ } ) . timeout ( 40000 ) ;
381479} ) ;
382480
383481describe ( 'BackbeatConsumer concurrency tests' , ( ) => {
0 commit comments