Files
git.stella-ops.org/docs/router/transports/development.md

535 lines
13 KiB
Markdown

# 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)