save progress
This commit is contained in:
534
docs/router/transports/development.md
Normal file
534
docs/router/transports/development.md
Normal file
@@ -0,0 +1,534 @@
|
||||
# Transport Plugin Development Guide
|
||||
|
||||
This guide explains how to create custom router transport plugins for StellaOps.
|
||||
|
||||
## Overview
|
||||
|
||||
Router transport plugins implement the `IRouterTransportPlugin` interface to provide custom communication protocols. The plugin system enables:
|
||||
|
||||
- Runtime loading of transport implementations
|
||||
- Isolation from the Gateway codebase
|
||||
- Hot-swappable transports (restart required)
|
||||
- Third-party transport extensions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK
|
||||
- Understanding of async socket programming
|
||||
- Familiarity with dependency injection
|
||||
|
||||
## Creating a Transport Plugin
|
||||
|
||||
### Step 1: Create Project
|
||||
|
||||
```bash
|
||||
mkdir MyCompany.Router.Transport.Custom
|
||||
cd MyCompany.Router.Transport.Custom
|
||||
dotnet new classlib -f net10.0
|
||||
dotnet add package Microsoft.Extensions.Configuration.Binder
|
||||
dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions
|
||||
dotnet add package Microsoft.Extensions.Logging.Abstractions
|
||||
dotnet add package Microsoft.Extensions.Options
|
||||
```
|
||||
|
||||
Add project reference to Router.Common:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="path/to/StellaOps.Router.Common/StellaOps.Router.Common.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Step 2: Create Options Class
|
||||
|
||||
```csharp
|
||||
namespace MyCompany.Router.Transport.Custom;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the custom transport.
|
||||
/// </summary>
|
||||
public sealed class CustomTransportOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Host address to bind/connect to.
|
||||
/// </summary>
|
||||
public string Host { get; set; } = "0.0.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Port number.
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 5200;
|
||||
|
||||
/// <summary>
|
||||
/// Connection timeout.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum concurrent connections.
|
||||
/// </summary>
|
||||
public int MaxConnections { get; set; } = 1000;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Implement Transport Server
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
|
||||
namespace MyCompany.Router.Transport.Custom;
|
||||
|
||||
public sealed class CustomTransportServer : ITransportServer
|
||||
{
|
||||
private readonly CustomTransportOptions _options;
|
||||
private readonly ILogger<CustomTransportServer> _logger;
|
||||
|
||||
public CustomTransportServer(
|
||||
IOptions<CustomTransportOptions> options,
|
||||
ILogger<CustomTransportServer> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting custom transport server on {Host}:{Port}",
|
||||
_options.Host, _options.Port);
|
||||
|
||||
// Initialize your transport server (socket, listener, etc.)
|
||||
// ...
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Stopping custom transport server");
|
||||
|
||||
// Graceful shutdown
|
||||
// ...
|
||||
}
|
||||
|
||||
public async Task<ITransportConnection> AcceptAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Accept incoming connection
|
||||
// Return ITransportConnection implementation
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Implement Transport Client
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
|
||||
namespace MyCompany.Router.Transport.Custom;
|
||||
|
||||
public sealed class CustomTransportClient : ITransportClient, IMicroserviceTransport
|
||||
{
|
||||
private readonly CustomTransportOptions _options;
|
||||
private readonly ILogger<CustomTransportClient> _logger;
|
||||
|
||||
public CustomTransportClient(
|
||||
IOptions<CustomTransportOptions> options,
|
||||
ILogger<CustomTransportClient> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ITransportConnection> ConnectAsync(
|
||||
string host,
|
||||
int port,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Connecting to {Host}:{Port}", host, port);
|
||||
|
||||
// Establish connection
|
||||
// Return ITransportConnection implementation
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Disconnect from server
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Implement Connection
|
||||
|
||||
```csharp
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
|
||||
namespace MyCompany.Router.Transport.Custom;
|
||||
|
||||
public sealed class CustomTransportConnection : ITransportConnection
|
||||
{
|
||||
public string ConnectionId { get; } = Guid.NewGuid().ToString("N");
|
||||
public bool IsConnected { get; private set; }
|
||||
public EndPoint? RemoteEndPoint { get; private set; }
|
||||
|
||||
public async Task<int> SendAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Send data over transport
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<int> ReceiveAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Receive data from transport
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task CloseAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
IsConnected = false;
|
||||
// Close connection
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Implement Plugin
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
using StellaOps.Router.Common.Plugins;
|
||||
|
||||
namespace MyCompany.Router.Transport.Custom;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin implementation for custom transport.
|
||||
/// </summary>
|
||||
public sealed class CustomTransportPlugin : IRouterTransportPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string TransportName => "custom";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Custom Transport";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services)
|
||||
{
|
||||
// Check if required dependencies are available
|
||||
// Return false if transport cannot be used in current environment
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Register(RouterTransportRegistrationContext context)
|
||||
{
|
||||
var services = context.Services;
|
||||
var configuration = context.Configuration;
|
||||
|
||||
// Bind configuration
|
||||
var configSection = context.ConfigurationSection is not null
|
||||
? configuration.GetSection(context.ConfigurationSection)
|
||||
: configuration.GetSection("Router:Transport:Custom");
|
||||
|
||||
services.AddOptions<CustomTransportOptions>();
|
||||
if (configSection.GetChildren().Any())
|
||||
{
|
||||
services.Configure<CustomTransportOptions>(options =>
|
||||
{
|
||||
configSection.Bind(options);
|
||||
});
|
||||
}
|
||||
|
||||
// Register server if requested
|
||||
if (context.Mode.HasFlag(RouterTransportMode.Server))
|
||||
{
|
||||
services.AddSingleton<CustomTransportServer>();
|
||||
services.AddSingleton<ITransportServer>(sp =>
|
||||
sp.GetRequiredService<CustomTransportServer>());
|
||||
}
|
||||
|
||||
// Register client if requested
|
||||
if (context.Mode.HasFlag(RouterTransportMode.Client))
|
||||
{
|
||||
services.AddSingleton<CustomTransportClient>();
|
||||
services.AddSingleton<ITransportClient>(sp =>
|
||||
sp.GetRequiredService<CustomTransportClient>());
|
||||
services.AddSingleton<IMicroserviceTransport>(sp =>
|
||||
sp.GetRequiredService<CustomTransportClient>());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building and Packaging
|
||||
|
||||
### Build Configuration
|
||||
|
||||
Add to your `.csproj`:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>MyCompany.Router.Transport.Custom</RootNamespace>
|
||||
|
||||
<!-- Plugin assembly attributes -->
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Output to plugins directory -->
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<OutputPath>$(SolutionDir)plugins\router\transports\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Debug build
|
||||
dotnet build
|
||||
|
||||
# Release build to plugins directory
|
||||
dotnet build -c Release
|
||||
|
||||
# Publish with all dependencies
|
||||
dotnet publish -c Release -o ./publish
|
||||
```
|
||||
|
||||
## Configuration Schema
|
||||
|
||||
Create `plugin.json` manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "2.0",
|
||||
"id": "mycompany.router.transport.custom",
|
||||
"name": "Custom Transport",
|
||||
"version": "1.0.0",
|
||||
"assembly": {
|
||||
"path": "MyCompany.Router.Transport.Custom.dll",
|
||||
"entryType": "MyCompany.Router.Transport.Custom.CustomTransportPlugin"
|
||||
},
|
||||
"capabilities": ["server", "client", "streaming"],
|
||||
"platforms": ["linux-x64", "win-x64", "osx-arm64"],
|
||||
"enabled": true,
|
||||
"priority": 100
|
||||
}
|
||||
```
|
||||
|
||||
Create `config.yaml` for runtime configuration:
|
||||
|
||||
```yaml
|
||||
id: mycompany.router.transport.custom
|
||||
name: Custom Transport
|
||||
enabled: true
|
||||
config:
|
||||
host: "0.0.0.0"
|
||||
port: 5200
|
||||
maxConnections: 1000
|
||||
connectTimeout: "00:00:30"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```csharp
|
||||
public class CustomTransportPluginTests
|
||||
{
|
||||
[Fact]
|
||||
public void TransportName_ReturnsCustom()
|
||||
{
|
||||
var plugin = new CustomTransportPlugin();
|
||||
Assert.Equal("custom", plugin.TransportName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_AddsServerServices()
|
||||
{
|
||||
var plugin = new CustomTransportPlugin();
|
||||
var services = new ServiceCollection();
|
||||
var config = new ConfigurationBuilder().Build();
|
||||
|
||||
var context = new RouterTransportRegistrationContext(
|
||||
services, config, RouterTransportMode.Server);
|
||||
|
||||
plugin.Register(context);
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
Assert.NotNull(provider.GetService<ITransportServer>());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```csharp
|
||||
public class CustomTransportIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Server_AcceptsConnections()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.Configure<CustomTransportOptions>(opts =>
|
||||
{
|
||||
opts.Port = 15200; // Test port
|
||||
});
|
||||
services.AddSingleton<CustomTransportServer>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetRequiredService<CustomTransportServer>();
|
||||
|
||||
await server.StartAsync(CancellationToken.None);
|
||||
|
||||
// Connect client and verify
|
||||
// ...
|
||||
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Error Handling
|
||||
|
||||
```csharp
|
||||
public async Task<ITransportConnection> ConnectAsync(
|
||||
string host,
|
||||
int port,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Attempt connection
|
||||
return await ConnectInternalAsync(host, port, cancellationToken);
|
||||
}
|
||||
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
||||
{
|
||||
_logger.LogWarning("Connection refused to {Host}:{Port}", host, port);
|
||||
throw new TransportConnectionException($"Connection refused to {host}:{port}", ex);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogDebug("Connection attempt cancelled");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to connect to {Host}:{Port}", host, port);
|
||||
throw new TransportConnectionException($"Failed to connect to {host}:{port}", ex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Management
|
||||
|
||||
```csharp
|
||||
public sealed class CustomTransportServer : ITransportServer, IAsyncDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
||||
private readonly List<ITransportConnection> _connections = [];
|
||||
private bool _disposed;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
await _connectionLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
foreach (var conn in _connections)
|
||||
{
|
||||
await conn.CloseAsync(CancellationToken.None);
|
||||
conn.Dispose();
|
||||
}
|
||||
_connections.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionLock.Release();
|
||||
_connectionLock.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```csharp
|
||||
// Use structured logging
|
||||
_logger.LogInformation(
|
||||
"Connection established {ConnectionId} from {RemoteEndPoint}",
|
||||
connection.ConnectionId,
|
||||
connection.RemoteEndPoint);
|
||||
|
||||
// Include correlation IDs
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["ConnectionId"] = connectionId,
|
||||
["TraceId"] = Activity.Current?.TraceId.ToString() ?? "N/A"
|
||||
}))
|
||||
{
|
||||
await ProcessConnectionAsync(connection, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Copy to Plugins Directory
|
||||
|
||||
```bash
|
||||
cp ./publish/*.dll /opt/stellaops/plugins/router/transports/
|
||||
```
|
||||
|
||||
### Verify Plugin Loading
|
||||
|
||||
```bash
|
||||
# Check logs for plugin discovery
|
||||
grep "Loaded router transport plugin" /var/log/stellaops/gateway.log
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```yaml
|
||||
# router.yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: custom
|
||||
Custom:
|
||||
Host: "0.0.0.0"
|
||||
Port: 5200
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Transport Overview](./README.md)
|
||||
- [Plugin SDK Guide](../../10_PLUGIN_SDK_GUIDE.md)
|
||||
- [IRouterTransportPlugin API](../../../src/Router/__Libraries/StellaOps.Router.Common/Plugins/IRouterTransportPlugin.cs)
|
||||
Reference in New Issue
Block a user