Improve load balancing and move to pull based model

This commit is contained in:
Stone_Red
2025-12-21 14:18:35 +01:00
parent 27558ba3f2
commit 8d5bb1eff4
8 changed files with 344 additions and 149 deletions
+2 -5
View File
@@ -2,9 +2,6 @@
public enum LoadBalancingStrategy public enum LoadBalancingStrategy
{ {
RoundRobin, ResourceAware,
Random, LeastBacklog
LeastConnections,
LeastActiveTasks,
ResourceAware
} }
+220 -123
View File
@@ -1,92 +1,135 @@
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using RemoteExec.Shared; using RemoteExec.Shared;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Threading.Channels; using System.Threading.Channels;
namespace RemoteExec.Client; namespace RemoteExec.Client;
public class RemoteExecutor public class RemoteExecutor : IDisposable
{ {
private readonly List<HubConnection> connections = []; private readonly List<ServerConnection> servers = [];
private readonly ConcurrentDictionary<HubConnection, ServerMetrics> serverMetrics = new(); private readonly BlockingCollection<PendingTask> globalQueue = [];
private int _currentConnectionIndex = 0; private readonly ConcurrentDictionary<Guid, TaskCompletionSource<RemoteExecutionResult>> pendingResults = new();
private readonly Lock @lock = new Lock(); private CancellationTokenSource distributorCts = new();
private Task? distributorTask;
private readonly LoadBalancingStrategy loadBalancingStrategy; private readonly LoadBalancingStrategy loadBalancingStrategy;
private bool disposedValue;
public event EventHandler<ServerMetricsUpdatedEventArgs>? MetricsUpdated; public event EventHandler<ServerMetricsUpdatedEventArgs>? MetricsUpdated;
public RemoteExecutor(string url) : this([url], LoadBalancingStrategy.RoundRobin) public RemoteExecutor(string url) : this([url], LoadBalancingStrategy.ResourceAware)
{ {
} }
public RemoteExecutor(string[] urls, LoadBalancingStrategy loadBalancingStrategy = LoadBalancingStrategy.RoundRobin) public RemoteExecutor(string[] urls, LoadBalancingStrategy loadBalancingStrategy = LoadBalancingStrategy.ResourceAware)
{ {
this.loadBalancingStrategy = loadBalancingStrategy; this.loadBalancingStrategy = loadBalancingStrategy;
foreach (string url in urls) foreach (string url in urls)
{ {
Uri baseUri = new(url);
Uri signalRUri = new(baseUri, "/remote");
HubConnection connection = new HubConnectionBuilder() HubConnection connection = new HubConnectionBuilder()
.WithUrl(url) .WithUrl(signalRUri)
.WithAutomaticReconnect() .WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
_ = logging.AddProvider(new RemoteExecLoggerProvider());
})
.Build(); .Build();
connections.Add(connection); HttpClient httpClient = new()
{
BaseAddress = baseUri
};
ServerConnection serverConnection = new ServerConnection(connection, httpClient);
servers.Add(serverConnection);
} }
} }
public async Task StartAsync(CancellationToken cancellationToken = default) public async Task StartAsync(CancellationToken cancellationToken = default)
{ {
distributorCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
List<Task> startTasks = []; List<Task> startTasks = [];
foreach (HubConnection connection in connections) foreach (ServerConnection server in servers)
{ {
_ = connection.On<ServerMetrics>("MetricsUpdated", metrics => _ = server.Connection.On<ServerMetrics>("MetricsUpdated", metrics =>
{ {
serverMetrics[connection] = metrics; server.Metrics = metrics;
MetricsUpdated?.Invoke(this, new ServerMetricsUpdatedEventArgs(connection, metrics)); MetricsUpdated?.Invoke(this, new ServerMetricsUpdatedEventArgs(server.Connection, metrics));
}); });
_ = connection.On($"RequestAssembly", async (string assemblyName, Guid requestId) => _ = server.Connection.On<Guid, RemoteExecutionResult>("TaskResult", (taskId, result) =>
{
if (pendingResults.TryRemove(taskId, out TaskCompletionSource<RemoteExecutionResult>? tcs))
{
tcs.SetResult(result);
}
});
_ = server.Connection.On($"RequestAssembly", async (string assemblyName, Guid requestId) =>
{ {
Assembly? assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == assemblyName) ?? Assembly.Load(new AssemblyName(assemblyName)); Assembly? assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().FullName == assemblyName) ?? Assembly.Load(new AssemblyName(assemblyName));
byte[] dllBytes = await File.ReadAllBytesAsync(assembly.Location!); byte[] dllBytes = await File.ReadAllBytesAsync(assembly.Location!);
Channel<byte> channel = Channel.CreateUnbounded<byte>(); ByteArrayContent content = new(dllBytes);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
foreach (byte b in dllBytes) _ = await server.HttpClient.PostAsync($"/provide-assembly?requestId={requestId}", content);
{
await channel.Writer.WriteAsync(b);
}
channel.Writer.Complete();
await connection.InvokeAsync("ProvideAssembly", requestId, channel.Reader);
}); });
startTasks.Add(connection.StartAsync(cancellationToken) startTasks.Add(server.Connection.StartAsync(cancellationToken)
.ContinueWith(async (task, state) => .ContinueWith(async (task, state) =>
{ {
HubConnection conn = (HubConnection)state!; ServerConnection conn = (ServerConnection)state!;
serverMetrics[conn] = await conn.InvokeAsync<ServerMetrics>("GetMetrics", cancellationToken); conn.Metrics = await conn.Connection.InvokeAsync<ServerMetrics>("GetMetrics", cancellationToken);
MetricsUpdated?.Invoke(this, new ServerMetricsUpdatedEventArgs(conn, serverMetrics[conn])); MetricsUpdated?.Invoke(this, new ServerMetricsUpdatedEventArgs(conn.Connection, conn.Metrics));
}, connection, TaskScheduler.Default).Unwrap());
await conn.Connection.SendAsync("StartTaskStream", conn.TaskChannel.Reader, cancellationToken);
}, server, TaskScheduler.Default).Unwrap());
} }
await Task.WhenAll(startTasks); await Task.WhenAll(startTasks);
distributorTask = Task.Run(() => DistributorLoop(distributorCts.Token), distributorCts.Token);
} }
public async Task StopAsync(CancellationToken cancellationToken = default) public async Task StopAsync(CancellationToken cancellationToken = default)
{ {
await distributorCts.CancelAsync();
if (distributorTask != null)
{
try
{
await distributorTask;
}
catch (OperationCanceledException)
{
// Expected
}
}
foreach (ServerConnection server in servers)
{
server.TaskChannel.Writer.Complete();
}
List<Task> stopTasks = []; List<Task> stopTasks = [];
foreach (HubConnection connection in connections) foreach (ServerConnection server in servers)
{ {
stopTasks.Add(connection.StopAsync(cancellationToken)); stopTasks.Add(server.Connection.StopAsync(cancellationToken));
} }
await Task.WhenAll(stopTasks); await Task.WhenAll(stopTasks);
@@ -94,52 +137,15 @@ public class RemoteExecutor
public Dictionary<string, ServerMetrics> GetCurrentServerMetrics() public Dictionary<string, ServerMetrics> GetCurrentServerMetrics()
{ {
Dictionary<string, ServerMetrics> metrics = []; return servers
.Select(server => server.Metrics)
foreach (HubConnection connection in connections) .Where(metrics => metrics != null)
{ .ToDictionary(metrics => metrics!.ServerId, metrics => metrics!);
if (serverMetrics.TryGetValue(connection, out ServerMetrics? newServerMetrics))
{
metrics[newServerMetrics.ServerId] = newServerMetrics;
}
}
return metrics;
} }
public bool TryExecute<TDelegate, TResult>(TDelegate del, out TResult? result, params object[] args) where TDelegate : Delegate public async Task<TResult> Execute<TDelegate, TResult>(TDelegate del, params object[] args) where TDelegate : Delegate
{ {
try object? execResult = await Execute(del, args);
{
result = Execute<TDelegate, TResult>(del, args);
return true;
}
catch
{
result = default;
return false;
}
}
public bool TryExecute<TDelegate, TResult>(TDelegate del, out TResult? result, [NotNullWhen(false)] out Exception? exception, params object[] args) where TDelegate : Delegate
{
try
{
result = Execute<TDelegate, TResult>(del, args);
exception = null;
return true;
}
catch (Exception ex)
{
result = default;
exception = ex;
return false;
}
}
public TResult Execute<TDelegate, TResult>(TDelegate del, params object[] args) where TDelegate : Delegate
{
object? execResult = Execute(del, args);
if (execResult is TResult typedResult) if (execResult is TResult typedResult)
{ {
@@ -155,7 +161,7 @@ public class RemoteExecutor
} }
} }
public object? Execute<T>(T del, params object[] args) where T : Delegate public async Task<object?> Execute<T>(T del, params object[] args) where T : Delegate
{ {
MethodInfo method = del.Method; MethodInfo method = del.Method;
Type declaringType = method.DeclaringType!; Type declaringType = method.DeclaringType!;
@@ -175,12 +181,20 @@ public class RemoteExecutor
Arguments = args Arguments = args
}; };
HubConnection connection = GetNextConnection(); Guid taskId = Guid.NewGuid();
TaskCompletionSource<RemoteExecutionResult> tcs = new();
pendingResults[taskId] = tcs;
RemoteExecutionResult result = connection PendingTask pendingTask = new PendingTask
.InvokeAsync<RemoteExecutionResult>("Execute", request) {
.GetAwaiter() TaskId = taskId,
.GetResult(); Request = request,
EnqueuedAt = DateTime.UtcNow
};
globalQueue.Add(pendingTask);
RemoteExecutionResult result = await tcs.Task;
if (result.Exception != null) if (result.Exception != null)
{ {
@@ -190,62 +204,145 @@ public class RemoteExecutor
return result.Result; return result.Result;
} }
private HubConnection GetNextConnection() private async Task DistributorLoop(CancellationToken cancellationToken)
{ {
if (connections.Count == 0) while (!cancellationToken.IsCancellationRequested)
{ {
throw new InvalidOperationException("No connections available"); try
{
// Check if there are tasks in the global queue
PendingTask pendingTask = globalQueue.Take(cancellationToken);
// Select the best server based on metrics
ServerConnection? bestServer = SelectBestServer();
if (bestServer != null)
{
// Create task item with ID
TaskItem taskItem = new TaskItem
{
TaskId = pendingTask.TaskId,
Request = pendingTask.Request
};
// Push to server's channel - SignalR will stream it
await bestServer.TaskChannel.Writer.WriteAsync(taskItem, cancellationToken);
}
else
{
// No available server, re-enqueue
globalQueue.Add(pendingTask, cancellationToken);
await Task.Delay(100, cancellationToken);
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Debug.WriteLine($"DistributorLoop exception: {ex}");
await Task.Delay(100, cancellationToken);
}
}
}
private ServerConnection? SelectBestServer()
{
if (servers.Count == 0)
{
return null;
} }
return loadBalancingStrategy switch return loadBalancingStrategy switch
{ {
LoadBalancingStrategy.RoundRobin => GetRoundRobinConnection(), LoadBalancingStrategy.ResourceAware => servers.MinBy(s =>
LoadBalancingStrategy.Random => GetRandomConnection(), {
LoadBalancingStrategy.LeastConnections => GetLeastConnections(), if (s.Metrics == null)
LoadBalancingStrategy.LeastActiveTasks => GetLeastActiveTasksConnections(), {
LoadBalancingStrategy.ResourceAware => GetResourceAwareConnections(), return double.MaxValue;
_ => throw new NotSupportedException($"Load balancing strategy {loadBalancingStrategy} is not supported") }
double cpuScore = s.Metrics.CpuUsage;
double activeTaskScore = s.Metrics.ActiveTasks * 10;
double backlogScore = s.TaskChannel.Reader.Count * 50;
return cpuScore + activeTaskScore + backlogScore;
}),
LoadBalancingStrategy.LeastBacklog => servers.MinBy(s => s.TaskChannel.Reader.Count),
_ => servers.FirstOrDefault()
}; };
} }
private HubConnection GetRoundRobinConnection() private sealed class ServerConnection(HubConnection connection, HttpClient httpClient)
{ {
lock (@lock) public HubConnection Connection { get; } = connection;
public HttpClient HttpClient { get; } = httpClient;
public Channel<TaskItem> TaskChannel { get; } = Channel.CreateUnbounded<TaskItem>();
public ServerMetrics? Metrics { get; set; }
}
private sealed class PendingTask
{
public required Guid TaskId { get; init; }
public required RemoteExecutionRequest Request { get; init; }
public required DateTime EnqueuedAt { get; init; }
}
// S2930: Dispose distributorCts when no longer needed
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{ {
HubConnection connection = connections[_currentConnectionIndex]; if (disposing)
_currentConnectionIndex = (_currentConnectionIndex + 1) % connections.Count; {
return connection; distributorCts.Dispose();
// Dispose managed state (managed objects) here if needed
}
// Free unmanaged resources (unmanaged objects) and override finalizer if needed
// Set large fields to null if needed
disposedValue = true;
} }
} }
private HubConnection GetRandomConnection() public void Dispose()
{ {
int index = Random.Shared.Next(connections.Count); // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
return connections[index]; Dispose(disposing: true);
} GC.SuppressFinalize(this);
private HubConnection GetLeastConnections()
{
// Purely looks at how many clients are connected to the Hub
return connections.OrderBy(c => serverMetrics.TryGetValue(c, out ServerMetrics? m) ? m.ActiveConnections : 0).First();
}
private HubConnection GetLeastActiveTasksConnections()
{
return connections.OrderBy(c => serverMetrics.TryGetValue(c, out ServerMetrics? m) ? m.ActiveTasks : 0).First();
}
private HubConnection GetResourceAwareConnections()
{
return connections.OrderBy(c =>
{
if (!serverMetrics.TryGetValue(c, out ServerMetrics? m))
{
return 0;
}
// Simple heuristic: CPU percentage + (Memory in MB / 1024)
return m.CpuUsage + (m.TotalMemoryUsage / 1024 / 1024 / 100);
}).First();
} }
} }
internal class RemoteExecLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new RemoteExecLogger();
}
public void Dispose()
{
throw new NotImplementedException();
}
}
internal class RemoteExecLogger : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
Console.WriteLine($"[{logLevel}] {formatter(state, exception)}");
}
}
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using RemoteExec.Server.Hubs;
namespace RemoteExec.Server.Controllers;
[ApiController]
[Route("/")]
public class AssemblyController(ILogger<AssemblyController> logger) : ControllerBase
{
[HttpPost("provide-assembly")]
public async Task<IActionResult> ProvideAssembly([FromQuery] Guid requestId)
{
try
{
using MemoryStream ms = new MemoryStream();
await Request.Body.CopyToAsync(ms);
byte[] assemblyBytes = ms.ToArray();
logger.LogInformation("Received assembly for request {RequestId}, size: {Size} bytes", requestId, assemblyBytes.Length);
await RemoteExecutionHub.ProvideAssembly(requestId, assemblyBytes);
return Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Error providing assembly for request {RequestId}", requestId);
return StatusCode(500, ex.Message);
}
}
}
+75 -14
View File
@@ -6,7 +6,6 @@ using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Threading.Channels;
namespace RemoteExec.Server.Hubs; namespace RemoteExec.Server.Hubs;
@@ -16,10 +15,13 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
private static readonly ConcurrentDictionary<Guid, TaskCompletionSource<byte[]>> pendingAssemblyRequests = new(); private static readonly ConcurrentDictionary<Guid, TaskCompletionSource<byte[]>> pendingAssemblyRequests = new();
private static ServerMetrics? lastMetrics;
private static DateTime lastMetricsTimestamp; private static DateTime lastMetricsTimestamp;
private static TimeSpan lastTotalProcessorTime; private static TimeSpan lastTotalProcessorTime;
private static int activeTasks = 0; private static int activeTasks = 0;
private static readonly int maxConcurrentTasks = Environment.ProcessorCount * 2;
private static readonly SemaphoreSlim taskSemaphore = new SemaphoreSlim(maxConcurrentTasks, maxConcurrentTasks);
public override Task OnConnectedAsync() public override Task OnConnectedAsync()
{ {
@@ -43,10 +45,62 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
return base.OnDisconnectedAsync(exception); return base.OnDisconnectedAsync(exception);
} }
public async Task StartTaskStream(IAsyncEnumerable<TaskItem> taskStream)
{
logger.LogInformation("Starting task stream for connection {ConnectionId}", Context.ConnectionId);
try
{
await foreach (TaskItem taskItem in taskStream.WithCancellation(Context.ConnectionAborted))
{
// Wait for available slot before processing
await taskSemaphore.WaitAsync(Context.ConnectionAborted);
// Process task asynchronously without blocking the stream
_ = Task.Run(async () =>
{
try
{
RemoteExecutionResult result = await ExecuteTask(taskItem.Request);
await Clients.Caller.SendAsync("TaskResult", taskItem.TaskId, result);
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing task {TaskId}", taskItem.TaskId);
RemoteExecutionResult errorResult = new RemoteExecutionResult
{
Exception = ex.ToString()
};
await Clients.Caller.SendAsync("TaskResult", taskItem.TaskId, errorResult);
}
finally
{
_ = taskSemaphore.Release();
}
}, Context.ConnectionAborted);
}
}
catch (OperationCanceledException ex)
{
logger.LogError(ex, "Task stream for connection {ConnectionId} was canceled", Context.ConnectionId);
}
catch (Exception ex)
{
logger.LogError(ex, "Error in task stream for connection {ConnectionId}", Context.ConnectionId);
}
}
public async Task<RemoteExecutionResult> Execute(RemoteExecutionRequest req) public async Task<RemoteExecutionResult> Execute(RemoteExecutionRequest req)
{
return await ExecuteTask(req);
}
private async Task<RemoteExecutionResult> ExecuteTask(RemoteExecutionRequest req)
{ {
_ = Interlocked.Increment(ref activeTasks); _ = Interlocked.Increment(ref activeTasks);
logger.LogInformation(req.MethodName);
try try
{ {
if (!connections.TryGetValue(Context.ConnectionId, out RemoteJobAssemblyLoadContext? assemblyLoadContext)) if (!connections.TryGetValue(Context.ConnectionId, out RemoteJobAssemblyLoadContext? assemblyLoadContext))
@@ -155,20 +209,8 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
} }
} }
public async Task ProvideAssembly(Guid requestId, ChannelReader<byte> stream) public static async Task ProvideAssembly(Guid requestId, byte[] assemblyBytes)
{ {
using MemoryStream ms = new MemoryStream();
while (await stream.WaitToReadAsync())
{
while (stream.TryRead(out byte item))
{
ms.WriteByte(item);
}
}
byte[] assemblyBytes = ms.ToArray();
if (pendingAssemblyRequests.TryRemove(requestId, out TaskCompletionSource<byte[]>? tcs)) if (pendingAssemblyRequests.TryRemove(requestId, out TaskCompletionSource<byte[]>? tcs))
{ {
tcs.SetResult(assemblyBytes); tcs.SetResult(assemblyBytes);
@@ -183,6 +225,24 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
public static async Task BroadcastMetricsAsync(IHubContext<RemoteExecutionHub> hubContext) public static async Task BroadcastMetricsAsync(IHubContext<RemoteExecutionHub> hubContext)
{ {
ServerMetrics metrics = await GetServerMetrics(); ServerMetrics metrics = await GetServerMetrics();
// Only broadcast if metrics have changed by a significant amount
if (lastMetrics is not null)
{
double cpuDiff = Math.Abs(metrics.CpuUsage - lastMetrics.CpuUsage);
long memoryDiff = Math.Abs(metrics.TotalMemoryUsage - lastMetrics.TotalMemoryUsage);
int connectionsDiff = Math.Abs(metrics.ActiveConnections - lastMetrics.ActiveConnections);
int tasksDiff = Math.Abs(metrics.ActiveTasks - lastMetrics.ActiveTasks);
int maxTasksDiff = Math.Abs(metrics.MaxConcurrentTasks - lastMetrics.MaxConcurrentTasks);
if (cpuDiff < 1.0 && memoryDiff < 10 * 1024 * 1024 && connectionsDiff == 0 && tasksDiff == 0 && maxTasksDiff == 0)
{
return; // No significant change
}
}
lastMetrics = metrics;
await hubContext.Clients.All.SendAsync("MetricsUpdated", metrics); await hubContext.Clients.All.SendAsync("MetricsUpdated", metrics);
} }
@@ -211,6 +271,7 @@ public class RemoteExecutionHub(ILogger<RemoteExecutionHub> logger) : Hub
ServerId = Environment.MachineName, ServerId = Environment.MachineName,
ActiveConnections = connections.Count, ActiveConnections = connections.Count,
ActiveTasks = activeTasks, ActiveTasks = activeTasks,
MaxConcurrentTasks = maxConcurrentTasks,
TotalMemoryUsage = currentProcess.WorkingSet64, TotalMemoryUsage = currentProcess.WorkingSet64,
CpuUsage = Math.Clamp(Math.Round(cpuUsagePercent, 2), 0, 100), CpuUsage = Math.Clamp(Math.Round(cpuUsagePercent, 2), 0, 100),
Timestamp = currentTime Timestamp = currentTime
@@ -6,7 +6,7 @@ namespace RemoteExec.Server.Services;
public class MetricsBroadcastService(IHubContext<RemoteExecutionHub> hubContext, ILogger<MetricsBroadcastService> logger) : BackgroundService public class MetricsBroadcastService(IHubContext<RemoteExecutionHub> hubContext, ILogger<MetricsBroadcastService> logger) : BackgroundService
{ {
private readonly TimeSpan _broadcastInterval = TimeSpan.FromSeconds(2); private readonly TimeSpan _broadcastInterval = TimeSpan.FromMilliseconds(500);
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
+1
View File
@@ -4,6 +4,7 @@ public sealed class ServerMetrics
{ {
public int ActiveConnections { get; set; } public int ActiveConnections { get; set; }
public int ActiveTasks { get; set; } public int ActiveTasks { get; set; }
public int MaxConcurrentTasks { get; set; }
public long TotalMemoryUsage { get; set; } public long TotalMemoryUsage { get; set; }
public double CpuUsage { get; set; } public double CpuUsage { get; set; }
public DateTime Timestamp { get; set; } public DateTime Timestamp { get; set; }
+7
View File
@@ -0,0 +1,7 @@
namespace RemoteExec.Shared;
public class TaskItem
{
public Guid TaskId { get; set; }
public required RemoteExecutionRequest Request { get; set; }
}
+6 -6
View File
@@ -17,16 +17,16 @@ singleHostExecutor.MetricsUpdated += (sender, e) =>
await singleHostExecutor.StartAsync(); await singleHostExecutor.StartAsync();
bool success = singleHostExecutor.TryExecute(Multiply, out int result, 2, 4); await Parallel.ForAsync(0, 1000, async (i, cancellationToken) =>
Console.WriteLine($"Success: {success}, Result: {result}"); {
int r = await singleHostExecutor.Execute<Func<int, int, Task<int>>, int>(Multiply, i, i + 1);
result = singleHostExecutor.Execute<Func<int, int, Task<int>>, int>(Multiply, 3, 5); Console.WriteLine($"Multiply {i} * {i + 1} = {r}");
Console.WriteLine($"Result: {result}"); });
await singleHostExecutor.StopAsync(); await singleHostExecutor.StopAsync();
static async Task<int> Multiply(int x, int y) static async Task<int> Multiply(int x, int y)
{ {
await Task.Delay(5000); await Task.Delay(Random.Shared.Next(100, 500));
return x.Multiply(y); return x.Multiply(y);
} }