Add unit tests for Router configuration and transport layers
- Implemented tests for RouterConfig, RoutingOptions, StaticInstanceConfig, and RouterConfigOptions to ensure default values are set correctly. - Added tests for RouterConfigProvider to validate configurations and ensure defaults are returned when no file is specified. - Created tests for ConfigValidationResult to check success and error scenarios. - Developed tests for ServiceCollectionExtensions to verify service registration for RouterConfig. - Introduced UdpTransportTests to validate serialization, connection, request-response, and error handling in UDP transport. - Added scripts for signing authority gaps and hashing DevPortal SDK snippets.
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using static StellaOps.Router.Common.Models.CancelReasons;
|
||||
|
||||
namespace StellaOps.Router.Transport.InMemory;
|
||||
|
||||
@@ -12,12 +13,13 @@ namespace StellaOps.Router.Transport.InMemory;
|
||||
/// In-memory transport client implementation for testing and development.
|
||||
/// Used by the Microservice SDK to send frames to the Gateway.
|
||||
/// </summary>
|
||||
public sealed class InMemoryTransportClient : ITransportClient, IDisposable
|
||||
public sealed class InMemoryTransportClient : ITransportClient, IMicroserviceTransport, IDisposable
|
||||
{
|
||||
private readonly InMemoryConnectionRegistry _registry;
|
||||
private readonly InMemoryTransportOptions _options;
|
||||
private readonly ILogger<InMemoryTransportClient> _logger;
|
||||
private readonly ConcurrentDictionary<string, TaskCompletionSource<Frame>> _pendingRequests = new();
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _inflightHandlers = new();
|
||||
private readonly CancellationTokenSource _clientCts = new();
|
||||
private bool _disposed;
|
||||
private string? _connectionId;
|
||||
@@ -172,29 +174,54 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var correlationId = frame.CorrelationId ?? Guid.NewGuid().ToString("N");
|
||||
|
||||
// Create a linked CancellationTokenSource for this handler
|
||||
// This allows cancellation via CANCEL frame or transport shutdown
|
||||
using var handlerCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_inflightHandlers[correlationId] = handlerCts;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await OnRequestReceived(frame, cancellationToken);
|
||||
var response = await OnRequestReceived(frame, handlerCts.Token);
|
||||
|
||||
// Ensure response has same correlation ID
|
||||
var responseFrame = response with { CorrelationId = frame.CorrelationId };
|
||||
await channel.ToGateway.Writer.WriteAsync(responseFrame, cancellationToken);
|
||||
var responseFrame = response with { CorrelationId = correlationId };
|
||||
|
||||
// Only send response if not cancelled
|
||||
if (!handlerCts.Token.IsCancellationRequested)
|
||||
{
|
||||
await channel.ToGateway.Writer.WriteAsync(responseFrame, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Not sending response for cancelled request {CorrelationId}", correlationId);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogDebug("Request {CorrelationId} was cancelled", frame.CorrelationId);
|
||||
_logger.LogDebug("Request {CorrelationId} was cancelled", correlationId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling request {CorrelationId}", frame.CorrelationId);
|
||||
// Send error response
|
||||
var errorFrame = new Frame
|
||||
_logger.LogError(ex, "Error handling request {CorrelationId}", correlationId);
|
||||
|
||||
// Only send error response if not cancelled
|
||||
if (!handlerCts.Token.IsCancellationRequested)
|
||||
{
|
||||
Type = FrameType.Response,
|
||||
CorrelationId = frame.CorrelationId,
|
||||
Payload = ReadOnlyMemory<byte>.Empty
|
||||
};
|
||||
await channel.ToGateway.Writer.WriteAsync(errorFrame, cancellationToken);
|
||||
var errorFrame = new Frame
|
||||
{
|
||||
Type = FrameType.Response,
|
||||
CorrelationId = correlationId,
|
||||
Payload = ReadOnlyMemory<byte>.Empty
|
||||
};
|
||||
await channel.ToGateway.Writer.WriteAsync(errorFrame, cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Remove from inflight tracking
|
||||
_inflightHandlers.TryRemove(correlationId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,13 +231,27 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
|
||||
|
||||
_logger.LogDebug("Received CANCEL for correlation {CorrelationId}", frame.CorrelationId);
|
||||
|
||||
// Cancel the inflight handler via its CancellationTokenSource
|
||||
if (_inflightHandlers.TryGetValue(frame.CorrelationId, out var handlerCts))
|
||||
{
|
||||
try
|
||||
{
|
||||
handlerCts.Cancel();
|
||||
_logger.LogInformation("Cancelled handler for request {CorrelationId}", frame.CorrelationId);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Handler already completed
|
||||
}
|
||||
}
|
||||
|
||||
// Complete any pending request with cancellation
|
||||
if (_pendingRequests.TryRemove(frame.CorrelationId, out var tcs))
|
||||
{
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
|
||||
// Notify handler
|
||||
// Notify external handler (for custom cancellation logic)
|
||||
if (OnCancelReceived is not null && Guid.TryParse(frame.CorrelationId, out var correlationGuid))
|
||||
{
|
||||
_ = OnCancelReceived(correlationGuid, null);
|
||||
@@ -381,6 +422,33 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
|
||||
await channel.ToGateway.Writer.WriteAsync(frame, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all in-flight handler requests.
|
||||
/// Called when connection is closed or transport is shutting down.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason for cancellation.</param>
|
||||
public void CancelAllInflight(string reason)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var kvp in _inflightHandlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
kvp.Value.Cancel();
|
||||
count++;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Already completed/disposed
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
_logger.LogInformation("Cancelled {Count} in-flight handlers: {Reason}", count, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from the transport.
|
||||
/// </summary>
|
||||
@@ -388,6 +456,9 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
|
||||
{
|
||||
if (_connectionId is null) return;
|
||||
|
||||
// Cancel all inflight handlers before disconnecting
|
||||
CancelAllInflight(CancelReasons.Shutdown);
|
||||
|
||||
await _clientCts.CancelAsync();
|
||||
|
||||
if (_receiveTask is not null)
|
||||
@@ -407,6 +478,9 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// Cancel all inflight handlers
|
||||
CancelAllInflight(Shutdown);
|
||||
|
||||
_clientCts.Cancel();
|
||||
|
||||
foreach (var tcs in _pendingRequests.Values)
|
||||
@@ -414,6 +488,7 @@ public sealed class InMemoryTransportClient : ITransportClient, IDisposable
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
_pendingRequests.Clear();
|
||||
_inflightHandlers.Clear();
|
||||
|
||||
if (_connectionId is not null)
|
||||
{
|
||||
|
||||
@@ -35,6 +35,7 @@ public static class ServiceCollectionExtensions
|
||||
// Register interfaces
|
||||
services.TryAddSingleton<ITransportServer>(sp => sp.GetRequiredService<InMemoryTransportServer>());
|
||||
services.TryAddSingleton<ITransportClient>(sp => sp.GetRequiredService<InMemoryTransportClient>());
|
||||
services.TryAddSingleton<IMicroserviceTransport>(sp => sp.GetRequiredService<InMemoryTransportClient>());
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -81,6 +82,7 @@ public static class ServiceCollectionExtensions
|
||||
services.TryAddSingleton<InMemoryConnectionRegistry>();
|
||||
services.TryAddSingleton<InMemoryTransportClient>();
|
||||
services.TryAddSingleton<ITransportClient>(sp => sp.GetRequiredService<InMemoryTransportClient>());
|
||||
services.TryAddSingleton<IMicroserviceTransport>(sp => sp.GetRequiredService<InMemoryTransportClient>());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user