@@ -44,6 +44,14 @@ const (
4444 testURI = "git://repo/monorepo/main/abc123"
4545)
4646
47+ // rescheduledMsg matches a gate-wait re-publish: same partition, but a fresh message id —
48+ // re-publishing under the in-flight delivery's id would be silently deduped against its
49+ // still-present message-store row and lost on ack.
50+ func rescheduledMsg (msg entityqueue.Message ) bool {
51+ // Fresh non-empty id, same queue.
52+ return msg .ID != testID && msg .ID != "" && msg .PartitionKey == testQueue
53+ }
54+
4755type processMocks struct {
4856 reqStore * storagemock.MockRequestStore
4957 queueStore * storagemock.MockQueueStore
@@ -74,6 +82,7 @@ func newControllerWithScope(t *testing.T, ctrl *gomock.Controller, scope tally.S
7482 queue := mqmock .NewMockQueue (ctrl )
7583 queue .EXPECT ().Publisher ().Return (m .publisher ).AnyTimes ()
7684 registry , err := consumer .NewTopicRegistry ([]consumer.TopicConfig {
85+ {Key : stovepipemq .TopicKeyProcess , Name : "process" , Queue : queue },
7786 {Key : stovepipemq .TopicKeyBuild , Name : "build" , Queue : queue },
7887 })
7988 require .NoError (t , err )
@@ -499,7 +508,7 @@ func TestProcess(t *testing.T) {
499508 },
500509 },
501510 {
502- name : "latest accepted head awaits slot when gate closed" ,
511+ name : "latest accepted head reschedules when gate closed" ,
503512 setup : func (m processMocks ) {
504513 m .reqStore .EXPECT ().Get (gomock .Any (), testID ).Return (acceptedRequest (testID ), nil )
505514 m .queueStore .EXPECT ().Get (gomock .Any (), testQueue ).Return (entity.Queue {
@@ -509,6 +518,52 @@ func TestProcess(t *testing.T) {
509518 LastGreenURI : "git://repo/monorepo/main/green" ,
510519 Version : 1 ,
511520 }, nil )
521+ m .publisher .EXPECT ().
522+ PublishAfter (gomock .Any (), "process" , gomock .Cond (rescheduledMsg ), int64 (5000 )).
523+ Return (nil )
524+ },
525+ },
526+ {
527+ name : "gate reschedule publish error surfaces" ,
528+ wantErr : true ,
529+ wantRetry : false ,
530+ setup : func (m processMocks ) {
531+ m .reqStore .EXPECT ().Get (gomock .Any (), testID ).Return (acceptedRequest (testID ), nil )
532+ m .queueStore .EXPECT ().Get (gomock .Any (), testQueue ).Return (entity.Queue {
533+ Name : testQueue ,
534+ LatestRequestID : testID ,
535+ InFlightCount : 1 ,
536+ Version : 1 ,
537+ }, nil )
538+ m .publisher .EXPECT ().
539+ PublishAfter (gomock .Any (), "process" , gomock .Cond (rescheduledMsg ), int64 (5000 )).
540+ Return (errors .New ("queue down" ))
541+ },
542+ },
543+ {
544+ name : "gate closed after slot claim race reschedules" ,
545+ setup : func (m processMocks ) {
546+ m .reqStore .EXPECT ().Get (gomock .Any (), testID ).Return (acceptedRequest (testID ), nil )
547+ m .queueStore .EXPECT ().Get (gomock .Any (), testQueue ).Return (entity.Queue {
548+ Name : testQueue ,
549+ LatestRequestID : testID ,
550+ Version : 1 ,
551+ }, nil )
552+ m .queueStore .EXPECT ().Update (gomock .Any (), entity.Queue {
553+ Name : testQueue ,
554+ LatestRequestID : testID ,
555+ InFlightCount : 1 ,
556+ Version : 1 ,
557+ }, int32 (1 ), int32 (2 )).Return (storage .ErrVersionMismatch )
558+ m .queueStore .EXPECT ().Get (gomock .Any (), testQueue ).Return (entity.Queue {
559+ Name : testQueue ,
560+ LatestRequestID : testID ,
561+ InFlightCount : 1 ,
562+ Version : 2 ,
563+ }, nil )
564+ m .publisher .EXPECT ().
565+ PublishAfter (gomock .Any (), "process" , gomock .Cond (rescheduledMsg ), int64 (5000 )).
566+ Return (nil )
512567 },
513568 },
514569 {
@@ -537,22 +592,6 @@ func TestProcess(t *testing.T) {
537592 expectBuildPublish (t , m , testID )
538593 },
539594 },
540- {
541- name : "gate closed after reload acks without failing" ,
542- setup : func (m processMocks ) {
543- m .reqStore .EXPECT ().Get (gomock .Any (), testID ).Return (acceptedRequest (testID ), nil )
544- m .queueStore .EXPECT ().Get (gomock .Any (), testQueue ).Return (entity.Queue {
545- Name : testQueue , LatestRequestID : testID , Version : 1 ,
546- }, nil )
547- m .queueStore .EXPECT ().Update (gomock .Any (), entity.Queue {
548- Name : testQueue , LatestRequestID : testID , InFlightCount : 1 , Version : 1 ,
549- }, int32 (1 ), int32 (2 )).Return (storage .ErrVersionMismatch )
550- // Reload: another admit took the last slot — gate now closed, defer (ack).
551- m .queueStore .EXPECT ().Get (gomock .Any (), testQueue ).Return (entity.Queue {
552- Name : testQueue , LatestRequestID : testID , InFlightCount : 1 , Version : 2 ,
553- }, nil )
554- },
555- },
556595 {
557596 name : "reload after claim mismatch supersedes a now-stale head" ,
558597 setup : func (m processMocks ) {
@@ -765,3 +804,13 @@ func TestProcess(t *testing.T) {
765804 })
766805 }
767806}
807+
808+ func TestRescheduleProcessRequiresPositiveDelay (t * testing.T ) {
809+ ctrl := gomock .NewController (t )
810+ c , _ := newController (t , ctrl )
811+
812+ err := c .rescheduleProcess (context .Background (), acceptedRequest (testID ), 1 , 0 )
813+
814+ require .Error (t , err )
815+ assert .False (t , errs .IsRetryable (err ))
816+ }
0 commit comments