feat(headless): add deterministic multi-session scheduler
This commit is contained in:
parent
b299e3738e
commit
7e8acb74dd
6 changed files with 694 additions and 54 deletions
|
|
@ -1,4 +1,3 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.Headless.Configuration;
|
||||
using AcDream.Headless.Credentials;
|
||||
using AcDream.Headless.Diagnostics;
|
||||
|
|
@ -10,12 +9,10 @@ namespace AcDream.Headless.Hosting;
|
|||
|
||||
internal sealed class HeadlessProcessHost : IDisposable
|
||||
{
|
||||
private static readonly TimeSpan K1TickInterval =
|
||||
TimeSpan.FromMilliseconds(15);
|
||||
|
||||
private readonly HeadlessSessionHost _session;
|
||||
private readonly HeadlessSessionHost[] _sessions;
|
||||
private readonly HeadlessProcessScheduler _scheduler;
|
||||
private readonly HeadlessDiagnosticWriter _diagnostics;
|
||||
private readonly string _sessionId;
|
||||
private int _disposeIndex;
|
||||
private bool _disposed;
|
||||
|
||||
internal HeadlessProcessHost(
|
||||
|
|
@ -30,67 +27,97 @@ internal sealed class HeadlessProcessHost : IDisposable
|
|||
ArgumentNullException.ThrowIfNull(paths);
|
||||
ArgumentNullException.ThrowIfNull(standardInput);
|
||||
ArgumentNullException.ThrowIfNull(diagnostics);
|
||||
if (configuration.Sessions.Count != 1
|
||||
|| configuration.Sessions[0] is not { } descriptor)
|
||||
if (configuration.Sessions.Count == 0)
|
||||
{
|
||||
throw new HeadlessConfigurationException(
|
||||
"Slice K1 run mode requires exactly one session.");
|
||||
"Headless run mode requires at least one session.");
|
||||
}
|
||||
|
||||
_sessionId = descriptor.Id;
|
||||
_diagnostics = new HeadlessDiagnosticWriter(diagnostics);
|
||||
var credentials = new HeadlessCredentialResolver(
|
||||
standardInput,
|
||||
paths.ConfigDirectory);
|
||||
HeadlessCredentialSecret secret =
|
||||
credentials.Resolve(descriptor.Id, descriptor.Credential);
|
||||
var sessions = new List<HeadlessSessionHost>(
|
||||
configuration.Sessions.Count);
|
||||
try
|
||||
{
|
||||
_session = new HeadlessSessionHost(
|
||||
descriptor,
|
||||
secret,
|
||||
_diagnostics,
|
||||
sessionOperations,
|
||||
foreach (HeadlessSessionDescriptor? candidate
|
||||
in configuration.Sessions)
|
||||
{
|
||||
HeadlessSessionDescriptor descriptor = candidate
|
||||
?? throw new HeadlessConfigurationException(
|
||||
"Headless run mode cannot contain a null session.");
|
||||
HeadlessCredentialSecret secret =
|
||||
credentials.Resolve(
|
||||
descriptor.Id,
|
||||
descriptor.Credential);
|
||||
try
|
||||
{
|
||||
sessions.Add(new HeadlessSessionHost(
|
||||
descriptor,
|
||||
secret,
|
||||
_diagnostics,
|
||||
sessionOperations,
|
||||
timeProvider));
|
||||
}
|
||||
catch
|
||||
{
|
||||
secret.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
_sessions = sessions.ToArray();
|
||||
_scheduler = new HeadlessProcessScheduler(
|
||||
_sessions,
|
||||
timeProvider);
|
||||
_disposeIndex = _sessions.Length - 1;
|
||||
}
|
||||
catch
|
||||
{
|
||||
secret.Dispose();
|
||||
for (int index = sessions.Count - 1; index >= 0; index--)
|
||||
sessions[index].Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal HeadlessSessionHost Session => _session;
|
||||
internal HeadlessSessionHost Session => _sessions.Length == 1
|
||||
? _sessions[0]
|
||||
: throw new InvalidOperationException(
|
||||
"The process owns more than one headless session.");
|
||||
internal IReadOnlyList<HeadlessSessionHost> Sessions => _sessions;
|
||||
internal HeadlessSchedulerSnapshot Scheduler =>
|
||||
_scheduler.CaptureSnapshot();
|
||||
|
||||
internal async Task<HeadlessExitCode> RunAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
RuntimeSessionStartResult started = _session.Start();
|
||||
if (started.Status != RuntimeSessionStartStatus.Connected)
|
||||
foreach (HeadlessSessionHost session in _sessions)
|
||||
{
|
||||
if (started.Error is { } error)
|
||||
_diagnostics.Failure(_sessionId, "start", error);
|
||||
return HeadlessExitCode.ConnectionError;
|
||||
RuntimeSessionStartResult started = session.Start();
|
||||
if (started.Status != RuntimeSessionStartStatus.Connected)
|
||||
{
|
||||
if (started.Error is { } error)
|
||||
{
|
||||
_diagnostics.Failure(
|
||||
session.SessionId,
|
||||
"start",
|
||||
error);
|
||||
}
|
||||
return HeadlessExitCode.ConnectionError;
|
||||
}
|
||||
|
||||
_diagnostics.Lifecycle(
|
||||
session.SessionId,
|
||||
"running",
|
||||
session.Runtime);
|
||||
}
|
||||
|
||||
_diagnostics.Lifecycle(_sessionId, "running", _session.Runtime);
|
||||
long previous = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(
|
||||
K1TickInterval,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
long current = Stopwatch.GetTimestamp();
|
||||
double deltaSeconds =
|
||||
(current - previous) / (double)Stopwatch.Frequency;
|
||||
previous = current;
|
||||
_session.Tick(deltaSeconds);
|
||||
if (_session.IsPolicyComplete)
|
||||
break;
|
||||
}
|
||||
await _scheduler.RunAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
when (cancellationToken.IsCancellationRequested)
|
||||
|
|
@ -98,7 +125,7 @@ internal sealed class HeadlessProcessHost : IDisposable
|
|||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
_diagnostics.Failure(_sessionId, "tick", error);
|
||||
_diagnostics.Failure("process", "tick", error);
|
||||
return HeadlessExitCode.RuntimeError;
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +136,11 @@ internal sealed class HeadlessProcessHost : IDisposable
|
|||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_session.Dispose();
|
||||
while (_disposeIndex >= 0)
|
||||
{
|
||||
_sessions[_disposeIndex].Dispose();
|
||||
_disposeIndex--;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue