fix(runtime): give Pingora workers an 8 MiB stack and measure what uses it - #1245
Merged
Merged
Conversation
added 10 commits
August 28, 2026 14:58
…es it
main is aborting on CI's request-path smoke lane with
thread 'Pingora HTTP Proxy Service' has overflowed its stack
fatal runtime error: stack overflow, aborting
edf8b93 is the commit that tipped it, but the AI request path was
already using more than half a worker's 2 MiB stack before that landed,
so it is the straw and not the load.
Pingora never set thread_stack_size, so every worker ran on tokio's
2 MiB default. The fork now defaults to 8 MiB, the RLIMIT_STACK default
Linux gives a main thread, and exposes it as runtime_thread_stack_size;
this bumps the pinned rev and adds SB_WORKER_STACK_BYTES beside
SB_WORKER_THREADS. The cost on a 64-bit target is reserved address
space, not memory: roughly 264 MiB across 33 threads out of 128 TiB,
resident nothing, because a thread stack is committed page by page as it
is touched.
Raising the ceiling without a floor gauge trades a painful signal for no
signal, so the guards change shape. The two that measured size_of on a
future are gone: they reported 13.9% and 74.9% of budgets that between
them could see about 4% of what fills the stack, and both stayed green
through three overflows. A future's size is the state it holds between
polls; the stack is the chain of frames live during one.
server::stack_probe measures the second thing. Pingora's runtime records
each worker's stack base, the request path takes the address of a local
at its deepest point, and the difference is bytes in use.
the_ai_dispatch_path_stays_inside_its_stack_budget drives a whole
streamed AI request through request_phase::request_filter on a worker
sized to scripts/stack-budget-baseline.count, and checks two things: the
request completes, which covers every frame below the probes; and the
probe reported a non-zero depth, so the check cannot pass vacuously. The
budget only falls, enforced by check-stack-budget-ratchet.sh.
The probe costs a call, two thread-local reads, a subtract and an
untaken branch per request, and uses no unsafe.
The smoke lane sat behind a twelve-path trigger that neither breaking
commit touched, which is why main sat broken with nothing to say so. A
stack budget cannot be gated on a path list: a frame added in any crate
spends the same budget. It moves to its own workflow with no filter and
gains the budget test as its first step.
`--exact` matches the full module path, so `--exact the_ai_dispatch_path_stays_inside_its_stack_budget` selected nothing and the run reported "0 passed; 2857 filtered out" with exit code 0. A CI step that cannot select its own test is a green lane that checks nothing, which is the failure this branch exists to end.
Lifting `ai_request_context` to module scope meant dedenting it, and the dedent ran over the raw string literal too, flattening the YAML the fixture pipeline compiles from. The test failed loudly rather than quietly measuring a shorter path, which is the behavior worth having. Also: the budget is now an assertion on a production-sized worker rather than an overflow on a budget-sized one. An overflow aborts, and an abort has no number and no message, which is the wrong failure for the check that exists to explain this class of bug. The abort stays underneath for a path that outgrows the whole production stack. Measured on this branch: 1,600,928 bytes of 4,194,304.
pingora-runtime's worker_stack::here() now returns a StackMark rather than a bare usize. In a debug build the mark carries the ThreadId that made it and used_here asserts the two agree, so comparing a mark against a stack it does not belong to fails loudly where tests run; a release build carries neither the field nor the check, so the probe still costs a call, two thread-local reads, a subtract and an untaken branch. The module doc also stops promising more than the fork delivers. A work-stealing worker, which is the flavor sbproxy runs, is marked from tokio's on_thread_start callback rather than from the top of the thread, so the number under-reports by roughly one closure frame: tens of bytes against a budget in the megabytes. The fork documents which thread flavors are marked from where; this points at it. Lockfile: the pinned rev only, 11 lines, one per patched crate.
…step Three things, all about the same confusion: the pinned fork declares 0.8.0 and is named for it, and neither is a release we track. Cargo.toml's comment above [patch.crates-io] now says what we run. The tree carries 167 upstream commits the 0.8.0 tag does not, plus our seven, so the version is an API generation and not something to match an advisory against. The concrete numbers come from scripts/divergence.sh in the fork, which its CI also prints on every PR: base: 0046038 (2026-08-07) ahead: 7 commits of ours behind: 23 upstream commits files: 20 differ, 14 outside .github/ CLAUDE.md and AGENTS.md both told the release procedure to rebase sbproxy-0.8.0 onto "the target upstream tag". That is the step somebody actually follows at a release, and following it would move the fork onto a different line: Cloudflare cuts releases on a release branch, so 0.8.1 is not an ancestor of main. It holds 8 commits main lacks while main holds 190 it lacks. It would also strand the current_handle() fix we have upstreamed at cloudflare/pingora#982, which lands on main. Both files now say to rebase onto a newer upstream main, give the release-branch reason in a sentence so nobody re-derives it, and point at scripts/divergence.sh for the before-and-after. The lockfile step gained the rule this branch followed: diff it and revert anything that is not the pingora-* rev bumps. Lockfile re-pinned to 2d9dc2b, the merged fork tip, which is the stack work plus the divergence report. Eleven rev lines and nothing else.
Independent review found that the guard this branch adds could not fail, which is the defect the branch exists to remove. Four of the findings are that same shape. C1. check-stack-budget-ratchet.sh resolved its base with `git merge-base HEAD origin/main` and ran in ci.yml's lint job, whose checkout has no fetch-depth. On a pull_request event neither origin/main nor main resolves, so it printed a note and exited 0: it ran on every pull request and checked nothing. The three sibling ratchets in that lane count source sites, which a depth-1 checkout serves, which is why none of them exposed it. It now fails closed when no base resolves and takes STACK_BUDGET_BASE_REF the way the changelog guard next door does, and the step moved to the guards job, whose checkout is full depth for exactly this reason. Its --self-test is wired there too. M1. the_ai_dispatch_path_stays_inside_its_stack_budget built its worker with .thread_stack_size(STACK_BUDGET), so the thread's stack was the budget and `used <= budget` could not fail without the process having already aborted, while the doc claimed the opposite. The abort also sat at half the production stack, so it could abort on a path production serves. The worker is now sized to DEFAULT_THREAD_STACK_SIZE, the budget is the assertion, and the two tiers are named in the doc and printed in the log line. M2. The budget was half the stack, 4,194,304 against 1,600,952 measured: 2.6x of slack, when the commit that broke main moved the depth 31,152 bytes. Now set from the measurement, pending the Linux number this PR's own smoke lane prints. M4. ServerConf::validate() runs only from from_yaml and sbproxy passes a struct literal, so the sub-page refusal documented in docs/manual.md never ran: SB_WORKER_STACK_BYTES=8 started and aborted on the first request. resolve_worker_stack_bytes refuses it with a warning and falls back to the default, and the manual says what actually happens. M5 gives that hunk its first test. M3 adds the key to the env-var table. M6 corrects five comments that still called the stack 2 MiB, one of them inside an assertion message. m1 restores the buffered path's stack coverage, which deleting the non-streaming test lost along with the only reach to the probe in relay_ai_response_with_cache. m3 makes the smoke lane count the tests that ran, because cargo test exits 0 on a filter that matches nothing, which is the third appearance of that defect in this change. m4 scopes the workflow trigger. m5 retypes the fragment. Measured after: streaming 1,600,952 and buffered 1,468,456 of a 2,621,440 budget on an 8,388,608 stack.
Reverted in the next commit. Runs the stack-budget ratchet in ci.yml's lint job, whose checkout has no fetch-depth, which is the exact environment where it used to resolve no base and exit 0. With the fail-closed fix it has to turn this job red. The guards job runs the same script against a full-depth checkout in the same workflow, so one run shows both halves.
The proof is recorded. Run 33220527341 on PR #1245: lint job, checkout fetch-depth: 1 cannot resolve a base to ratchet against job FAILED guards job, checkout fetch-depth: 0 check-stack-budget-ratchet self-test: ok stack budget: 2621440 bytes (new on this branch) job PASSED That is the exact environment where the ratchet used to print a note and exit 0, now failing closed, beside the job where it actually runs. This reverts the temporary step; the real one stays in guards.
Moving the request-path smoke job to its own unfiltered workflow left context-compression-eval.yml with one job, and readme_and_workflow_cover_reproducibility_and_external_data_boundaries asserted `timeout-minutes:` appeared at least twice. The comment above that line already said to pin the property and not the number, and the line did the opposite. It now counts `runs-on:`, which appears once per job, and requires a timeout for each. Verified against both workflows: 1 job, 1 timeout, each. Not run locally. This harness needs the 1.98 pin and the only toolchains on this machine are Homebrew 1.95 and rustup 1.90, so CI is the check.
2,359,296 = ceil(1_604_072 / 256 KiB) * 256 KiB + 512 KiB, where 1,604,072 is STACK_HIGH_WATER_BYTES printed by this PR's own production request-path smoke lane on Linux. Buffered measured 1,471,496 on the same run. One thing the two numbers settle, because I had assumed otherwise: the same fixture measures 1,600,952 on aarch64 macOS, 3,120 bytes less than Linux. Linux debug frames are not meaningfully larger for this path. The distance between 1.6 MiB here and the 2 MiB stack that actually overflowed in CI is the configuration that lane runs, compression and a CEL policy plane and guardrails, not the target.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
mainis aborting.edf8b937(#1235) is the commit that tipped it, but the AI request path wasalready using more than half a Pingora worker's 2 MiB stack before that landed, so #1235 is the
straw and not the load. This raises the stack, and then measures what actually sits on it so the
next straw is visible before it lands rather than after.
That is the whole diagnostic. It does not unwind, it carries no backtrace, and it names no frame.
Part 1: an 8 MiB worker stack
Tokio gives a worker 2 MiB. Pingora never set
thread_stack_size, so that default applied toevery worker sbproxy runs. The fix is in our fork
(soapbucket/pingora#2); this PR bumps the pinned
rev to pick it up and adds
SB_WORKER_STACK_BYTESbeside the existingSB_WORKER_THREADS.The number, and the arithmetic
Measured:
Half the old budget was already spent on a stripped-down path, and the real one exceeds all of
it. Both of the old probes broke at the same boundary, which says the cost belongs to the shared
dispatch chain rather than to any one branch of it.
Shrinking the path is not an available answer. Two future extractions on this codebase moved 528
bytes against a 2,097,152-byte stack: 0.025 percent, for an afternoon. Recovering a real margin
that way needs about two thousand of them.
4 MiB is not an answer either. It puts us past half again on day one, which is the position that
produced this outage, and buys one release.
8 MiB is the
RLIMIT_STACKdefault Linux gives a process's main thread, so it is the size theplatform already treats as normal for a thread running arbitrary code, and it is four times
tokio's worker default.
What it costs, for the worker count sbproxy actually runs (
threads = available_parallelism(), one proxy service, plus pingora's 1-thread server runtime, so roughly17 to 33 threads on a 16-vCPU host):
That is reserved address space, out of the 128 TiB a 64-bit process has, and it is 0.0002 percent
of it. Resident memory does not move: a thread stack is an anonymous mapping the kernel commits
page by page on first touch, so RSS tracks the depth a request reaches, which this does not
change.
The reason it stops at 8 and not 16:
thread_stack_sizereaches the blocking pool too, whosetokio default cap is 512 threads. Worst-case reservation per runtime is 4 GiB at 8 MiB and 8 GiB
at 16 MiB. Both are
VmSizeand notVmRSS, but 8 GiB is a number an operator stops and asksabout, and some container runtimes cap address space outright.
If 8 turns out to be wrong,
SB_WORKER_STACK_BYTESand pingora'sruntime_thread_stack_sizeareboth one value away, with no fork change.
Part 2: measuring what uses the stack
Raising the ceiling without a floor gauge trades a painful signal for no signal.
Why the old guards could not see this
They measured
core::mem::size_ofon a single future. A future's size is the state it holdsbetween polls. The stack is the whole chain of frames live during one, and these futures sit
at the top of that chain rather than containing it. On the tree that was aborting, they reported:
request_filter_fits_its_share_of_a_pingora_worker_stackthe_dispatch_future_has_not_grownRoughly 96 percent of what fills the stack was outside what either could see, and both stayed
green through three overflows.
The numbers
Measured with the new probe, macOS arm64, dev profile, driving
request_filteron a realPingora worker:
c84feb1c(#1236, last green)edf8b937(#1235, brokemain)The two adjacent commits measure identically, and that is worth stating plainly rather than
hiding.
edf8b937adds oneRequestContextfield on this path, about 64 bytes, which landsin existing debug frame slack; its other 590 lines sit behind
authentication: type: hmac_auth,which this fixture does not configure. The measurement does resolve growth (+31,152 bytes
between
edf8b937and16b94ea2), it just has nothing to resolve across that particular pair.Which is the real finding: at 74.8 percent of the old budget before #1235, the commit that
tips it over does not have to be large, and looking for it in the diff was never going to work.
Read the number as the depth of the configuration it drives, which is a floor. The e2e origin
that actually aborts (
compression-cel.localhost) wires compression, a CEL policy plane andguardrails. This check holds the floor with a number on every PR; the smoke lane, now
unfiltered, holds the ceiling with a real binary.
Red-first
Since the calibration came back null, here is the proof the number moves, aimed at the exact
function #1235 grew. One local added to
request_phase::request_filter, same commit, samefixture:
c84feb1cc84feb1c+let red_first_pad = [0u8; 96 * 1024]Roughly twice the local, which is expected:
request_filteris anasync fn, so a local heldacross an
.awaitlives in the future's state and in the frame the future is polled in, and adebug build keeps both.
The budget, set from the Linux measurement
2,359,296, which is
ceil(1_604_072 / 256 KiB) * 256 KiB + 512 KiB, where 1,604,072 is theSTACK_HIGH_WATER_BYTESthis PR's ownproduction request-path smokelane printed on Linux.An earlier draft of this used half the worker stack, 4,194,304, which was 2.6x of slack against
a measured 1.6 MiB and would not have noticed fifty commits the size of the one that broke
main. It measured the right quantity and held it to nothing.The delta above also corrects a claim I made repeatedly while building this: that Linux debug
frames are much larger and macOS numbers only rank candidates. For this path they agree to
within 0.2 percent. Linux is not why the smoke lane overflowed 2 MiB while this fixture sits at
1.6 MiB; the configuration that lane runs is, and the fixture wires none of it. The margin for
that lives in the 8 MiB stack, not in this budget.
What replaces them
pingora_runtime::worker_stackrecords the address of a local in each worker thread's entryframe.
crates/sbproxy-core/src/server/stack_probe.rstakes the address of a local at thedeepest point of the request path and reports the difference: bytes actually in use.
the_ai_dispatch_path_stays_inside_its_stack_budgetdrives a whole streamed AI request throughrequest_phase::request_filter, on a real Pingora worker whose stack is set to exactly thebudget in
scripts/stack-budget-baseline.count. Entering atrequest_filterrather than athandle_ai_proxyis the point:request_filterstays live while everything it dispatches intoruns above it, and #1235 grew
request_filter,proxy_http,context,trust_tierandhmac_authwhile touching no AI file at all. A measurement that started lower would not haveseen a byte of it.
Two things are checked, and the second matters as much as the first:
this test and nothing else, and that covers the entire chain including every frame below the
probes: reqwest, hyper, serde, the TLS stack.
measurement ever stopped working, which is exactly the failure mode this whole change exists
to end.
Cost on the request path
Release builds: one call, two thread-local reads, a subtraction, a comparison, and a branch
that is not taken once a worker has settled. The process-wide maximum is written only when a
thread beats its own record, so the steady state touches no shared memory and no lock. Probes
fire once per request, not once per chunk: a loop body's frame is one size on every iteration,
so a second measurement learns nothing.
Debug builds cost more than that, and my earlier claim did not say so. A
StackMarkcarriesthe
ThreadIdthat made it soused_herecan refuse a cross-thread comparison, which meanseach probe also makes two
std::thread::current().id()calls, andthread::current()clones anArc, so that is two atomic increments and two decrements per probe. Two probes per request.It is gone entirely under
cfg(not(debug_assertions)), so production is unaffected, but "costseffectively nothing" was only ever true of the build that is not the one CI runs.
No
unsafe. Taking the address of a local, casting a reference to a raw pointer and casting thatpointer to an integer are all safe; only a dereference would not be, and nothing here
dereferences. Neither constraint was traded.
What it cannot see, stated plainly: the reported number is the depth at the probes, not the
deepest point reached, so read it as a floor and a trend. The budget in (1) is the part that
covers everything.
The operator surface, and what it cannot see
publishwarns once per process, when a request first passes three quarters of the worker stack,with the bytes used and the stack size. Nothing has failed at that point, but a stack overflow
leaves no diagnosis, so this is the last chance to say anything at all. Documented in
docs/manual.mdbeside the knob that answers it.It can only fire for AI traffic. Both probes are on the AI dispatch path: one in the
streaming relay's loop, one at the buffered relay's terminal write. A deployment that proxies
plain HTTP and never dispatches to a provider has no probe on its request path, so its
high-water stays zero and the warning cannot fire no matter how deep that path goes. That is the
right first place to instrument, because the AI path is the deep one and the one that aborted,
but the warning is not a general stack-safety signal for the proxy and should not be read as
one.
Part 3: the lane that did not run
The smoke lane lived inside
context-compression-eval.ymlbehind a twelve-path trigger. Neithercommit since it last passed touched a trigger path, which is why
mainsat broken with nothingto say so. The lane did not go red; it did not run.
A stack budget cannot be gated on a path list. The stack is consumed by the whole call chain, so
a frame added in a crate that trigger list has never heard of spends exactly as much as one added
in
ai_dispatch.rs. Any filter narrower than "every pull request" is narrower than the thingbeing guarded.
request-path-smoke.ymlis that job moved into its own workflow with no path filter, keeping thecheck name
production request-path smoke. It gains a first step that runs the stack budget testwith
--nocapture, so the Linux number is printed in the lane that broke, ahead of the three e2eruns it would otherwise fail behind.
Ratchet
scripts/check-stack-budget-ratchet.sh, the fourth of the family and the same shape as the otherthree: one integer in one file, compared against the merge base, allowed to fall and not to rise.
It is wired into
check.sh,check-fast.sh, and theci.ymltest lane.The budget is half of
DEFAULT_THREAD_STACK_SIZE. The invariant is "the request path fits in halfa worker's stack", which stays meaningful if the worker stack is ever changed again and leaves the
other half for the TLS, guardrail, translator and holdback frames the fixture does not wire.
post-merge-rederive.shprints it rather than recomputing it, with the merge rule written down:this number has no scanner behind it, so on a merge the correct value is the lower of the two
sides, never their maximum.
What the fork review changed here
Fork PR soapbucket/pingora#2 came back MERGE with findings; the one that reaches this repo is
M1. Pingora's offload pools spawned plain std threads with neither the configured stack nor
a stack base, and sbproxy runs two of them:
upstream_connect_offload_threadpools: Some(2)atcrates/sbproxy-core/src/server/lifecycle.rs, plus the downstream TLS handshake pool insidepingora's rustls listener. So upstream DNS and
connect, and the TLS handshake, were still onthe platform default stack while the workers had four times as much, and the instrument could
not see either of them.
The fork now carries a process-wide default that
Server::runsets fromruntime_thread_stack_sizebefore anything spawns, and the offload pools read it. sbproxy needsno change for this: leaving
runtime_thread_stack_sizeunset resolves to 8 MiB for workers,blocking pools and offload pools alike, and
SB_WORKER_STACK_BYTESmoves all of them together.The one API change that reaches this diff:
worker_stack::here()returns aStackMarkinsteadof a bare
usize, carrying in debug builds theThreadIdthat made it soused_herecan refusea cross-thread comparison. Release builds carry neither the field nor the check, so the hot-path
cost is unchanged.
Fork provenance, and a wrong instruction in the release procedure
Riding with the lockfile re-pin because it is the same subject.
The comment above
[patch.crates-io]now records what the fork actually is. The crates declare0.8.0and the branch is named for it, and neither is a release we track: the tree carries 167upstream commits the
0.8.0tag does not, plus our seven. Read the version as an API generation,not as something to match an advisory against. The concrete numbers come from
scripts/divergence.shin the fork, which its CI prints on every PR:CLAUDE.mdandAGENTS.mdboth told the release procedure to rebasesbproxy-0.8.0onto "thetarget upstream tag". That is the step somebody actually follows at a release, and following it
would move the fork onto a different line. Cloudflare cuts releases on a release branch, so
0.8.1is not an ancestor ofmain: it holds 8 commitsmainlacks whilemainholds 190 itlacks (
git rev-list --count origin/main..0.8.1and the reverse). It would also strand thecurrent_handle()fix upstreamed at cloudflare/pingora#982, which lands onmain.Both files now say to rebase onto a newer upstream
main, give the release-branch reason in asentence so nobody re-derives it, point at
scripts/divergence.sh, and carry the lockfile rulethis branch followed. Fixing only
CLAUDE.mdwould have left the identical wrong instruction inthe file half the tooling reads.
Lockfile
branch = "sbproxy-0.8.0"is unchanged; only the pinned rev moves, to2d9dc2b, the mergedfork tip. The entire diff against
main:Eleven rev bumps, one per patched crate, and one dependency edge for
worker_stack. Nothingelse: no
windows-sys, noitertools, no version changes anywhere. Produced withcargo metadata --offlinerather thancargo update -p, and read line by line rather than by stat,because a
cargo update -pon this workspace has silently downgraded unrelated dependenciesbefore.
Adversarial review
Reviewer: independent review agent against
.github/code-review-rubric.md, verdict NOT READY, report atdocs/sbproxy/loop-2026-08-28/review-pingora-stack-sbproxy.mdFindings: 1 Blocker, 6 Major, 5 Minor
Verification: both stack budget tests re-run with
--nocaptureand their numbers read out of the log (streaming 1,600,952, buffered 1,468,456, budget 2,621,440, worker stack 8,388,608); the ratchet's fail-closed paths exercised in a scratch repository with nomainand noorigin; the same script run in CI in the shallow-checkout job that used to skip it, to watch it go red there rather than only on a laptop; fullscripts/check.shre-run after every fix.scripts/check-stack-budget-ratchet.sh:150- resolved its base withgit merge-base HEAD origin/mainwhile wired into ci.yml'slintjob, whose checkout has nofetch-depth, so on apull_requestevent no base resolved and it exited 0: it ran on every pull request and checked nothing. Fixed ina70136c87: fails closed, takesSTACK_BUDGET_BASE_REFthe way the changelog guard does, and moved to theguardsjob whose checkout is full depth.crates/sbproxy-core/src/server/ai_dispatch.rs:23445- the worker was built.thread_stack_size(STACK_BUDGET), soused <= budgetcould not fail without the process having already aborted, and the abort sat at half the production stack. Fixed ina70136c87: the worker is sized toDEFAULT_THREAD_STACK_SIZEand the budget is the assertion.scripts/stack-budget-baseline.count:1- 4,194,304 against 1,600,952 measured is 2.6x of slack, and the commit that brokemainmoved the depth 31,152 bytes. Fixed ina70136c87: set from the measurement.docs/manual.md:2355-SB_WORKER_STACK_BYTESmissing from the environment-variable table, and the count still said fourteen. Fixed ina70136c87.crates/sbproxy-core/src/server/lifecycle.rs:3625- the sub-page refusal documented indocs/manual.mdnever ran, becauseServerConf::validate()is called only fromfrom_yamland sbproxy passes a struct literal, soSB_WORKER_STACK_BYTES=8started and aborted on the first request. Fixed ina70136c87:resolve_worker_stack_bytesrefuses it and the manual says what actually happens.crates/sbproxy-core/src/server/lifecycle.rs:3612- the whole hunk that reads the knob had no test and could be reverted with every test still passing. Fixed ina70136c87:worker_stack_env_tests, four cases.crates/sbproxy-core/src/server/request_phase.rs:1035- five comments still described the worker stack as 2 MiB, one of them inside an assertion message an operator reads. Fixed ina70136c87; two further references left at 2 MiB because they are historical.crates/sbproxy-core/src/server/ai_dispatch.rs:16446- deletinga_non_streaming_dispatch_fits_a_pingora_worker_stackleft the buffered relay with no stack coverage and its probe unreached by any test. Fixed ina70136c87:the_buffered_ai_dispatch_path_stays_inside_its_stack_budget..github/workflows/ci.yml:358- the ratchet's--self-testwas never invoked while its three siblings all run. Fixed ina70136c87..github/workflows/request-path-smoke.yml:69- filtered by substring, andcargo testexits 0 when a filter matches nothing, so a renamed test would make the step pass while checking nothing. Fixed ina70136c87: the step counts the tests that ran and fails if it is not exactly two..github/workflows/request-path-smoke.yml:24- unscopedpull_request:trigger. Fixed ina70136c87: scoped tobranches: [main].docs/.changes/20260828-pingora-worker-threads-now-get-an.json:1- fragment typedfixedwhile introducing a knob. Fixed ina70136c87: retypedchanged.