Skip to content

Wait for the ASB emulator's management api, and pin the image - #3783

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3783/asb-emulator-readiness
Aug 2, 2026
Merged

Wait for the ASB emulator's management api, and pin the image#3783
jeremydmiller merged 1 commit into
mainfrom
gh-3783/asb-emulator-readiness

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Follow-up from the #3781 work: while measuring CI to plan the flakiness burn-down, CIAzureServiceBus turned out to be reporting green while burning 22 of its 25-retry budget every run — 85% of all retries in the repository, all of them one class.

It was never flaky

commit when retries
0bf49b4f8 8/1 21:50 1
3ad7986b6 8/1 21:53 1
1dfdd42a1 8/2 00:08 22
46dd558c2 8/2 12:59 22
141d94880 8/2 14:14 22
0d95ad0bf 8/2 15:11 22

22/22 identical, four runs running, always InlineSendingAndReceivingCompliance.

The actual failure

Forcing retries off in CI surfaced it:

Azure.Messaging.ServiceBus.ServiceBusException :
  Service is warming up. Please try after some time.
Status: 503 (Service Unavailable)   SystemTracker: localhost:$Resources/topics

The emulator binds AMQP on 5673 as soon as Kestrel starts, but the management api on 5300 — the one ServiceBusAdministrationClient uses to provision — answers 503 for another ~26 seconds. WaitForAzureServiceBusEmulatorToBeReady was a bare TcpClient.Connect("localhost", 5673), so it printed "up and ready!" in under a second while provisioning still failed.

InlineComplianceFixture threw out of InitializeAsync → xUnit fails all 22 tests in the class → Bobcat reran each alone in a fresh process → warm by then → green.

No commit caused it

servicebus-emulator:latest moved 2.0.0 → 2.0.1, and 2.0.1 warms more slowly:

digest
:latest today sha256:5a96d8… = 2.0.1
a machine that pulled months ago sha256:a00c96… = 2.0.0

CI pulls fresh every run; developer machines don't. That explains the deterministic CI failure, the total non-reproduction locally, and the tidy-looking three-commit window with no plausible commit in it. I ran a two-arm CI probe reverting the only candidate in that window (GH-3774) — no effect, exactly as this explanation predicts.

Changes

  • Gate polls the management endpoint until it stops returning 503, and throws rather than warning and carrying on. The old warn-and-continue meant an emulator that never started still fed the whole suite into a broker that couldn't serve it, and the failures read as flaky tests. Budget 60s → 3 min.
  • Both images pinned. Emulator → 2.0.1 (what :latest is today). azure-sql-edge → by digest, since no concrete tag matches its :latest manifest; byte-for-byte what CI already runs, so nothing changes except that it can't move.

Verification

Pulled 2.0.1 locally and removed the containers for a genuine cold start — the first time this machine could reproduce CI's conditions. The gate waits 26s and gets a 200 where it previously returned instantly.

Expected on this PR: CIAzureServiceBus reports 0 retries instead of 22, and drops ~3 minutes (22 fresh-process retries at ~8-9s each) off what is currently the longest job in the matrix.

Not in this PR

WaitForPubsubEmulatorToBeReady and the Kafka gate are the same TCP-only, warn-and-continue shape, and :latest is used more widely in the compose file. Neither is currently costing retries, so they're a follow-up sweep rather than scope creep here.

🤖 Generated with Claude Code

https://claude.ai/code/session_0116vfBcKwcjWn8msM4ZjkuA

…follow-up)

CIAzureServiceBus has been reporting green while burning 22 of its 25-retry
budget on every single run -- 85% of all retries in the repository -- and all 22
were one class, InlineSendingAndReceivingCompliance, failing identically four
consecutive main runs in a row. Nothing about that is flaky.

With retries forced off in CI, the real failure surfaced:

    Azure.Messaging.ServiceBus.ServiceBusException :
      Service is warming up. Please try after some time.
    Status: 503 (Service Unavailable)  SystemTracker: localhost:$Resources/topics

The emulator binds its AMQP listener on 5673 as soon as Kestrel starts, but its
MANAGEMENT api on 5300 -- the one ServiceBusAdministrationClient uses to create
queues and topics -- answers 503 for another ~26 seconds. The readiness gate was
a bare TCP connect to 5673, so it announced "up and ready!" in under a second
while every provisioning call still failed. InlineComplianceFixture threw out of
InitializeAsync, which fails all 22 tests in the class at once, and the retry
policy reran each of them alone in a fresh process -- warm by then -- so the job
went green and nobody saw it.

Nothing in Wolverine caused this. servicebus-emulator:latest moved 2.0.0 ->
2.0.1, and 2.0.1 warms more slowly. CI pulls :latest fresh every run and got the
new image; a developer machine keeps whatever it pulled months ago, which is why
this was never reproducible locally and why it appeared to land in a tidy
three-commit window with no plausible commit in it. Reverting the only candidate
in that window (GH-3774) changed nothing -- verified with a two-arm CI probe.

So, both halves:

- The gate now polls the management endpoint until it stops answering 503, and
  THROWS instead of logging a warning and carrying on. The old behaviour meant an
  emulator that never started at all still fed the whole suite into a broker that
  could not serve it, and the failures read as flaky tests rather than as
  infrastructure that never came up. Budget raised 60s -> 3 minutes; the measured
  warm-up is ~26s on a fast machine and a 4-vCPU runner is slower.
- Both images pinned. The emulator to 2.0.1, which is what :latest resolves to
  today. azure-sql-edge publishes no concrete tag matching its :latest manifest,
  so it is pinned by digest -- byte-for-byte what CI has already been running, so
  it changes nothing except that it can no longer move underneath us.

Verified locally against the exact CI image (pulled 2.0.1, removed the containers
for a genuine cold start): the gate waits 26s and gets a 200 where it previously
returned instantly on the TCP connect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116vfBcKwcjWn8msM4ZjkuA
@jeremydmiller
jeremydmiller merged commit 42d25b7 into main Aug 2, 2026
33 checks passed
jeremydmiller added a commit that referenced this pull request Aug 2, 2026
…-3786)

The March 2026 sweep turned out to be neither wholly stale nor wholly sound.
Measured, on a freshly recreated emulator (2.0.1) with the GH-3783 readiness gate
and retries off -- 304 tests, 24 failed, 67.6 minutes:

  CLEAN, stay untagged   4 classes,  32 tests,   2.2 min
  BROKEN, tagged again  11 classes,  29 tests,  57.0 min

Every one of the 11 is a conventional-routing test, and 21 of the 24 failures
carry the identical `BrokerInitializationException : Unable to initialize the
Broker asb in time`, each burning ~2m15s on the timeout before failing. That is
not flakiness: it is 100% failure, deterministically, and it has been hidden
since 2026-03-21. Filed as GH-3786.

Each re-tag now carries its measured numbers in a comment beside it. Not one of
the original 19 tags recorded a reason, a number or an issue link, which is
precisely why nobody could tell a hard failure from a timing race -- and why 21%
of this project sat dark for four months.

Restored: BufferedSendingAndReceivingCompliance (23 tests, 0.36m),
StatefulResourceSmokeTests, Bug_2283_purge_session_subscription, and
Bug_1684_separated_handlers_and_conventional_routing. Discovery goes from 239
tests running to 271; hidden drops from 65 to 33.

No sharding. The earlier 20-minute cap breach was 57 minutes of broker timeouts,
not capacity -- without the broken classes the suite measures 10.7 minutes, which
is what the job already runs. Splitting it would have spread the timeouts across
three jobs and hidden them a second time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116vfBcKwcjWn8msM4ZjkuA
erdtsieck pushed a commit to erdtsieck/wolverine that referenced this pull request Aug 4, 2026
…JasperFxGH-3763)

Two failure modes that have each already cost this repository real time, and
both were still live.

## Readiness gates

Seven of the eight gates logged a warning and carried on. That is precisely the
shape that made CIAzureServiceBus spend 22 of its 25-retry budget on four
consecutive GREEN main runs: the gate declared the emulator ready in under a
second, provisioning ran against a management api still answering 503, a class
fixture threw, and 22 tests failed together -- reported as flaky tests rather
than as infrastructure that never came up. It was found by accident. JasperFxGH-3783
made that one gate fatal and left the rest.

They now share one path, awaitService, which throws when its budget runs out
and names the service, the budget, the attempt count and the last real error.
A single path matters as much as the behaviour: a gate added later cannot
quietly reintroduce warn-and-continue.

