Skip to content

Commit 4d798bb

Browse files
committed
[ogar-adapter-csharp] serialize dotnet invocations — fix flaky NuGet-migration shm race
The parity tests shell out to dotnet on two threads. On a cold runner (~/.dotnet absent), each first dotnet process runs .NET's first-time-use NuGet migration, which opens a named mutex under /tmp/.dotnet/shm/session<N>. Two migrations racing lose with mkdir==EEXIST / stat==ENOENT — the intermittent failure seen on OGAR #179 (main was green on the same commit). A static Mutex gate serializes all dotnet invocations, so the migration completes once uncontended and later calls skip it. Verified with dotnet-sdk 8.0.128: cold-start (rm -rf /tmp/.dotnet before each run) x8 -> 8/8 green; the same stress on the unserialized code flakes ~4/6. Note: an earlier attempt isolated TMPDIR/HOME/DOTNET_CLI_HOME/NUGET_PACKAGES per test — that does NOT work; the shm dir stays at /tmp/.dotnet/shm regardless of those vars (empirically confirmed), so serialization, not env relocation, is the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1d4c22c commit 4d798bb

1 file changed

Lines changed: 18 additions & 0 deletions

File tree

crates/ogar-adapter-csharp/tests/parity.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
1414
use std::fs;
1515
use std::path::{Path, PathBuf};
1616
use std::process::Command;
17+
use std::sync::Mutex;
18+
19+
/// Serializes all `dotnet` invocations within this test binary.
20+
///
21+
/// The two parity tests run on separate threads; on a COLD machine
22+
/// (`~/.dotnet` absent, as on a fresh CI runner) each first `dotnet`
23+
/// process triggers .NET's first-time-use NuGet migration, which opens a
24+
/// named mutex under `/tmp/.dotnet/shm/session<N>`. That shm path is fixed
25+
/// by the runtime — it is NOT relocated by `TMPDIR`/`HOME`/`DOTNET_CLI_HOME`
26+
/// (verified empirically: the session dir lands in `/tmp/.dotnet/shm`
27+
/// regardless). So two migrations racing at once lose with
28+
/// `mkdir(...) == EEXIST` / `stat(...) == ENOENT` — the intermittent CI
29+
/// failure. Serializing the invocations lets the migration complete once,
30+
/// uncontended; every later `dotnet` sees it done and skips it. Poison is
31+
/// ignored so a genuine assertion failure in one test never masks the other.
32+
static DOTNET_GATE: Mutex<()> = Mutex::new(());
1733

1834
const NAMESPACE: &str = "Ogar.CapabilitySurface";
1935

@@ -116,6 +132,7 @@ fn dotnet_run(dir: &Path) -> String {
116132

117133
#[test]
118134
fn emitted_library_builds_and_dump_matches_ground_truth() {
135+
let _gate = DOTNET_GATE.lock().unwrap_or_else(|e| e.into_inner());
119136
if !dotnet_available() {
120137
eprintln!(
121138
"SKIP emitted_library_builds_and_dump_matches_ground_truth: \
@@ -144,6 +161,7 @@ fn emitted_library_builds_and_dump_matches_ground_truth() {
144161

145162
#[test]
146163
fn facet_bytes_built_in_rust_decode_correctly_in_csharp() {
164+
let _gate = DOTNET_GATE.lock().unwrap_or_else(|e| e.into_inner());
147165
if !dotnet_available() {
148166
eprintln!(
149167
"SKIP facet_bytes_built_in_rust_decode_correctly_in_csharp: \

0 commit comments

Comments
 (0)