315 lines
11 KiB
C#
315 lines
11 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
internal sealed class DynamicCodeExecutionScheduler : IDisposable
|
|
{
|
|
private const int BusyHandoffWindowMilliseconds = 50;
|
|
private const int CancelledPrewarmHandoffWindowMilliseconds = 500;
|
|
|
|
private readonly Action _disposeResources;
|
|
private readonly DynamicCodeExecutionSchedulerHooks _hooks;
|
|
private readonly int _busyHandoffWindowMilliseconds;
|
|
private readonly int _cancelledPrewarmHandoffWindowMilliseconds;
|
|
private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
|
|
private readonly CancellationTokenSource _lifetimeCancellationTokenSource = new();
|
|
private readonly TaskCompletionSource<bool> _shutdownCompletionSource =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
private readonly object _disposeLock = new();
|
|
private readonly object _executionStateLock = new();
|
|
private readonly object _backgroundPrewarmTransitionLock = new();
|
|
private CancellationTokenSource _backgroundPrewarmCancellationTokenSource;
|
|
private bool _backgroundPrewarmInProgress;
|
|
private bool _resourcesDisposed;
|
|
private bool _disposed;
|
|
|
|
public DynamicCodeExecutionScheduler(
|
|
Action disposeResources,
|
|
DynamicCodeExecutionSchedulerHooks hooks = null,
|
|
int busyHandoffWindowMilliseconds = BusyHandoffWindowMilliseconds,
|
|
int cancelledPrewarmHandoffWindowMilliseconds = CancelledPrewarmHandoffWindowMilliseconds)
|
|
{
|
|
_disposeResources = disposeResources ?? throw new ArgumentNullException(nameof(disposeResources));
|
|
_hooks = hooks ?? new DynamicCodeExecutionSchedulerHooks();
|
|
_busyHandoffWindowMilliseconds = busyHandoffWindowMilliseconds;
|
|
_cancelledPrewarmHandoffWindowMilliseconds = cancelledPrewarmHandoffWindowMilliseconds;
|
|
}
|
|
|
|
public void ThrowIfDisposed()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(DynamicCodeExecutionScheduler));
|
|
}
|
|
}
|
|
|
|
public async Task<T> RunForegroundAsync<T>(
|
|
Func<CancellationToken, Task<T>> action,
|
|
Func<T> createBusyResult,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ThrowIfDisposed();
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
bool entered = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
|
if (!entered)
|
|
{
|
|
await _hooks.InvokeAfterBusySemaphoreProbeFailedAsync();
|
|
if (!TryCancelBackgroundPrewarm())
|
|
{
|
|
entered = await TryAcquireAfterBusyHandoffAsync(cancellationToken);
|
|
if (!entered)
|
|
{
|
|
return createBusyResult();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
entered = await TryAcquireAfterBusyHandoffAsync(
|
|
cancellationToken,
|
|
_cancelledPrewarmHandoffWindowMilliseconds);
|
|
if (!entered)
|
|
{
|
|
return createBusyResult();
|
|
}
|
|
}
|
|
}
|
|
|
|
CancellationTokenSource executionCancellationTokenSource = null;
|
|
try
|
|
{
|
|
_hooks.AfterSemaphoreEntered?.Invoke();
|
|
ThrowIfDisposed();
|
|
executionCancellationTokenSource =
|
|
CreateExecutionCancellationTokenSource(cancellationToken);
|
|
SetExecutionState(false, executionCancellationTokenSource);
|
|
return await action(executionCancellationTokenSource.Token);
|
|
}
|
|
finally
|
|
{
|
|
ClearExecutionState(false, executionCancellationTokenSource);
|
|
executionCancellationTokenSource?.Dispose();
|
|
DisposeResourcesIfRequested();
|
|
_executionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<(bool Entered, T Result)> TryRunIfIdleAsync<T>(
|
|
bool yieldToForegroundRequests,
|
|
Func<CancellationToken, Task<T>> action,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ThrowIfDisposed();
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (!yieldToForegroundRequests)
|
|
{
|
|
return await TryRunWithoutForegroundYieldAsync(action, cancellationToken);
|
|
}
|
|
|
|
bool entered;
|
|
CancellationTokenSource executionCancellationTokenSource = null;
|
|
lock (_backgroundPrewarmTransitionLock)
|
|
{
|
|
entered = _executionSemaphore.Wait(0);
|
|
if (!entered)
|
|
{
|
|
return (false, default);
|
|
}
|
|
|
|
ThrowIfDisposed();
|
|
executionCancellationTokenSource =
|
|
CreateExecutionCancellationTokenSource(cancellationToken);
|
|
SetExecutionState(true, executionCancellationTokenSource);
|
|
}
|
|
|
|
try
|
|
{
|
|
await _hooks.InvokeAfterBackgroundExecutionStatePublishedAsync();
|
|
_hooks.AfterSemaphoreEntered?.Invoke();
|
|
T result = await action(executionCancellationTokenSource.Token);
|
|
return (true, result);
|
|
}
|
|
finally
|
|
{
|
|
ClearExecutionState(true, executionCancellationTokenSource);
|
|
executionCancellationTokenSource?.Dispose();
|
|
DisposeResourcesIfRequested();
|
|
_executionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
_lifetimeCancellationTokenSource.Cancel();
|
|
if (!_executionSemaphore.Wait(0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
DisposeResourcesIfRequested();
|
|
}
|
|
finally
|
|
{
|
|
_executionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public Task ShutdownAsync()
|
|
{
|
|
Dispose();
|
|
return _shutdownCompletionSource.Task;
|
|
}
|
|
|
|
private async Task<(bool Entered, T Result)> TryRunWithoutForegroundYieldAsync<T>(
|
|
Func<CancellationToken, Task<T>> action,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
bool entered = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
|
if (!entered)
|
|
{
|
|
return (false, default);
|
|
}
|
|
|
|
CancellationTokenSource executionCancellationTokenSource = null;
|
|
try
|
|
{
|
|
_hooks.AfterSemaphoreEntered?.Invoke();
|
|
ThrowIfDisposed();
|
|
executionCancellationTokenSource =
|
|
CreateExecutionCancellationTokenSource(cancellationToken);
|
|
SetExecutionState(false, executionCancellationTokenSource);
|
|
T result = await action(executionCancellationTokenSource.Token);
|
|
return (true, result);
|
|
}
|
|
finally
|
|
{
|
|
ClearExecutionState(false, executionCancellationTokenSource);
|
|
executionCancellationTokenSource?.Dispose();
|
|
DisposeResourcesIfRequested();
|
|
_executionSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
private bool TryCancelBackgroundPrewarm()
|
|
{
|
|
lock (_executionStateLock)
|
|
{
|
|
if (!_backgroundPrewarmInProgress || _backgroundPrewarmCancellationTokenSource == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_backgroundPrewarmCancellationTokenSource.Cancel();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private async Task<bool> TryAcquireAfterBusyHandoffAsync(
|
|
CancellationToken cancellationToken,
|
|
int handoffWindowMilliseconds = -1)
|
|
{
|
|
if (handoffWindowMilliseconds < 0)
|
|
{
|
|
handoffWindowMilliseconds = _busyHandoffWindowMilliseconds;
|
|
}
|
|
|
|
using CancellationTokenSource handoffCancellationTokenSource =
|
|
CancellationTokenSource.CreateLinkedTokenSource(
|
|
cancellationToken,
|
|
_lifetimeCancellationTokenSource.Token);
|
|
handoffCancellationTokenSource.CancelAfter(handoffWindowMilliseconds);
|
|
|
|
try
|
|
{
|
|
await _executionSemaphore.WaitAsync(handoffCancellationTokenSource.Token);
|
|
return true;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
if (cancellationToken.IsCancellationRequested ||
|
|
_lifetimeCancellationTokenSource.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private CancellationTokenSource CreateExecutionCancellationTokenSource(
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return CancellationTokenSource.CreateLinkedTokenSource(
|
|
cancellationToken,
|
|
_lifetimeCancellationTokenSource.Token);
|
|
}
|
|
|
|
private void SetExecutionState(
|
|
bool yieldToForegroundRequests,
|
|
CancellationTokenSource executionCancellationTokenSource)
|
|
{
|
|
lock (_executionStateLock)
|
|
{
|
|
_backgroundPrewarmInProgress = yieldToForegroundRequests;
|
|
_backgroundPrewarmCancellationTokenSource = yieldToForegroundRequests
|
|
? executionCancellationTokenSource
|
|
: null;
|
|
}
|
|
}
|
|
|
|
private void ClearExecutionState(
|
|
bool yieldToForegroundRequests,
|
|
CancellationTokenSource executionCancellationTokenSource)
|
|
{
|
|
if (!yieldToForegroundRequests)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_executionStateLock)
|
|
{
|
|
if (!ReferenceEquals(_backgroundPrewarmCancellationTokenSource, executionCancellationTokenSource))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_backgroundPrewarmInProgress = false;
|
|
_backgroundPrewarmCancellationTokenSource = null;
|
|
}
|
|
}
|
|
|
|
private void DisposeResourcesIfRequested()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_disposeLock)
|
|
{
|
|
if (_resourcesDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_resourcesDisposed = true;
|
|
}
|
|
|
|
_disposeResources();
|
|
_lifetimeCancellationTokenSource.Dispose();
|
|
_shutdownCompletionSource.TrySetResult(true);
|
|
}
|
|
}
|
|
}
|