Linux-Wackeltests: Instanz endet erst auf die Stopp-Bitte, Lesetest schreibt bis genug gelesen ist (#722)

Drei Manager-Tests liessen den Prozess vor der Stopp-Bitte enden; unter Last
sah der Aufsichtstakt das zuerst und meldete Crashed statt Stopped. Die
FakeChannel bekommt OnStopRequest, die Tests beenden darin. Der Drain-Test
bekommt 20 s statt 2 s Luft. AtomicFile schreibt, bis der Leser 20 Blicke
hatte, statt fest 40 Mal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-02 11:50:51 +02:00
co-authored by Claude Opus 5.5
parent e42ec100b0
commit 753d633cf3
5 changed files with 29 additions and 16 deletions
@@ -172,9 +172,18 @@ internal sealed class FakeChannel : IInstanceChannel
: Answer);
}
/// <summary>
/// #722: What the instance does when the request arrives — typically: end. A test that
/// ended the process before asking raced the supervisor's own check: under load the check
/// saw the process gone first and called it a crash, not a stop.
/// </summary>
public Action<string>? OnStopRequest { get; set; }
public Task<bool> RequestStopAsync(string instanceDirectory, CancellationToken ct = default)
{
StopRequests++;
if (StopRequestArrives)
OnStopRequest?.Invoke(instanceDirectory);
return Task.FromResult(StopRequestArrives);
}
@@ -139,8 +139,8 @@ public sealed class InstanceRegistryTests : IAsyncDisposable
register.Instances.Single().Condition.ShouldBe(InstanceCondition.Running);
// Die Instanz endet auf die Bitte hin.
_launcher.Last!.Exit(0);
_kanal.Answer = null;
var prozess = _launcher.Last!;
_kanal.OnStopRequest = _ => { _kanal.Answer = null; prozess.Exit(0); };
(await register.StopAsync(pfad)).ShouldBeTrue();
register.Instances.Single().Condition.ShouldBe(InstanceCondition.Stopped);
@@ -230,8 +230,8 @@ public sealed class InstanceRegistryTests : IAsyncDisposable
var a = register.Create("Alpha");
await register.StartAsync(a);
_launcher.Last!.Exit(0);
_kanal.Answer = null;
var prozess = _launcher.Last!;
_kanal.OnStopRequest = _ => { _kanal.Answer = null; prozess.Exit(0); };
await register.StopAllAsync("Lizenz widerrufen");
@@ -59,10 +59,10 @@ public sealed class InstanceSupervisorTests : IDisposable
catch { /* Ein liegengebliebener Testordner faerbt keinen Lauf rot. */ }
}
private InstanceSupervisor Aufsicht()
private InstanceSupervisor Aufsicht(SupervisionOptions? options = null)
{
var supervisor = new InstanceSupervisor(
_dir, _launcher, _kanal, NullLogger.Instance, Options);
_dir, _launcher, _kanal, NullLogger.Instance, options ?? Options);
supervisor.Observed += _ereignisse.Add;
return supervisor;
@@ -307,16 +307,18 @@ public sealed class InstanceSupervisorTests : IDisposable
{
_kanal.Answer = Test;
await using var aufsicht = Aufsicht();
// #722: Viel Luft bis MaxDrainWait — unter Parallellast kam die Verzoegerung von 700 ms
// sonst erst nach den 2 s an, und der Test sah einen Abschuss, wo keiner hingehoert.
await using var aufsicht = Aufsicht(Options with { MaxDrainWait = TimeSpan.FromSeconds(20) });
await aufsicht.StartAsync();
var prozess = _launcher.Last.ShouldNotBeNull();
// Sie antwortet und meldet: Ich raeume auf. Laenger als StopGrace (300ms), aber
// kuerzer als MaxDrainWait (2s).
// kuerzer als MaxDrainWait.
_kanal.Answer = Test with { Phase = ShutdownPhase.Draining, OpenWork = 1 };
_ = Task.Run(async () =>
_kanal.OnStopRequest = _ => Task.Run(async () =>
{
await Task.Delay(700);
_kanal.Answer = null;
@@ -182,8 +182,8 @@ public sealed class ManagerCoreTests : IAsyncDisposable
manager.Instances.Instances.Single().Condition.ShouldBe(InstanceCondition.Running);
// Die Instanz endet auf die Bitte hin.
_launcher.Last!.Exit(0);
_kanal.AnswerFor[Path.GetFullPath(pfad)] = null;
var prozess = _launcher.Last!;
_kanal.OnStopRequest = _ => { _kanal.AnswerFor[Path.GetFullPath(pfad)] = null; prozess.Exit(0); };
await manager.HandleLicenseRevokedAsync("Widerrufen durch den Hersteller");