Repository navigation
Report exactly the dead-letter drops the reset takes - #722
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (5)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. SummaryUse an atomic Add the Loom model Pre-merge checks
Remaining workNo outstanding work was identified in the supplied information. Review finding counts are unavailable. WalkthroughThe dead-letter diagnostic now atomically takes accumulated drops when its logging threshold is reached. Loom-only instrumentation tracks reported drops, and a Loom test checks that reported and remaining drops total three across interleavings. ChangesDead-letter drop reporting
Priority: ⬇️ Low Change: Bug fix · Severity of issue fixed: Low Merge Risk: ⚪ Minimal · up to No further changes are identified before merge. Complete the usual pre-merge checks. 🚥 Pre-merge checks | ✅ 12 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (12 passed)
Full details: User-Facing DocumentationExplanation The PR changes user-visible DLQ warning behaviour. Resolution Update the Full details: Developer DocumentationExplanation Fail: the PR updates Resolution Update Full details: ObservabilityExplanation The change improves the DLQ warning. Resolution Add a production counter such as Count each drop; take the sum, Comment |
Reviewer's GuideThe dead-letter drop throttle now atomically swaps the accumulated counter to zero and logs exactly the value it claims, preventing lost or over-reported drops under producer contention. New Loom coverage validates the invariant across interleavings and priorities, while Loom-only accounting and project documentation record the fix and its rationale. Sequence diagram for atomic dead-letter drop reportingsequenceDiagram
participant ProducerA
participant ProducerB
participant DLQCounter
participant Logger
ProducerA->>DLQCounter: fetch_add(1)
ProducerB->>DLQCounter: fetch_add(1)
ProducerA->>DLQCounter: swap(0)
DLQCounter-->>ProducerA: reported count
ProducerA->>Logger: warn(dropped=reported)
ProducerB->>DLQCounter: swap(0)
DLQCounter-->>ProducerB: 0
Note over ProducerB: No log when another producer won the swap
State diagram for dead-letter drop counter ownershipstateDiagram-v2
[*] --> Accumulating
Accumulating --> Accumulating: fetch_add(1)
Accumulating --> Reporting: threshold reached
Reporting --> Accumulating: swap(0) returns count
Reporting --> Accumulating: swap(0) returns 0
Reporting --> Reported: warn(dropped=reported)
Reported --> Accumulating
Flow diagram for Loom dead-letter drop invariantflowchart LR
Producers[Three concurrent producers] --> Drops[DLQ drop counter]
Drops --> Swap["Atomic swap(0)"]
Swap --> Reported[Reported drops]
Swap --> Remaining[Remaining counter]
Reported --> Invariant[reported + remaining = 3]
Remaining --> Invariant
Invariant --> Pass[All interleavings and priorities pass]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@coderabbitai Is this pull request ready to merge at head e78d690? Please answer with a pre-merge checks table for this head, and name anything that still needs work. The throttled dead-letter drop diagnostic now takes the count with |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -u
echo '=== checkout ==='
git rev-parse HEAD
echo '=== PR state ==='
gh pr view 722 --repo leynos/wireframe --json headRefOid,baseRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,url,updatedAt
echo '=== check runs at requested SHA ==='
gh api repos/leynos/wireframe/commits/e78d690ad1e35112080480aca5e8341b2bb2bd17/check-runs --jq '[.check_runs[] | {name,status,conclusion,html_url,app:.app.slug}]'
echo '=== combined status at requested SHA ==='
gh api repos/leynos/wireframe/commits/e78d690ad1e35112080480aca5e8341b2bb2bd17/status --jq '{state,statuses:[.statuses[] | {context,state,target_url}]}'
echo '=== hosted Advanced Tests run ==='
gh api repos/leynos/wireframe/actions/runs/36181691446 --jq '{id,head_sha,status,conclusion,html_url,name,event}'
echo '=== files in PR ==='
gh pr diff 722 --repo leynos/wireframe --name-only
echo '=== model outline ==='
ast-grep outline crates/wireframe-loom/tests/push_dlq.rs --items all --match 'every_drop_is_reported_or_still_counted|PushHandleProbe'
echo '=== references ==='
rg -n -C 3 'every_drop_is_reported_or_still_counted|dlq_reported_count|dlq_reported|swap\(0\)|RFC 0002|test-loom' crates/wireframe-loom/tests/push_dlq.rs crates README.md Makefile 2>/dev/null | head -220Length of output: 10692 🏁 Script executed: #!/bin/bash
set -u
echo '=== change diff ==='
gh pr diff 722 --repo leynos/wireframe -- crates/wireframe-loom/tests/push_dlq.rs docs/developers-guide.md docs/rfcs/0002-model-checking-the-write-loop.md src/push/queues/handle.rs src/push/queues/mod.rs 2>&1 || git diff main...HEAD -- crates/wireframe-loom/tests/push_dlq.rs docs/developers-guide.md docs/rfcs/0002-model-checking-the-write-loop.md src/push/queues/handle.rs src/push/queues/mod.rs
echo '=== file outlines ==='
ast-grep outline src/push/queues/handle.rs --items all --match 'PushHandle|PushHandleProbe|dlq_reported_count|dlq_drop_count'
ast-grep outline src/push/queues/mod.rs --items all --match 'Queue'
echo '=== source slices ==='
rg -n -C 12 'dlq_reported|dlq_drop_count|swap\(0\)|dead.letter|dlq_drop' src/push/queues/handle.rs src/push/queues/mod.rs | head -300
echo '=== hosted jobs ==='
gh api 'repos/leynos/wireframe/actions/runs/36181691446/jobs?per_page=100' --jq '[.jobs[] | {name,conclusion,steps:[.steps[] | {name,conclusion}]}]'
echo '=== PR review metadata ==='
gh pr view 722 --repo leynos/wireframe --json reviews,latestReviews --jq '{reviews:[.reviews[] | {author:.author.login,state,submittedAt}],latestReviews:[.latestReviews[] | {author:.author.login,state}]}'
echo '=== branch protection API ==='
gh api repos/leynos/wireframe/branches/main/protection --jq '{required_status_checks,required_pull_request_reviews,enforce_admins}' 2>&1 | head -70Length of output: 22958 🏁 Script executed: #!/bin/bash
echo '=== targeted code ==='
sed -n '1,235p' crates/wireframe-loom/tests/push_dlq.rs
echo '=== RFC and guide references ==='
rg -n -C 5 'swap\(0\)|every_drop_is_reported_or_still_counted|dropped|dead.letter' docs/rfcs/0002-model-checking-the-write-loop.md docs/developers-guide.md | head -160Length of output: 16442 🏁 Script executed: #!/bin/bash
set -u
gh pr view 722 --repo leynos/wireframe --json headRefOid,mergeable,mergeStateStatus,reviewDecision,isDraft,statusCheckRollup --jq '{headRefOid,mergeable,mergeStateStatus,reviewDecision,isDraft,checks:[.statusCheckRollup[] | {name:(.name // .context),status:(.status // .state),conclusion:(.conclusion // .state)}]}'
gh api repos/leynos/wireframe/commits/e78d690ad1e35112080480aca5e8341b2bb2bd17/status --jq '{state,statuses:[.statuses[] | {context,state}]}'Length of output: 1553
Still needs work: Wait for You are interacting with an AI system. |
The throttled "DLQ dropped frames" diagnostic reported `dropped`, the value the reporting producer's own increment returned, and then reset the counter with `store(0)`. Increments other producers made in between were cleared without being reported, and drops the report counted could be left in the counter too, so under contention the log over-reported. RFC 0002 recorded Loom finding this with three producers. The reporter now takes the count with `swap(0)` and reports exactly what it took, with no separate load and store. A logger that loses the race to another producer's swap has nothing left and does not log. `every_drop_is_reported_or_still_counted` runs three producers against a logging threshold of two and asserts that reported plus remaining equals the three drops on every interleaving. Only a Loom build keeps the running total of reported drops it reads, through `PushHandleProbe::dlq_reported_count`. On the old code the model fails for both priorities (reported 2 plus remaining 3); with the swap all eight push models pass. RFC 0002 records the fix in place of the open question, and the developers' guide describes it.
Review found the documentation set still described the old counter: - the outbound messaging design described two Loom threads and a threshold reset only; it now describes the swap-and-report, the three-producer invariant and `dlq_reported_count`; - RFC 0002 still listed the approximate counter as an open question and recommended taking that decision first; both are closed; - the developers' guide's Loom field table lacked the Loom-only `dlq_reported` total, and said the models assert on the drop counter and nothing else; - the users' guide did not say what the `dropped` count in the throttled warning means, or that no `dropped=0` line is emitted.
726f5e3 to
ed59770
Compare
|
@coderabbitai Is this pull request ready to merge at head ed59770? Please answer with a pre-merge checks table for this head, and name anything that still needs work. Since your review at e78d690, which had no error rows, the documentation warnings are taken:
The branch is then rebased onto main, which gained only a workflow checkout change. The Rust under test is identical to e78d690, where the hosted Advanced Tests run 36181691446 ran every Loom model green, the new one included. CI is green at this head. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -u
echo '=== checkout and PR metadata ==='
git rev-parse HEAD
gh pr view 722 --repo leynos/wireframe --json headRefOid,baseRefOid,baseRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,url --jq '{headRefOid,baseRefOid,baseRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,checks:[.statusCheckRollup[]|{name:(.name // .context),status:(.status // .state),conclusion:(.conclusion // .state)}],url}'
echo '=== checks on head ==='
gh api repos/leynos/wireframe/commits/ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90/check-runs --jq '[.check_runs[]|{name,status,conclusion,app:.app.slug,html_url}]'
echo '=== combined status ==='
gh api repos/leynos/wireframe/commits/ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90/status --jq '{state,statuses:[.statuses[]|{context,state,target_url}]}'
echo '=== hosted run ==='
gh api repos/leynos/wireframe/actions/runs/36181691446 --jq '{id,head_sha,status,conclusion,html_url,name}'
echo '=== PR files ==='
gh pr diff 722 --repo leynos/wireframe --name-only
echo '=== commits ==='
gh pr view 722 --repo leynos/wireframe --json commits --jq '[.commits[]|{oid:.oid,messageHeadline:.messageHeadline}]'
echo '=== documentation paths ==='
fd -i 'asynchronous-outbound-messaging-design.md|wireframe-users-guide.md|developers-guide.md|0002-model-checking-the-write-loop.md'
echo '=== working tree status ==='
git status --shortLength of output: 5509 🏁 Script executed: #!/bin/bash
set -u
echo '=== exact Rust changes since previous head ==='
git cat-file -e e78d690ad1e35112080480aca5e8341b2bb2bd17^{commit} 2>/dev/null && git diff --stat e78d690ad1e35112080480aca5e8341b2bb2bd17 ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90 -- src/push/queues/handle.rs src/push/queues/mod.rs crates/wireframe-loom/tests/push_dlq.rs || echo 'previous head unavailable locally; compare via API separately'
echo '=== Rust blob SHA on each head ==='
for rev in e78d690ad1e35112080480aca5e8341b2bb2bd17 ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90; do
echo "$rev"
for file in src/push/queues/handle.rs src/push/queues/mod.rs crates/wireframe-loom/tests/push_dlq.rs; do
printf '%s ' "$file"
gh api "repos/leynos/wireframe/contents/$file?ref=$rev" --jq '.sha' || true
done
done
echo '=== documentation change summary ==='
git show --stat --format='%h %s' ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90
echo '=== document declarations and descriptions ==='
rg -n -i -C 3 'every_drop_is_reported_or_still_counted|dlq_reported_count|dlq_reported|swap\(0\)|dropped=0|approximate|drop counter alone|three.producer|three concurrent|throttled|drop.*report' docs/asynchronous-outbound-messaging-design.md docs/developers-guide.md docs/rfcs/0002-model-checking-the-write-loop.md docs/users-guide.md | head -220
echo '=== hosted advanced run jobs ==='
gh api 'repos/leynos/wireframe/actions/runs/36181691446/jobs?per_page=100' --jq '[.jobs[]|{name,conclusion,loom_steps:[.steps[]|select(.name|test("loom";"i"))|{name,conclusion}]}]'Length of output: 55701
Still needs work: No blocker appears in the verified results. The hosted Loom run was not run again at this head; its result applies to the identical Rust files checked above. You are interacting with an AI system. |
Add the Progress entries for the second compile repair and for the audit of every file both sides of the merge changed. The repair entry records the `E0271` on `tests/prepared_app/mod.rs`, the ancestry probe that attributed it to a latent defect in the commit that introduced the shared factory rather than to the rebase, and the clean `cargo check` that followed. The audit entry records how each of the ten dual-changed files resolves to a deliberate union: the two prepared-application test files that both sides restructured, the scheduler carrying the target's layout with this branch's counters, and the clean unions in the manifests and module facades. It also records the content-level check that `#722`'s dead-letter fix survived the move into `dlq.rs`, which SHA ancestry cannot answer once a rebase has replayed the commits, and the two stale path references that belong to the base lag rather than to this branch. Plan only; no behaviour changes.
Add the Progress entries for the second compile repair and for the audit of every file both sides of the merge changed. The repair entry records the `E0271` on `tests/prepared_app/mod.rs`, the ancestry probe that attributed it to a latent defect in the commit that introduced the shared factory rather than to the rebase, and the clean `cargo check` that followed. The audit entry records how each of the ten dual-changed files resolves to a deliberate union: the two prepared-application test files that both sides restructured, the scheduler carrying the target's layout with this branch's counters, and the clean unions in the manifests and module facades. It also records the content-level check that `#722`'s dead-letter fix survived the move into `dlq.rs`, which SHA ancestry cannot answer once a rebase has replayed the commits, and the two stale path references that belong to the base lag rather than to this branch. Plan only; no behaviour changes.
* Add observability scaffolding for runtime ownership baselines (#639)
Establish the measurement primitives the four Epic #635 hot paths need
before their ownership topology changes, so baselines can be captured
against the current implementation and compared afterwards.
Add three pieces of instrumentation:
- `pool_scheduler_task_spawns()` exposes a process-wide count of
scheduler worker tasks spawned by `PoolScheduler::kick`. It lives in a
new `scheduler_spawns.rs` rather than `scheduler.rs` because adding it
inline pushed that module to 403 lines, past the workspace's 400-line
limit, and past whitaker's `module_max_lines` lint. The separate
module mirrors the existing `connection::counter` arrangement.
- `wireframe::test_helpers::connection_startup` holds the counting startup
fixtures — `ConnectionStartupInstrumentation`,
`StartupFixturePlan`, `TransformCountingMiddleware`, and
`counted_app_factory` — that were previously private to
`tests/prepared_app.rs`. It lives in the `wireframe` crate rather than
`wireframe_testing` because `Transform` is declared with
`#[async_trait]` and `wireframe_testing` does not depend on that crate.
All four prepared-app test crates now share one definition.
- `wireframe_testing::benchmark_alloc_support` extracts the counting
global allocator and scoped `count_allocations` window from
`benches/codec_performance_alloc.rs`, leaving each benchmark binary to
declare only its one-line `#[global_allocator]` static.
Signal is a plain atomic counter rather than a `metrics` gauge because
benchmark binaries install no recorder, which would make a `metrics`
counter unobservable exactly where it is needed.
The counts are exact rather than inferred from timing, so they can be
asserted directly once the benchmark harness lands.
* Add runtime ownership benchmark support modules (#639)
Add measurement helpers for the four hot paths named by issue #639,
mirroring the existing `codec_benchmarks` arrangement so no second
benchmark framework is introduced:
- connection actor drain, with cancellation disabled, triggered before
the run, and triggered mid-drain from a `before_send` hook;
- push handle sends, split into uncontended and overflow regimes;
- application connection startup, for the per-connection factory and
prepared-root topologies;
- client pool acquisition, uncontended and contended under the FIFO and
round-robin fairness policies.
Each module exposes its scenarios as data and a single fallible
`measure_*` entry point, so a benchmark binary, a test, or an ad-hoc
experiment can drive the same code, and scenarios stay comparable across
the ownership change.
They sit behind a new `runtime-benchmarks` feature, which enables
`wireframe/test-support` and `wireframe/pool`; gating keeps
`cargo check -p wireframe_testing` working with default features.
A failing test exposed that `PushHandle::try_push` returns `Ok(())` for a
frame a drop policy routes to the dead-letter queue, so a single
"delivered" counter would conflate "reported success" with "queued".
`PushMeasurement` therefore reports `accepted`, `drained`, and
`dead_lettered` separately, which also makes each regime's signature
legible.
Three further defects in the new doc examples were found by running the
crate's doctests directly: a mis-aliased constant, an assertion naming
the wrong scenario, and an allocation assertion that no doctest binary
can satisfy because the counting allocator must be installed as the
process global allocator. The surviving assertion is that the counting
window closes, which holds regardless.
* Clear clippy and spelling findings in benchmark support (#639)
Scrutineer's first gate run on 9bb6b90 was red at two of the five gates,
and both vetoes were real.
`make lint` stopped at clippy with six errors. The `as isize` casts in
`PoolMeasurement::alive_task_delta` trip `cast_possible_wrap`, so the
conversions now use `cast_signed`, which documents the intent that a task
count is small and non-negative. The two `%` expressions used to cycle a
frame or route identifier trip `integer_division_remainder_used`, which
this workspace denies outright; both become power-of-two masks, which
preserve the only property the callers rely on, that consecutive values
differ, without a division. The three-tuple returned by `build_queues`
trips `type_complexity` and is now the named `PushFixture` alias. Finally,
`Err(PushError::QueueFull) | Err(_)` trips `unnested_or_patterns`; the
catch-all already covered the named variant, so the catch-all is the
honest form.
Clippy aborts the lint target before whitaker runs, so whitaker's verdict
from that run was UNKNOWN rather than passing. This commit is what makes
a green lint claim checkable.
`make markdownlint` failed at its spelling prerequisite on `mis-aliased`
and `summarised`. The former becomes "incorrectly aliased"; the latter
becomes "summarized" because `typos.toml` is generated from a shared
en-GB-oxendict base that pins the -ize form, so hand-editing it is not an
option.
The plan document also gains the findings that produced these fixes, so
the next reader does not rediscover the denied-lint surface by failing a
gate.
* Correct pool acquisition counters and clear spelling gate (#639)
Measure the acquisition phase rather than the whole pool lifecycle, and
replace two documented claims that direct measurement falsified.
`PooledClientLease::drop` calls `notify_capacity_available`
unconditionally, and `try_acquire_immediately` arms even a fast-path
lease with a release handle, so every release reaches
`PoolScheduler::kick`. A temporary probe measured spawn deltas of 1, 1,
and 2 for bursts of 1, 16, and 256 acquisitions across pool sizes 1, 4,
and 16 — never 0. The module documentation asserted the opposite and is
corrected.
The live-task gauge was also measuring the wrong phase: `bb8` connects
eagerly, so bracketing the whole measurement reported a delta equal to
the pool size. `pool_once` now builds and closes the pool outside the
bracketed region and samples the gauge only around acquisition.
Add cases pinning the true contract: a burst spawns at least one worker,
the spawn count is bounded by the number of acquire/release transitions,
contended acquisition reaches the slow path, and a closed pool leaves no
construction tasks behind. The last was verified by mutation — sampling
before construction fails all three cases with deltas of 2, 5, and 17 —
so the bracket is load-bearing rather than cosmetic.
Also fix `characterised` to `characterized` in the ExecPlan, which
failed the spelling prerequisite and so prevented `markdownlint-cli2`
from running at all. The gate now completes end to end over 119 files.
* Add runtime-ownership baselines and fix spurious disconnect (#639)
Measure the four scopes issue #639 names before #637 and #638 change
their ownership topology: connection-actor event processing, the push
handle send path, application connection startup, and client pool
acquisition. The recorded values are the baseline the later refactors
are compared against, so the harness is built to outlive them.
Two Criterion binaries back the four groups.
`runtime_ownership_baselines` records timing;
`runtime_ownership_baselines_alloc` records allocation counts and is
separate because `#[global_allocator]` is per-process, so installing the
counting allocator would tax every timing sample in the sibling. Both
drive the same workload helpers, so a scope cannot be added to one and
silently skipped by the other.
The workload helpers live in `wireframe_testing` beside the existing
codec benchmark support, behind a new `runtime-benchmarks` feature,
rather than introducing a second benchmark framework. Each scope exposes
a workload enum, a measurement struct, and a `measure_*` function
returning `Result`, mirroring `codec_benchmarks`.
Acceptance criteria this discharges:
- Factory invocation and middleware transformation counts are asserted
directly, not inferred from timing: one shared prepared root builds
once and transforms once per route per layer, while the
per-connection factory path rebuilds and re-applies for every
connection.
- Pool scheduler task creation is observable through a process-wide
counter in `kick`, so the spawns a pool scenario caused are read
rather than guessed.
- The allocation harness proves it can resolve what criterion 5 asks of
it: a zero-allocation closure reports exactly zero, a 1024-byte
allocation reports non-zero, and per-item `Arc` construction reports
one allocation per item more than cloning a shared `Arc`. The last is
the measurement that decides whether nested-`Arc` removal would move
the count at all.
Building the contended pool scope surfaced a pre-existing library defect,
fixed here because it is small, local, verified, and user-visible:
`PoolScheduler::acquire_for_handle` returned a spurious
`ClientError::disconnected()` whenever it dequeued a waiter whose session
had already stopped listening, telling a caller the pool had gone away
while it was still serving traffic. Two windows reach it — a contender
that observes the last busy slot and returns while a worker concurrently
services that same waiter, and `close()` racing in-flight acquires. The
fix keeps the healthy discarded lease instead of converting it into an
error. Only one line of `src/client/pool/scheduler.rs` differs from
`main` on this branch and it is the new counter increment, so the defect
predates this work.
The pool counter is documented as a bracket rather than a zero, because
measurement showed the obvious claim is false: `PooledClientLease::drop`
notifies the scheduler unconditionally, so a release reaches `kick` even
when nothing is waiting and an uncontended burst spawns between one and
`2n` workers.
The push workload label no longer appends the priority lane, which every
scenario name already states; the overflow scenarios all send high, so
it spelled `high` twice.
No CI job runs `cargo bench`: the results are ratios and reviewed
evidence, not brittle absolute timing thresholds. `make
bench-runtime-ownership` is the reproduction command. The baseline table
and regression policy land in `docs/runtime-ownership-baselines.md`.
* Record the pool acquire-path defects instead of patching them (#639)
Measuring contended pool acquisition exposed two pre-existing defects in
the client pool's acquire path: a spurious `disconnected` error, and an
intermittent stall in which every contender waits with the queue still
populated. Both reproduce against unmodified library code.
A one-line candidate repair in `PoolScheduler::acquire_for_handle` was
implemented and measured, then rejected. It removed the spurious error
but converted it into an intermittent deadlock: `make
bench-runtime-ownership` hung during Criterion warm-up, with the main
thread parked in `iter_custom` and every tokio worker parked. Trading a
fast failure for a hang is strictly worse, so the change is reverted and
`src/client/pool/scheduler.rs` returns to its original arm.
The stall is inherent to sustained contention rather than to the
harness. With the repair reverted it still reproduces, at first-stall
bursts ranging from 272 to 18069 across both fairness policies, rebuilt
and long-lived pools, fresh and reused handles, and 8 or 16 contenders.
A control separates it from the fixture's task leak: 20000 uncontended
iterations leaked the same four tasks per iteration and never stalled.
Because a stall occurs during Criterion warm-up, contended acquisition
cannot be timed at all, so it is now reported as a bounded `tracing`
observation rather than a benchmark group. `CONTENDED_BURST_TIMEOUT`
turns a stall into a reported error so a benchmark or `cargo test
--all-targets` run cannot block forever. Uncontended acquisition stays
Criterion-timed.
The regression test asserting the spurious disconnect was fixed becomes
a documented known-defect tolerance, since it passes only because its
workload is too light to reach the stall, and asserting correctness
there would be false.
Fixing the acquire path belongs to ADR 013 and issue #647; this change
records the baseline, which is #639's stated purpose.
* Apply rustfmt to the runtime-ownership benchmark support
The previous commit added the known-defect tolerance and the spawn-counter
assertions without running `cargo fmt`, so `make check-fmt` reported three
diffs: two in the new contention test (an over-wrapped function signature and
a string literal that fits on one line) and one in a `PushWorkload::label`
doctest.
Pure formatting; no behaviour changes.
* Report contended pool allocations as an observation
The allocation benchmark's first real run failed outright:
allocation timing pass failed: 1 of 16 contenders failed; first:
transport error: connection closed by peer
That is the pre-existing spurious-disconnect defect in the pool acquire path,
not a fault in the harness. The counting window itself had already succeeded —
it records one window, not a Criterion sample series — but the timing pass that
follows it in the same group runs far more iterations, so it reliably reaches
the interleaving that fails.
The contended scope is therefore removed from `allocation_scopes` and reported
once after `Criterion::final_summary` through a bounded observation, exactly as
the timing binary already reports it. The allocation count is unaffected: it
still comes from a counted window, so it is the same measurement with only the
repetition and the timing dropped.
The observation installs a subscriber filtered to this binary's own target, so
the connection-startup workloads' per-connection INFO spans do not bury the
count.
See `docs/runtime-ownership-baselines.md` and issue #647.
* Record the runtime-ownership baselines (#639)
Capture the four scopes the issue names and transcribe the numbers into
`docs/runtime-ownership-baselines.md`, replacing the two placeholders.
The baseline was taken with `make bench-runtime-ownership` on the named
machine: 24 timing labels, 8 allocation labels, 2 arc-sensitivity labels,
and 2 contended observations, with no panic and no error.
Notable readings, all of which the document now records:
- The startup pair is the sharpest contrast in every table. Over four
connections and two routes, the per-connection factory path costs
21.881 us and 408 allocations against the prepared root's 10.578 us and
160 allocations, because it evaluates the factory four times and runs
sixteen middleware transformations instead of one and four. At a single
connection the two paths cost about the same, which is the point:
preparation is a fixed cost the prepared root amortizes.
- The nested-`Arc` sensitivity pair recorded 1 allocation for 256 shared
clones against 256 for 256 fresh constructions, a delta of exactly
`ARC_PROBE_ITEMS - 1`. That is the direct answer to acceptance
criterion 5 and the proof that the counter resolves a per-item
allocation.
- An uncontended pool burst spawns 1 scheduler task for 1 or 16
acquisitions and 2 for 256, identically at pool sizes 1, 4, and 16. The
hidden cost is a per-idle-to-busy-transition wake-up, not a
per-acquisition one.
Also record the contended observations and why they are observations
rather than timings: the allocation binary died on its first capture with
`1 of 16 contenders failed; first: transport error: connection closed by
peer`, reached from unmodified benchmark code driving unmodified library
code. Criterion's timing pass runs orders of magnitude more iterations
than an allocation window, so it hits the acquire-path interleaving
nearly every run. The count was never in doubt; it comes from a counted
window that completed before the failure.
Mark the ExecPlan's capture item complete.
* Clear the runtime-ownership gate failures (#639)
Fix every fault the commit gates reported for the baseline harness, and
retire a flaky assertion the gates could not have caught reliably.
Clippy rejected eleven sites. The benchmark registration closures were
nested too deeply to read, so the route/connection grid is now built as
data by `startup_workloads` and each group's timed body moved into its
own `time_*` function. The two reporting `match` arms exceeded the
cognitive-complexity budget because a `tracing` macro expands to an
`enabled` conditional; each arm is now a named function. The allocation
binary's workload table is an `AllocationWorkloads` struct with
documented fields, which also removes its extra parameter count. The
probes that cannot fail now return `()` rather than wrapping `()` in
`Ok`, removing two `unnecessary_wraps`, and the probe pointer type is a
named `ArcProbe` alias rather than an inline `type_complexity` site.
The `test-doc` gate failed on `src/test_helpers/connection_startup.rs`:
its example used `?` across `WireframeError` and `PrepareError`, which do
not unify. The example now returns `Box<dyn Error>`, the one type both
convert into.
`block_on` in the helper tests returned a `Result` through `expect`,
which `expect_used` denies outside a `#[test]`; it now propagates the
runtime-construction error as the workloads' own `String` so all
fourteen call sites keep the `Result` they already match on.
The live-task assertion was load-sensitive and would have failed spuriously
under the full suite. The gauge counts *every* task on the runtime, so on a
multi-thread runtime it measures the whole scheduler: sampling an idle→busy
burst there produced deltas from −12 to +10 with a long tail above +2 in a
third of runs, at every pool size, on a workload whose own churn is a fixed
constant. On a current-thread runtime the same workload recorded only −1
and 0 over 180 samples. That one case therefore runs on a current-thread
runtime, where the reading answers the question asked, and the bound is a
non-scaling ceiling rather than a fit to the observed values: the property
is that the delta does not grow with `pool_size`, which is what the
construction-bracket mutation demonstrates.
Settlement is now established rather than assumed. A worker woken by the
final release exits on its first poll, so a gauge read taken immediately
after can count a task already on its way out; `settled_alive_tasks` waits
for the gauge to hold steady across consecutive polls, and reports a
non-settling gauge rather than yielding once and hoping.
The gauge's validity domain and the reasoning behind the bound are recorded
in `docs/runtime-ownership-baselines.md` under instrumentation sensitivity,
so a future reader does not re-derive them from a flake.
* Split the runtime-ownership benchmark support modules (#639)
Whitaker's `module_max_lines` lint enforces the 400-line module limit as a
hard error, and all four benchmark-support modules written for this issue
broke it: `client_pool_benchmark_support` at 822 lines,
`push_benchmark_support` at 605, `connection_actor_benchmark_support` at 463,
and `connection_startup_benchmark_support` at 418. Because `make lint` runs
rustdoc, then clippy, then whitaker, the finding only became visible once the
two earlier steps were green.
Split each module into `mod.rs` (documentation, declarations, re-exports),
a data module (`workload.rs` or `scenario.rs`), a `measurement` module that
drives the data, and `tests.rs`. Where a module contained two genuinely
different control flows, those get their own submodules rather than sharing
one body: `push/measurement/{uncontended,overflow}.rs` and
`client_pool/measurement/{phase,contended}.rs`.
A shared body would have obscured which branch a regression sits in, so the
split is an improvement in its own right, not only a way to satisfy the lint.
It also moves each regime's rationale next to that regime instead of leaving
one long module header to explain both. No `#[allow(module_max_lines)]` was
used; `AGENTS.md` reserves lint suppression as a last resort.
The split is internal: every consumer — the two bench binaries and
`tests/runtime_ownership_benchmark_helpers.rs` — imports only from the module
root, so no public path changed and the recorded baselines still compare.
All 21 files are now under 240 lines, and the four `module_max_lines`
violations are confirmed cleared by a full `make lint` run.
Two hazards surfaced while splitting, now recorded in the ExecPlan. A name
used only in a Rustdoc intra-doc link trips `unused_imports` once the code
that used it lands in a sibling file, so such links carry the full path.
And one doc link in `client_pool_benchmark_support/mod.rs` reaches into a
private submodule; it is rewritten as a plain-text path, since rustdoc would
reject the link were that feature ever enabled for the docs build.
Also records both findings in the ExecPlan's Surprises section and ticks the
corresponding Progress item.
* Qualify Rustdoc links that the split left dangling (#639)
Splitting the benchmark-support modules into submodules moved several items
out of the scope of the documentation that linked to them, and rustdoc rejects
the resulting links under `-D warnings`:
- a public module's docs linking to a *private* submodule
(`rustdoc::private-intra-doc-links`); and
- `[`PushError::QueueFull`]` and `[`Envelope`]` links to items that are no
longer imported in the file that names them
(`rustdoc::broken-intra-doc-links`).
The gate missed all sixteen because `make lint` documents the workspace
*without* `runtime-benchmarks`, and the benchmark-support modules are
`#[cfg]`-gated behind that feature. Measuring the unimplemented regime
directly (`RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc -p
wireframe_testing --features runtime-benchmarks --no-deps`) reported sixteen
errors; the same command now reports none.
Fix each site to keep the readable name while resolving: paths into private
submodules are plain code spans rather than links, and links to `wireframe`
items carry the full path. `scenario.rs`'s `PushMeasurement` field had the
same defect as the table above it, which an earlier pass over the same hazard
had missed, so this was not a one-site mistake.
The previous commit's message described this repair as done; it was not, and
that commit is left alone rather than rewritten. This one records it honestly.
* Record the lint gate's blind spot for gated modules (#639)
The sixteen Rustdoc errors fixed in 9985ed1 were invisible to `make lint`
because its rustdoc step runs `cargo doc --workspace --no-deps` while the
clippy step beside it runs `--all-features`. Modules behind a `#[cfg]` feature
are therefore compiled by clippy but never documented, so their intra-doc
links go unchecked.
Record the measurement, the sixteen-error transcript it produced, and the
one-word remedy — adding `--all-features` to the rustdoc command, which exits
0 on this tree — so the next agent working in a feature-gated module knows the
standard gate will not catch dangling links.
The remedy is recommended as its own base pull request rather than applied
here: it changes a gate every branch and agent runs, and this branch's diff
should stay about benchmark harnesses.
* Resolve the CodeRabbit findings on the benchmark baselines (#639)
Three review findings, all verified against the source before acting.
The public rustdoc for `pool_scheduler_task_spawns` promised callers
could assert that an uncontended burst "does not create additional
scheduler tasks", contradicting the module doc twenty lines above it,
which explains the counter is a bound rather than a zero because every
lease release reaches `kick`. The rustdoc now states the real bound.
The contended pool workload rendered `acquisitions_256` in its label
even though the contended path never reads `PoolWorkload::acquisitions`;
a burst is driven by a per-contender round count instead, so the label
advertised a parameter the run did not honour. Promote that count from a
private `CONTENDED_ROUNDS` constant to public `CONTENDED_ROUNDS_PER_CONTENDER`
workload data, state it in the contended label as `rounds_8`, and
document on the field that it is read only in the uncontended regime. A
parameterized test pins both regimes' labels so they cannot drift back
together. Only the identifier changed: the recorded baseline counters
stand, and a re-run confirmed the burst still grants 128 leases (16 x 8),
which is what the corrected prose in `docs/runtime-ownership-baselines.md`
now says.
Two integration tests called `.expect` on a single contended burst,
which the acquire path can fail spuriously at roughly one burst in 913
at 16 contenders. Both that test and its sibling in the support crate
now retry a bounded eight attempts before failing, putting a false
failure below one in 10^27 while still reporting a pool that fails every
attempt.
The 400-round known-defect test could not flake -- it records failures
rather than propagating them -- but its loop had no wall-clock bound, so
a pathological run could hold the gate for an unbounded time. Bound it
with a time budget and a floor on attempts rather than reducing the
round count, which is deliberately matched to the documented failure
rate. Its doc comment also claimed it would start failing once the
defect is fixed, which is false; the assertion tolerates zero failures.
* Close the second CodeRabbit pass on the baselines (#639)
Resolve all five findings from the follow-up review. Four are
documentation contradictions that the lint gates cannot see, because
each is prose or a comment disagreeing with the code beside it rather
than a type or formatting error.
Close the pool on the acquisition error path
(`client_pool_benchmark_support/measurement/phase.rs`). A contended
burst that times out has already been detached: the timeout drops
`contended_round` and with it that function's `JoinHandle`s, which
detaches the contender tasks instead of aborting them. Those tasks hold
`PoolHandle` clones, so dropping the pool left its inner alive and
nothing shut it down, and the stalled contenders kept racing against
the next iteration's budget and the callers' bounded retries. The
acquisition result is now carried past `pool.close().await`, so
shutdown wakes the queued waiters with `disconnected` and ends them.
This cannot block: `shutdown` is synchronous, and the detached handles
keep the inner alive, so the following drop is an `Arc` decrement
rather than a teardown. The settled-gauge read moved into
`settled_observation` so it still runs only on the success path, where
quiescence is meaningful.
Correct the remaining four sites:
- `tests.rs` claimed the task-gauge delta is clean because the pool is
closed before `pool_once` returns. Closure happens *after* the
`alive_after` sample and cannot affect it; what makes the delta clean
is that `alive_before` is already taken past construction, so the
eagerly connected tasks cancel on both sides of the subtraction.
- `docs/runtime-ownership-baselines.md` said a default actor workload
leaves both queues under back-pressure. Each lane receives exactly
its capacity, so neither reaches it.
- `tests/runtime_ownership_benchmark_helpers.rs` pointed at the wrong
home for the V-O6 counter assertions. They live in
`verify_allocation_harness` in the alloc bench, not in
`wireframe_testing`'s unit tests, which can only observe that the
window opens and closes.
- The ExecPlan's decision log still recorded leaving the support
modules over 400 lines. A later discovery in the same document
falsified that, so the entry is marked superseded and the Progress
snapshot no longer quotes a headroom that subsequent commits changed.
The `module_max_lines` episode is worth stating plainly, because the
misreading was mine: the "whitaker exited 0 with no diagnostic" evidence
came from a run where an earlier step in the same target had already
failed, so the whitaker step never ran. A gate that aborts early
produces no evidence about the step after it.
* Record the close-out of the baseline work (#639)
Tick the ExecPlan's last two items and summarize what the second review
pass found. The plan is the hand-over document for this branch, so it
should say what a reader would otherwise have to reconstruct from the
commit log: that both review passes are clear, that all five gates are
green, and that one of the five findings was a real defect rather than a
wording fix.
The defect is worth naming here because it is the only finding on this
branch that changes behaviour: a failed acquisition left the pool open,
so a timed-out contended burst's detached contenders outlived their
iteration.
* Record the rebase and the module split in the plan (#639)
The ExecPlan's living sections did not yet describe the rebase onto
`origin/main`, the three conflicted steps, or the module extraction that
followed it.
Add Progress entries for the replay, the `make lint` failure the rebase
exposed, and the green gate run at the resulting head. Add two Decision
log entries: one for resolving the `module_max_lines` failure by
extracting the in-module tests to a sibling `scheduler_tests.rs` using
the `#[path = "..."]` pattern `main` introduced in #710, and one for
resolving both `scheduler.rs` conflicts to the target's structure on the
grounds that this branch's series contains an add-then-revert pair whose
net effect is a single added line.
Both entries record why the chosen resolution beat the alternatives, so
a later reader does not have to reconstruct the reasoning from the diff.
* Note that the rebase target changed the measured pool path (#639)
The rebase target's #535 reworked client-pool waiter state transitions
and, per its own commit message, deliberately stopped clearing the
servicing guard from the immediate-lease resolver. That is the mechanism
this branch identified as the cause of the contended-acquisition stall,
and #535 landed after this branch was cut, so every recorded reading
predates it.
Add the discovery to the ExecPlan so the distinction is not lost: the
timings and allocation counts remain true statements about the code state
they name, but the "known defects" narrative is a claim about live
behaviour and cannot be left standing unverified when the code under it
changed. #535 does not obviously close the spurious `disconnected`
window, since both the old and new forms still map a failed send to
`Err(disconnected)`, so the two modes need re-testing separately.
A bounded experiment re-measures the stall on the rebased tree; the
recorded defect material is reconciled to its verdict.
* Record the range-diff audit of the rebased series (#639)
The replay was accepted without auditing why four of sixteen commits
differed from their originals. Add the two decisions that disposition
each difference.
The spawn counter now lands in commit 1 rather than commit 5. The net
change to `scheduler.rs` is still exactly one added line, so nothing was
lost; the earlier placement is the better grouping, since commit 1 is
where the counter's scaffold is introduced.
The other three differences are target-driven renames and context drift:
`#710` renamed `src/test_helpers.rs` and `tests/prepared_app.rs` to
`mod.rs` forms, and the add-then-revert pair means commits 5 and 6 start
from a different `scheduler.rs` text on each side and cannot replay
identically. Each was inspected individually rather than accepted because
`range-diff` reported it.
* Spell out which pool defect the recorded panic names (#639)
The baselines document states that the allocation binary's first capture
aborted with `transport error: connection closed by peer`, leaving the
reader to work out whether that names the spurious `disconnected` error
or the stall. The two are easy to conflate and the mapping is verifiable,
so state it.
`ClientError::disconnected()` builds an `io::Error` reading "connection
closed by peer", and the `WireframeError::Io` arm renders it as
"transport error: {error}". The recorded panic is therefore the spurious
disconnect. A stall has a different signature entirely: no contender
error, only a burst that never completes.
This matters because the rebase target changed the code path that
produces the stall while leaving the disconnect path alone, so the two
modes now have to be reasoned about separately.
* Record the H1 verdict on the pool acquire-path stall (#639)
An experiment re-tested the recorded acquire-path stall against the
rebase target's #535, which reworked the waiter state transitions. The
prediction that the stall no longer reproduces was **not falsified**:
24,000 rounds per fairness policy at the recorded 8-contender shape
produced zero stalls, against roughly 60 expected per policy if the
recorded one-in-400 rate still held.
The spurious `disconnected` defect reproduced throughout, at 327 of
24,000 FIFO rounds and 38 of 24,000 round-robin rounds. The FIFO rate is
materially above the 1-in-400 previously recorded for that shape; the
experiment cannot separate a genuine change from machine and load
profile, so it is recorded as an open question rather than asserted.
Verify the mechanism structurally rather than from the commit message:
`stop_servicing`, the only writer that clears `is_servicing`, now has one
caller, `take_next_waiter_or_stop`, and the immediate-lease arm reaches
`next_waiter`, which never clears the flag.
State the scope of the result: a one-sided test cannot prove absence, and
the experiment covered the 8-contender shape only, so the 16-contender
shape is re-tested before any documentation claim is narrowed.
* Separate the two pool acquire-path defects in the recorded baselines
The rebase moved this branch onto `main`'s #535, which reworked the
client-pool waiter state transitions on exactly the path whose two
defects the baselines recorded as a single narrative. That narrative no
longer holds, so re-test it rather than leave a claim about live
behaviour standing on code that has since changed.
Re-tested at the recorded shape (pool size 4, 16 acquisitions, 8 rounds
per contender, 8 and 16 contenders), zero stalls occurred in 80,000
rounds — while the spurious `disconnected` reproduced throughout, at
roughly 1 round in 73 on FIFO at 8 contenders. The two defects are
therefore now recorded separately:
- The spurious `disconnected` is live and remains the dominant
observable failure at this scope.
- The stall no longer reproduces. It is kept as an observation, marked
pre-#535, because it is the evidence that motivated the rework and the
only measurement of its original frequency.
Frame the stall as "no longer reproducible" rather than "fixed". A
zero-count is one-sided: 0 in 16,000 bounds the per-round rate only at
roughly 1 in 5,300, so absence is not established. `CONTENDED_BURST_TIMEOUT`
is kept for the same reason it was written: it converts a hang into a
bounded error, and the disconnect defect still fails bursts, so removing
it would trade a bounded failure for an unbounded one.
The re-tested disconnect rates are recorded as observations with their
round counts, not as a revised rate. The 8-contender FIFO figure sits
materially above the pre-#535 measurement while the 16-contender figures
do not, and a genuine shift is not distinguishable from machine and load
profile in data taken on a loaded machine.
Correct three sites that claimed issue #647 "remains open on the
strength of" the disconnect defect. #647 is a rework proposal that does
not cite either defect as its motivation, so the corrected text states
only what is verifiable: both defects live on the path it proposes to
rework, and neither is its stated motivation.
Gates: `make check-fmt`, `make lint` (rustdoc, clippy, whitaker),
`make typecheck`, `make test` (1306 passed), `make markdownlint`,
`make nixie` — all green on a content-stable tree.
* Record the publication and the gate closure in the plan
Add the three Progress entries the close-out needs: the defect
reconciliation, the force push, and the gate-coverage gap the
documentation edits opened.
That last one is the entry worth keeping. The first `make check-fmt` and
the rustdoc step of `make lint` both ran before the final Markdown
rewrites and the module doc edit landed, and since a broken intra-doc
link is a rustdoc-only failure mode, the earlier green did not cover the
content that was actually committed. Re-running both gates on the frozen
commit with the tree hash verified identical before and after closed it.
A green gate is only evidence for the tree it read.
* Sync the plan's series count with the published branch
The publication entry named 23 commits, which was accurate when the push
happened and stale by the time the entry above it was written: the
reconciliation and close-out commits took the series to 24. Correct the
count, the head it names, and add the re-run range-diff audit so the
plan's account of the series matches what is on the remote.
* Correct the pool measurement boundaries and recapture baselines (#639)
Address the seven items raised in the PR #756 review: four measurement
defects and three completeness gaps.
Measurement defects:
- Build and warm the pool outside the timing and allocation windows, and
report construction cost separately. `PoolHarness` now owns that bracket
and is passed to the measuring code, so nothing inside a window can
construct anything. This removes a `pool_size`-scaling TCP handshake
term from every timed iteration.
- Scope the churn counters to the pool rather than the process. A global
`AtomicU64` gave a correct reading only when one pool was live, and the
benchmark binaries run several. `PoolTaskChurnObserver` now lives in the
pool's own shared state (`src/client/pool/churn.rs`), replacing
`scheduler_spawns.rs`. Serializing the assertion sites would not have
sufficed: the counters are read inside the measurement window.
- Sample contended bursts as a distribution rather than one elapsed value,
with failures counted and reported beside the successes. A failed burst
is never retried into a timing sample.
- Separate awaited back-pressure from the uncontended regime with an
observable counter, and add an explicit no-backpressure assertion.
Completeness gaps:
- Refresh the current pool baseline and identify its source revision,
preserving the original capture under `d28b54e`.
- Justify the startup workload sizes and add a larger case.
Also split `measurement/distribution.rs` into a directory module: whitaker's
`module_max_lines` lint rejected the file at 450 lines against the 400
limit. The split is at the type/tests seam and changes no public surface.
The recapture is confounded by machine state — the load averaged 10-15 on
24 threads from other work — so scopes whose code is byte-identical moved
1.3-2.1x between captures. No cross-capture timing delta is attributed to
the correction; the bracket's existence is instead proved by a
deterministic zero-iteration equality in the allocation binary.
Two claims in the documentation were found to describe experiments that
were never performed, and are corrected to cite the evidence actually
gathered: a validation "across 20 readings" of the bracket check, and a
pool allocation spread that mixed two measurement boundaries.
Refs #639
* Serialize the allocation-window tests to stop a process-global race
`benchmark_alloc_support::tests` has two tests that both assert on
`ALLOCATION_COUNTING_ENABLED`, a process-global flag. Under the default
multi-threaded harness one test's open window could be observed by the
other's closing assertion, failing about one run in five.
The race reproduced in isolation, which ruled out interference from the
rest of the suite and identified the pair as racing each other.
Mark both `#[serial]` and add `serial_test` as a dev-dependency of
`wireframe_testing`. `#[serial]` rather than a re-scope, because the flag
genuinely is a property of the process: the counting allocator is
installed once as `#[global_allocator]` and is not owned by any pool.
No new external crate enters the tree. `serial_test = "4.0.1"` is already a
workspace dev-dependency and already resolved in `Cargo.lock`; the only
lockfile change is one edge in `wireframe_testing`'s dependency list.
Refs #639
* Remove the duplicated group and runtime setup the baseline files share
CodeScene's advisory code-health gate failed on code duplication in both
files this branch adds, and both were real:
- `benches/runtime_ownership_baselines.rs` repeated the same
`sample_size`/`measurement_time` pair in all four `benchmark_*` groups.
Extracted to `baseline_group`, so one place decides the pinned sample
budget rather than four that can drift apart silently.
- `tests/runtime_ownership_benchmark_helpers.rs` had two `block_on`
flavours whose build-and-drive tails were identical apart from which
runtime they constructed. Extracted to `run_on`, which takes the builder.
Both are behaviour-preserving: the 36 helper tests pass, and all 39
Criterion benchmarks register and run under `--test`, with the contended
sampling observations unchanged.
Note that the gate was failing before this round of work as well, so this is
not a regression introduced by the measurement corrections; it is the
branch's own new code being brought up to the advisory rule.
Refs #639
* Register the Criterion groups from one path
Declare each of the four runtime-ownership benchmark scopes as a
`BenchScope` value — group name, workloads, throughput selector, and
measurement helper — and register all four through a single `run_scope`.
The four `benchmark_*` functions were near-identical: each opened a
group, looped its workloads, registered a `bench_function`, and called
`finish()`. They are now `actor_scope`, `push_scope`, `startup_scope`,
and `pool_scope`, returning values rather than driving Criterion.
CodeScene's advisory duplication gate flagged the module at 9.38. The
finding was real, not a false positive: every other bench file in the
repository scores 10.00, including `benches/codec_performance.rs`, whose
`benchmark_encode` and `benchmark_decode` use the same registration
idiom but number only two, below the three-function threshold. The
module now scores 10.00.
Two rejected alternatives are worth recording. Extracting only the
shared four-token syntax tail changes no logic — that tail is the
Criterion API's own punctuation — and left the score unchanged. A first
attempt at a `register_workloads` helper scored *worse* (9.09) and drew
a new "Excess Number of Function Arguments" finding from the six-argument
registration, so it was reverted rather than kept as churn.
`BenchLabel` exists because the four workload types are distinct and the
shared driver needs one name to reach each type's existing inherent
`label` through. The implementations only forward, so the labelling
stays owned by the type that knows its own fields.
The refactor is behaviour-preserving, which matters because the recorded
baseline is keyed by benchmark id. Group names (`actor/drain`,
`push/send`, `startup/connection`, `pool/acquisition`), registration
order, workload iteration order (the actor scope still nests
`ACTOR_CANCELLATIONS` outside `ACTOR_FRAME_COUNTS`), throughput
selectors, and measurement helpers are all unchanged, so the recorded
figures still key the same scenarios.
* Record the duplication fix and the stale-base attribution
Add a Decision log entry for registering the four Criterion groups from
one path, including the two rejected alternatives and the evidence that
the CodeScene finding was real rather than a false positive.
Add a Surprises entry for two reusable findings. The local `cs` CLI
reproduces CodeScene's delta analysis, which took the bench module from
9.38 to 10.00 in one local iteration instead of three push round-trips.
The companion caveat is that `cs delta <ref>` compares against the ref as
it is now: on a branch whose base has advanced it re-reports changes the
branch never made. `cs delta origin/main` named
`src/client/tracing_config.rs` as new duplication, but that file is not
in the pull request, is byte-identical from the merge base to `HEAD`, and
was last touched by an unrelated 2025 commit — the local `origin/main`
had simply moved two commits ahead. That is the same stale-base failure
mode the restacked-ancestor attribution rule already covers.
* Add the plan's outcomes section and restate the scope breach
The ExecPlan's preamble declares `Outcomes & retrospective` among its
mandatory living sections, but the section itself was never written. Add
it, covering the seven acceptance criteria, the four measurement defects
and three completeness gaps the review raised, the lessons worth carrying
into the #637 and #638 refactors, and the two residual items left
unrepaired on purpose.
Restate the scope-breach figure from its canonical source. The previous
entry counted a union of committed, working-tree, and untracked paths,
which double-counts a path present in more than one set and is not
reproducible from a clean tree; it read 57 paths and roughly 9,050 net
lines. The merge-base numstat gives 56 paths and 11,247 net lines (11,610
added, 363 deleted) against `ef858aa`. The verdict is unchanged — the
ceiling was already breached by a wide margin — but the figure now comes
from a command a reader can re-run.
* Apply mdtablefix's wrapping to the outcomes section
`make check-fmt` reflowed three paragraphs in the new outcomes section
(+13 -13). `mdtablefix` is the wrap authority for this repository, so its
choice of breaks governs; a hand-wrap that keeps every line within 80
columns still fails the gate when the breaks differ.
Pure rewrap: the prose is byte-identical once newlines are normalised,
and the file stays at 2484 lines.
* Record the outcomes section and the wrap fix in the plan
Note in `Progress` that the mandatory `Outcomes & retrospective` section
was added, that the scope-breach figure was corrected to its canonical
merge-base numstat, and that `check-fmt` then caught a hand-wrap in the
new prose which `make fmt` reflowed.
Record why no code gate was invalidated: every path changed since the
code-gated head is a `.md`.
* Correct a spelling violation CI caught in the progress entry
`make spelling` in CI rejected `normalised` and demanded `normalized`.
The estate dictionary is en-GB-oxendict but pins `normalize` as the
correct form, so the `-ise` spelling is a violation regardless of the
general rule.
The local gate run that preceded this commit was green because it ran
before the offending line existed; the line was written afterwards and
not re-gated. Re-ran `make spelling` over the corrected tree (exit 0) and
confirmed the gate still detects this class of error with a seeded
negative control, which reported exit 2 and the identical diagnostic.
* Record the spelling-gate lesson in the plan
The gate mandates `-ize` where the convention's name suggests `-ise`, and
a gate run only vouches for the tree it ran against. Also record that
the gate reads tracked files only, so an untracked probe file cannot
serve as a negative control.
* Rebuild the lockfile after the rebase onto the harden-lint stack
The rebase onto `harden-lint-connection-fragment` conflicted in
`Cargo.lock`: the target pins `thiserror` at 2.0.20 where this branch's
commits were resolved against 2.0.21. The base branch's lockfile was taken
verbatim, as instructed, and regenerated here.
`wireframe_testing` gained a `serial_test` dev-dependency in the
allocation-window work, so the rebuild adds that one edge to its package
stanza. Nothing else changes: the crate was already resolved and already a
workspace dev-dependency, and the target's `thiserror` pin survives the
rebuild.
Refs #639
* Record the restack onto the connection-fragment layer (#756)
Add the ExecPlan entries for the restack onto
`origin/harden-lint-connection-fragment` and the validation that followed,
keeping the plan usable as the continuing record of this work.
Progress records the boundary evidence (`ef858aa..84bfbf3` as the owned
series, proven rather than guessed), the per-commit `range-diff` result, the
two conflict resolutions whose obvious answer was wrong, and the post-restack
defect the merge introduced.
Surprises records that the ambient `weave` merge driver corrupts files while
exiting cleanly, with the four observed modes and the artefact scan that
follows from them.
Decision log records the per-conflict authority rule, and the reasoning for
treating the local `mdtablefix` failure as tool drift rather than repairing
it here.
Two environmental findings are recorded with evidence instead of being folded
into the branch: `make check-fmt`'s `mdtablefix` half is red only because this
host runs 0.6.1 against CI's pinned 0.6.0, and the spelling gate rewrites
`typos.toml` from a newer shared dictionary on a pristine tree. Neither is
this branch's to fix, and committing either would publish an unrelated change.
* Record the merge-hybrid audit in the plan (#639)
Add the Progress entries for the second compile repair and for the audit
of every file both sides of the merge changed.
The repair entry records the `E0271` on `tests/prepared_app/mod.rs`, the
ancestry probe that attributed it to a latent defect in the commit that
introduced the shared factory rather than to the rebase, and the clean
`cargo check` that followed.
The audit entry records how each of the ten dual-changed files resolves
to a deliberate union: the two prepared-application test files that both
sides restructured, the scheduler carrying the target's layout with this
branch's counters, and the clean unions in the manifests and module
facades. It also records the content-level check that `#722`'s
dead-letter fix survived the move into `dlq.rs`, which SHA ancestry
cannot answer once a rebase has replayed the commits, and the two stale
path references that belong to the base lag rather than to this branch.
Plan only; no behaviour changes.
* Record the churn-counter guard evidence in the plan (#639)
Note that the 23 client-pool benchmark-support tests pass, and why the two
capacity-wait assertions are worth running together: one holds
`capacity_waits == 0` when a single caller never contends, the other
requires `capacity_waits >= contenders` when a burst holds the pool
empty. Recording the counter in the `else` branch satisfies both, while
the position the merge produced satisfies only the second.
Plan only; no behaviour changes.
* Record the test and formatter gate evidence in the plan (#639)
Add the `make test` result for the rebased candidate: exit 0 across 83
test binaries, 1313 passed, 0 failed, with both compile repairs confirmed
by the absence of `E0271` and `E0382` and by all three `prepared_app`
binaries running.
Record why `make check-fmt` reports `docs/execplans/adopt-shared-
mutation-workflow.md` and why the file was deliberately left alone. The
flag is tool drift, not content: the local `mdtablefix` is 0.6.1 while CI
pins 0.6.0. Local 0.6.1 rejects the target's inherited copy and accepts
the copy on `origin/main` that `b479338` re-wrapped, and CI run
`35963755221` passed on the very commit whose tree carries the identical
blob. Reformatting would edit a file this branch never touched, in a tree
that will re-inherit it from main when the stack rebases.
* Record the typecheck, validator and CI-log evidence in the plan (#639)
Add the `make typecheck` result for the final head: exit 0, no errors,
with only the expected sccache-unreachable notices. Record the
standalone `mbake validate Makefile` pass that the branch's Makefile edit
requires.
Strengthen the support for leaving the `make check-fmt` flag alone by
quoting the CI job log rather than inferring from the run conclusion. The
log shows CI installing `MDTABLEFIX_VERSION: 0.6.0` and then reporting
`116 files left unchanged` with nothing flagged. Reconcile the counts to
show the two tools disagree on exactly one input, a stale copy of a file
neither this branch nor CI changed:
CI (0.6.0): 116 unchanged + 0 flagged
local (0.6.1): 117 unchanged + 1 flagged = 118
118 - 116 = 2 = the 2 Markdown files this branch adds
* Record the lint result and terminal gate verdict in the plan (#639)
Add the `make lint` result for the final head: exit 0 across all three
stages — doc (9m03s), clippy (13m06s) and Whitaker (17m32s) — with zero
warnings under `-D warnings`.
Add a terminal gate table so the acceptance state is readable without
parsing the per-gate Progress entries, and state explicitly which
revision each gate saw. The code gates did not all run at one revision,
which is sound only because `git diff --name-only 701f343 HEAD` returns
zero non-`.md` paths; recording the revisions is what makes that checkable
rather than asserted. `check-fmt` is listed as drift rather than waived.
* Record the rebase onto main in the plan (#639)
The branch was retargeted from the connection-fragment layer onto
`origin/main` at the user's request. Record the replay in the living
plan: boundary evidence, the eight conflicts and their resolutions, the
three commits dropped as empty, and the two `harden-lint` findings the
replay falsified.
The boundary moves to `OLD_BASE` `69d8fdc7`, `OLD_HEAD` `661fce8a` and
`TARGET` `3c50de2a`, with an owned series of 44 commits and 0 merges.
Mark the two superseded `harden-lint` claims in place rather than
deleting them. The claim that the previous target deliberately dropped
`from_app_serves_concurrent_tcp_connections_from_one_prepared_root` is
false: the test is absent at `OLD_BASE` but present at both `TARGET` and
the new head, in `tests/prepared_app_tcp.rs` line 91. The companion
claim that the `39221cf` bound mismatch was a latent defect in
`5f31996`, not a rebase artefact, is likewise superseded: it was an
artefact of the previous target's module layout and does not arise
against main.
Add two discoveries and two decision-log entries. A restack's recorded
resolutions are evidence about its target, so replaying onto a different
target invalidates them and they must be re-derived rather than
replayed. A lockfile conflict can be narrower than the lockfile diff:
taking main's side wholesale satisfied the letter of the lockfile rule
while shipping a lockfile that cannot satisfy `--locked`, because the
one edge this branch contributes fell outside the reported hunk.
Extend the retrospective with the replay's cost and outcome, and correct
the `test-doc` residual to say the base rather than the merge base, now
that `src/server/runtime/server.rs` is byte-identical between the target
and the branch head.
* Fix the spelling violation that failed the hosted gate (#639)
`build-test` run 38947673516 failed at the en-GB-oxendict spelling step
on the American spelling of "artefact" at this plan, line 2872. The line
was written in `4366ec1`, after the last local spelling run, so the fix
had never been gated.
The new progress entry also records the decisive evidence for the
pending lint question: on the same run the `Lint` step passed before the
spelling step failed, so rustdoc, Clippy and `whitaker --all` are green
on the published head in an environment that does not contend with this
host's compile admission. Two local attempts had been aborted by
admission refusals rather than by any diagnostic pointing at repository
source.
That verdict also settles the 400-line question. The four benchmark and
test files over 400 lines are not flagged by whitaker, so the stale
review finding citing the guide's 400-line figure describes guidance
rather than an enforced rule for those paths. The observation is
recorded with that distinction so a later reader does not mistake an
unenforced guideline for a passing gate.
`make spelling`, `make check-fmt` and `make markdownlint` all exit 0 on
the corrected tree; `typos.toml` is unchanged by the regeneration.
* Repair the review findings on the baselines PR (#639)
Address the 21 inline review comments on PR #756. Fifteen were implemented;
six were rebutted with evidence, and the rebuttals are recorded in the
ExecPlan's Decision log.
- The push capacity counter records a wait on the first `Pending` poll of
the send future rather than at the `Full` refusal. A refusal only says a
poll *would* park, so counting it reported suspensions that never
happened. The new `await_send_capacity` helper holds the poll loop, and
`record_first_pending` holds the predicate, keeping both under the
repository's nesting threshold without a lint suppression.
- `PoolHarness` no longer folds warm-up churn into its totals: the delta a
caller reads now covers only the phases that were bracketed.
- `measure_push` folds every round through `SendOutcome::accumulate`, so
all of its counters describe one run instead of the last round.
- The allocation bench registers stable Criterion IDs and publishes its
counts as `tracing` events. A count inside the ID made every run a new
benchmark with nothing to compare against and a fresh directory under
`target/criterion`. The pool replay returns its measured `Duration` so
construction and teardown sit outside the timed span, and the
zero-iteration guard runs on a dedicated current-thread runtime.
- The timing bench emits one push wait-counter event per workload from
`main`, after `final_summary`, rather than from `time_push` where
Criterion calls it on every iteration.
- Docs and tests were corrected in step: the broken `churn.rs` link, the
`scheduler_spawns` `0..=2n` bracket with its idle-scheduler precondition,
the `drained` and `awaited_capacity_waits` field docs, the allocation-ID
prose, the misleadingly named zero-spawn test, and a floor that now rests
on successful bursts with a non-vacuity assertion.
The four 400-line findings and two related CodeRabbit findings are rebutted
as guidance-only: whitaker's `module_max_lines` cannot see out-of-line
`mod` bodies or crate roots, and the hosted lint passed on the exact
published head with those file sizes.
* Split the over-limit benchmark and test files (#639)
Four files added by this branch exceeded the repository's 400-line limit
for code files. Split each along its natural seam, keeping every new file
well under the limit (the largest is 265 lines):
- `benches/runtime_ownership_baselines.rs` becomes a thin `main.rs` plus
`scenarios`, `driver`, and `observe`.
- `benches/runtime_ownership_baselines_alloc.rs` becomes a thin `main.rs`
plus `scopes`, `pool_bracket`, `probes`, and `reporting`.
- `tests/runtime_ownership_benchmark_helpers.rs` becomes a `mod.rs` root
plus `harness`, `startup`, `actor`, `push`, and `pool`.
- `client_pool_benchmark_support/tests.rs` becomes a `tests/mod.rs` root
plus `labels`, `uncontended`, `contended`, and `sampling`.
The split preserves behaviour. All 112 single-line string literals, 136
whitespace-normalised literals, 39 `assert!`, 36 `assert_eq!`, 12
`#[test]`, and 27 `#[case::` invocations are unchanged, as are the
Criterion group names, sample size, and measurement time. The counting
allocator is still installed at the allocation binary's crate root,
because `#[global_allocator]` is per-process.
`Cargo.toml` gains explicit `path` entries for the two `[[bench]]` targets
and a new `[[test]]` entry for the helpers crate, since Cargo does not
auto-discover `tests/<name>/mod.rs`.
Also fix the zero-iteration pool bracket guard, which review flagged as a
correctness bug: it used to run after `Runtime::new()` had started worker
threads, so a worker could allocate inside a window read as pool
construction. It is now split in two. The zero-iteration half builds and
drops its own current-thread runtime before any other runtime exists, and
asserts the bracketed count is zero while the unbracketed count is
positive; the measured half asserts the measured phase is non-zero
through the benchmark runtime.
* Fix the spelling gate failure in the plan (#639)
The hosted `build-test` job rejected `normalised` in the progress entry
added after the local Markdown gate had already run, so the local run
could not have caught it: the wording was written afterwards.
Write `normalized`, which is what the shared en-GB-oxendict policy
requires, and re-run `make spelling`, `make fmt`, `make check-fmt`, and
`make markdownlint` to confirm the file is clean. `typos.toml` is
unchanged.
---------
Co-authored-by: leynos <leynos@rohga>
What
The throttled dead-letter drop diagnostic now takes the drop count with
swap(0)and reports exactly what it took. It used to report the value its ownfetch_addreturned and thenstore(0), a separate load and store.Why
RFC 0002 recorded Loom finding the reset inexact with three producers. The producer that reached the threshold reported its own count, then stored zero after increments it never saw, so drops were cleared without being reported and the "N dropped" log over-reported under contention. The user ruled to fix it with an atomic swap-and-report.
A logger that loses the race to another producer's swap takes zero and does not log, rather than printing
dropped=0.The model
every_drop_is_reported_or_still_countedincrates/wireframe-loom/tests/push_dlq.rsruns three producers against a logging threshold of two, for both priorities, and asserts that reported plus remaining equals the three drops on every interleaving. Only a Loom build keeps the running total of reported drops it reads (dlq_reported, throughPushHandleProbe::dlq_reported_count); nothing is added outsidecfg(loom).make test-loomdropped, thenstore(0))swap(0))The existing models are unchanged and still pass. RFC 0002 records the fix in place of the open question, and the developers' guide describes it.
Gates
make test,make lint,make test-loom,make check-fmt,make markdownlint(withmake spelling) andmake test-workflow-contractspass locally. The hosted Advanced Tests run ofmake test-loomon this branch, run 36181691446 at e78d690, is green.Summary by Sourcery
Ensure concurrent dead-letter drop diagnostics report each failed drop exactly once.
Bug Fixes:
Enhancements:
Documentation:
Tests:
Chores: