Skip to content

Publish the stack-frame regex once instead of racing on lazy init - #10702

Open
Jakub Jareš (nohwnd) wants to merge 2 commits into
mainfrom
nohwnd-publish-stack-frame-regex-once
Open

Publish the stack-frame regex once instead of racing on lazy init#10702
Jakub Jareš (nohwnd) wants to merge 2 commits into
mainfrom
nohwnd-publish-stack-frame-regex-once

Conversation

@nohwnd

@nohwnd Jakub Jareš (nohwnd) commented Aug 24, 2026

Copy link
Copy Markdown
Member

GetFrameRegex() could hand a different Regex to each concurrent caller on targets that don't use [GeneratedRegex]. The check and the assign were not atomic, and the method returned the field instead of the instance it had just built, so two threads racing on first use each got their own object and the second write replaced the first. That is what made GetFrameRegex_HasExplicitCapturesAndNoTimeout fail on net462 in CI.

Publish the first instance with Interlocked.CompareExchange and return it, so every caller gets the same regex. Callers racing on the very first use still each build one, only the published instance survives, and every call after that skips the construction. The regex is still built lazily on first use. The copy in Microsoft.Testing.Platform.MSBuild had the same shape and is fixed the same way.

Verified: the new GetFrameRegex_ConcurrentCallers_ShareASingleInstance test fails 19 out of 20 runs against the old code and passes 20 out of 20 against the new one, run on its own so the regex is still cold. Ten consecutive full net462 runs of Microsoft.Testing.Platform.UnitTests are green, and build.cmd -c Release passes.

🤖

GetFrameRegex could hand a different Regex to each concurrent caller on targets
that don't use GeneratedRegex. The check and the assign were not atomic, and the
method returned the field instead of the instance it had just built, so two
threads racing on first use each got their own object and the second write
replaced the first.

Publish the first instance with Interlocked.CompareExchange and return it, so
every caller shares one regex and the RegexOptions.Compiled construction is paid
for once. The regex is still built lazily on first use. The MSBuild copy had the
same shape and is fixed the same way.

🤖
Copilot AI balanced review requested due to automatic review settings August 24, 2026 16:53

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

✅ 22/22 dimensions clean — no findings.

The Volatile.Read + Interlocked.CompareExchange pattern is the correct lock-free lazy initialization idiom. The return expression Interlocked.CompareExchange(ref s_regex, regex, null) ?? regex correctly returns the winning instance regardless of which thread publishes first. The concurrency test adequately exercises the race window with Parallel.For across 32 lanes.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Atomically publishes one shared stack-frame regex across concurrent callers on non-generated-regex targets.

Changes:

  • Uses volatile reads and Interlocked.CompareExchange for regex publication.
  • Applies the fix to platform and MSBuild helpers.
  • Adds a concurrent-caller regression test.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
StackTraceRegexHelperTests.cs Adds concurrency coverage.
StackTraceHelper.cs Makes platform regex publication atomic.
Microsoft.Testing.Platform.MSBuild/Tasks/StackTraceHelper.cs Applies equivalent MSBuild fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Platform/Microsoft.Testing.Platform/Helpers/StackTraceHelper.cs Outdated
Comment thread src/Platform/Microsoft.Testing.Platform.MSBuild/Tasks/StackTraceHelper.cs Outdated
Racing callers on first use do each build a Regex, only the published one
survives, so saying the construction is paid for once was wrong. The saving is
that every call after publication skips it.

🤖
Copilot AI review requested due to automatic review settings August 24, 2026 17:04
@github-actions

Copy link
Copy Markdown
Contributor

🧵 Parallel-safety audit — PR #10702

Parallelization — test assembly audited:

Test assembly Scope Workers Analyzer coverage
Microsoft.Testing.Platform.UnitTests MethodLevel CPU count (Workers = 0) coverable once the parallel-safety analyzers ship (assembly attribute in Program.cs)

Findings: A (global-state) 0 · B (paths) 0 · C (declaration) 0 · D (over-serialization) 0 — by severity: Critical 0 · High 0 · Warning 0 · Info 0.

