Skip to content

fix(bench): cap a rung's backlog and retry per-scenario connects - #177

Merged
zannis merged 5 commits into
mainfrom
zannis/fix/bench-ladder-byte-cap
Sep 9, 2026
Merged

fix(bench): cap a rung's backlog and retry per-scenario connects#177
zannis merged 5 commits into
mainfrom
zannis/fix/bench-ladder-byte-cap

Conversation

@zannis

@zannis zannis commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Both item-4 re-runs died at the 64 KiB ladder rung at 25,000/s: Redis at cell 17, RabbitMQ at cell 32. The rung offers 1.6 GB/s to a container in an 8 GB VM; the broker stopped answering, the next cell's connect hit its expect, and the pass ended. Both brokers had survived a 3.2 GB drain corpus minutes earlier; what killed them was the same bytes piling up as backlog while they also served consumers and reclaimed the previous cell. Three harness changes, no matrix change:

  • Backlog cap on offered-load rungs. Once lag × payload_bytes passes --load-backlog-max-bytes (default 1 GiB, checked at compile time to stay below the 2 GiB drain cap) the rung's producers stop, the rung is recorded as not sustained with the window it actually ran and backlog_capped set on its row, and the ladder skips the rungs above as it already does after any unsustained rung. A sustained rung never approaches the cap, so no passing cell changes. Validation accepts the shorter window and re-derives the verdict the same way; the post-cap drain waits five seconds and leaves the rest to the next cell's purge.
  • connect_with_retries for every stress binary's per-scenario connect. Doubling delays capped at ten seconds, up to ten attempts, a panic only when the broker stays down. Kafka's and SQS's callbacks ping, because their connect builds clients without contacting the broker.
  • --drain-max-bytes keeps its single meaning and its refusal without --drain-messages.

History in this PR

The first cut read the cap from --drain-max-bytes. Review found it half-wired (a capped row failed the document's validation, the verdict ignored the cap, the residual drain could still error, the flag was refused for load-only runs, the SQS and Kafka retries could never fire); all fixed. The re-runs then showed the 3 GiB matrix value fires too late for either broker, hence the separate 1 GiB default.

Tests

  • a_rung_whose_backlog_reaches_the_byte_cap_is_stopped_early: an in-process rung with a sleeping handler and a 500-message cap stops in under 2 s of a 5 s window, records peak lag at the cap, is not sustained, and its row passes validate_run.
  • connect_with_retries_survives_a_broker_that_answers_on_the_third_try.
  • the_backlog_cap_has_its_own_flag_and_a_lower_default_than_the_drain_cap.
  • Full harness suite passes (146). cargo fmt -- --check and both clippy gates pass. benches/README.md documents the cap next to the SQS deviation.

…te cap

An unsustained rung's backlog is resident on the broker for as long as the
producers run. At 64 KiB the 25 000/s rung offers 1.6 GB/s to a container in
an 8 GB VM; on 2026-09-08 Redis stopped answering under it, the next cell's
connect timed out, and the pass died at cell 19 of 168. The rung had already
shown the consumers cannot keep up; letting the producers run out the window
only buried the broker.

Stop a rung's producers once lag x payload_bytes passes the byte cap that
already keeps a drain corpus resident (--drain-max-bytes, default 2 GiB), and
record the rung as not sustained with the window it actually ran; the ladder
skips the rungs above as it already does after any unsustained rung. A
sustained rung never approaches the cap, so nothing that passes changes.

The cap lives on HarnessConfig so the runners read it without a CLI in hand,
and the CLI's --drain-max-bytes overrides it in both run loops. Tested
against the in-process backend with a sleeping handler: a 5 s rung stops in
under 2 s with the backlog at the cap and the verdict not sustained.
…g up

Every stress binary's connect closure gave up on the first failure with an
expect, so a broker still recovering from a heavy cell ended the whole pass
instead of the one cell (Redis after the 64 KiB rung, 2026-09-08: "connect
Redis: standalone ping failed: timed out" at cell 19). Route them through a
harness helper that retries with doubling delays capped at ten seconds, up
to ten attempts, and panics only when the broker stays down, which is the
fail-fast the harness wants for a broker that is really gone.
@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
shove-docs 255f118 Commit Preview URL

Branch Preview URL
Sep 09 2026, 07:14 AM

…rain honour it

Review of the backlog cap found the stop half-wired: the row still claimed
the nominal window, so the document's validation refused the whole results
file for a window shorter than the rung's; the verdict never saw the cap, so
a rung stopped at a lag ratio under the threshold could read as sustained
and let the ladder climb; the residual drain still ran the sixty-second
grace and failed the cell when slow consumers could not clear the backlog;
--drain-max-bytes without --drain-messages was refused, so a load-only run
could not set the cap it now reads; and the SQS connect never touched
LocalStack, so its retry had nothing to retry on.

Carry the cap through LoadOutcome into the row as backlog_capped (absent,
hence false, on rows written before it existed), force the verdict to not
sustained, let validation accept the shorter window and re-derive the
verdict the same way, bound the post-cap drain to five seconds and leave the
rest to the next cell's purge, accept the flag whenever a ladder is
selected, and ping inside the SQS connect callback. The backlog test now
pushes its row and validates it; a new test pins the flag's acceptance.
Kafka's connect builds the producer without contacting the cluster, the same
shape review found on SQS: a broker that is down passes the retry and fails
the scenario later. Ask the cluster inside the callback.
The first pass of the cap read --drain-max-bytes, and at the matrix's 3 GiB
it fired too late: Redis and RabbitMQ both survived a 3.2 GB drain corpus
published to an idle broker and both died under a 3.2 GB backlog piled onto
a broker that was also serving the consumers and still reclaiming the
previous cell (2026-09-08/09, the 64 KiB rung at 25 000/s on each). The two
caps bound different things and cannot share a number.

Make the backlog cap --load-backlog-max-bytes, default 1 GiB, with a
compile-time check that it stays below the drain cap; give --drain-max-bytes
back its single meaning and its original refusal without --drain-messages.
A rung a gigabyte behind is unsustained by any definition, so nothing that
passes changes.
@zannis zannis changed the title fix(bench): cap a rung's backlog at the byte cap and retry per-scenario connects fix(bench): cap a rung's backlog and retry per-scenario connects Sep 9, 2026
@zannis
zannis merged commit eb9a7fa into main Sep 9, 2026
19 checks passed
zannis added a commit that referenced this pull request Sep 9, 2026
The harness now stops an offered-load rung whose backlog passes the byte
cap, records the window it actually ran and flags the row backlog_capped
(#177, #180). chartgen validates the same window invariant the harness did
and refused the first six-backend document to carry such a row: "measured
window is shorter than the rung's nominal window", which blocked publishing
the RabbitMQ re-run.

Read the flag into LoadAccount (absent, hence false, on older rows), let
the window check pass when it is set, and fold it into the re-derived
verdict so a capped row's not-sustained claim matches, as the harness does.
A short window without the flag is still malformed.
zannis added a commit that referenced this pull request Sep 9, 2026
* fix(chartgen): accept a rung the harness's backlog cap stopped

The harness now stops an offered-load rung whose backlog passes the byte
cap, records the window it actually ran and flags the row backlog_capped
(#177, #180). chartgen validates the same window invariant the harness did
and refused the first six-backend document to carry such a row: "measured
window is shorter than the rung's nominal window", which blocked publishing
the RabbitMQ re-run.

Read the flag into LoadAccount (absent, hence false, on older rows), let
the window check pass when it is set, and fold it into the re-derived
verdict so a capped row's not-sustained claim matches, as the harness does.
A short window without the flag is still malformed.

* test(chartgen): prove the cap flips the verdict, not just the window check

The first capped fixture already failed the lag threshold, so the verdict
override had no coverage of its own. A capped row whose consumers kept up
must render as not sustained and be refused if it claims sustained.
zannis added a commit that referenced this pull request Sep 9, 2026
Re-measure RabbitMQ on the pinned matrix with the harness fixes from #172,
#177 and #180 in place: the memory watermark that let the 64 KiB corpus stay
resident, the FIFO flow without --concurrent, and the backlog cap on the
ladder. 140 rows, one failure.

Against the published run the 64 KiB drains are 2 to 14x higher (they were
memory-alarm throttled), FIFO at 64 B recovers from 2.4k to 18.4k msg/s,
and the 4-consumer dips are gone. The 64 B and 1 KiB drains read 20 to 50 %
lower than the published run, most on consumer_group; an isolated
re-measurement of two of those cells reproduced the consumer_group figures,
so they are recorded as measured rather than dismissed as noise, and the
cause is still open. The one failure is the 64 KiB consume_parallel drain
at eight consumers, whose corpus was fully consumed before the last worker
attached.

Charts follow once chartgen accepts a backlog-capped row (#182); the three
64 KiB rungs the cap stopped are in this document.
zannis added a commit that referenced this pull request Sep 9, 2026
)

Re-measure RabbitMQ on the pinned matrix with the harness fixes from #172,
#177 and #180 in place: the memory watermark that let the 64 KiB corpus stay
resident, the FIFO flow without --concurrent, and the backlog cap on the
ladder. 140 rows, one failure.

Against the published run the 64 KiB drains are 2 to 14x higher (they were
memory-alarm throttled), FIFO at 64 B recovers from 2.4k to 18.4k msg/s,
and the 4-consumer dips are gone. The 64 B and 1 KiB drains read 20 to 50 %
lower than the published run, most on consumer_group; an isolated
re-measurement of two of those cells reproduced the consumer_group figures,
so they are recorded as measured rather than dismissed as noise, and the
cause is still open. The one failure is the 64 KiB consume_parallel drain
at eight consumers, whose corpus was fully consumed before the last worker
attached.

Charts follow once chartgen accepts a backlog-capped row (#182); the three
64 KiB rungs the cap stopped are in this document.
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