Skip to content

Commit 085d43c

Browse files
committed
Explain why dlq reconciliation frees slots only for processing requests
Review question on the buildsignal reconciler: failRequest releases the queue slot only for a request in processing, so does it need to widen that? It does not. Processing is the only non-terminal state that can own a slot: process claims the slot and CAS-marks accepted->processing, compensating its own claim when that CAS does not land, and processing exits only to a terminal outcome, which releases the slot itself. Releasing for accepted would decrement for the common request that never claimed one, over-admitting against MaxConcurrent. Say that where failRequest gates on the state, note at the buildsignal call site that a build row implies processing-or-terminal, and pin the intent on the accepted test case, which passes no queue expectations.
1 parent 061bde0 commit 085d43c

3 files changed

Lines changed: 20 additions & 2 deletions

File tree

stovepipe/controller/dlq/buildsignal.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ func (c *BuildSignalController) Process(ctx context.Context, delivery consumer.D
138138
return nil
139139
}
140140

141+
// Every request reachable from a build row is either still processing, and holding
142+
// the slot failRequest releases, or already terminal, and past releasing it: build
143+
// triggers only once process has written the strategy, which lands in the same CAS
144+
// as accepted→processing, and processing exits only to a terminal outcome.
141145
if err := failRequest(ctx, store, c.logger, build.RequestID); err != nil {
142146
metrics.NamedCounter(c.metricsScope, _buildSignalOpName, "reconcile_errors", 1)
143147
return err

stovepipe/controller/dlq/dlq.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,19 @@ func TopicKey(main consumer.TopicKey) consumer.TopicKey {
6262
// in a terminal state. If the request had reached RequestStateProcessing — meaning process's
6363
// admit step already CAS-incremented the queue's in_flight_count for it and no terminal
6464
// outcome has released it yet — the queue's
65-
// slot is released first. Queue and Request are separate entities with no cross-entity
65+
// slot is released first.
66+
//
67+
// Processing is the only non-terminal state that can own a slot, so the condition is not a
68+
// narrowing of some broader set: process claims the slot and CAS-marks accepted→processing,
69+
// releasing its own claim if that CAS never lands, and the exits from processing are the
70+
// terminal outcomes, which release the slot themselves. Widening the release to accepted
71+
// would decrement for the far more common request that never claimed a slot, over-admitting
72+
// against MaxConcurrent. The one case that escapes both this reconciler and process's
73+
// compensation is a hard crash between the two admit writes, which leaves an accepted
74+
// request holding a slot that nothing here can tell apart from a request that never
75+
// claimed one; distinguishing them needs per-request slot ownership on the row.
76+
//
77+
// Queue and Request are separate entities with no cross-entity
6678
// transaction, so the two writes cannot be atomic and the ordering picks which crash
6779
// failure mode we accept: a crash between the writes leaves the request non-terminal,
6880
// redelivery re-runs reconciliation, and releaseSlot (which tracks no per-request slot

stovepipe/controller/dlq/dlq_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ func TestProcess(t *testing.T) {
101101
wantErr bool
102102
}{
103103
{
104-
name: "accepted request is marked failed",
104+
// No queue expectations: an accepted request never claimed a slot,
105+
// so releasing one here would over-admit against MaxConcurrent.
106+
name: "accepted request is marked failed without releasing a slot",
105107
setup: func(m dlqMocks) {
106108
m.reqStore.EXPECT().Get(gomock.Any(), testID).Return(requestWithState(entity.RequestStateAccepted), nil)
107109
updated := requestWithState(entity.RequestStateAccepted)

0 commit comments

Comments
 (0)