Add XML docs

This commit is contained in:
Stone_Red
2025-12-23 15:15:18 +01:00
parent 55f26b3a92
commit 3e339b2b7a
26 changed files with 377 additions and 4 deletions
@@ -1,9 +1,16 @@
namespace RemoteExec.Client;
/// <summary>
/// An async-compatible manual reset event that can be awaited.
/// </summary>
internal class AsyncManualResetEvent
{
private volatile TaskCompletionSource<bool> _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
/// <summary>
/// Initializes a new instance of the <see cref="AsyncManualResetEvent"/> class.
/// </summary>
/// <param name="initialState">If true, the event is initially signaled.</param>
public AsyncManualResetEvent(bool initialState)
{
if (initialState)
@@ -12,16 +19,27 @@ internal class AsyncManualResetEvent
}
}
/// <summary>
/// Asynchronously waits for the event to be signaled.
/// </summary>
/// <param name="ct">A cancellation token to cancel the wait operation.</param>
/// <returns>A task that completes when the event is signaled.</returns>
public Task WaitAsync(CancellationToken ct)
{
return _tcs.Task.WaitAsync(ct);
}
/// <summary>
/// Sets the event to a signaled state, releasing all waiting tasks.
/// </summary>
public void Set()
{
_ = _tcs.TrySetResult(true);
}
/// <summary>
/// Resets the event to an unsignaled state.
/// </summary>
public void Reset()
{
while (true)
@@ -1,5 +1,8 @@
namespace RemoteExec.Client.Exceptions;
/// <summary>
/// Exception thrown when a task exceeds the maximum number of retry attempts.
/// </summary>
public class MaxRetriesException(string message) : Exception(message)
{
}
@@ -1,5 +1,8 @@
namespace RemoteExec.Client.Exceptions;
/// <summary>
/// Exception thrown when a remote method execution fails on the server.
/// </summary>
public class RemoteExecutionException(string message) : Exception(message)
{
}
@@ -1,6 +1,14 @@
namespace RemoteExec.Client;
/// <summary>
/// Defines a strategy for selecting a server from a pool of available servers for task distribution.
/// </summary>
public interface ILoadBalancingStrategy
{
/// <summary>
/// Selects the most appropriate server from the available servers based on the strategy's logic.
/// </summary>
/// <param name="availableServers">The collection of available servers to choose from.</param>
/// <returns>The selected server, or <c>null</c> if no suitable server is found.</returns>
ServerConnection? SelectServer(IEnumerable<ServerConnection> availableServers);
}
@@ -1,7 +1,11 @@
namespace RemoteExec.Client.LoadBalancingStrategies;
/// <summary>
/// A load balancing strategy that selects servers based on CPU usage, active tasks, and backlog.
/// </summary>
internal class ResourceAwareStrategy : ILoadBalancingStrategy
{
/// <inheritdoc/>
public ServerConnection? SelectServer(IEnumerable<ServerConnection> availableServers)
{
return availableServers.MinBy(s =>
@@ -4,6 +4,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -2,21 +2,33 @@
namespace RemoteExec.Client;
/// <summary>
/// Logger provider that forwards log messages to an existing logger instance.
/// </summary>
internal class RemoteExecLoggerProvider : ILoggerProvider
{
private bool disposedValue;
private readonly ILogger logger;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteExecLoggerProvider"/> class.
/// </summary>
/// <param name="logger">The logger instance to forward messages to.</param>
public RemoteExecLoggerProvider(ILogger logger)
{
this.logger = logger;
}
/// <inheritdoc/>
public ILogger CreateLogger(string categoryName)
{
return logger;
}
/// <summary>
/// Disposes the logger provider.
/// </summary>
/// <param name="disposing">True if disposing managed resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
@@ -26,6 +38,7 @@ internal class RemoteExecLoggerProvider : ILoggerProvider
}
}
/// <inheritdoc/>
public void Dispose()
{
Dispose(disposing: true);
@@ -10,6 +10,10 @@ namespace RemoteExec.Client;
public partial class RemoteExecutor
{
/// <summary>
/// Starts the remote executor, connecting to all configured servers and beginning task distribution.
/// </summary>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
public async Task StartAsync(CancellationToken cancellationToken = default)
{
// Dispose previous cancellation token source if exists
@@ -40,6 +44,10 @@ public partial class RemoteExecutor
distributorTask = Task.Run(() => DistributorLoop(distributorCts.Token), distributorCts.Token);
}
/// <summary>
/// Stops the remote executor, disconnecting from all servers and stopping task distribution.
/// </summary>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
public async Task StopAsync(CancellationToken cancellationToken = default)
{
logger.LogInformation("Stopping RemoteExecutor...");
+77
View File
@@ -11,6 +11,9 @@ using System.Text.Json;
namespace RemoteExec.Client;
/// <summary>
/// Manages remote execution of static methods across one or more server connections with load balancing and fault tolerance.
/// </summary>
public partial class RemoteExecutor : IAsyncDisposable
{
private readonly BlockingCollection<PendingTask> globalQueue;
@@ -27,20 +30,45 @@ public partial class RemoteExecutor : IAsyncDisposable
private bool disposedValue;
/// <summary>
/// Occurs when server metrics are updated.
/// </summary>
public event EventHandler<ServerMetricsUpdatedEventArgs>? MetricsUpdated;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteExecutor"/> class with a single server URL.
/// </summary>
/// <param name="url">The URL of the remote server.</param>
/// <param name="configure">An action to configure the executor options.</param>
public RemoteExecutor(string url, Action<RemoteExecutorOptions> configure) : this([url], NullLogger.Instance, configure)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteExecutor"/> class with a single server URL and logger.
/// </summary>
/// <param name="url">The URL of the remote server.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="configure">An action to configure the executor options.</param>
public RemoteExecutor(string url, ILogger logger, Action<RemoteExecutorOptions> configure) : this([url], logger, configure)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteExecutor"/> class with multiple server URLs.
/// </summary>
/// <param name="urls">The URLs of the remote servers.</param>
/// <param name="configure">An action to configure the executor options.</param>
public RemoteExecutor(string[] urls, Action<RemoteExecutorOptions> configure) : this(urls, NullLogger.Instance, configure)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteExecutor"/> class with multiple server URLs and logger.
/// </summary>
/// <param name="urls">The URLs of the remote servers.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="configure">An action to configure the executor options.</param>
public RemoteExecutor(string[] urls, ILogger logger, Action<RemoteExecutorOptions> configure)
{
this.logger = logger;
@@ -53,6 +81,12 @@ public partial class RemoteExecutor : IAsyncDisposable
InitializeServers(urls);
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteExecutor"/> class with preconfigured options.
/// </summary>
/// <param name="urls">The URLs of the remote servers.</param>
/// <param name="options">The executor options.</param>
/// <param name="logger">The logger instance.</param>
public RemoteExecutor(string[] urls, RemoteExecutorOptions options, ILogger logger)
{
this.logger = logger;
@@ -99,6 +133,10 @@ public partial class RemoteExecutor : IAsyncDisposable
}
}
/// <summary>
/// Gets the current metrics for all connected servers.
/// </summary>
/// <returns>A dictionary mapping server IDs to their metrics.</returns>
public Dictionary<string, ServerMetrics> GetCurrentServerMetrics()
{
return servers
@@ -107,6 +145,15 @@ public partial class RemoteExecutor : IAsyncDisposable
.ToDictionary(metrics => metrics!.ServerId, metrics => metrics!);
}
/// <summary>
/// Executes a delegate remotely and returns the strongly-typed result.
/// </summary>
/// <typeparam name="TDelegate">The delegate type.</typeparam>
/// <typeparam name="TResult">The return type.</typeparam>
/// <param name="delegate">The delegate to execute.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <param name="args">The arguments to pass to the method.</param>
/// <returns>The result of the remote execution.</returns>
public async Task<TResult> ExecuteAsync<TDelegate, TResult>(TDelegate @delegate, CancellationToken cancellationToken, params object[] args) where TDelegate : Delegate
{
object? execResult = await ExecuteAsync(@delegate, cancellationToken, args);
@@ -125,11 +172,29 @@ public partial class RemoteExecutor : IAsyncDisposable
}
}
/// <summary>
/// Executes a delegate remotely and returns the strongly-typed result.
/// </summary>
/// <typeparam name="TDelegate">The delegate type.</typeparam>
/// <typeparam name="TResult">The return type.</typeparam>
/// <param name="delegate">The delegate to execute.</param>
/// <param name="args">The arguments to pass to the method.</param>
/// <returns>The result of the remote execution.</returns>
public async Task<TResult> ExecuteAsync<TDelegate, TResult>(TDelegate @delegate, params object[] args) where TDelegate : Delegate
{
return await ExecuteAsync<TDelegate, TResult>(@delegate, CancellationToken.None, args);
}
/// <summary>
/// Executes a delegate remotely and returns the result.
/// </summary>
/// <typeparam name="T">The delegate type.</typeparam>
/// <param name="delegate">The delegate to execute. Must be a static method.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <param name="args">The arguments to pass to the method.</param>
/// <returns>The result of the remote execution.</returns>
/// <exception cref="InvalidOperationException">Thrown when the delegate is not a static method or uses a dynamic assembly.</exception>
/// <exception cref="RemoteExecutionException">Thrown when the remote execution fails.</exception>
public async Task<object?> ExecuteAsync<T>(T @delegate, CancellationToken cancellationToken, params object[] args) where T : Delegate
{
MethodInfo method = @delegate.Method;
@@ -189,11 +254,22 @@ public partial class RemoteExecutor : IAsyncDisposable
}
}
/// <summary>
/// Executes a delegate remotely and returns the result.
/// </summary>
/// <typeparam name="T">The delegate type.</typeparam>
/// <param name="delegate">The delegate to execute. Must be a static method.</param>
/// <param name="args">The arguments to pass to the method.</param>
/// <returns>The result of the remote execution.</returns>
public Task<object?> ExecuteAsync<T>(T @delegate, params object[] args) where T : Delegate
{
return ExecuteAsync(@delegate, CancellationToken.None, args);
}
/// <summary>
/// Asynchronously disposes the executor and its resources.
/// </summary>
/// <param name="disposing">True if disposing managed resources.</param>
protected virtual async Task DisposeAsync(bool disposing)
{
if (!disposedValue)
@@ -220,6 +296,7 @@ public partial class RemoteExecutor : IAsyncDisposable
}
}
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
await DisposeAsync(disposing: true);
+31 -4
View File
@@ -2,22 +2,49 @@
namespace RemoteExec.Client;
/// <summary>
/// Configuration options for the remote executor client.
/// </summary>
public class RemoteExecutorOptions
{
/// <summary>
/// Gets or sets the load balancing strategy used to select servers for task execution.
/// Default is <see cref="ResourceAwareStrategy"/>.
/// </summary>
public ILoadBalancingStrategy Strategy { get; set; } = new ResourceAwareStrategy();
/// <summary>
/// Gets or sets the API key used for authenticating with remote servers.
/// </summary>
public string ApiKey { get; set; } = string.Empty;
// How long to wait for a result before throwing a TimeoutException
/// <summary>
/// Gets or sets the maximum time to wait for a task result before throwing a TimeoutException.
/// Default is 5 minutes.
/// </summary>
public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMinutes(5);
// Backoff settings for server reconnections
/// <summary>
/// Gets or sets the initial delay before attempting to reconnect to a disconnected server.
/// Default is 5 seconds.
/// </summary>
public TimeSpan ServerReconnectInitialDelay { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Gets or sets the maximum delay between server reconnection attempts.
/// Default is 60 seconds.
/// </summary>
public TimeSpan ServerReconnectMaxDelay { get; set; } = TimeSpan.FromSeconds(60);
// Reliability: How many times a task can be requeued before failing
/// <summary>
/// Gets or sets the maximum number of times a failed task can be requeued before failing permanently.
/// Default is 3.
/// </summary>
public int MaxTaskRetries { get; set; } = 3;
// Capacity: Stop accepting Execute calls if the global queue is too full
/// <summary>
/// Gets or sets the maximum number of tasks that can be queued globally before blocking new execute calls.
/// Default is 1000.
/// </summary>
public int GlobalQueueCapacity { get; set; } = 1000;
}
+22
View File
@@ -6,11 +6,33 @@ using System.Threading.Channels;
namespace RemoteExec.Client;
/// <summary>
/// Represents a connection to a remote execution server, including communication channels and metrics.
/// </summary>
public class ServerConnection(HubConnection connection, HttpClient httpClient)
{
/// <summary>
/// Gets the SignalR hub connection to the server.
/// </summary>
public HubConnection Connection { get; } = connection;
/// <summary>
/// Gets the HTTP client for REST API communication with the server.
/// </summary>
public HttpClient HttpClient { get; } = httpClient;
/// <summary>
/// Gets the channel used to queue tasks for this server.
/// </summary>
public Channel<TaskItem> TaskChannel { get; } = Channel.CreateUnbounded<TaskItem>();
/// <summary>
/// Gets or sets the current metrics for this server.
/// </summary>
public ServerMetrics? Metrics { get; set; }
/// <summary>
/// Gets or sets the handler for connection closed events.
/// </summary>
public Func<Exception?, Task>? ClosedEventHandler { get; set; }
}
@@ -4,8 +4,18 @@ using RemoteExec.Shared;
namespace RemoteExec.Client;
/// <summary>
/// Provides data for the <see cref="RemoteExecutor.MetricsUpdated"/> event.
/// </summary>
public class ServerMetricsUpdatedEventArgs(HubConnection connection, ServerMetrics metrics) : EventArgs
{
/// <summary>
/// Gets the hub connection associated with the metrics update.
/// </summary>
public HubConnection Connection { get; } = connection;
/// <summary>
/// Gets the updated server metrics.
/// </summary>
public ServerMetrics Metrics { get; } = metrics;
}