Skip to content

Commit 17bbf48

Browse files
behinddwallsgithub-actions[bot]
authored andcommitted
fix(orchestrator): mint distinct message IDs for cancel re-publishes
## Summary ### Why? The cancel controller published its speculate hand-off with the bare batch ID as the message ID. The queue deduplicates on (topic, partition key, message ID) against every row it has not garbage-collected yet, consumed ones included, so a redelivery's re-publish for the same batch was a silent no-op — leaving a batch stuck Cancelling with nothing driving it to terminal. Same class of bug as b1f5795 in stovepipe. ### What? `publishBatchID` now goes through `submitqueue/core/publish`: `publish.UniqueID` mints a distinct message ID per publish (and documents the dedup rule once), and `publish.Message` owns the registry-lookup plumbing the controller previously hand-rolled. The batch-path test now asserts the payload still carries the batch ID while the message ID is distinct per publish, and the multi-batch tests assert on the payload's batch ID for the same reason. ## Test Plan ✅ `bazel test //submitqueue/orchestrator/controller/cancel/...` # Conflicts: # submitqueue/orchestrator/controller/cancel/BUILD.bazel # submitqueue/orchestrator/controller/cancel/cancel.go # Please enter the commit message for your changes. Lines starting # with '#' will be kept; you may remove them yourself if you want to. # An empty message aborts the commit. # # interactive rebase in progress; onto 8548220f # Last command done (1 command done): # pick c167e22 # fix(orchestrator): mint distinct message IDs for cancel re-publishes # No commands remaining. # You are currently rebasing branch 'preetam/speculation-cancel-msgid' on '8548220f'. # # Changes to be committed: # modified: submitqueue/orchestrator/controller/cancel/BUILD.bazel # modified: submitqueue/orchestrator/controller/cancel/cancel.go # modified: submitqueue/orchestrator/controller/cancel/cancel_test.go #
1 parent 6331c0f commit 17bbf48

3 files changed

Lines changed: 38 additions & 30 deletions

File tree

