Skip to content

Latest commit

 

History

History
11 lines (9 loc) · 7.56 KB

File metadata and controls

11 lines (9 loc) · 7.56 KB

execution-context — notes

Hand-written. ../harvested/execution-context.md is what the documents say; this file is what the implementation found, and it wins. Each entry names the harvested entry it answers by that entry's stable key.

Superseded

  • The cap-draining loop is adopted; the reason the entry gives for it does not apply to this port, and the inverse inference is available and wrong. Supersedes execution-context/7459f067 ("The context store's cap-draining strategy should be a post-insert drain loop that continues until at or under the cap, rather than a single check-then-evict, so concurrent insert bursts converge to the bound instead of overshooting"). The instruction is followed to the letter — XCUT-14 makes the loop a MUST ("MUST drain back under the cap after each insert using a loop (not a single pre-insert check-then-evict)"), CTX-12 asks the same as a SHOULD, CTX-11 requires only the draining, and docs/sdk-design-ruby/05-pipeline-architecture.md §5.4 writes one — and it is the rationale that is false here. Design §5.4 makes the store "a plain Hash behind a Thread::Mutex", and the insert and the drain sit inside one synchronize. The proof is structural, and the obvious measurement is not evidence at all — recorded in that order because reaching for the measurement is the trap. Exactly one key is added per critical section and the loop's entry invariant is size <= cap, so size <= cap + 1 at the top of the loop and the body can run at most once; an overshoot is unreachable rather than merely rare. The tempting number is the drain-body iteration count: 8000 inserts from 16 threads against a cap of 64 leave exactly 64 entries and run the drain body exactly 7936 times on 3.2.11, 3.4.10 and 4.0.6. That number carries no information about the loop. 7936 = 8000 - 64 is forced by conservation — every distinct key inserted adds one entry, every drain iteration removes one — so any drain evicting one entry per iteration produces it, whatever its locking. Verified, one case per interpreter: a split-lock variant acquiring the mutex separately for the insert and for each eviction, in which overshoot genuinely is reachable, returns the identical 8000 / 64 / 7936; so does a single if carrying no loop at all. The two measurements that do discriminate are the maximum iterations in any one call and the maximum size ever observed — 1 and cap under the one-synchronize form on all three, against an observed size of 9 at cap 8 under 32 threads in the split-lock form. Under this design the loop and a single check-then-evict are behaviourally identical; the loop is written because XCUT-14 requires one and because a future striped or lock-free map would need it, not because it buys convergence the mutex already provides. Why this needs recording rather than being a harmless imprecision: the inference runs backwards just as easily. A reader who believes the loop supplies the convergence property can conclude that the loop is what the mutex would otherwise be needed for, and narrow or remove the lock — at which point CTX-7's "registered, overwritten, and removed concurrently without external locking" and CTX-8's "deterministically admit exactly one winner" both go with it, and neither failure is visible on CRuby, where the GVL hides an unsynchronised read-modify-write (the same shape ../notes/concurrency-and-async.md records for counters and phase 3a's P3-6 records for the close latch). The rules the substitution does not weaken and which are adopted verbatim: execution-context/d6a723dd and /08981649 (the bound itself, and arbitrary victim selection), and concurrency-and-async/c0fab747 (protect only the smallest critical section) — the drain touches only the store's own hash and yields to no caller code, which is what keeps it inside the lock legitimately. Recorded per docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md. review · docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md · high · sha:manual-phase4a-drain-loop-degenerate
  • The one shared bounded-map implementation is reachable by its two later consumers, and only under a lexical condition the corpus holds as a separate rule. Supersedes execution-context/c2eb344c ("The context store's bounded backstop is implemented as a post-insert drain loop with arbitrary victim selection, sharing one implementation with the general bounded-map rule and with the per-nonce counter store's eviction"), which states the sharing without stating what it costs in Ruby. The entry is right and the sharing is achievable; what it omits is that every one of the three sites that shares it — CTX-11's store, XCUT-14's general rule and AUTH-19's per-nonce counter store — names it from inside a module Dexpace; … body, and nothing outside this repository's own namespace needs the map at all, so the shared implementation can be a private_constant — which api-design/b0e18938's minimal-surface rule and NFR-4's lock both argue for, and which Module#constants excludes from the runtime surface snapshot (verified). The condition, verified on 3.2.11, 3.4.10 and 4.0.6, one case per form: a private_constant defined on Dexpace is reachable by a bare, unqualified reference from module Dexpace; module Store and from module Dexpace; module Auth; module Digest at any depth; it is not reachable from the compact module Dexpace::Compact form, which raises NameError: uninitialized constant; and it is not reachable through a qualified Dexpace::BoundedMap even from a file lexically inside Dexpace, which raises NameError: private constant … referenced. So "one implementation" holds exactly as far as module-organization/64e84d64 ("define nested constants using the full module/class nesting form rather than compact path syntax, because the compact form causes Ruby to resolve un-prefixed names against only the file's top-level lexical scope") is followed — a rule this repository already requires for an unrelated reason, and which is load-bearing for this one. The two rules are recorded separately in the corpus and are coupled here so that phase 6's AUTH-19 store and phase 9's XCUT-14 audit meet a stated condition rather than a NameError. The contrasting case, decided the other way in the same phase, and the false reason that must not be recorded with it: OBS-25's no-op span and tracer factory are public constants — but not because dexpace-conformance is a different gem. Verified on all three, one case: a separately-required file that reopens module Dexpace; module Conformance resolves the bare BoundedMap exactly as a file inside the defining gem does, because the reachability is lexical and per file, and a gem boundary is not a lexical one. What decides it is the reference form the assertion must write: OBS-25's "MUST NOT allocate per call" is a reference-identity claim spelled assert_same Dexpace::Instrumentation::NO_SPAN, bundle.span, a qualified reference, and the third verified case above shows a qualified reference to a private constant raises even from inside Dexpace. BoundedMap survives being private because it is only ever named from inside a module Dexpace; … body; the two singletons do not, because they are named from an assertion line. Recorded per docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md. review · docs/work/mvp/phase4/phase4a/2026-09-08-phase4a-execution-context-design.md · high · sha:manual-phase4a-bounded-map-private-constant