fix(stovepipe): mint a distinct message id for each buildsignal re-poll - #465
Open
behinddwalls wants to merge 1 commit into
Open
fix(stovepipe): mint a distinct message id for each buildsignal re-poll#465behinddwalls wants to merge 1 commit into
behinddwalls wants to merge 1 commit into
Conversation
This was referenced Jul 29, 2026
behinddwalls
marked this pull request as ready for review
July 29, 2026 23:06
roychying
reviewed
Jul 30, 2026
| // acked yet, so its row is still present: reusing the build id would make | ||
| // every re-poll collide with the message that scheduled it and be silently | ||
| // discarded, ending the poll loop after one tick. The timestamp is for human | ||
| // readability; the random suffix is what guarantees uniqueness, since two |
Contributor
There was a problem hiding this comment.
in this case, we leave one build with two rows and two poll chains? letting dedup drop one seems fine? A build only needs one chain, and two just doubles the poll rate and races reconcile's CAS for no benefit.
my understanding is the potential value of the random suffix is the NTP case since time.Now() is wall clock..
## Summary
### Why?
`buildsignal` schedules its next poll by re-publishing to its own topic, and it reused the build id as the message id — byte-identical to the message `build` published to start the loop. The MySQL queue dedups on the `(topic, partition_key, id)` unique key and `InsertDelayed` swallows the collision with `ON DUPLICATE KEY UPDATE topic = topic`, returning success. So the reschedule was accepted and silently discarded.
This is deterministic, not a race. The re-publish happens before the delivery is acked, and GC only collects up to the minimum *acked* offset on idle ticks, so the colliding row is always still present.
The effect: any build that is not terminal on its first poll is never polled again. `Build.Status` freezes at `accepted`/`running`, the request never leaves `processing`, nothing is published to `record`, and the queue's `in_flight_count` slot is never released — so after `MaxConcurrent` such builds the queue stops admitting work entirely.
Nothing caught it because the fake build runner could not report a non-terminal status until the previous commit, and the unit tests matched the published message with `gomock.Any()`.
### What?
`publishBuildSignal` now mints `{buildID}/poll/{unixMilli}/{rand32}`, following the precedent already established for the gate-wait reschedule in the process controller. The partition key stays the build id, so each build's poll loop keeps its own partition.
The timestamp is for human readability; the random suffix is what guarantees uniqueness. A bare millisecond stamp is not enough — a redelivery racing the original can publish twice within the same millisecond, which would resurrect the bug intermittently.
## Test Plan
✅ `bazel test //stovepipe/...` — new unit test asserts successive re-polls mint distinct ids and never reuse the build id. Verified it genuinely regresses: reverting just the id line fails it with `"map[bk-1:{}]" should have 3 item(s), but has 1`.
✅ `bazel test //test/e2e/stovepipe/...` — new e2e ingests a queue carrying the `build-slow` marker and waits for the build row to reach `succeeded`, which requires more than one poll tick. The service log shows three distinct ids on the `buildsignal` topic for one build.
behinddwalls
force-pushed
the
preetam/stovepipe-poll-id
branch
from
July 30, 2026 07:51
e80ad12 to
28977bd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why?
buildsignalschedules its next poll by re-publishing to its own topic, and it reused the build id as the message id — byte-identical to the messagebuildpublished to start the loop. The MySQL queue dedups on the(topic, partition_key, id)unique key andInsertDelayedswallows the collision withON DUPLICATE KEY UPDATE topic = topic, returning success. So the reschedule was accepted and silently discarded.This is deterministic, not a race. The re-publish happens before the delivery is acked, and GC only collects up to the minimum acked offset on idle ticks, so the colliding row is always still present.
The effect: any build that is not terminal on its first poll is never polled again.
Build.Statusfreezes ataccepted/running, the request never leavesprocessing, nothing is published torecord, and the queue'sin_flight_countslot is never released — so afterMaxConcurrentsuch builds the queue stops admitting work entirely.Nothing caught it because the fake build runner could not report a non-terminal status until the previous commit, and the unit tests matched the published message with
gomock.Any().What?
publishBuildSignalnow mints{buildID}/poll/{unixMilli}/{rand32}, following the precedent already established for the gate-wait reschedule in the process controller. The partition key stays the build id, so each build's poll loop keeps its own partition.The timestamp is for human readability; the random suffix is what guarantees uniqueness. A bare millisecond stamp is not enough — a redelivery racing the original can publish twice within the same millisecond, which would resurrect the bug intermittently.
Test Plan
✅
bazel test //stovepipe/...— new unit test asserts successive re-polls mint distinct ids and never reuse the build id. Verified it genuinely regresses: reverting just the id line fails it with"map[bk-1:{}]" should have 3 item(s), but has 1.✅
bazel test //test/e2e/stovepipe/...— new e2e ingests a queue carrying thebuild-slowmarker and waits for the build row to reachsucceeded, which requires more than one poll tick. The service log shows three distinct ids on thebuildsignaltopic for one build.Stack