feat(consumergate): runtime stop/start of queue controllers#375
feat(consumergate): runtime stop/start of queue controllers#375sbalabanov wants to merge 1 commit into
Conversation
|
|
d04bf53 to
fb4fde8
Compare
7ff27fb to
1c25eb9
Compare
fb4fde8 to
0b06f08
Compare
0b06f08 to
6b0aa49
Compare
4e229a3 to
2db191e
Compare
617595b to
8c7078c
Compare
8c7078c to
b17ed5a
Compare
b17ed5a to
40d682f
Compare
40d682f to
357d933
Compare
357d933 to
dd2a358
Compare
7622a3a to
ea4e622
Compare
Implement the consumer-gate RFC with a file-backed gate shared by gateway, orchestrator, and runway consumers. - clear deliveries through a consumer-group/partition gate before controller processing while extending visibility for blocked deliveries - expose caller-owned delivery descriptors and let gate implementations stamp gate-owned parked-record fields - keep parked payload files only while deliveries are actively blocked and remove them on every terminal watch path - fail open on gate or visibility-extension failures and leave blocked deliveries unacked during shutdown for normal redelivery - run service containers with the host test UID/GID under rootful Docker while preserving rootless Docker ownership mapping - make the cancellation E2E scenario deterministic by parking the runway merge-conflict-check delivery until cancellation reaches a terminal state - consolidate consumer white-box and behavioral unit tests in the consumer package Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ea4e622 to
bf2e709
Compare
|
Could you clarify what happens in the situation where there's 10 buffered messages awaiting controller X and the controller is paused? Codex is telling me that the first message would wait at the closed gate and have visibility continually extended but the trailing 9 would remain buffered without visibility extension. Every 60 seconds, those 9 expire and are redelivered, consuming retry attempts and creating duplicate buffered delivery. And after 3 attempts they move to DLQ. Assuming this is true, wouldn't this cause the undesirable impact of dropping the first 9 buffered messages while controller X is in a prolonged paused state? |
|
It seems that with this design that uses file-based marking of gated controllers, the gating would not survive node crashes. For example, let's say:
Similarly, if the fleet has 10 nodes, I don't think we fan-out the file write to all 10 nodes -- so the pause behavior would only impact the single node that the request landed on. Basically this design seems to work with a single-node only and assumes the filesystem is stateful and persistent across the lifetime of gate usage. We could fix this by moving the gating implementation from file-based to a storage interface backed with a db. It seems to fit right in with existing Queue implementation which uses db storage so seems better to include as part of that vs a special-snowflake file-based implementation. Something like this could work as the core: |
It depends on a delivery order. For synchronized queues (only one message could be processed at a time), other messages would be held and won't be extended. For parallel queues, if there is enough controller instances on a production or e2e test system, multiple messages would be parked and their life extended as a result of this. It is true that messages not accepted will not be extended. For e2e tests, this is not a problem as we control the number of messages to be validated. |
File-based implementation is for e2e tests only running on the same machine. While it is theoretically possible to hook it with configuration delivery (to let it survive a node crash / restart), it is indeed easier to come up with a centralized database-backed implementation. The cost would be a single synchronous database call per each controller run - files are way faster and simpler too - the gate could be closed or open with basic shell commands. |
Thanks, the parallel-partition case makes sense. Since the RFC documents production debugging use-case ("Operational pause during incident"), my concern is multiple messages in the same partition, where one lease owner processes serially. Only the head reaches the gate; the prefetched tail remains in flight without visibility extensions and can exhaust retries. Potential fixes:
For the E2E-only scope, option 1 seems simplest, at the cost of throughput |
| // written — without further interpretation, as what to do with a failed wait | ||
| // is the caller's policy. If ctx is cancelled while monitoring, the channel | ||
| // yields ctx.Err(). The implementation removes the parked record before | ||
| // yielding on every terminal path, so ListParked contains only deliveries |
There was a problem hiding this comment.
not sure if we need to document it, message gated is never removed from the queue for a given consumer and partition
|
|
||
| // Admin is the write surface used by tests and tooling to operate gates and | ||
| // inspect what a stopped controller is holding. | ||
| type Admin interface { |
There was a problem hiding this comment.
should we rename it to Manage? or Manager maybe
ideally we just extend for everything in the buffer...so the semantics remain usable for batched polling as well |
Implements the consumer-gate RFC (#374) and showcases a Request Cancellation e2e test using it.
Summary
platform/extension/consumergatewith theGate,Entry,Admin,Factory, and configuration contracts, plus generated mocks.0:0under rootless Docker so bind-mounted gate files remain readable by the host test process.Deterministic cancellation E2E coverage
TestCancel_CaughtPreBatch_NeverLandscloses the runway merge-conflict-check gate for the test queue before landing a request. The test observes the exact parked delivery, cancels the request to its terminal state, opens the gate, waits for the parked record to disappear, and uses a sentinel request to prove the stale message was consumed without reviving or landing the cancelled request.Testing
make test— passed (76/76 Bazel test targets).//test/integration/...— passed (8/8 integration targets).//test/e2e/...— passed (2/2 E2E targets).git diff --check— passed.