mirror of
https://github.com/Stone-Red-Code/RemoteExec.git
synced 2026-09-04 00:56:17 +02:00
Improve configuration of RemoteExecutor
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RemoteExec.Client\RemoteExec.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
// In RemoteExec.Client.DependencyInjection (Extension Project)
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace RemoteExec.Client.DependencyInjection;
|
||||
|
||||
public static class RemoteExecutorServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddRemoteExecutor(this IServiceCollection services, string[] urls, Action<RemoteExecutorOptions>? configure = null)
|
||||
{
|
||||
RemoteExecutorOptions options = new RemoteExecutorOptions();
|
||||
configure?.Invoke(options);
|
||||
|
||||
_ = services.AddSingleton(options);
|
||||
|
||||
_ = services.AddSingleton(sp =>
|
||||
{
|
||||
RemoteExecutorOptions opt = sp.GetRequiredService<RemoteExecutorOptions>();
|
||||
ILogger<RemoteExecutor> logger = sp.GetRequiredService<ILogger<RemoteExecutor>>();
|
||||
|
||||
return new RemoteExecutor(urls, opt, logger);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace RemoteExec.Client.Exceptions;
|
||||
|
||||
public class MaxRetriesException(string message) : Exception(message)
|
||||
{
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace RemoteExec.Client;
|
||||
namespace RemoteExec.Client.Exceptions;
|
||||
|
||||
public class RemoteExecutionException(string message) : Exception(message)
|
||||
{
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace RemoteExec.Client;
|
||||
|
||||
public interface ILoadBalancingStrategy
|
||||
{
|
||||
ServerConnection? SelectServer(IEnumerable<ServerConnection> availableServers);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RemoteExec.Client.LoadBalancingStrategies;
|
||||
|
||||
public class LeastBacklogStrategy : ILoadBalancingStrategy
|
||||
{
|
||||
public ServerConnection? SelectServer(IEnumerable<ServerConnection> availableServers)
|
||||
{
|
||||
return availableServers.MinBy(s => s.TaskChannel.Reader.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace RemoteExec.Client.LoadBalancingStrategies;
|
||||
|
||||
internal class ResourceAwareStrategy : ILoadBalancingStrategy
|
||||
{
|
||||
public ServerConnection? SelectServer(IEnumerable<ServerConnection> availableServers)
|
||||
{
|
||||
return availableServers.MinBy(s =>
|
||||
{
|
||||
if (s.Metrics == null)
|
||||
{
|
||||
return double.MaxValue;
|
||||
}
|
||||
|
||||
double cpuScore = s.Metrics.CpuUsage;
|
||||
double activeTaskScore = s.Metrics.ActiveTasks * 10;
|
||||
double backlogScore = s.TaskChannel.Reader.Count * 50;
|
||||
|
||||
return cpuScore + activeTaskScore + backlogScore;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace RemoteExec.Client;
|
||||
|
||||
public enum LoadBalancingStrategy
|
||||
{
|
||||
ResourceAware,
|
||||
LeastBacklog
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using RemoteExec.Client.Exceptions;
|
||||
using RemoteExec.Shared;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
@@ -92,8 +93,8 @@ public partial class RemoteExecutor
|
||||
|
||||
private async Task AttemptReconnectionAsync(ServerConnection server, CancellationToken cancellationToken)
|
||||
{
|
||||
int retryDelay = 5000; // Start with 5 seconds
|
||||
int maxRetryDelay = 60000; // Max 1 minute
|
||||
int retryDelay = (int)options.ServerReconnectInitialDelay.TotalMilliseconds;
|
||||
int maxRetryDelay = (int)options.ServerReconnectMaxDelay.TotalMilliseconds;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested && server.Connection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
@@ -151,8 +152,17 @@ public partial class RemoteExecutor
|
||||
{
|
||||
if (taskDict.TryRemove(id, out PendingTask? task) && pendingResults.ContainsKey(id))
|
||||
{
|
||||
globalQueue.Add(task);
|
||||
count++;
|
||||
task.RetryCount++;
|
||||
|
||||
if (task.RetryCount <= options.MaxTaskRetries)
|
||||
{
|
||||
globalQueue.Add(task);
|
||||
count++;
|
||||
}
|
||||
else if (pendingResults.TryRemove(id, out TaskCompletionSource<RemoteExecutionResult>? tcs))
|
||||
{
|
||||
tcs.SetException(new MaxRetriesException("Max retry attempts reached."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
using RemoteExec.Shared;
|
||||
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace RemoteExec.Client;
|
||||
|
||||
public partial class RemoteExecutor
|
||||
{
|
||||
private sealed class ServerConnection(HubConnection connection, HttpClient httpClient)
|
||||
{
|
||||
public HubConnection Connection { get; } = connection;
|
||||
public HttpClient HttpClient { get; } = httpClient;
|
||||
public Channel<TaskItem> TaskChannel { get; } = Channel.CreateUnbounded<TaskItem>();
|
||||
public ServerMetrics? Metrics { get; set; }
|
||||
public Func<Exception?, Task>? ClosedEventHandler { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PendingTask
|
||||
{
|
||||
public required Guid TaskId { get; init; }
|
||||
public required RemoteExecutionRequest Request { get; init; }
|
||||
public required DateTime EnqueuedAt { get; init; }
|
||||
public int RetryCount { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
@@ -79,24 +79,6 @@ public partial class RemoteExecutor
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadBalancingStrategy switch
|
||||
{
|
||||
LoadBalancingStrategy.ResourceAware => connectedServers.MinBy(s =>
|
||||
{
|
||||
if (s.Metrics == null)
|
||||
{
|
||||
return double.MaxValue;
|
||||
}
|
||||
|
||||
double cpuScore = s.Metrics.CpuUsage;
|
||||
double activeTaskScore = s.Metrics.ActiveTasks * 10;
|
||||
double backlogScore = s.TaskChannel.Reader.Count * 50;
|
||||
|
||||
return cpuScore + activeTaskScore + backlogScore;
|
||||
}),
|
||||
LoadBalancingStrategy.LeastBacklog => connectedServers.MinBy(s => s.TaskChannel.Reader.Count),
|
||||
|
||||
_ => connectedServers.FirstOrDefault()
|
||||
};
|
||||
return options.Strategy.SelectServer(connectedServers);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
using RemoteExec.Client.Exceptions;
|
||||
using RemoteExec.Shared;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
@@ -12,8 +13,8 @@ namespace RemoteExec.Client;
|
||||
|
||||
public partial class RemoteExecutor : IAsyncDisposable
|
||||
{
|
||||
private readonly BlockingCollection<PendingTask> globalQueue;
|
||||
private readonly List<ServerConnection> servers = [];
|
||||
private readonly BlockingCollection<PendingTask> globalQueue = [];
|
||||
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<RemoteExecutionResult>> pendingResults = new();
|
||||
private readonly ConcurrentDictionary<ServerConnection, ConcurrentDictionary<Guid, PendingTask>> serverAssignedTasks = new();
|
||||
|
||||
@@ -21,38 +22,65 @@ public partial class RemoteExecutor : IAsyncDisposable
|
||||
private CancellationTokenSource distributorCts = new();
|
||||
private Task? distributorTask;
|
||||
|
||||
private readonly LoadBalancingStrategy loadBalancingStrategy;
|
||||
private readonly RemoteExecutorOptions options = new();
|
||||
private readonly ILogger logger;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
public event EventHandler<ServerMetricsUpdatedEventArgs>? MetricsUpdated;
|
||||
|
||||
public RemoteExecutor(string url) : this([url], LoadBalancingStrategy.ResourceAware, NullLogger.Instance)
|
||||
public RemoteExecutor(string url) : this([url], new RemoteExecutorOptions(), NullLogger.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteExecutor(string url, ILogger logger) : this([url], LoadBalancingStrategy.ResourceAware, logger)
|
||||
public RemoteExecutor(string url, ILogger logger) : this([url], new RemoteExecutorOptions(), logger)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteExecutor(string url, LoadBalancingStrategy loadBalancingStrategy) : this([url], loadBalancingStrategy, NullLogger.Instance)
|
||||
public RemoteExecutor(string url, Action<RemoteExecutorOptions> configure) : this([url], NullLogger.Instance, configure)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteExecutor(string[] urls) : this(urls, LoadBalancingStrategy.ResourceAware, NullLogger.Instance)
|
||||
public RemoteExecutor(string url, ILogger logger, Action<RemoteExecutorOptions> configure) : this([url], logger, configure)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteExecutor(string[] urls, LoadBalancingStrategy loadBalancingStrategy) : this(urls, loadBalancingStrategy, NullLogger.Instance)
|
||||
public RemoteExecutor(string[] urls) : this(urls, new RemoteExecutorOptions(), NullLogger.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteExecutor(string[] urls, LoadBalancingStrategy loadBalancingStrategy, ILogger logger)
|
||||
public RemoteExecutor(string[] urls, ILogger logger) : this(urls, new RemoteExecutorOptions(), logger)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteExecutor(string[] urls, Action<RemoteExecutorOptions> configure) : this(urls, NullLogger.Instance, configure)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteExecutor(string[] urls, ILogger logger, Action<RemoteExecutorOptions> configure)
|
||||
{
|
||||
this.loadBalancingStrategy = loadBalancingStrategy;
|
||||
this.logger = logger;
|
||||
|
||||
options = new RemoteExecutorOptions();
|
||||
globalQueue = new BlockingCollection<PendingTask>(options.GlobalQueueCapacity);
|
||||
|
||||
configure(options);
|
||||
|
||||
InitializeServers(urls);
|
||||
}
|
||||
|
||||
public RemoteExecutor(string[] urls, RemoteExecutorOptions options, ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.options = options;
|
||||
|
||||
globalQueue = new BlockingCollection<PendingTask>(this.options.GlobalQueueCapacity);
|
||||
|
||||
InitializeServers(urls);
|
||||
}
|
||||
|
||||
private void InitializeServers(string[] urls)
|
||||
{
|
||||
foreach (string url in urls)
|
||||
{
|
||||
Uri baseUri = new(url);
|
||||
@@ -85,9 +113,9 @@ public partial class RemoteExecutor : IAsyncDisposable
|
||||
.ToDictionary(metrics => metrics!.ServerId, metrics => metrics!);
|
||||
}
|
||||
|
||||
public async Task<TResult> Execute<TDelegate, TResult>(TDelegate del, params object[] args) where TDelegate : Delegate
|
||||
public async Task<TResult> ExecuteAsync<TDelegate, TResult>(TDelegate @delegate, CancellationToken cancellationToken, params object[] args) where TDelegate : Delegate
|
||||
{
|
||||
object? execResult = await Execute(del, args);
|
||||
object? execResult = await ExecuteAsync(@delegate, cancellationToken, args);
|
||||
|
||||
if (execResult is TResult typedResult)
|
||||
{
|
||||
@@ -103,12 +131,21 @@ public partial class RemoteExecutor : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object?> Execute<T>(T del, params object[] args) where T : Delegate
|
||||
public async Task<TResult> ExecuteAsync<TDelegate, TResult>(TDelegate @delegate, params object[] args) where TDelegate : Delegate
|
||||
{
|
||||
MethodInfo method = del.Method;
|
||||
return await ExecuteAsync<TDelegate, TResult>(@delegate, CancellationToken.None, args);
|
||||
}
|
||||
|
||||
public async Task<object?> ExecuteAsync<T>(T @delegate, CancellationToken cancellationToken, params object[] args) where T : Delegate
|
||||
{
|
||||
MethodInfo method = @delegate.Method;
|
||||
Type declaringType = method.DeclaringType!;
|
||||
Assembly assembly = declaringType.Assembly;
|
||||
|
||||
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, distributorCts.Token);
|
||||
using CancellationTokenSource timeoutCts = new CancellationTokenSource(options.ExecutionTimeout);
|
||||
using CancellationTokenSource finalCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, timeoutCts.Token);
|
||||
|
||||
if (!method.IsStatic)
|
||||
{
|
||||
throw new InvalidOperationException("Only static methods supported");
|
||||
@@ -141,14 +178,26 @@ public partial class RemoteExecutor : IAsyncDisposable
|
||||
|
||||
globalQueue.Add(pendingTask);
|
||||
|
||||
RemoteExecutionResult result = await tcs.Task;
|
||||
|
||||
if (result.Exception != null)
|
||||
try
|
||||
{
|
||||
throw new RemoteExecutionException(result.Exception);
|
||||
}
|
||||
RemoteExecutionResult result = await tcs.Task.WaitAsync(finalCts.Token);
|
||||
|
||||
return result.Result;
|
||||
if (result.Exception != null)
|
||||
{
|
||||
throw new RemoteExecutionException(result.Exception);
|
||||
}
|
||||
|
||||
return result.Result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = pendingResults.TryRemove(taskId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<object?> ExecuteAsync<T>(T @delegate, params object[] args) where T : Delegate
|
||||
{
|
||||
return ExecuteAsync(@delegate, CancellationToken.None, args);
|
||||
}
|
||||
|
||||
protected virtual async Task DisposeAsync(bool disposing)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using RemoteExec.Client.LoadBalancingStrategies;
|
||||
|
||||
namespace RemoteExec.Client;
|
||||
|
||||
public class RemoteExecutorOptions
|
||||
{
|
||||
public ILoadBalancingStrategy Strategy { get; set; } = new ResourceAwareStrategy();
|
||||
|
||||
// How long to wait for a result before throwing a TimeoutException
|
||||
public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
// Backoff settings for server reconnections
|
||||
public TimeSpan ServerReconnectInitialDelay { get; set; } = TimeSpan.FromSeconds(5);
|
||||
public TimeSpan ServerReconnectMaxDelay { get; set; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
// Reliability: How many times a task can be requeued before failing
|
||||
public int MaxTaskRetries { get; set; } = 3;
|
||||
|
||||
// Capacity: Stop accepting Execute calls if the global queue is too full
|
||||
public int GlobalQueueCapacity { get; set; } = 1000;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
using RemoteExec.Shared;
|
||||
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace RemoteExec.Client;
|
||||
|
||||
public class ServerConnection(HubConnection connection, HttpClient httpClient)
|
||||
{
|
||||
public HubConnection Connection { get; } = connection;
|
||||
public HttpClient HttpClient { get; } = httpClient;
|
||||
public Channel<TaskItem> TaskChannel { get; } = Channel.CreateUnbounded<TaskItem>();
|
||||
public ServerMetrics? Metrics { get; set; }
|
||||
public Func<Exception?, Task>? ClosedEventHandler { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<Solution>
|
||||
<Project Path="RemoteExec.Client.DependencyInjection/RemoteExec.Client.DependencyInjection.csproj" Id="e9895828-9e57-475e-98ea-2afff271b7e9" />
|
||||
<Project Path="RemoteExec.Client/RemoteExec.Client.csproj" Id="8ac1533b-f69a-4b1b-bb3a-fb381cd4bb7b" />
|
||||
<Project Path="RemoteExec.Server/RemoteExec.Server.csproj" Id="f88cb8bb-8221-4e5e-880a-0c58e1255602" />
|
||||
<Project Path="RemoteExec.Shared/RemoteExec.Shared.csproj" Id="99317a9c-c0ad-42a2-a830-56aa9c6b9dd4" />
|
||||
|
||||
@@ -19,7 +19,7 @@ await singleHostExecutor.StartAsync();
|
||||
|
||||
await Parallel.ForAsync(0, 1000, async (i, cancellationToken) =>
|
||||
{
|
||||
int r = await singleHostExecutor.Execute<Func<int, int, Task<int>>, int>(Multiply, i, i + 1);
|
||||
int r = await singleHostExecutor.ExecuteAsync<Func<int, int, Task<int>>, int>(Multiply, i, i + 1);
|
||||
Console.WriteLine($"Multiply {i} * {i + 1} = {r}");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user