Skip to content

Commit 75d4c5a

Browse files
committed
Wait for killed crash hosts to release files on Windows
Bound the file-release and teardown waits after an OS process kill, and preserve the original recovery exception when cleanup also fails. Keep corruption and atomicity assertions unchanged. Validation: Release recovery build; focused seed 1715 with 50 actual process kills passed locally. GitHub CI verifies the full Windows recovery suite.
1 parent 97032ad commit 75d4c5a

1 file changed

Lines changed: 36 additions & 1 deletion

File tree

‎tests/KeyLoad.RecoveryTests/RecoveryTests.cs‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ public async Task FiftySeededRealProcessCrashesPreserveAtomicTransactions(int ba
2929
using var process = Process.Start(start)!;
3030
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
3131
timeout.CancelAfter(TimeSpan.FromSeconds(15));
32+
Exception? originalFailure = null;
3233
try
3334
{
3435
var line = await process.StandardOutput.ReadLineAsync(timeout.Token);
3536
Assert.Equal("crash-point", line);
3637
process.Kill(); await process.WaitForExitAsync(timeout.Token);
38+
await WaitForKilledProcessFilesAsync(root, timeout.Token);
3739
using var store = new ZoneTreeStore(new(root));
3840
var values = store.Read(view => Enumerable.Range(0, 3).Select(i => JsonDefaults.Deserialize<int>(view.Get(KeyCodec.Encode("item", (long)i))!)).ToArray());
3941
await evidence.WriteLineAsync(JsonSerializer.Serialize(new { Seed = 1701 + batch, Trial = trial, Stage = stage.ToString(),
@@ -43,13 +45,46 @@ await evidence.WriteLineAsync(JsonSerializer.Serialize(new { Seed = 1701 + batch
4345
Assert.True(values.All(v => v == 0) || values.All(v => v == 1), $"Non-atomic recovery at seed {batch}, trial {trial}, stage {stage}.");
4446
if (stage >= CommitStage.JournalFlushed) Assert.All(values, value => Assert.Equal(1, value));
4547
}
48+
catch (Exception exception)
49+
{
50+
originalFailure = exception;
51+
TestContext.Current.TestOutputHelper?.WriteLine($"Seed {1701 + batch}, trial {trial}, stage {stage}: {exception}");
52+
throw;
53+
}
4654
finally
4755
{
4856
if (!process.HasExited) { process.Kill(); await process.WaitForExitAsync(TestContext.Current.CancellationToken); }
49-
if (Directory.Exists(root)) Directory.Delete(root, true);
57+
try { if (Directory.Exists(root)) await DeleteTrialAsync(root, TestContext.Current.CancellationToken); }
58+
catch (IOException exception) when (originalFailure is not null)
59+
{ TestContext.Current.TestOutputHelper?.WriteLine($"Cleanup after the original failure: {exception.Message}"); }
5060
}
5161
}
5262
}
63+
private static async Task WaitForKilledProcessFilesAsync(string root, CancellationToken cancellationToken)
64+
{
65+
var started = Stopwatch.StartNew();
66+
while (true)
67+
{
68+
try
69+
{
70+
foreach (var name in new[] { "owner.lock", "commands.wal" })
71+
using (File.Open(Path.Combine(root, name), FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { }
72+
return;
73+
}
74+
catch (IOException) when (started.Elapsed < TimeSpan.FromSeconds(5))
75+
{ await Task.Delay(25, cancellationToken); }
76+
}
77+
}
78+
private static async Task DeleteTrialAsync(string root, CancellationToken cancellationToken)
79+
{
80+
var started = Stopwatch.StartNew();
81+
while (true)
82+
{
83+
try { Directory.Delete(root, true); return; }
84+
catch (IOException) when (started.Elapsed < TimeSpan.FromSeconds(5))
85+
{ await Task.Delay(25, cancellationToken); }
86+
}
87+
}
5388
[Fact]
5489
public void CorruptCompleteFrameFailsClosedInsteadOfBeingDiscardedAsATornTail()
5590
{

0 commit comments

Comments
 (0)