V2/Application/AsyncOperationRegistry.cs

Registry that owns fire-and-forget asynchronous operations. It accepts ValueTask or Task factories while open, tracks active operations, records exceptions, and provides a one-shot DrainAsync that stops admission and returns a summary result when all operations complete.

Native Interop
#nullable enable

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Hexagon.V2.Application;

/// <summary>
/// One fault observed while draining an owned asynchronous operation.
/// </summary>
public sealed record AsyncOperationFailure(
	long OperationId,
	string Name,
	Exception Exception)
{
	public bool WasCanceled => Exception is OperationCanceledException;
}

/// <summary>
/// Immutable terminal result for a one-way operation registry drain.
/// </summary>
public sealed record AsyncDrainResult(
	int AcceptedOperationCount,
	int CompletedOperationCount,
	IReadOnlyList<AsyncOperationFailure> Failures)
{
	public bool Succeeded => Failures.Count == 0;
}

/// <summary>
/// Owns fire-and-forget work so shutdown can close admission, observe every
/// exception, and wait for quiescence without retaining completed tasks.
/// A registry is intentionally one-shot: admission cannot reopen after it is
/// stopped.
/// </summary>
public sealed class AsyncOperationRegistry
{
	private readonly object _sync = new();
	private readonly Dictionary<long, OperationEntry> _active = new();
	private readonly List<AsyncOperationFailure> _failures = new();
	private TaskCompletionSource<AsyncDrainResult>? _drainCompletion;
	private long _nextOperationId;
	private int _acceptedOperationCount;
	private int _completedOperationCount;
	private bool _accepting = true;

	public bool IsAccepting
	{
		get { lock ( _sync ) return _accepting; }
	}

	public int ActiveOperationCount
	{
		get { lock ( _sync ) return _active.Count; }
	}

	/// <summary>
	/// Starts and owns a ValueTask-producing operation if admission is open.
	/// Synchronous factory exceptions are observed as operation failures.
	/// </summary>
	public bool TryStart( string name, Func<ValueTask> operation )
	{
		ArgumentException.ThrowIfNullOrWhiteSpace( name );
		ArgumentNullException.ThrowIfNull( operation );
		var entry = Reserve( name );
		if ( entry is null ) return false;

		ValueTask pending;
		try
		{
			pending = operation();
		}
		catch ( Exception exception )
		{
			Complete( entry, exception );
			return true;
		}

		if ( pending.IsCompletedSuccessfully )
		{
			pending.GetAwaiter().GetResult();
			Complete( entry, null );
			return true;
		}

		_ = ObserveAsync( entry, pending );
		return true;
	}

	/// <summary>
	/// Starts and owns a Task-producing operation if admission is open.
	/// </summary>
	public bool TryStartTask( string name, Func<Task> operation )
	{
		ArgumentException.ThrowIfNullOrWhiteSpace( name );
		ArgumentNullException.ThrowIfNull( operation );
		var entry = Reserve( name );
		if ( entry is null ) return false;

		Task pending;
		try
		{
			pending = operation()
				?? throw new InvalidOperationException( "Asynchronous operation factory returned null." );
		}
		catch ( Exception exception )
		{
			Complete( entry, exception );
			return true;
		}

		_ = ObserveTaskAsync( entry, pending );
		return true;
	}

	/// <summary>
	/// Permanently closes admission. Returns true only for the caller that
	/// performed the transition.
	/// </summary>
	public bool StopAdmission()
	{
		lock ( _sync )
		{
			if ( !_accepting ) return false;
			_accepting = false;
			EnsureDrainCompletionUnsafe();
			CompleteDrainIfQuiescedUnsafe();
			return true;
		}
	}

	/// <summary>
	/// Closes admission and returns the same terminal drain for every caller.
	/// Faults and cancellations are returned in the result rather than thrown.
	/// </summary>
	public ValueTask<AsyncDrainResult> DrainAsync()
	{
		StopAdmission();
		lock ( _sync )
		{
			EnsureDrainCompletionUnsafe();
			return new ValueTask<AsyncDrainResult>( _drainCompletion!.Task );
		}
	}

	private OperationEntry? Reserve( string name )
	{
		lock ( _sync )
		{
			if ( !_accepting ) return null;
			var entry = new OperationEntry( checked( ++_nextOperationId ), name );
			_active.Add( entry.Id, entry );
			_acceptedOperationCount++;
			return entry;
		}
	}

	private async Task ObserveAsync( OperationEntry entry, ValueTask pending )
	{
		Exception? failure = null;
		try
		{
			await pending;
		}
		catch ( Exception exception )
		{
			failure = exception;
		}
		Complete( entry, failure );
	}

	private async Task ObserveTaskAsync( OperationEntry entry, Task pending )
	{
		Exception? failure = null;
		try
		{
			await pending;
		}
		catch ( Exception exception )
		{
			failure = exception;
		}
		Complete( entry, failure );
	}

	private void Complete( OperationEntry entry, Exception? failure )
	{
		lock ( _sync )
		{
			if ( !_active.Remove( entry.Id ) ) return;
			_completedOperationCount++;
			if ( failure is not null )
				_failures.Add( new AsyncOperationFailure( entry.Id, entry.Name, failure ) );
			CompleteDrainIfQuiescedUnsafe();
		}
	}

	private void EnsureDrainCompletionUnsafe() =>
		_drainCompletion ??= new TaskCompletionSource<AsyncDrainResult>(
			TaskCreationOptions.RunContinuationsAsynchronously );

	private void CompleteDrainIfQuiescedUnsafe()
	{
		if ( _accepting || _active.Count != 0 || _drainCompletion is null ) return;
		_drainCompletion.TrySetResult( new AsyncDrainResult(
			_acceptedOperationCount,
			_completedOperationCount,
			Array.AsReadOnly( _failures.ToArray() ) ) );
	}

	private sealed record OperationEntry( long Id, string Name );
}