138 lines
5.0 KiB
C#
138 lines
5.0 KiB
C#
using System.Diagnostics;
|
|
using System.IO.Pipes;
|
|
using System.Text;
|
|
|
|
namespace Dada.Supervisor;
|
|
|
|
internal sealed class ManagedChildProcess : IAsyncDisposable
|
|
{
|
|
private readonly NamedPipeServerStream controlPipe;
|
|
private readonly StreamWriter controlWriter;
|
|
private bool stopping;
|
|
|
|
private ManagedChildProcess(Process process, NamedPipeServerStream controlPipe, StreamWriter controlWriter)
|
|
{
|
|
Process = process;
|
|
this.controlPipe = controlPipe;
|
|
this.controlWriter = controlWriter;
|
|
}
|
|
|
|
internal Process Process { get; }
|
|
internal bool IsStopping => stopping;
|
|
internal static TimeSpan ShutdownDeadline { get; } = TimeSpan.FromSeconds(15);
|
|
|
|
internal static async Task<ManagedChildProcess> StartAsync(
|
|
ProcessStartInfo startInfo,
|
|
ChildRole role,
|
|
ICredentialStore credentialStore,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var pipeName = $"Dada.P0A.Control.{role}.{Guid.NewGuid():N}";
|
|
var pipe = new NamedPipeServerStream(
|
|
pipeName,
|
|
PipeDirection.InOut,
|
|
1,
|
|
PipeTransmissionMode.Byte,
|
|
PipeOptions.Asynchronous);
|
|
startInfo.ArgumentList.Add("--dada-control-pipe");
|
|
startInfo.ArgumentList.Add(pipeName);
|
|
startInfo.ArgumentList.Add("--dada-credential-stdin");
|
|
Process? process = null;
|
|
try
|
|
{
|
|
process = await CredentialProcessLauncher.StartAsync(startInfo, role, credentialStore, cancellationToken);
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
timeout.CancelAfter(TimeSpan.FromSeconds(10));
|
|
await pipe.WaitForConnectionAsync(timeout.Token);
|
|
var reader = new StreamReader(pipe, Encoding.UTF8, false, leaveOpen: true);
|
|
var ready = await reader.ReadLineAsync(timeout.Token);
|
|
if (!string.Equals(ready, "ready", StringComparison.Ordinal)) throw new InvalidOperationException("Managed child did not report ready.");
|
|
var writer = new StreamWriter(pipe, new UTF8Encoding(false), leaveOpen: true) { AutoFlush = true };
|
|
return new ManagedChildProcess(process, pipe, writer);
|
|
}
|
|
catch
|
|
{
|
|
pipe.Dispose();
|
|
if (process is { HasExited: false }) process.Kill(entireProcessTree: true);
|
|
process?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
internal async Task StopAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
|
|
{
|
|
if (stopping || Process.HasExited) return;
|
|
stopping = true;
|
|
await controlWriter.WriteLineAsync("shutdown".AsMemory(), cancellationToken);
|
|
var exited = Process.WaitForExitAsync(cancellationToken);
|
|
var deadline = Task.Delay(timeout ?? ShutdownDeadline, cancellationToken);
|
|
if (await Task.WhenAny(exited, deadline) != exited && !Process.HasExited)
|
|
{
|
|
Process.Kill(entireProcessTree: true);
|
|
await Process.WaitForExitAsync(cancellationToken);
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (!Process.HasExited) await StopAsync();
|
|
controlWriter.Dispose();
|
|
controlPipe.Dispose();
|
|
Process.Dispose();
|
|
}
|
|
}
|
|
|
|
internal sealed class ManagedComponentSupervisor : IAsyncDisposable
|
|
{
|
|
private readonly Func<CancellationToken, Task<ManagedChildProcess>> start;
|
|
private readonly SupervisorState degradedState;
|
|
private ManagedChildProcess? child;
|
|
private int completedRestarts;
|
|
private DateTimeOffset firstFailure;
|
|
private bool stopping;
|
|
|
|
internal ManagedComponentSupervisor(Func<CancellationToken, Task<ManagedChildProcess>> start, SupervisorState degradedState)
|
|
{
|
|
this.start = start;
|
|
this.degradedState = degradedState;
|
|
}
|
|
|
|
internal event Action<SupervisorState>? StateChanged;
|
|
|
|
internal async Task StartAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
child = await start(cancellationToken);
|
|
child.Process.EnableRaisingEvents = true;
|
|
child.Process.Exited += OnChildExited;
|
|
}
|
|
|
|
private async void OnChildExited(object? sender, EventArgs eventArgs)
|
|
{
|
|
if (stopping || child?.IsStopping == true) return;
|
|
if (completedRestarts == 0) firstFailure = DateTimeOffset.UtcNow;
|
|
if (!RestartPolicy.CanRestart(completedRestarts, DateTimeOffset.UtcNow - firstFailure))
|
|
{
|
|
StateChanged?.Invoke(degradedState);
|
|
return;
|
|
}
|
|
var delay = RestartPolicy.Delays[completedRestarts++];
|
|
await Task.Delay(delay);
|
|
try
|
|
{
|
|
child = await start(CancellationToken.None);
|
|
child.Process.EnableRaisingEvents = true;
|
|
child.Process.Exited += OnChildExited;
|
|
}
|
|
catch
|
|
{
|
|
OnChildExited(null, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
stopping = true;
|
|
if (child is not null) await child.DisposeAsync();
|
|
}
|
|
}
|