Skip to content

fix(justdummies): make UseSeed scopes safe under out-of-order disposal - #323

Merged
Reefact merged 3 commits into
mainfrom
claude/justdummies-useseed-lifo
Jul 27, 2026
Merged

fix(justdummies): make UseSeed scopes safe under out-of-order disposal#323
Reefact merged 3 commits into
mainfrom
claude/justdummies-useseed-lifo

Conversation

@Reefact

@Reefact Reefact commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

Any.UseSeed(...) returns a disposable seed scope that is documented to nest — "disposing restores whatever was pinned before, and disposing twice is harmless." The implementation only honoured that for LIFO disposal: it kept a single AsyncLocal slot and each scope blindly restored its snapshot-at-open on Dispose. Any other disposal order either unpinned a still-open inner scope or resurrected a closed scope's seed as the ambient context for whatever ran next. This makes disposal order-independent, honouring the contract UseSeed already documents.

Type of change

  • Bug fix

Changes

  • JustDummies/RandomSource.cs — turn the ambient seed state into a linked stack of frames. Each frame remembers its parent; SeedScope.Dispose tombstones its own frame, and only the current top rewrites the AsyncLocal slot, walking past tombstoned ancestors to the nearest still-open frame (or null). LIFO disposal stays bit-for-bit identical; out-of-order disposal no longer strands a still-open inner scope (repro 2 in JustDummies: Any.UseSeed scopes are not safe under out-of-order disposal #321) or leaks a closed scope's seed to what runs next (repro 1); double-dispose remains a no-op.
  • JustDummies.UnitTests/AmbientSeedScopeTests.cs — three named regression tests (specific structural cases, ADR-0040), each confirmed red against the old blind-restore before the fix:
    • out-of-order disposal of an outer scope leaves the still-open inner scope's seed pinned;
    • out-of-order disposal does not leak a pinned seed to what runs next;
    • disposing a middle scope out of order leaves the top scope and its live ancestors intact.

Testing

  • dotnet build FirstClassErrors.sln — 0 errors (one transient file-lock copy-retry warning from a concurrent process, no code warning).
  • dotnet test FirstClassErrors.sln — not run in full locally; ran the affected suites instead: JustDummies.UnitTests (558), JustDummies.PropertyTests (220) and JustDummies.Xunit.UnitTests (14, the adapter that shares this scope machinery) — all green. The three new tests were confirmed red before the fix.
  • Mutation gate (build/stryker/justdummies.json --since) launched locally on the diff; CI runs it authoritatively on this PR.

Documentation

  • No documentation change required — the fix makes the code match the behaviour Any.UseSeed's XML docs already promise; no user-facing behaviour changes for correct (LIFO) callers.

Architecture decisions

  • No architectural decision in this pull request

Related issues

Closes #321


Generated by Claude Code

claude and others added 3 commits July 27, 2026 16:23
A seed scope kept only a snapshot of the ambient state at open time and
restored it blindly on Dispose, which is correct only for LIFO disposal.
Any other order either unpinned a still-open inner scope or resurrected a
closed scope's seed as the ambient context for whatever ran next.

Turn the ambient state into a linked stack of frames: Dispose tombstones
its own frame, and only the current top rewrites the AsyncLocal slot,
walking past tombstoned ancestors to the nearest live frame. LIFO stays
bit-for-bit identical; out-of-order disposal no longer strands or leaks;
double-dispose remains a no-op. This makes the code honour the contract
UseSeed already documents.

Refs: #321
Reconcile the out-of-order-disposal fix with ADR-0045 (guard public and
internal arguments against null), which landed on main via #322.

* AmbientState constructor: main added a null guard on `random`; this branch
  added a `parent` parameter to the same constructor — keep both.
* SeedScope: bring its new internal constructor under the same convention by
  guarding its non-nullable `frame` argument, matching the guard #322 applied
  to AmbientState.

The reflection-driven NullArgumentGuardConventionTests added by #322 passes
against the merged code.
@Reefact
Reefact merged commit 6759b94 into main Jul 27, 2026
29 checks passed
@Reefact
Reefact deleted the claude/justdummies-useseed-lifo branch July 27, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JustDummies: Any.UseSeed scopes are not safe under out-of-order disposal

2 participants