Skip to content

test(e2e): event-driven harness with per-controller parking and deterministic cancel test#349

Draft
sbalabanov wants to merge 2 commits into
fix-merge-topic-collisionfrom
e2e-event-harness
Draft

test(e2e): event-driven harness with per-controller parking and deterministic cancel test#349
sbalabanov wants to merge 2 commits into
fix-merge-topic-collisionfrom
e2e-event-harness

Conversation

@sbalabanov

@sbalabanov sbalabanov commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Stacks on #348 (base branch fix-merge-topic-collision).

What this is

Reworks the SubmitQueue e2e suite into a push-based event plane: test-owned consumer groups subscribe to the log topic and runway's check/signal topics over the published mysql-queue port. Delivery state in the MySQL queue is keyed per consumer group, so the observer receives a copy of every message without stealing from the services and with zero service changes. Tests await pushed events (status transitions, runway's answers on the wire) instead of polling.

There are no hard-coded timeouts: every wait is bounded by a suite context whose deadline derives from t.Deadline() (the Bazel test timeout) minus a teardown margin. The remaining polls (Status RPC and request_log, both eventually consistent behind the gateway log consumer) run at a fixed cadence under that context.

NEW: per-controller parking with StageHold

testutil.StageHold parks any single queue consumer controller by planting a phantom partition lease — a row with a far-future lease_renewed_at — in the MySQL queue's partition-leases table for (consumer_group, topic, partition_key). The queue's expiry-based steal semantics (verified in partition_lease_store.go) mean no consumer in that group can acquire the partition until the row is removed, so the stage is starved deterministically with zero service changes. The plant is a plain INSERT that fails loudly if the partition is already leased, so holds must be created before the stage first consumes (pre-hold contract). Granularity is per controller x partition. Release() deletes the row (idempotent after success, retryable on failure) and consumption resumes on the service's next discovery tick. Held messages are never delivered, so there is no retry-budget or DLQ interaction.

Stage catalog

test/e2e/submitqueue/stage_test.go enumerates every queue consumer controller across the three services — all 27 consumers: gateway (1), orchestrator (12 primary + 12 DLQ), runway (2) — with a holdStage() suite helper, so any controller x partition can be starved from a test.

Lease-semantics contract test

test/integration/extension/messagequeue/mysql/stagehold_test.go pins the guarantees StageHold relies on: a held partition starves the group's consumer while other partitions keep flowing, releasing the hold resumes delivery, and Release is idempotent.

Deterministic cancel test (reimplemented)

TestCancel_BeforeBatch_ReachesCancelled now parks the pipeline with a StageHold on runway's merge-conflict-check consumer (partition = queue name) instead of stopping/starting the runway container: park (confirmed by a pushed event), cancel, await the pushed "cancelled" event, assert the terminal store state and that no batch ever enrolled the request, then release the hold and assert runway's late signal does not resurrect it. The connectRunway re-dial helper is gone.

No on-demand service lifecycle

ComposeStack.StartService is removed — services start once in SetupSuite and stop once in TearDownSuite; there is no start/stop lever mid-suite anymore. StopService is retained for the graceful-shutdown verification in teardown.

Also in the suite: runway is first-class — gRPC ping test, graceful-shutdown exit-code check, and happy-path assertions that runway answered both the merge-conflict check and the merge with SUCCEEDED (the assertion that exposed the merge topic collision fixed in #348).

🤖 Generated with Claude Code

…y merge queue

The orchestrator registers its internal merge pipeline stage and the
runway-owned merge queue in one topic registry over one shared queue
backend, and both used the topic key "merge" (topickey.TopicKeyMerge ==
runwaymq.TopicKeyMerge). The publish-only runway entry silently shadowed
the internal stage entry (last write wins), collapsing the two topics into
one:

- runway consumed the internal batch-ID messages as zero-step
  MergeRequests and reported SUCCEEDED without ever seeing the real steps;
- the orchestrator merge controller's real MergeRequest publish was then
  silently dropped by the queue's (topic, partition_key, id) dedupe,
  because it reuses the batch ID already published to the same topic.

The pipeline still reached "landed" only because the zero-step result
correlates by batch ID — with a real merger nothing would ever have been
merged. Found by the e2e harness asserting runway's merge signal carries
the request's step IDs.

Rename the internal stage key and wire name to "mergebatch", and make
consumer.NewTopicRegistry reject duplicate topic keys so a future
collision fails at startup instead of silently misrouting messages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

…ministic cancel test

Rework the SubmitQueue e2e suite so tests block on signals the pipeline
pushes to them instead of polling with hard-coded timeouts, add a
per-controller parking primitive so any pipeline stage can be starved
deterministically, and add a cancellation test that deterministically
reaches the terminal "cancelled" state.

- Queue observer (observer_test.go): test-owned consumer groups subscribe
  to the log topic and runway's check/signal topics over the published
  mysql-queue port. Delivery state is keyed per consumer group, so the
  observer receives a copy of every message without stealing from the
  services and without any service changes. Tests await pushed events
  (status transitions, runway's answers on the wire).
- No hard-coded timeouts: every wait is bounded by a suite context whose
  deadline derives from the test binary deadline (Bazel test timeout)
  minus a teardown margin. The old persistTimeout constant is gone; the
  remaining polls (Status RPC and request_log, both eventually consistent
  behind the gateway log consumer) run at a fixed cadence under that
  context.
- StageHold (testutil/stagehold.go): parks any single queue consumer
  controller with zero service changes by planting a phantom partition
  lease (far-future lease_renewed_at) in the MySQL queue's
  partition-leases table for (consumer_group, topic, partition_key). The
  plain INSERT fails loudly if the partition is already leased, so holds
  must be planted before the stage first consumes (pre-hold contract).
  Release() deletes the row (idempotent after success, retryable on
  failure) and consumption resumes on the service's next discovery tick.
  Held messages are never delivered, so parking has no retry-budget or
  DLQ interaction.
- Stage catalog (stage_test.go): enumerates every queue consumer
  controller across the three services — gateway (1), orchestrator
  (12 primary + 12 DLQ), and runway (2) — with a holdStage() suite
  helper, so tests can starve any controller x partition.
- Lease-semantics contract test (integration, messagequeue/mysql):
  pins the guarantees StageHold relies on — a held partition starves the
  group's consumer while other partitions keep flowing, releasing the
  hold resumes delivery, and Release is idempotent.
- Deterministic cancel test: park the request at the merge-conflict-check
  boundary with a StageHold on runway's merge-conflict-check consumer
  (partition = queue name) instead of stopping/starting the runway
  container (confirmed by a pushed event), cancel, await the pushed
  "cancelled" event, then assert the terminal store state, that no batch
  ever enrolled the request, and that runway's late signal after the hold
  is released does not resurrect it. The connectRunway re-dial helper is
  gone: services start once in SetupSuite and stop once in TearDownSuite.
- Runway is now first-class in the suite: gRPC ping test, graceful
  shutdown exit-code check, and happy-path assertions that runway
  answered both the merge-conflict check and the merge with SUCCEEDED
  (the merge signal must carry the request's step IDs — the assertion
  that exposed the merge topic collision fixed in the parent commit).
- testutil: ComposeStack drops StartService — there is no on-demand
  service start/stop mid-suite anymore (StopService is retained for the
  graceful-shutdown verification in teardown); compose command prefers
  docker compose V2 (the harness relies on V2-only "up --wait"); the
  stack uses a background context so teardown outlives the suite wait
  budget; TestLogger is safe for concurrent use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sbalabanov sbalabanov changed the title test(e2e): event-driven harness and deterministic cancel-to-cancelled test test(e2e): event-driven harness with per-controller parking and deterministic cancel test Jul 14, 2026
@sbalabanov sbalabanov marked this pull request as ready for review July 14, 2026 06:44
@sbalabanov sbalabanov requested review from a team and behinddwalls as code owners July 14, 2026 06:44
@sbalabanov sbalabanov marked this pull request as draft July 14, 2026 06:47
@sbalabanov sbalabanov force-pushed the fix-merge-topic-collision branch 2 times, most recently from 94e242a to 087124c Compare July 14, 2026 18:12
@behinddwalls

Copy link
Copy Markdown
Collaborator

⚠️ Automatic stack rebase failed

This PR could not be automatically rebased after its base PR was merged. The rebase hit conflicts that need manual resolution.

To fix manually:

git fetch origin
git checkout e2e-event-harness
git rebase --onto origin/main 087124c1ac6613d95786c55c7b12dad0e3509203 e2e-event-harness
# resolve conflicts, then:
git push --force-with-lease

Then update this PR's base branch:

gh pr edit 349 --base main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants