Skip to content

Commit 55580f4

Browse files
coneilenCopilot
andcommitted
Stabilize remote bridge privacy stress
Use the deterministic Python runner during concurrent privacy checks, allow realistic scheduling margins, assert thread termination, and drain child output asynchronously to avoid redirected-pipe stalls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens <coneilen@microsoft.com>
1 parent 68361fe commit 55580f4

2 files changed

Lines changed: 36 additions & 35 deletions

File tree

Tools/windows/Tests/RemoteBridgePrivacyRace.Tests.ps1

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,17 @@ function Start-CapturedProcess([string] $fileName, [string[]] $arguments) {
1919
$process = [Diagnostics.Process]::new()
2020
$process.StartInfo = $startInfo
2121
[void] $process.Start()
22-
$process.BeginOutputReadLine()
23-
$process.BeginErrorReadLine()
24-
return $process
22+
return [pscustomobject]@{
23+
Process = $process
24+
Stdout = $process.StandardOutput.ReadToEndAsync()
25+
Stderr = $process.StandardError.ReadToEndAsync()
26+
}
2527
}
2628

2729
$testPath = Join-Path $repoRoot "investigation\spikes\remote-bridge"
2830
$remoteArguments = @(
2931
"-B",
30-
"-m",
31-
"unittest",
32-
"discover",
33-
"-s",
34-
$testPath,
35-
"-p",
36-
"test_*.py"
32+
(Join-Path $testPath "run_tests.py")
3733
)
3834
$privacyArguments = @(
3935
"-NoProfile",
@@ -55,28 +51,32 @@ $privacyProcesses = @(
5551
)
5652

5753
try {
58-
foreach ($process in $remoteProcesses + $privacyProcesses) {
59-
$process.WaitForExit()
54+
foreach ($entry in $remoteProcesses + $privacyProcesses) {
55+
$entry.Process.WaitForExit()
6056
}
6157

62-
$remoteFailures = $remoteProcesses | Where-Object ExitCode -ne 0
58+
$remoteFailures = $remoteProcesses | Where-Object { $_.Process.ExitCode -ne 0 }
6359
if ($remoteFailures) {
6460
throw "Remote bridge test process failed during privacy race: $(
65-
@($remoteFailures | ForEach-Object { "pid=$($_.Id), exit=$($_.ExitCode)" }) -join "; "
61+
@($remoteFailures | ForEach-Object {
62+
"pid=$($_.Process.Id), exit=$($_.Process.ExitCode), stdout=$($_.Stdout.Result), stderr=$($_.Stderr.Result)"
63+
}) -join "; "
6664
)"
6765
}
68-
$privacyFailures = $privacyProcesses | Where-Object ExitCode -ne 0
66+
$privacyFailures = $privacyProcesses | Where-Object { $_.Process.ExitCode -ne 0 }
6967
if ($privacyFailures) {
7068
throw "Privacy validation failed while remote tests ran concurrently: $(
71-
@($privacyFailures | ForEach-Object { "pid=$($_.Id), exit=$($_.ExitCode)" }) -join "; "
69+
@($privacyFailures | ForEach-Object {
70+
"pid=$($_.Process.Id), exit=$($_.Process.ExitCode), stdout=$($_.Stdout.Result), stderr=$($_.Stderr.Result)"
71+
}) -join "; "
7272
)"
7373
}
7474
} finally {
75-
foreach ($process in $remoteProcesses + $privacyProcesses) {
76-
if (-not $process.HasExited) {
77-
$process.Kill()
75+
foreach ($entry in $remoteProcesses + $privacyProcesses) {
76+
if (-not $entry.Process.HasExited) {
77+
$entry.Process.Kill()
7878
}
79-
$process.Dispose()
79+
$entry.Process.Dispose()
8080
}
8181
}
8282

investigation/spikes/remote-bridge/test_remote_bridge.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414

1515
SPIKE_ROOT = Path(__file__).resolve().parent
16+
THREAD_TIMEOUT = 5.0
1617
sys.path.insert(0, str(SPIKE_ROOT))
1718

1819
from remote_bridge import ( # noqa: E402
@@ -369,14 +370,14 @@ def interleaving_read():
369370
store.read = interleaving_read
370371

371372
def replace_state():
372-
read_started.wait(1)
373+
read_started.wait(THREAD_TIMEOUT)
373374
store.write(replacement)
374375
replacement_done.set()
375376

376377
replacement_thread = threading.Thread(target=replace_state, daemon=True)
377378
replacement_thread.start()
378379
self.bridge.stop()
379-
replacement_thread.join(1)
380+
replacement_thread.join(THREAD_TIMEOUT)
380381

381382
self.assertFalse(replacement_thread.is_alive())
382383
self.assertTrue(replacement_done.is_set())
@@ -500,7 +501,7 @@ def test_rotation_overlap_starts_after_state_lock_release(self):
500501
def hold_state_lock():
501502
with self.bridge.state_store.transaction():
502503
lock_ready.set()
503-
release_lock.wait(1)
504+
release_lock.wait(THREAD_TIMEOUT)
504505

505506
def rotate():
506507
try:
@@ -512,15 +513,15 @@ def rotate():
512513

513514
holder = threading.Thread(target=hold_state_lock, daemon=True)
514515
holder.start()
515-
lock_ready.wait(1)
516+
lock_ready.wait(THREAD_TIMEOUT)
516517
rotation = threading.Thread(target=rotate, daemon=True)
517518
rotation.start()
518519
time.sleep(0.2)
519520
self.assertFalse(rotation_done.is_set())
520521
release_at = time.time()
521522
release_lock.set()
522-
holder.join(1)
523-
rotation.join(1)
523+
holder.join(THREAD_TIMEOUT)
524+
rotation.join(THREAD_TIMEOUT)
524525

525526
self.assertFalse(holder.is_alive())
526527
self.assertFalse(rotation.is_alive())
@@ -576,7 +577,7 @@ def read_state():
576577
if wait_for_readers:
577578
missing_reads += 1
578579
if wait_for_readers:
579-
read_barrier.wait(1)
580+
read_barrier.wait(THREAD_TIMEOUT)
580581
raise
581582

582583
return read_state
@@ -585,7 +586,7 @@ def read_state():
585586
bridge.state_store.read = synchronized_missing(read)
586587

587588
def start_bridge(bridge):
588-
start_barrier.wait(1)
589+
start_barrier.wait(THREAD_TIMEOUT)
589590
try:
590591
bridge.start()
591592
outcomes.append((bridge, "started"))
@@ -598,9 +599,9 @@ def start_bridge(bridge):
598599
]
599600
for thread in threads:
600601
thread.start()
601-
start_barrier.wait(1)
602+
start_barrier.wait(THREAD_TIMEOUT)
602603
for thread in threads:
603-
thread.join(1)
604+
thread.join(THREAD_TIMEOUT)
604605

605606
self.assertTrue(all(not thread.is_alive() for thread in threads))
606607
started = [bridge for bridge, result in outcomes if result == "started"]
@@ -629,7 +630,7 @@ def test_stop_waits_for_start_publication(self):
629630

630631
def delayed_publish(expected, state):
631632
publication_started.set()
632-
release_publication.wait(1)
633+
release_publication.wait(THREAD_TIMEOUT)
633634
return original_publish(expected, state)
634635

635636
store.write_if_matches = delayed_publish
@@ -651,13 +652,13 @@ def stop_bridge():
651652
starter = threading.Thread(target=start_bridge, daemon=True)
652653
stopper = threading.Thread(target=stop_bridge, daemon=True)
653654
starter.start()
654-
publication_started.wait(1)
655+
publication_started.wait(THREAD_TIMEOUT)
655656
stopper.start()
656657
time.sleep(0.1)
657658
self.assertFalse(stop_done.is_set())
658659
release_publication.set()
659-
starter.join(1)
660-
stopper.join(1)
660+
starter.join(THREAD_TIMEOUT)
661+
stopper.join(THREAD_TIMEOUT)
661662

662663
self.assertFalse(starter.is_alive())
663664
self.assertFalse(stopper.is_alive())
@@ -722,7 +723,7 @@ def read_states():
722723
finally:
723724
stop_readers.set()
724725
for reader in readers:
725-
reader.join(1)
726+
reader.join(THREAD_TIMEOUT)
726727

727728
self.assertTrue(all(not reader.is_alive() for reader in readers))
728729
self.assertEqual(errors, [])

0 commit comments

Comments
 (0)