Hold a rewrite recording per flow instead of per thread - #863
Open
Rafael-SOWNet wants to merge 1 commit into
Open
Hold a rewrite recording per flow instead of per thread#863Rafael-SOWNet wants to merge 1 commit into
Rafael-SOWNet wants to merge 1 commit into
Conversation
Rafael-SOWNet
force-pushed
the
fix/rewriterecording-asynclocal
branch
from
August 10, 2026 02:22
c402607 to
48160f2
Compare
Closes #859. RewriteRecording kept its ambient scope in a [ThreadStatic] field and documented the consequence rather than fixing it -- "A synchronous scope, and it has to be. Do not await inside one." It no longer has to be. The scope is an AsyncLocal, as MathS.Settings now is and as the cancellation token in MathS.Multithreading already was. The trap #857 flagged applies here and is why this is more than a field swap. An AsyncLocal flows the reference, so once the pointer reaches child tasks two of them can report to one recording at once, and the steps were accumulating into a List. That is a torn write, not a merged list. The store is a ConcurrentQueue, `closed` is volatile since it is now read from flows other than the one that set it, and Steps copies out rather than handing back a live view. One existing test encoded the old semantics and is rewritten rather than deleted: ARecordingOnOneThreadDoesNotSeeAnother started a thread inside an open recording and asserted its work was not collected. ExecutionContext flows to a manually started thread, so that work is now collected -- which is the point of the change, not a regression of it. It becomes WorkStartedUnderARecordingIsCollectedWhereverItRuns, and the isolation it was really reaching for is covered by SiblingRecordingsDoNotSeeEachOther. RewriteAllocationTest still passes untouched, so being off is still free: one ambient read per rule set, nothing allocated, which is what #746 asks of every layer above the tree. Verified: 6064 C# tests and 130 F# tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet
force-pushed
the
fix/rewriterecording-asynclocal
branch
from
August 10, 2026 02:48
48160f2 to
5a5ef43
Compare
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.
Closes #859.
RewriteRecordingheld its ambient scope in a[ThreadStatic]field, and documented the consequence rather than fixing it:It no longer has to be. The scope is an
AsyncLocal, asMathS.Settingsnow is (#857) and as the cancellation token inMathS.Multithreadingalready was.Why this is more than a field swap
The trap #857 ran into applies directly here. An
AsyncLocalflows the reference, so once the pointer reaches child tasks, two of them can report to one recording at the same time — and the steps were accumulating into aList<RewriteStep>. That is a torn write, not a merged list.So alongside the field:
ConcurrentQueue<RewriteStep>;closedisvolatile, since it is now read from flows other than the one that set it;Stepscopies out rather than handing back a live view.Behaviour
awaitOne existing test is rewritten, not deleted
ARecordingOnOneThreadDoesNotSeeAnotherstarted a thread inside an open recording and asserted its work was not collected.ExecutionContextflows to a manually started thread, so that work is now collected — which is the point of the change rather than a regression of it.It becomes
WorkStartedUnderARecordingIsCollectedWhereverItRuns, and the isolation it was really reaching for — that a parallel caller records its own work and nobody else's — is covered properly bySiblingRecordingsDoNotSeeEachOther, with a barrier forcing both recordings open before either does any work.Also new:
ARecordingSurvivesAnAwait(the regression) andARecordingOpenedInsideATaskDoesNotEscapeIt.AClosedRecordingIgnoresWhateverItIsStillHandedpasses unchanged.Off is still free
RewriteAllocationTestpasses untouched: no recording open still costs one ambient read per rule set and allocates nothing, which is what #746 asks of every layer above the tree.Verification
Breaking, so it wants the 2.0 window; recorded in
BREAKING-CHANGES.mdwith the migration, theSteps-is-now-a-snapshot note, and the fact that step order across parallel work is not defined.Note on merge order: this touches
BREAKING-CHANGES.mdat the same anchors as #853 and #857. Whichever merges later needs a trivial "keep both" resolution in that file.🤖 Generated with Claude Code