The only changed test is a new method, GetFrameRegex_ConcurrentCallers_ShareASingleInstance, added to StackTraceRegexHelperTests.cs (test/UnitTests/Microsoft.Testing.Platform.UnitTests/Helpers/StackTraceRegexHelperTests.cs). Under this suite's MethodLevel scope, this method runs concurrently with every other method in the assembly, including its own sibling tests in the same class — which is exactly what it is designed to exercise:

  • It only touches StackTraceHelper.GetFrameRegex(), a process-wide static (s_regex) that the PR itself is fixing to be race-free via Interlocked.CompareExchange. The test asserts all 32 concurrent callers observe the same Regex instance — it is a deliberate stress test of that publish-once guarantee, not an unguarded mutation of shared state that other tests also depend on.
  • No [ResourceLock] / [DoNotParallelize] is needed: nothing else in the assembly relies on s_regex being in a particular state, and the field is populated at most once and then stable, so there is no under-declared cross-test hazard (category C) and no coverage gap.
  • No filesystem paths, environment variables, current directory, console state, or culture are touched (categories A/B).
  • The sibling copy in Microsoft.Testing.Platform.MSBuild (Microsoft.Testing.Platform.MSBuild.UnitTests) got the identical production fix but no new concurrent-callers test was added there; that suite is unaffected by this PR’s test changes and out of scope for this audit (no changed test file in that project).
  • The existing GetFrameRegex_HasExplicitCapturesAndNoTimeout test (unchanged) does an Assert.AreSame(regex, StackTraceHelper.GetFrameRegex()) sanity check; the new test’s own comment already notes it is a supplement, not a replacement, and correctly does not assume ordering relative to other tests in the class.

No unsafe call sites, no near-misses, no over-serialization. This is a case where a test was deliberately written to be safe under MethodLevel parallel execution while validating the very race the PR fixes.

Advisory only — heuristic, non-blocking. Re-run with /parallel-audit. This audit answers “is it parallel-safe?”; for testability, smells, or flakiness see the detect-static-dependencies / test-smell-detection / test-anti-patterns analyses.

🤖 Automated content by GitHub Copilot. Generated by the Parallel-safety audit on PR (on open / sync) workflow. · auto · 48.6 AIC · ⌖ 3.99 AIC · ⊞ 24.8K · [◷]( · )

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Expert test review — PR #10702

No new or modified test methods were identified in the changed regions
of this PR. Nothing to review.

Re-run with /review-tests.

🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 29.7 AIC · ⌖ 3.85 AIC · ⊞ 16.9K · [◷]( · )

// assembly already initialized the regex, every caller here takes the fast path and the test passes trivially.
var instances = new Regex[32];

Parallel.For(0, instances.Length, i => instances[i] = StackTraceHelper.GetFrameRegex());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not reliably exercise the cold-start race. In a normal assembly run, another test or a product call can initialize s_regex first, so all 32 iterations take the fast path and this test still passes if the CompareExchange fix is later reverted. Even on a cold run, Parallel.For is allowed to execute the iterations serially and does not guarantee that callers overlap.

Could we make the regression deterministic? One option is to move the publication logic behind a helper that accepts a fresh storage location and coordinate callers with a Barrier, then assert that every caller receives the same published instance. Alternatively, using LazyInitializer.EnsureInitialized would remove the custom publication algorithm and make this test unnecessary. Resetting the production static through reflection would be less desirable because this assembly runs tests in parallel and other platform code can use the same field.


// Racing callers on first use may each build an instance. Publish the first one and return it to all of them,
// so every caller observes the same object, and once it is published no caller builds the regex again.
return Interlocked.CompareExchange(ref s_regex, regex, null) ?? regex;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same publication behavior is changing in the MSBuild copy, but its existing test only verifies that s_regex becomes non-null and has the expected timeout; it would not catch callers receiving different instances during first use. Could this implementation share the tested initialization primitive with the platform helper, or gain equivalent deterministic concurrency coverage? Otherwise a future edit can regress one copy while tests continue to cover only the other.

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.

3 participants