submitqueue/orchestrator/controller/cancel/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ go_library(
66
importpath = "github.com/uber/submitqueue/submitqueue/orchestrator/controller/cancel",
77
visibility = ["//visibility:public"],
88
deps = [
9-
"//platform/base/messagequeue:go_default_library",
109
"//platform/consumer:go_default_library",
1110
"//platform/metrics:go_default_library",
1211
"//submitqueue/core/batch:go_default_library",
12+
"//submitqueue/core/publish:go_default_library",
1313
"//submitqueue/core/request:go_default_library",
1414
"//submitqueue/core/topickey:go_default_library",
1515
"//submitqueue/entity:go_default_library",

submitqueue/orchestrator/controller/cancel/cancel.go

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ import (
5757
"sort"
5858

5959
"github.com/uber-go/tally"
60-
entityqueue "github.com/uber/submitqueue/platform/base/messagequeue"
6160
"github.com/uber/submitqueue/platform/consumer"
6261
"github.com/uber/submitqueue/platform/metrics"
6362
corebatch "github.com/uber/submitqueue/submitqueue/core/batch"
63+
"github.com/uber/submitqueue/submitqueue/core/publish"
6464
corerequest "github.com/uber/submitqueue/submitqueue/core/request"
6565
"github.com/uber/submitqueue/submitqueue/core/topickey"
6666
"github.com/uber/submitqueue/submitqueue/entity"
@@ -359,29 +359,18 @@ func (c *Controller) cancelBatch(ctx context.Context, store storage.Storage, bat
359359

360360
// publishBatchID publishes a BatchID-payload message to the specified topic
361361
// key, stamped with and partitioned by the batch's queue.
362+
//
363+
// The message ID is distinct per publish (publish.UniqueID). The queue
364+
// deduplicates on (topic, partition key, message ID) against every row it has
365+
// not collected yet, consumed ones included, so a bare batch ID would make the
366+
// redelivery re-publish documented above a silent no-op — leaving a batch
367+
// Cancelling with nothing driving it to terminal.
362368
func (c *Controller) publishBatchID(ctx context.Context, key consumer.TopicKey, batchID string, queue string) error {
363-
bid := entity.BatchID{ID: batchID, Queue: queue}
364-
payload, err := bid.ToBytes()
369+
payload, err := entity.BatchID{ID: batchID, Queue: queue}.ToBytes()
365370
if err != nil {
366371
return fmt.Errorf("failed to serialize batch ID: %w", err)
367372
}
368-
369-
msg := entityqueue.NewMessage(batchID, payload, queue, nil)
370-
371-
q, ok := c.registry.Queue(key)
372-
if !ok {
373-
return fmt.Errorf("no queue registered for topic key %s", key)
374-
}
375-
376-
topicName, ok := c.registry.TopicName(key)
377-
if !ok {
378-
return fmt.Errorf("no topic name registered for topic key %s", key)
379-
}
380-
381-
if err := q.Publisher().Publish(ctx, topicName, msg); err != nil {
382-
return fmt.Errorf("failed to publish message: %w", err)
383-
}
384-
return nil
373+
return publish.Message(ctx, c.registry, key, publish.UniqueID(batchID), payload, queue)
385374
}
386375

387376
// Name returns the controller name for logging and metrics.

submitqueue/orchestrator/controller/cancel/cancel_test.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -358,23 +358,31 @@ func TestProcess_UnbatchedRequestDisappears_Retryable(t *testing.T) {
358358

359359
// TestProcess_BatchPath_HandsOffToSpeculate asserts the entire batch path:
360360
// the request intent CAS runs, the batch intent CAS to Cancelling runs, and
361-
// exactly one publish lands on the speculate topic with the batch ID as the
362-
// message ID. The controller does NOT perform a terminal batch CAS, does
363-
// NOT publish to conclude, and does NOT emit a per-request log on this path
364-
// (the gateway already wrote the Cancelling intent log; conclude writes the
365-
// terminal log when it reconciles request state).
361+
// exactly one publish lands on the speculate topic carrying the batch ID. The
362+
// message ID is not the bare batch ID: the queue deduplicates on it, so a
363+
// redelivery's re-publish would be silently dropped and the batch left
364+
// Cancelling with nothing driving it. The controller does NOT perform a
365+
// terminal batch CAS, does NOT publish to conclude, and does NOT emit a
366+
// per-request log on this path (the gateway already wrote the Cancelling
367+
// intent log; conclude writes the terminal log when it reconciles request
368+
// state).
366369
func TestProcess_BatchPath_HandsOffToSpeculate(t *testing.T) {
367370
ctrl := gomock.NewController(t)
368371
registry, pub := newRegistry(t, ctrl)
369372

370373
type pubRec struct {
371374
topic string
372375
msgID string
376+
// payloadID is the batch ID the message actually carries, which is what
377+
// the consumer acts on — the message ID is only the queue's dedup key.
378+
payloadID string
373379
}
374380
var records []pubRec
375381
pub.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
376382
func(_ context.Context, topic string, msg entityqueue.Message) error {
377-
records = append(records, pubRec{topic: topic, msgID: msg.ID})
383+
bid, err := entity.BatchIDFromBytes(msg.Payload)
384+
require.NoError(t, err)
385+
records = append(records, pubRec{topic: topic, msgID: msg.ID, payloadID: bid.ID})
378386
return nil
379387
}).AnyTimes()
380388

@@ -406,7 +414,12 @@ func TestProcess_BatchPath_HandsOffToSpeculate(t *testing.T) {
406414
err := controller.Process(context.Background(), newDelivery(t, ctrl, cancelPayload(t, "q/1", "stop"), "q/1"))
407415
require.NoError(t, err)
408416

409-
assert.Equal(t, []pubRec{{topic: "speculate", msgID: "q/batch/1"}}, records)
417+
require.Len(t, records, 1)
418+
assert.Equal(t, "speculate", records[0].topic)
419+
assert.Equal(t, batch.ID, records[0].payloadID)
420+
assert.NotEqual(t, batch.ID, records[0].msgID,
421+
"a bare batch ID as the message ID lets the queue swallow the redelivery re-publish")
422+
assert.Contains(t, records[0].msgID, batch.ID)
410423
}
411424

412425
func TestProcess_CancelsEveryApplicableBatch(t *testing.T) {
@@ -438,7 +451,11 @@ func TestProcess_CancelsEveryApplicableBatch(t *testing.T) {
438451
)
439452
publisher.EXPECT().Publish(gomock.Any(), "speculate", gomock.Any()).DoAndReturn(
440453
func(_ context.Context, _ string, msg entityqueue.Message) error {
441-
operations = append(operations, "publish:"+msg.ID)
454+
// The message ID is the queue's dedup key and is distinct per
455+
// publish; the payload carries the batch ID the consumer acts on.
456+
bid, err := entity.BatchIDFromBytes(msg.Payload)
457+
require.NoError(t, err)
458+
operations = append(operations, "publish:"+bid.ID)
442459
return nil
443460
},
444461
).Times(2)
@@ -477,7 +494,9 @@ func TestProcess_BatchFailureDoesNotPreventLaterCancellation(t *testing.T) {
477494
batchStore.EXPECT().Update(gomock.Any(), batchWithState(batch2, entity.BatchStateCancelling), int32(2), int32(3)).Return(nil)
478495
publisher.EXPECT().Publish(gomock.Any(), "speculate", gomock.Any()).DoAndReturn(
479496
func(_ context.Context, _ string, msg entityqueue.Message) error {
480-
assert.Equal(t, batch2.ID, msg.ID)
497+
bid, err := entity.BatchIDFromBytes(msg.Payload)
498+
require.NoError(t, err)
499+
assert.Equal(t, batch2.ID, bid.ID)
481500
return nil
482501
},
483502
)

0 commit comments

Comments
 (0)