Worth singling out, because it was the thinnest by a wide margin: the Postgres
gate allowed ten attempts separated by 250ms -- a 2.5 SECOND budget for a
container start -- and then logged an error and let the suite run anyway. It
was already running out of room in production. In CIMQTT5 on main run
30847233633 it spent four of its ten attempts before Postgres answered, roughly
1.1s of its 2.5s allowance, so a slower runner would have sailed past the end
and started tests against a database that was not up. Every failure after that
would have looked like a test problem.

Two probes changed shape rather than only their ending:

  - Pub/Sub went from a TCP connect to an HTTP request, so it asks the emulator
    a question instead of only checking that something is listening.
  - The attempt-count budgets became wall-clock deadlines. "60 seconds" in the
    old SQL Server warning was never true: each failed attempt could burn the
    connection's own 5s timeout before the 2s sleep, making 30 attempts
    anywhere from 60s to 210s.

Kafka is knowingly still TCP-only -- a broker accepts connections before it
will serve metadata -- because a real probe needs Confluent.Kafka, which this
build project does not reference. Filed separately; making it fatal is the part
that matters today.

The ASB gate moved onto the shared path with its semantics preserved exactly,
including keying on "not 503" rather than "== 200": CI answers 400 there and a
developer machine answers 200, so asserting 200 would hang for the whole budget
and then fail the job.

## Image pins

Every remaining rolling tag that CI pulls is now pinned, each to exactly what
the rolling tag resolved to at the time, so nothing about what CI runs changes
-- it just stops changing on its own.

  - apachepulsar/pulsar :latest -> 4.2.4, in two places. This one was live:
    :latest moved to 4.2.4 on 2026-08-03, the same day. docker-compose.yml said
    4.0.3, so CI (which uses Testcontainers) and a developer running docker
    compose were on different Pulsar versions and only one of them could move
    without warning. Both now say 4.2.4.
  - nats :latest -> 2.14.4, in three places, via one shared constant.
  - fsouza/fake-gcs-server :latest -> 1.55.1 (same digest).
  - mcr.microsoft.com/mssql/server:2025-latest -> pinned by DIGEST, because no
    concrete 2025-CU* tag matches that manifest -- the same reason azure-sql-edge
    is pinned by digest.

Not changed: jaegertracing/all-in-one:latest in
src/Testing/OpenTelemetry/docker-compose.yml. That file is referenced by no
build target and no workflow -- it is a local tracing demo, so it is not a CI
exposure.

Verified locally: the shared throw path reports
"BogusService was not ready after 3s (3 attempts). Last attempt: Connection
refused", and the rewritten Pub/Sub and Postgres gates both pass against live
containers. The remaining gates are exercised by CI itself, since every job
starts its services through them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHAuhdWS3XeAk16swV9G8m
portlogicsvn pushed a commit to portlogicsvn/wolverine that referenced this pull request Aug 6, 2026
…perFxGH-3814)

Split out of JasperFxGH-3813, which made every readiness gate fatal but left this one's
probe unchanged.

WaitForKafkaToBeReady opened a TCP connection to localhost:9092 and called that
ready. Measured against a cold broker three times, that gate is not merely weak,
it is effectively a no-op:

    TCP connect succeeded at 0.0s   <-- old gate passed HERE
    Metadata served at      3.0s   <-- broker actually usable HERE

The TCP connect succeeds at 0.0s every time because Docker's port proxy accepts
as soon as the container is created, before the broker is listening inside it.
Metadata was not served until 2.4-3.0s across the three runs. So the gate passed
instantly and unconditionally, then handed a ~3 second window to a suite that
immediately creates topics -- and "Leader not available", "Not enough replicas"
and topic-creation errors all read as flaky tests rather than as a broker that
was not up. Same defect class as the ASB emulator gate in JasperFxGH-3783, which cost 22
retries per run across four green main runs before it was noticed by accident.

Probes with AdminClient.GetMetadata instead, which round-trips the Kafka protocol
and does not answer until the broker will serve. Also treats an empty broker list
as not-ready, since a broker mid-startup can answer metadata with an empty cluster
view and still refuse a topic creation.

Confluent.Kafka is added to the build project and centrally versioned at 2.14.0,
matching the Confluent.SchemaRegistry.Serdes.* packages already in the tree. That
dependency addition is the reason this was not folded into JasperFxGH-3813.

librdkafka's stderr chatter is silenced because awaitService already reports every
attempt and carries the last real failure into its final error.

Verified by running the CIKafka target against a deliberately removed Kafka
container: the gate waited and passed rather than timing out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHAuhdWS3XeAk16swV9G8m
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.

1 participant