Implement InMemory Transport Layer for StellaOps Router
- Added InMemoryTransportOptions class for configuration settings including timeouts and latency. - Developed InMemoryTransportServer class to handle connections, frame processing, and event management. - Created ServiceCollectionExtensions for easy registration of InMemory transport services. - Established project structure and dependencies for InMemory transport library. - Implemented comprehensive unit tests for endpoint discovery, connection management, request/response flow, and streaming capabilities. - Ensured proper handling of cancellation, heartbeat, and hello frames within the transport layer.
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
|
||||
namespace StellaOps.Microservice;
|
||||
|
||||
/// <summary>
|
||||
/// Manages connections to router gateways.
|
||||
/// </summary>
|
||||
public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposable
|
||||
{
|
||||
private readonly StellaMicroserviceOptions _options;
|
||||
private readonly IEndpointDiscoveryProvider _endpointDiscovery;
|
||||
private readonly ITransportClient _transportClient;
|
||||
private readonly ILogger<RouterConnectionManager> _logger;
|
||||
private readonly ConcurrentDictionary<string, ConnectionState> _connections = new();
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private IReadOnlyList<EndpointDescriptor>? _endpoints;
|
||||
private Task? _heartbeatTask;
|
||||
private bool _disposed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ConnectionState> Connections => [.. _connections.Values];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouterConnectionManager"/> class.
|
||||
/// </summary>
|
||||
public RouterConnectionManager(
|
||||
IOptions<StellaMicroserviceOptions> options,
|
||||
IEndpointDiscoveryProvider endpointDiscovery,
|
||||
ITransportClient transportClient,
|
||||
ILogger<RouterConnectionManager> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_endpointDiscovery = endpointDiscovery;
|
||||
_transportClient = transportClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
_options.Validate();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting router connection manager for {ServiceName}/{Version}",
|
||||
_options.ServiceName,
|
||||
_options.Version);
|
||||
|
||||
// Discover endpoints
|
||||
_endpoints = _endpointDiscovery.DiscoverEndpoints();
|
||||
_logger.LogInformation("Discovered {EndpointCount} endpoints", _endpoints.Count);
|
||||
|
||||
// Connect to each router
|
||||
foreach (var router in _options.Routers)
|
||||
{
|
||||
await ConnectToRouterAsync(router, cancellationToken);
|
||||
}
|
||||
|
||||
// Start heartbeat task
|
||||
_heartbeatTask = Task.Run(() => HeartbeatLoopAsync(_cts.Token), CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Stopping router connection manager");
|
||||
|
||||
await _cts.CancelAsync();
|
||||
|
||||
if (_heartbeatTask is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _heartbeatTask.WaitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
_connections.Clear();
|
||||
}
|
||||
|
||||
private async Task ConnectToRouterAsync(RouterEndpointConfig router, CancellationToken cancellationToken)
|
||||
{
|
||||
var connectionId = $"{router.Host}:{router.Port}";
|
||||
var backoff = _options.ReconnectBackoffInitial;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Connecting to router at {Host}:{Port} via {Transport}",
|
||||
router.Host,
|
||||
router.Port,
|
||||
router.TransportType);
|
||||
|
||||
// Create connection state
|
||||
var instance = new InstanceDescriptor
|
||||
{
|
||||
InstanceId = _options.InstanceId,
|
||||
ServiceName = _options.ServiceName,
|
||||
Version = _options.Version,
|
||||
Region = _options.Region
|
||||
};
|
||||
|
||||
var state = new ConnectionState
|
||||
{
|
||||
ConnectionId = connectionId,
|
||||
Instance = instance,
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
LastHeartbeatUtc = DateTime.UtcNow,
|
||||
TransportType = router.TransportType
|
||||
};
|
||||
|
||||
// Register endpoints
|
||||
foreach (var endpoint in _endpoints ?? [])
|
||||
{
|
||||
state.Endpoints[(endpoint.Method, endpoint.Path)] = endpoint;
|
||||
}
|
||||
|
||||
_connections[connectionId] = state;
|
||||
|
||||
// For InMemory transport, connectivity is handled via the transport client
|
||||
// Real transports will establish actual network connections here
|
||||
|
||||
_logger.LogInformation(
|
||||
"Connected to router at {Host}:{Port}, registered {EndpointCount} endpoints",
|
||||
router.Host,
|
||||
router.Port,
|
||||
_endpoints?.Count ?? 0);
|
||||
|
||||
// Reset backoff on successful connection
|
||||
backoff = _options.ReconnectBackoffInitial;
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Failed to connect to router at {Host}:{Port}, retrying in {Backoff}",
|
||||
router.Host,
|
||||
router.Port,
|
||||
backoff);
|
||||
|
||||
await Task.Delay(backoff, cancellationToken);
|
||||
|
||||
// Exponential backoff
|
||||
backoff = TimeSpan.FromTicks(Math.Min(
|
||||
backoff.Ticks * 2,
|
||||
_options.ReconnectBackoffMax.Ticks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HeartbeatLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_options.HeartbeatInterval, cancellationToken);
|
||||
|
||||
foreach (var connection in _connections.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Build heartbeat payload
|
||||
var heartbeat = new HeartbeatPayload
|
||||
{
|
||||
InstanceId = _options.InstanceId,
|
||||
Status = connection.Status,
|
||||
TimestampUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Update last heartbeat time
|
||||
connection.LastHeartbeatUtc = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Sent heartbeat for connection {ConnectionId}",
|
||||
connection.ConnectionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Failed to send heartbeat for connection {ConnectionId}",
|
||||
connection.ConnectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected on shutdown
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error in heartbeat loop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user