docs consolidation and others
This commit is contained in:
@@ -91,14 +91,14 @@ StellaOps.Router.slnx
|
||||
| [aspnet-endpoint-bridge.md](aspnet-endpoint-bridge.md) | Using ASP.NET endpoint registration as Router endpoint registration |
|
||||
| [messaging-valkey-transport.md](messaging-valkey-transport.md) | Messaging transport over Valkey |
|
||||
|
||||
### Implementation Guides (docs/router/)
|
||||
### Implementation Guides (docs/modules/router/guides/)
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [README.md](../../router/README.md) | Quick start and feature overview |
|
||||
| [ARCHITECTURE.md](../../router/ARCHITECTURE.md) | Detailed architecture walkthrough |
|
||||
| [GETTING_STARTED.md](../../router/GETTING_STARTED.md) | Step-by-step setup guide |
|
||||
| [rate-limiting.md](../../router/rate-limiting.md) | Rate limiting configuration guide |
|
||||
| [transports/](../../router/transports/) | Transport plugin documentation |
|
||||
| [README.md](guides/README.md) | Quick start and feature overview |
|
||||
| [ARCHITECTURE.md](guides/ARCHITECTURE.md) | Detailed architecture walkthrough |
|
||||
| [GETTING_STARTED.md](guides/GETTING_STARTED.md) | Step-by-step setup guide |
|
||||
| [rate-limiting-config.md](guides/rate-limiting-config.md) | Rate limiting configuration guide |
|
||||
| [transports.md](guides/transports.md) | Transport plugin documentation |
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
370
docs/modules/router/guides/GETTING_STARTED.md
Normal file
370
docs/modules/router/guides/GETTING_STARTED.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Getting Started with StellaOps Router
|
||||
|
||||
This guide walks you through building your first microservice with the StellaOps Router framework.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK
|
||||
- Docker (optional, for containerized deployment)
|
||||
|
||||
## Step 1: Create the Microservice Project
|
||||
|
||||
```bash
|
||||
mkdir MyService
|
||||
cd MyService
|
||||
dotnet new console -n MyService
|
||||
cd MyService
|
||||
```
|
||||
|
||||
Add the required packages:
|
||||
|
||||
```bash
|
||||
dotnet add package StellaOps.Microservice
|
||||
dotnet add package StellaOps.Router.Transport.InMemory
|
||||
```
|
||||
|
||||
## Step 2: Define Your Data Models
|
||||
|
||||
Create `Models/OrderModels.cs`:
|
||||
|
||||
```csharp
|
||||
namespace MyService.Models;
|
||||
|
||||
public sealed class CreateOrderRequest
|
||||
{
|
||||
public required string CustomerId { get; init; }
|
||||
public required decimal Amount { get; init; }
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CreateOrderResponse
|
||||
{
|
||||
public required string OrderId { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public string Status { get; init; } = "created";
|
||||
}
|
||||
|
||||
public sealed class Order
|
||||
{
|
||||
public required string OrderId { get; init; }
|
||||
public required string CustomerId { get; init; }
|
||||
public required decimal Amount { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Create Endpoints
|
||||
|
||||
Create `Endpoints/CreateOrderEndpoint.cs`:
|
||||
|
||||
```csharp
|
||||
using MyService.Models;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace MyService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new order.
|
||||
/// </summary>
|
||||
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
|
||||
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
|
||||
{
|
||||
private readonly ILogger<CreateOrderEndpoint> _logger;
|
||||
|
||||
public CreateOrderEndpoint(ILogger<CreateOrderEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<CreateOrderResponse> HandleAsync(
|
||||
CreateOrderRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orderId = $"ORD-{Guid.NewGuid():N}"[..16];
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created order {OrderId} for customer {CustomerId}, amount: {Amount}",
|
||||
orderId, request.CustomerId, request.Amount);
|
||||
|
||||
return Task.FromResult(new CreateOrderResponse
|
||||
{
|
||||
OrderId = orderId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create `Endpoints/GetOrderEndpoint.cs`:
|
||||
|
||||
```csharp
|
||||
using MyService.Models;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace MyService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an order by ID.
|
||||
/// </summary>
|
||||
[StellaEndpoint("GET", "/orders/{id}", TimeoutSeconds = 10)]
|
||||
public sealed class GetOrderEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
private readonly ILogger<GetOrderEndpoint> _logger;
|
||||
|
||||
public GetOrderEndpoint(ILogger<GetOrderEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<RawResponse> HandleAsync(
|
||||
RawRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orderId = context.PathParameters["id"];
|
||||
|
||||
_logger.LogInformation("Fetching order {OrderId}", orderId);
|
||||
|
||||
// In a real app, you'd look up the order from a database
|
||||
var order = new Order
|
||||
{
|
||||
OrderId = orderId,
|
||||
CustomerId = "CUST-001",
|
||||
Amount = 99.99m,
|
||||
Description = "Sample order",
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddHours(-1)
|
||||
};
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(order);
|
||||
var body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));
|
||||
|
||||
var headers = new HeaderCollection();
|
||||
headers.Set("Content-Type", "application/json");
|
||||
|
||||
return Task.FromResult(new RawResponse
|
||||
{
|
||||
StatusCode = 200,
|
||||
Headers = headers,
|
||||
Body = body
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Configure the Service
|
||||
|
||||
Update `Program.cs`:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MyService.Endpoints;
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Configure logging
|
||||
builder.Logging.AddConsole();
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||
|
||||
// Configure the microservice
|
||||
builder.Services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "order-service";
|
||||
options.Version = "1.0.0";
|
||||
options.Region = "local";
|
||||
options.InstanceId = $"order-{Environment.MachineName}";
|
||||
|
||||
// Connect to gateway (use InMemory for development)
|
||||
options.Routers =
|
||||
[
|
||||
new RouterEndpointConfig
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 5100,
|
||||
TransportType = TransportType.InMemory
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
// Register transport
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
// Register endpoint handlers
|
||||
builder.Services.AddScoped<CreateOrderEndpoint>();
|
||||
builder.Services.AddScoped<GetOrderEndpoint>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
Console.WriteLine("Order Service starting...");
|
||||
Console.WriteLine("Endpoints:");
|
||||
Console.WriteLine(" POST /orders");
|
||||
Console.WriteLine(" GET /orders/{id}");
|
||||
|
||||
await host.RunAsync();
|
||||
```
|
||||
|
||||
## Step 5: Create the Gateway
|
||||
|
||||
Create a separate project for the gateway:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
dotnet new web -n MyGateway
|
||||
cd MyGateway
|
||||
dotnet add package StellaOps.Router.Gateway
|
||||
dotnet add package StellaOps.Router.Transport.InMemory
|
||||
```
|
||||
|
||||
Update `Program.cs`:
|
||||
|
||||
```csharp
|
||||
using StellaOps.Router.Gateway;
|
||||
using StellaOps.Router.Gateway.Authorization;
|
||||
using StellaOps.Router.Gateway.DependencyInjection;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add gateway services
|
||||
builder.Services.AddRouterGateway(builder.Configuration);
|
||||
|
||||
// Add InMemory transport for development
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
// No-op authorization for demo (use real auth in production)
|
||||
builder.Services.AddNoOpAuthorityIntegration();
|
||||
builder.Services.AddAuthentication();
|
||||
|
||||
// OpenAPI
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Middleware pipeline
|
||||
app.UseAuthentication();
|
||||
app.UseClaimsAuthorization();
|
||||
|
||||
// OpenAPI endpoint (aggregates from all microservices)
|
||||
app.MapRouterOpenApi();
|
||||
|
||||
// Health check
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));
|
||||
|
||||
// Router gateway handles all other routes
|
||||
app.UseRouterGateway();
|
||||
|
||||
Console.WriteLine("Gateway starting on http://localhost:5000");
|
||||
app.Run("http://localhost:5000");
|
||||
```
|
||||
|
||||
## Step 6: Run the Services
|
||||
|
||||
Open two terminals:
|
||||
|
||||
**Terminal 1 - Gateway:**
|
||||
```bash
|
||||
cd MyGateway
|
||||
dotnet run
|
||||
```
|
||||
|
||||
**Terminal 2 - Microservice:**
|
||||
```bash
|
||||
cd MyService
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Step 7: Test the API
|
||||
|
||||
Create an order:
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/orders \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"customerId": "CUST-001", "amount": 99.99}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"orderId": "ORD-abc123def456",
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"status": "created"
|
||||
}
|
||||
```
|
||||
|
||||
Get an order:
|
||||
```bash
|
||||
curl http://localhost:5000/orders/ORD-abc123def456
|
||||
```
|
||||
|
||||
## Step 8: Add Validation (Optional)
|
||||
|
||||
Add JSON Schema validation to your endpoints:
|
||||
|
||||
```csharp
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Microservice.Validation;
|
||||
|
||||
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
|
||||
[ValidateSchema] // Enables JSON Schema validation
|
||||
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The source generator will create a JSON Schema based on your request type's properties and validate incoming requests automatically.
|
||||
|
||||
## Step 9: Add Streaming Support (Optional)
|
||||
|
||||
For large file uploads or real-time data:
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("POST", "/orders/{id}/documents", SupportsStreaming = true)]
|
||||
public sealed class UploadDocumentEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
public async Task<RawResponse> HandleAsync(
|
||||
RawRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var orderId = context.PathParameters["id"];
|
||||
var contentType = context.Headers["Content-Type"] ?? "application/octet-stream";
|
||||
|
||||
// Stream the body directly without buffering
|
||||
await using var bodyStream = context.Body;
|
||||
var totalBytes = 0L;
|
||||
|
||||
var buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await bodyStream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
totalBytes += bytesRead;
|
||||
// Process chunk...
|
||||
}
|
||||
|
||||
var headers = new HeaderCollection();
|
||||
headers.Set("Content-Type", "application/json");
|
||||
|
||||
var response = $$"""{"orderId":"{{orderId}}","bytesReceived":{{totalBytes}}}""";
|
||||
return new RawResponse
|
||||
{
|
||||
StatusCode = 200,
|
||||
Headers = headers,
|
||||
Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(response))
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Production Deployment**: Switch from InMemory to TCP or TLS transport
|
||||
- **Authentication**: Integrate with the Authority module for OAuth/OIDC
|
||||
- **Rate Limiting**: Configure rate limits in router.yaml
|
||||
- **Observability**: Add OpenTelemetry tracing
|
||||
- **Testing**: Write integration tests using the Router.Testing library
|
||||
|
||||
See the [examples](../examples/) directory for complete working examples.
|
||||
122
docs/modules/router/guides/rate-limiting-config.md
Normal file
122
docs/modules/router/guides/rate-limiting-config.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Router Rate Limiting
|
||||
|
||||
Router rate limiting is a **gateway-owned** control plane feature implemented in `StellaOps.Router.Gateway`. It enforces limits centrally so microservices do not implement ad-hoc HTTP throttling.
|
||||
|
||||
## Behavior
|
||||
|
||||
When a request is denied the Router returns:
|
||||
- `429 Too Many Requests`
|
||||
- `Retry-After: <seconds>`
|
||||
- `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (Unix seconds)
|
||||
- JSON body:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "rate_limit_exceeded",
|
||||
"message": "Rate limit exceeded. Try again in 12 seconds.",
|
||||
"retryAfter": 12,
|
||||
"limit": 100,
|
||||
"current": 101,
|
||||
"window": 60,
|
||||
"scope": "environment"
|
||||
}
|
||||
```
|
||||
|
||||
## Model
|
||||
|
||||
Two scopes exist:
|
||||
- **Instance (`for_instance`)**: in-memory sliding window; protects a single Router process.
|
||||
- **Environment (`for_environment`)**: Valkey-backed fixed window; protects the whole environment across Router instances.
|
||||
|
||||
Environment checks are gated by an **activation threshold** (`process_back_pressure_when_more_than_per_5min`) to avoid unnecessary Valkey calls at low traffic.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is under the `rate_limiting` root.
|
||||
|
||||
### Minimal (instance only)
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
process_back_pressure_when_more_than_per_5min: 5000
|
||||
|
||||
for_instance:
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
```
|
||||
|
||||
### Environment (Valkey)
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
process_back_pressure_when_more_than_per_5min: 0 # always check environment
|
||||
|
||||
for_environment:
|
||||
valkey_connection: "valkey.stellaops.local:6379"
|
||||
valkey_bucket: "stella-router-rate-limit"
|
||||
|
||||
circuit_breaker:
|
||||
failure_threshold: 5
|
||||
timeout_seconds: 30
|
||||
half_open_timeout: 10
|
||||
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
```
|
||||
|
||||
### Rule stacking (AND logic)
|
||||
|
||||
Multiple rules on the same target are evaluated with **AND** semantics:
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
for_environment:
|
||||
rules:
|
||||
- per_seconds: 1
|
||||
max_requests: 10
|
||||
- per_seconds: 3600
|
||||
max_requests: 3000
|
||||
```
|
||||
|
||||
If any rule is exceeded the request is denied. The Router returns the **most restrictive** `Retry-After` among violated rules.
|
||||
|
||||
### Microservice overrides
|
||||
|
||||
Overrides are **replacement**, not merge:
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
for_environment:
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
|
||||
microservices:
|
||||
scanner:
|
||||
rules:
|
||||
- per_seconds: 10
|
||||
max_requests: 50
|
||||
```
|
||||
|
||||
### Route overrides
|
||||
|
||||
Route-level configuration is under:
|
||||
|
||||
`rate_limiting.for_environment.microservices.<microservice>.routes.<route_name>`
|
||||
|
||||
See `docs/modules/router/guides/rate-limiting-routes.md` for match types and specificity rules.
|
||||
|
||||
## Notes
|
||||
|
||||
- If `rules` is present, it takes precedence over legacy single-window keys (`per_seconds`, `max_requests`, `allow_*`).
|
||||
- For allowed requests, headers represent the **smallest window** rule for deterministic, low-cardinality output (not a full multi-rule snapshot).
|
||||
- If Valkey is unavailable, environment limiting is **fail-open** (instance limits still apply).
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests: `dotnet test StellaOps.Router.slnx -c Release`
|
||||
- Valkey integration tests (Docker required): `STELLAOPS_INTEGRATION_TESTS=true dotnet test StellaOps.Router.slnx -c Release --filter FullyQualifiedName~ValkeyRateLimitStoreIntegrationTests`
|
||||
- k6 load tests: `tests/load/router-rate-limiting-load-test.js` (see `tests/load/README.md`)
|
||||
|
||||
90
docs/modules/router/guides/rate-limiting-routes.md
Normal file
90
docs/modules/router/guides/rate-limiting-routes.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Per-Route Rate Limiting (Router)
|
||||
|
||||
This document describes **per-route** rate limiting configuration for the Router gateway (`StellaOps.Router.Gateway`).
|
||||
|
||||
## Overview
|
||||
|
||||
Per-route rate limiting lets you apply different limits to specific HTTP paths **within the same microservice**.
|
||||
|
||||
Configuration is nested as:
|
||||
|
||||
`rate_limiting.for_environment.microservices.<microservice>.routes.<route_name>`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example (rules + routes)
|
||||
|
||||
```yaml
|
||||
rate_limiting:
|
||||
for_environment:
|
||||
valkey_connection: "valkey.stellaops.local:6379"
|
||||
valkey_bucket: "stella-router-rate-limit"
|
||||
|
||||
# Default environment rules (used when no microservice override exists)
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
|
||||
microservices:
|
||||
scanner:
|
||||
# Default rules for the microservice (used when no route override exists)
|
||||
rules:
|
||||
- per_seconds: 60
|
||||
max_requests: 600
|
||||
|
||||
routes:
|
||||
scan_submit:
|
||||
pattern: "/api/scans"
|
||||
match_type: exact
|
||||
rules:
|
||||
- per_seconds: 10
|
||||
max_requests: 50
|
||||
|
||||
scan_status:
|
||||
pattern: "/api/scans/*"
|
||||
match_type: prefix
|
||||
rules:
|
||||
- per_seconds: 1
|
||||
max_requests: 100
|
||||
|
||||
scan_by_id:
|
||||
pattern: "^/api/scans/[a-f0-9-]+$"
|
||||
match_type: regex
|
||||
rules:
|
||||
- per_seconds: 1
|
||||
max_requests: 50
|
||||
```
|
||||
|
||||
### Match types
|
||||
|
||||
`match_type` supports:
|
||||
|
||||
- `exact`: exact path match (case-insensitive), ignoring a trailing `/`.
|
||||
- `prefix`: literal prefix match; patterns commonly end with `*` (e.g. `/api/scans/*`).
|
||||
- `regex`: regular expression (compiled at startup; invalid regex fails fast).
|
||||
|
||||
### Specificity rules
|
||||
|
||||
When multiple routes match a path, the most specific match wins:
|
||||
|
||||
1. `exact`
|
||||
2. `prefix` (longest prefix wins)
|
||||
3. `regex` (longest pattern wins)
|
||||
|
||||
## Inheritance (resolution)
|
||||
|
||||
Rate limiting rules resolve with **replacement** semantics:
|
||||
|
||||
- `routes.<route_name>.rules` replaces the microservice rules.
|
||||
- `microservices.<name>.rules` replaces the environment rules.
|
||||
- If a level provides no rules, the next-less-specific level applies.
|
||||
|
||||
## Notes
|
||||
|
||||
- Per-route rate limiting applies at the **environment** scope (Valkey-backed).
|
||||
- The Router returns `429 Too Many Requests` and a `Retry-After` header when a limit is exceeded.
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/modules/router/guides/rate-limiting-config.md` (full configuration guide)
|
||||
- `docs/modules/router/rate-limiting.md` (module dossier)
|
||||
@@ -32,8 +32,8 @@ This page is the module-level dossier for centralized rate limiting in the Route
|
||||
- If a microservice must keep internal protection (e.g., expensive job submission), ensure it is semantically distinct from HTTP admission control and does not produce conflicting client UX.
|
||||
|
||||
## Documents
|
||||
- Configuration guide: `docs/router/rate-limiting.md`
|
||||
- Per-route guide: `docs/router/rate-limiting-routes.md`
|
||||
- Ops runbook: `docs/operations/router-rate-limiting.md`
|
||||
- Configuration guide: `./guides/rate-limiting-config.md`
|
||||
- Per-route guide: `./guides/rate-limiting-routes.md`
|
||||
- Ops runbook: `../../operations/router-rate-limiting.md`
|
||||
- Testing: `tests/StellaOps.Router.Gateway.Tests/` and `tests/load/router-rate-limiting-load-test.js`
|
||||
|
||||
|
||||
37
docs/modules/router/samples/Examples.Router.sln
Normal file
37
docs/modules/router/samples/Examples.Router.sln
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Gateway", "src\Examples.Gateway\Examples.Gateway.csproj", "{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Billing.Microservice", "src\Examples.Billing.Microservice\Examples.Billing.Microservice.csproj", "{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Inventory.Microservice", "src\Examples.Inventory.Microservice\Examples.Inventory.Microservice.csproj", "{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Integration.Tests", "tests\Examples.Integration.Tests\Examples.Integration.Tests.csproj", "{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
297
docs/modules/router/samples/README.md
Normal file
297
docs/modules/router/samples/README.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# StellaOps Router Example
|
||||
|
||||
This example demonstrates the StellaOps Router, Gateway, and Microservice SDK working together.
|
||||
|
||||
## Overview
|
||||
|
||||
The example includes:
|
||||
|
||||
- **Examples.Gateway** - HTTP gateway that routes requests to microservices
|
||||
- **Examples.Billing.Microservice** - Sample billing service with typed and streaming endpoints
|
||||
- **Examples.Inventory.Microservice** - Sample inventory service demonstrating multi-service routing
|
||||
- **Examples.Integration.Tests** - End-to-end integration tests
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10 SDK
|
||||
- Docker and Docker Compose (for containerized deployment)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
examples/router/
|
||||
├── Examples.Router.sln
|
||||
├── docker-compose.yaml
|
||||
├── README.md
|
||||
├── src/
|
||||
│ ├── Examples.Gateway/
|
||||
│ │ ├── Program.cs
|
||||
│ │ ├── router.yaml
|
||||
│ │ └── appsettings.json
|
||||
│ ├── Examples.Billing.Microservice/
|
||||
│ │ ├── Program.cs
|
||||
│ │ ├── microservice.yaml
|
||||
│ │ └── Endpoints/
|
||||
│ │ ├── CreateInvoiceEndpoint.cs
|
||||
│ │ ├── GetInvoiceEndpoint.cs
|
||||
│ │ └── UploadAttachmentEndpoint.cs
|
||||
│ └── Examples.Inventory.Microservice/
|
||||
│ ├── Program.cs
|
||||
│ └── Endpoints/
|
||||
│ ├── ListItemsEndpoint.cs
|
||||
│ └── GetItemEndpoint.cs
|
||||
└── tests/
|
||||
└── Examples.Integration.Tests/
|
||||
```
|
||||
|
||||
## Running Locally
|
||||
|
||||
### Build the Solution
|
||||
|
||||
```bash
|
||||
cd examples/router
|
||||
dotnet build Examples.Router.sln
|
||||
```
|
||||
|
||||
### Run with Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
This starts:
|
||||
- Gateway on port 8080 (HTTP) and 5100 (TCP transport)
|
||||
- Billing microservice
|
||||
- Inventory microservice
|
||||
- RabbitMQ (optional, for message-based transport)
|
||||
|
||||
### Run Without Docker
|
||||
|
||||
Start each service in separate terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Gateway
|
||||
cd src/Examples.Gateway
|
||||
dotnet run
|
||||
|
||||
# Terminal 2: Billing Microservice
|
||||
cd src/Examples.Billing.Microservice
|
||||
dotnet run
|
||||
|
||||
# Terminal 3: Inventory Microservice
|
||||
cd src/Examples.Inventory.Microservice
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Example API Calls
|
||||
|
||||
### Billing Service
|
||||
|
||||
Create an invoice:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/invoices \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"customerId": "CUST-001", "amount": 99.99, "description": "Service fee"}'
|
||||
```
|
||||
|
||||
Get an invoice:
|
||||
```bash
|
||||
curl http://localhost:8080/invoices/INV-12345
|
||||
```
|
||||
|
||||
Upload an attachment (streaming):
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/invoices/INV-12345/attachments \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @document.pdf
|
||||
```
|
||||
|
||||
### Inventory Service
|
||||
|
||||
List items:
|
||||
```bash
|
||||
curl "http://localhost:8080/items?page=1&pageSize=20"
|
||||
```
|
||||
|
||||
List items by category:
|
||||
```bash
|
||||
curl "http://localhost:8080/items?category=widgets"
|
||||
```
|
||||
|
||||
Get a specific item:
|
||||
```bash
|
||||
curl http://localhost:8080/items/SKU-001
|
||||
```
|
||||
|
||||
## Adding New Endpoints
|
||||
|
||||
### 1. Create the Endpoint Class
|
||||
|
||||
```csharp
|
||||
using StellaOps.Microservice;
|
||||
|
||||
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
|
||||
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
|
||||
{
|
||||
public Task<CreateOrderResponse> HandleAsync(
|
||||
CreateOrderRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Implementation
|
||||
return Task.FromResult(new CreateOrderResponse { OrderId = "ORD-123" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register in Program.cs
|
||||
|
||||
```csharp
|
||||
builder.Services.AddScoped<CreateOrderEndpoint>();
|
||||
```
|
||||
|
||||
### 3. Update router.yaml (if needed)
|
||||
|
||||
Add routing rules for the new endpoint path.
|
||||
|
||||
## Streaming Endpoints
|
||||
|
||||
For endpoints that handle large payloads (file uploads, etc.), implement `IRawStellaEndpoint`:
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("POST", "/files/{id}", SupportsStreaming = true)]
|
||||
public sealed class UploadFileEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
public async Task<RawResponse> HandleAsync(
|
||||
RawRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var id = context.PathParameters["id"];
|
||||
|
||||
// Stream body directly without buffering
|
||||
await using var stream = context.Body;
|
||||
// Process stream...
|
||||
|
||||
return RawResponse.Ok("{}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cancellation Behavior
|
||||
|
||||
All endpoints receive a `CancellationToken` that is triggered when:
|
||||
|
||||
1. The client disconnects
|
||||
2. The request timeout is exceeded
|
||||
3. The gateway shuts down
|
||||
|
||||
Always respect the cancellation token in long-running operations:
|
||||
|
||||
```csharp
|
||||
public async Task<Response> HandleAsync(Request request, CancellationToken ct)
|
||||
{
|
||||
// Check cancellation periodically
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Or pass to async operations
|
||||
await SomeLongOperation(ct);
|
||||
}
|
||||
```
|
||||
|
||||
## Payload Limits
|
||||
|
||||
Default limits are configured in `router.yaml`:
|
||||
|
||||
```yaml
|
||||
payloadLimits:
|
||||
maxRequestBodySizeBytes: 10485760 # 10 MB
|
||||
maxChunkSizeBytes: 65536 # 64 KB
|
||||
```
|
||||
|
||||
For streaming endpoints, the body is not buffered so these limits apply per-chunk.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
cd tests/Examples.Integration.Tests
|
||||
dotnet test
|
||||
```
|
||||
|
||||
The integration tests verify:
|
||||
- End-to-end request routing
|
||||
- Multi-service registration
|
||||
- Streaming uploads
|
||||
- Request cancellation
|
||||
- Payload limit enforcement
|
||||
|
||||
## Configuration
|
||||
|
||||
### Gateway (router.yaml)
|
||||
|
||||
```yaml
|
||||
# Microservice routing rules
|
||||
services:
|
||||
billing:
|
||||
routes:
|
||||
- path: /invoices
|
||||
methods: [GET, POST]
|
||||
- path: /invoices/{id}
|
||||
methods: [GET, PUT, DELETE]
|
||||
- path: /invoices/{id}/attachments
|
||||
methods: [POST]
|
||||
inventory:
|
||||
routes:
|
||||
- path: /items
|
||||
methods: [GET]
|
||||
- path: /items/{sku}
|
||||
methods: [GET]
|
||||
```
|
||||
|
||||
### Microservice (microservice.yaml)
|
||||
|
||||
```yaml
|
||||
service:
|
||||
name: billing
|
||||
version: 1.0.0
|
||||
region: demo
|
||||
|
||||
endpoints:
|
||||
- path: /invoices
|
||||
method: POST
|
||||
timeoutSeconds: 30
|
||||
- path: /invoices/{id}
|
||||
method: GET
|
||||
timeoutSeconds: 10
|
||||
|
||||
routers:
|
||||
- host: localhost
|
||||
port: 5100
|
||||
transportType: InMemory
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Microservice not registering
|
||||
|
||||
Check that:
|
||||
1. Gateway is running and healthy
|
||||
2. Router host/port in microservice.yaml matches gateway
|
||||
3. Network connectivity between services
|
||||
|
||||
### Request timeouts
|
||||
|
||||
Increase the timeout in the endpoint attribute:
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("POST", "/long-operation", TimeoutSeconds = 120)]
|
||||
```
|
||||
|
||||
### Streaming not working
|
||||
|
||||
Ensure the endpoint:
|
||||
1. Is marked with `SupportsStreaming = true`
|
||||
2. Implements `IRawStellaEndpoint`
|
||||
3. Does not buffer the entire body before processing
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0-or-later
|
||||
75
docs/modules/router/samples/docker-compose.yaml
Normal file
75
docs/modules/router/samples/docker-compose.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
gateway:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/Examples.Gateway/Dockerfile
|
||||
ports:
|
||||
- "8080:8080" # HTTP ingress
|
||||
- "5100:5100" # TCP transport
|
||||
- "5101:5101" # TLS transport
|
||||
environment:
|
||||
- ASPNETCORE_URLS=http://+:8080
|
||||
- GatewayNode__Region=demo
|
||||
- GatewayNode__NodeId=gw-01
|
||||
- GatewayNode__ListenPort=5100
|
||||
volumes:
|
||||
- ./src/Examples.Gateway/router.yaml:/app/router.yaml:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
billing:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/Examples.Billing.Microservice/Dockerfile
|
||||
environment:
|
||||
- Stella__ServiceName=billing
|
||||
- Stella__Region=demo
|
||||
- Stella__Routers__0__Host=gateway
|
||||
- Stella__Routers__0__Port=5100
|
||||
- Stella__Routers__0__TransportType=InMemory
|
||||
volumes:
|
||||
- ./src/Examples.Billing.Microservice/microservice.yaml:/app/microservice.yaml:ro
|
||||
depends_on:
|
||||
gateway:
|
||||
condition: service_healthy
|
||||
|
||||
inventory:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/Examples.Inventory.Microservice/Dockerfile
|
||||
environment:
|
||||
- Stella__ServiceName=inventory
|
||||
- Stella__Region=demo
|
||||
- Stella__Routers__0__Host=gateway
|
||||
- Stella__Routers__0__Port=5100
|
||||
- Stella__Routers__0__TransportType=InMemory
|
||||
depends_on:
|
||||
gateway:
|
||||
condition: service_healthy
|
||||
|
||||
# Optional: RabbitMQ for message-based transport
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management-alpine
|
||||
ports:
|
||||
- "5672:5672" # AMQP
|
||||
- "15672:15672" # Management UI
|
||||
environment:
|
||||
- RABBITMQ_DEFAULT_USER=stellaops
|
||||
- RABBITMQ_DEFAULT_PASS=stellaops
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: stellaops-router-example
|
||||
|
||||
volumes:
|
||||
rabbitmq-data:
|
||||
@@ -0,0 +1,70 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace Examples.Billing.Microservice.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for creating an invoice.
|
||||
/// </summary>
|
||||
public sealed record CreateInvoiceRequest
|
||||
{
|
||||
public required string CustomerId { get; init; }
|
||||
public required decimal Amount { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public List<LineItem> LineItems { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Line item for an invoice.
|
||||
/// </summary>
|
||||
public sealed record LineItem
|
||||
{
|
||||
public required string Description { get; init; }
|
||||
public required decimal Amount { get; init; }
|
||||
public int Quantity { get; init; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model after creating an invoice.
|
||||
/// </summary>
|
||||
public sealed record CreateInvoiceResponse
|
||||
{
|
||||
public required string InvoiceId { get; init; }
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
public required string Status { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint for creating a new invoice.
|
||||
/// Demonstrates a typed endpoint with JSON request/response.
|
||||
/// </summary>
|
||||
[StellaEndpoint("POST", "/invoices", TimeoutSeconds = 30)]
|
||||
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
|
||||
{
|
||||
private readonly ILogger<CreateInvoiceEndpoint> _logger;
|
||||
|
||||
public CreateInvoiceEndpoint(ILogger<CreateInvoiceEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<CreateInvoiceResponse> HandleAsync(
|
||||
CreateInvoiceRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Creating invoice for customer {CustomerId} with amount {Amount}",
|
||||
request.CustomerId,
|
||||
request.Amount);
|
||||
|
||||
// Simulate invoice creation
|
||||
var invoiceId = $"INV-{Guid.NewGuid():N}".ToUpperInvariant()[..16];
|
||||
|
||||
return Task.FromResult(new CreateInvoiceResponse
|
||||
{
|
||||
InvoiceId = invoiceId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Status = "draft"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace Examples.Billing.Microservice.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for getting an invoice.
|
||||
/// </summary>
|
||||
public sealed record GetInvoiceRequest
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for an invoice.
|
||||
/// </summary>
|
||||
public sealed record GetInvoiceResponse
|
||||
{
|
||||
public required string InvoiceId { get; init; }
|
||||
public required string CustomerId { get; init; }
|
||||
public required decimal Amount { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
public DateTime? PaidAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint for retrieving an invoice by ID.
|
||||
/// Demonstrates a GET endpoint with path parameters.
|
||||
/// </summary>
|
||||
[StellaEndpoint("GET", "/invoices/{id}", TimeoutSeconds = 10, RequiredClaims = ["invoices:read"])]
|
||||
public sealed class GetInvoiceEndpoint : IStellaEndpoint<GetInvoiceRequest, GetInvoiceResponse>
|
||||
{
|
||||
private readonly ILogger<GetInvoiceEndpoint> _logger;
|
||||
|
||||
public GetInvoiceEndpoint(ILogger<GetInvoiceEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<GetInvoiceResponse> HandleAsync(
|
||||
GetInvoiceRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Fetching invoice {InvoiceId}", request.Id);
|
||||
|
||||
// Simulate invoice lookup
|
||||
return Task.FromResult(new GetInvoiceResponse
|
||||
{
|
||||
InvoiceId = request.Id,
|
||||
CustomerId = "CUST-001",
|
||||
Amount = 199.99m,
|
||||
Status = "paid",
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-7),
|
||||
PaidAt = DateTime.UtcNow.AddDays(-1)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace Examples.Billing.Microservice.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint for uploading attachments to an invoice.
|
||||
/// Demonstrates streaming upload using IRawStellaEndpoint.
|
||||
/// </summary>
|
||||
[StellaEndpoint("POST", "/invoices/{id}/attachments", SupportsStreaming = true, TimeoutSeconds = 300)]
|
||||
public sealed class UploadAttachmentEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
private readonly ILogger<UploadAttachmentEndpoint> _logger;
|
||||
|
||||
public UploadAttachmentEndpoint(ILogger<UploadAttachmentEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RawResponse> HandleAsync(
|
||||
RawRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var invoiceId = context.PathParameters.GetValueOrDefault("id") ?? "unknown";
|
||||
|
||||
var contentType = context.Headers["Content-Type"] ?? "application/octet-stream";
|
||||
_logger.LogInformation(
|
||||
"Uploading attachment for invoice {InvoiceId}, Content-Type: {ContentType}",
|
||||
invoiceId,
|
||||
contentType);
|
||||
|
||||
// Read the streamed body
|
||||
long totalBytes = 0;
|
||||
var buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await context.Body.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
totalBytes += bytesRead;
|
||||
// In a real implementation, you would write to storage here
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Received {TotalBytes} bytes for invoice {InvoiceId}",
|
||||
totalBytes,
|
||||
invoiceId);
|
||||
|
||||
// Return success response
|
||||
var response = new
|
||||
{
|
||||
invoiceId,
|
||||
attachmentId = $"ATT-{Guid.NewGuid():N}"[..16].ToUpperInvariant(),
|
||||
size = totalBytes,
|
||||
uploadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return RawResponse.Ok(JsonSerializer.Serialize(response));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
|
||||
<!-- Reference the source generator -->
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice.SourceGen\StellaOps.Microservice.SourceGen.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="microservice.yaml" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,40 @@
|
||||
using Examples.Billing.Microservice.Endpoints;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Configure the Stella microservice
|
||||
builder.Services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "billing";
|
||||
options.Version = "1.0.0";
|
||||
options.Region = "demo";
|
||||
options.InstanceId = $"billing-{Environment.MachineName}";
|
||||
options.ConfigFilePath = "microservice.yaml";
|
||||
options.Routers =
|
||||
[
|
||||
new RouterEndpointConfig
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 5100,
|
||||
TransportType = TransportType.InMemory
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
// Register endpoint handlers
|
||||
builder.Services.AddScoped<CreateInvoiceEndpoint>();
|
||||
builder.Services.AddScoped<GetInvoiceEndpoint>();
|
||||
builder.Services.AddScoped<UploadAttachmentEndpoint>();
|
||||
|
||||
// Add in-memory transport
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
Console.WriteLine("Billing microservice starting...");
|
||||
await host.RunAsync();
|
||||
@@ -0,0 +1,21 @@
|
||||
# Microservice YAML Configuration for Billing Service
|
||||
# Overrides code-defined endpoint settings
|
||||
|
||||
endpoints:
|
||||
# Override timeout for invoice creation
|
||||
- method: POST
|
||||
path: /invoices
|
||||
timeout: 45s # Allow more time for complex invoice creation
|
||||
|
||||
# Override streaming settings for file upload
|
||||
- method: POST
|
||||
path: /invoices/{id}/attachments
|
||||
timeout: 5m # Allow large file uploads
|
||||
streaming: true
|
||||
|
||||
# Add claim requirements for getting invoices
|
||||
- method: GET
|
||||
path: /invoices/{id}
|
||||
requiringClaims:
|
||||
- type: "scope"
|
||||
value: "invoices:read"
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Gateway\StellaOps.Router.Gateway.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Config\StellaOps.Router.Config.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="router.yaml" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
50
docs/modules/router/samples/src/Examples.Gateway/Program.cs
Normal file
50
docs/modules/router/samples/src/Examples.Gateway/Program.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using StellaOps.Router.Gateway;
|
||||
using StellaOps.Router.Gateway.Authorization;
|
||||
using StellaOps.Router.Gateway.DependencyInjection;
|
||||
using StellaOps.Router.Config;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Router configuration from YAML
|
||||
builder.Services.AddRouterConfig(options =>
|
||||
{
|
||||
options.ConfigPath = "router.yaml";
|
||||
options.EnableHotReload = true;
|
||||
});
|
||||
|
||||
// Router gateway services
|
||||
builder.Services.AddRouterGateway(builder.Configuration);
|
||||
|
||||
// In-memory transport for demo (can switch to TCP/TLS for production)
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
// Authority integration (no-op for demo)
|
||||
builder.Services.AddNoOpAuthorityIntegration();
|
||||
|
||||
// Required for app.UseAuthentication() even when running without a real auth scheme (demo/tests).
|
||||
builder.Services.AddAuthentication();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Middleware pipeline
|
||||
app.UseForwardedHeaders();
|
||||
app.UseAuthentication();
|
||||
app.UseClaimsAuthorization();
|
||||
|
||||
// Map OpenAPI endpoints
|
||||
app.MapRouterOpenApi();
|
||||
|
||||
// Simple health endpoint
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));
|
||||
|
||||
// Router gateway middleware (endpoint resolution, routing decision, dispatch)
|
||||
app.UseRouterGateway();
|
||||
|
||||
app.Run();
|
||||
|
||||
// Partial class for WebApplicationFactory integration testing
|
||||
namespace Examples.Gateway
|
||||
{
|
||||
public partial class Program { }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"GatewayNode": {
|
||||
"Region": "demo",
|
||||
"NodeId": "gw-demo-01"
|
||||
}
|
||||
}
|
||||
50
docs/modules/router/samples/src/Examples.Gateway/router.yaml
Normal file
50
docs/modules/router/samples/src/Examples.Gateway/router.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
# Router Configuration for Example Gateway
|
||||
# This file configures how the gateway routes requests to microservices
|
||||
|
||||
gateway:
|
||||
nodeId: "gw-demo-01"
|
||||
region: "demo"
|
||||
listenPort: 8080
|
||||
|
||||
# Payload limits
|
||||
payloadLimits:
|
||||
maxRequestBodyBytes: 10485760 # 10 MB
|
||||
maxStreamingChunkBytes: 65536 # 64 KB
|
||||
|
||||
# Health monitoring
|
||||
healthMonitoring:
|
||||
staleThreshold: "00:00:30"
|
||||
checkInterval: "00:00:05"
|
||||
|
||||
# Transport configuration
|
||||
transports:
|
||||
# In-memory transport (for demo)
|
||||
inMemory:
|
||||
enabled: true
|
||||
|
||||
# TCP transport (production)
|
||||
# tcp:
|
||||
# enabled: true
|
||||
# port: 5100
|
||||
# backlog: 100
|
||||
|
||||
# TLS transport (production with encryption)
|
||||
# tls:
|
||||
# enabled: true
|
||||
# port: 5101
|
||||
# certificatePath: "certs/gateway.pfx"
|
||||
# certificatePassword: "demo"
|
||||
|
||||
# Routing configuration
|
||||
routing:
|
||||
# Default routing algorithm
|
||||
algorithm: "round-robin"
|
||||
|
||||
# Region affinity (prefer local microservices)
|
||||
regionAffinity: true
|
||||
affinityWeight: 0.8
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
level: "Information"
|
||||
requestLogging: true
|
||||
@@ -0,0 +1,64 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace Examples.Inventory.Microservice.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for getting a single inventory item.
|
||||
/// </summary>
|
||||
public sealed record GetItemRequest
|
||||
{
|
||||
public required string Sku { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for a single inventory item with details.
|
||||
/// </summary>
|
||||
public sealed record GetItemResponse
|
||||
{
|
||||
public required string Sku { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required string Category { get; init; }
|
||||
public required int QuantityOnHand { get; init; }
|
||||
public required int ReorderPoint { get; init; }
|
||||
public required decimal UnitPrice { get; init; }
|
||||
public required string Location { get; init; }
|
||||
public required DateTime LastUpdated { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint for getting a single inventory item by SKU.
|
||||
/// Demonstrates path parameter extraction.
|
||||
/// </summary>
|
||||
[StellaEndpoint("GET", "/items/{sku}", TimeoutSeconds = 10)]
|
||||
public sealed class GetItemEndpoint : IStellaEndpoint<GetItemRequest, GetItemResponse>
|
||||
{
|
||||
private readonly ILogger<GetItemEndpoint> _logger;
|
||||
|
||||
public GetItemEndpoint(ILogger<GetItemEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<GetItemResponse> HandleAsync(
|
||||
GetItemRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Fetching inventory item {Sku}", request.Sku);
|
||||
|
||||
// Simulate item lookup
|
||||
return Task.FromResult(new GetItemResponse
|
||||
{
|
||||
Sku = request.Sku,
|
||||
Name = "Widget A",
|
||||
Description = "A high-quality widget for general purpose use",
|
||||
Category = "widgets",
|
||||
QuantityOnHand = 100,
|
||||
ReorderPoint = 25,
|
||||
UnitPrice = 9.99m,
|
||||
Location = "Warehouse A, Aisle 3, Shelf 2",
|
||||
LastUpdated = DateTime.UtcNow.AddHours(-2)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace Examples.Inventory.Microservice.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for listing inventory items.
|
||||
/// </summary>
|
||||
public sealed record ListItemsRequest
|
||||
{
|
||||
public int Page { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 20;
|
||||
public string? Category { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for listing inventory items.
|
||||
/// </summary>
|
||||
public sealed record ListItemsResponse
|
||||
{
|
||||
public required List<InventoryItem> Items { get; init; }
|
||||
public required int TotalCount { get; init; }
|
||||
public required int Page { get; init; }
|
||||
public required int PageSize { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inventory item model.
|
||||
/// </summary>
|
||||
public sealed record InventoryItem
|
||||
{
|
||||
public required string Sku { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Category { get; init; }
|
||||
public required int QuantityOnHand { get; init; }
|
||||
public required decimal UnitPrice { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint for listing inventory items.
|
||||
/// Demonstrates pagination and filtering.
|
||||
/// </summary>
|
||||
[StellaEndpoint("GET", "/items", TimeoutSeconds = 15)]
|
||||
public sealed class ListItemsEndpoint : IStellaEndpoint<ListItemsRequest, ListItemsResponse>
|
||||
{
|
||||
private readonly ILogger<ListItemsEndpoint> _logger;
|
||||
|
||||
public ListItemsEndpoint(ILogger<ListItemsEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<ListItemsResponse> HandleAsync(
|
||||
ListItemsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Listing inventory items - Page: {Page}, PageSize: {PageSize}, Category: {Category}",
|
||||
request.Page,
|
||||
request.PageSize,
|
||||
request.Category ?? "(all)");
|
||||
|
||||
// Simulate item list
|
||||
var items = new List<InventoryItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Sku = "SKU-001",
|
||||
Name = "Widget A",
|
||||
Category = "widgets",
|
||||
QuantityOnHand = 100,
|
||||
UnitPrice = 9.99m
|
||||
},
|
||||
new()
|
||||
{
|
||||
Sku = "SKU-002",
|
||||
Name = "Widget B",
|
||||
Category = "widgets",
|
||||
QuantityOnHand = 50,
|
||||
UnitPrice = 14.99m
|
||||
},
|
||||
new()
|
||||
{
|
||||
Sku = "SKU-003",
|
||||
Name = "Gadget X",
|
||||
Category = "gadgets",
|
||||
QuantityOnHand = 25,
|
||||
UnitPrice = 29.99m
|
||||
}
|
||||
};
|
||||
|
||||
// Filter by category if specified
|
||||
if (!string.IsNullOrWhiteSpace(request.Category))
|
||||
{
|
||||
items = items.Where(i =>
|
||||
i.Category.Equals(request.Category, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
return Task.FromResult(new ListItemsResponse
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = items.Count,
|
||||
Page = request.Page,
|
||||
PageSize = request.PageSize
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
|
||||
<!-- Reference the source generator -->
|
||||
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice.SourceGen\StellaOps.Microservice.SourceGen.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,38 @@
|
||||
using Examples.Inventory.Microservice.Endpoints;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Configure the Stella microservice
|
||||
builder.Services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "inventory";
|
||||
options.Version = "1.0.0";
|
||||
options.Region = "demo";
|
||||
options.InstanceId = $"inventory-{Environment.MachineName}";
|
||||
options.Routers =
|
||||
[
|
||||
new RouterEndpointConfig
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 5100,
|
||||
TransportType = TransportType.InMemory
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
// Register endpoint handlers
|
||||
builder.Services.AddScoped<ListItemsEndpoint>();
|
||||
builder.Services.AddScoped<GetItemEndpoint>();
|
||||
|
||||
// Add in-memory transport
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
Console.WriteLine("Inventory microservice starting...");
|
||||
await host.RunAsync();
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace Examples.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the Billing microservice endpoints.
|
||||
/// </summary>
|
||||
public sealed class BillingEndpointTests : IClassFixture<GatewayFixture>
|
||||
{
|
||||
private readonly GatewayFixture _fixture;
|
||||
|
||||
public BillingEndpointTests(GatewayFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateInvoice_WithValidRequest_ReturnsCreatedInvoice()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
customerId = "CUST-001",
|
||||
amount = 99.99m,
|
||||
description = "Test invoice"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.PostAsJsonAsync("/invoices", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("invoiceId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetInvoice_WithValidId_ReturnsInvoice()
|
||||
{
|
||||
// Arrange
|
||||
var invoiceId = "INV-12345";
|
||||
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync($"/invoices/{invoiceId}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain(invoiceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadAttachment_WithStreamingData_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var invoiceId = "INV-12345";
|
||||
var attachmentData = Encoding.UTF8.GetBytes("This is test attachment content");
|
||||
using var content = new ByteArrayContent(attachmentData);
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
|
||||
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.PostAsync(
|
||||
$"/invoices/{invoiceId}/attachments",
|
||||
content);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
responseContent.Should().Contain("attachmentId");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Examples.Gateway\Examples.Gateway.csproj" />
|
||||
<ProjectReference Include="..\..\src\Examples.Billing.Microservice\Examples.Billing.Microservice.csproj" />
|
||||
<ProjectReference Include="..\..\src\Examples.Inventory.Microservice\Examples.Inventory.Microservice.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,147 @@
|
||||
using Examples.Billing.Microservice.Endpoints;
|
||||
using Examples.Inventory.Microservice.Endpoints;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StellaOps.Microservice;
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Transport.InMemory;
|
||||
using Xunit;
|
||||
|
||||
namespace Examples.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test fixture that sets up the gateway and microservices for integration testing.
|
||||
/// Uses in-memory transport for fast, isolated tests.
|
||||
/// </summary>
|
||||
public sealed class GatewayFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly InMemoryConnectionRegistry _registry = new();
|
||||
private WebApplicationFactory<Examples.Gateway.Program>? _gatewayFactory;
|
||||
private IHost? _billingHost;
|
||||
private IHost? _inventoryHost;
|
||||
|
||||
public HttpClient GatewayClient { get; private set; } = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Start the gateway
|
||||
_gatewayFactory = new WebApplicationFactory<Examples.Gateway.Program>()
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<InMemoryConnectionRegistry>();
|
||||
services.AddSingleton(_registry);
|
||||
});
|
||||
});
|
||||
|
||||
GatewayClient = _gatewayFactory.CreateClient();
|
||||
|
||||
// Start billing microservice
|
||||
var billingBuilder = Host.CreateApplicationBuilder();
|
||||
billingBuilder.Services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "billing";
|
||||
options.Version = "1.0.0";
|
||||
options.Region = "test";
|
||||
options.InstanceId = "billing-test";
|
||||
options.Routers =
|
||||
[
|
||||
new RouterEndpointConfig
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 5100,
|
||||
TransportType = TransportType.InMemory
|
||||
}
|
||||
];
|
||||
});
|
||||
billingBuilder.Services.AddScoped<CreateInvoiceEndpoint>();
|
||||
billingBuilder.Services.AddScoped<GetInvoiceEndpoint>();
|
||||
billingBuilder.Services.AddScoped<UploadAttachmentEndpoint>();
|
||||
billingBuilder.Services.AddSingleton(_registry);
|
||||
billingBuilder.Services.AddInMemoryTransportClient();
|
||||
|
||||
_billingHost = billingBuilder.Build();
|
||||
await _billingHost.StartAsync();
|
||||
|
||||
// Start inventory microservice
|
||||
var inventoryBuilder = Host.CreateApplicationBuilder();
|
||||
inventoryBuilder.Services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "inventory";
|
||||
options.Version = "1.0.0";
|
||||
options.Region = "test";
|
||||
options.InstanceId = "inventory-test";
|
||||
options.Routers =
|
||||
[
|
||||
new RouterEndpointConfig
|
||||
{
|
||||
Host = "localhost",
|
||||
Port = 5100,
|
||||
TransportType = TransportType.InMemory
|
||||
}
|
||||
];
|
||||
});
|
||||
inventoryBuilder.Services.AddScoped<ListItemsEndpoint>();
|
||||
inventoryBuilder.Services.AddScoped<GetItemEndpoint>();
|
||||
inventoryBuilder.Services.AddSingleton(_registry);
|
||||
inventoryBuilder.Services.AddInMemoryTransportClient();
|
||||
|
||||
_inventoryHost = inventoryBuilder.Build();
|
||||
await _inventoryHost.StartAsync();
|
||||
|
||||
await WaitForGatewayReadyAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
GatewayClient.Dispose();
|
||||
|
||||
if (_billingHost is not null)
|
||||
{
|
||||
await _billingHost.StopAsync();
|
||||
_billingHost.Dispose();
|
||||
}
|
||||
|
||||
if (_inventoryHost is not null)
|
||||
{
|
||||
await _inventoryHost.StopAsync();
|
||||
_inventoryHost.Dispose();
|
||||
}
|
||||
|
||||
_gatewayFactory?.Dispose();
|
||||
}
|
||||
|
||||
private async Task WaitForGatewayReadyAsync(TimeSpan timeout)
|
||||
{
|
||||
if (_gatewayFactory is null)
|
||||
{
|
||||
throw new InvalidOperationException("Gateway factory not initialized.");
|
||||
}
|
||||
|
||||
var routingState = _gatewayFactory.Services.GetRequiredService<IGlobalRoutingState>();
|
||||
var deadline = DateTimeOffset.UtcNow.Add(timeout);
|
||||
|
||||
while (DateTimeOffset.UtcNow < deadline)
|
||||
{
|
||||
var connections = routingState.GetAllConnections();
|
||||
if (connections.Count >= 2 &&
|
||||
routingState.ResolveEndpoint("GET", "/items") is not null &&
|
||||
routingState.ResolveEndpoint("POST", "/invoices") is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
var currentConnections = routingState.GetAllConnections();
|
||||
throw new TimeoutException(
|
||||
$"Gateway routing state not ready after {timeout}. Connections={currentConnections.Count}.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace Examples.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the Inventory microservice endpoints.
|
||||
/// </summary>
|
||||
public sealed class InventoryEndpointTests : IClassFixture<GatewayFixture>
|
||||
{
|
||||
private readonly GatewayFixture _fixture;
|
||||
|
||||
public InventoryEndpointTests(GatewayFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListItems_WithoutFilters_ReturnsAllItems()
|
||||
{
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync("/items");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("items");
|
||||
content.Should().Contain("totalCount");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListItems_WithCategoryFilter_ReturnsFilteredItems()
|
||||
{
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync("/items?category=widgets");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("widgets");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListItems_WithPagination_ReturnsPaginatedResponse()
|
||||
{
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync("/items?page=1&pageSize=10");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("\"page\":1");
|
||||
content.Should().Contain("\"pageSize\":10");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetItem_WithValidSku_ReturnsItem()
|
||||
{
|
||||
// Arrange
|
||||
var sku = "SKU-001";
|
||||
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync($"/items/{sku}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain(sku);
|
||||
content.Should().Contain("name");
|
||||
content.Should().Contain("quantityOnHand");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Net;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace Examples.Integration.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that verify multiple microservices can register and receive
|
||||
/// correctly routed requests through the gateway.
|
||||
/// </summary>
|
||||
public sealed class MultiServiceRoutingTests : IClassFixture<GatewayFixture>
|
||||
{
|
||||
private readonly GatewayFixture _fixture;
|
||||
|
||||
public MultiServiceRoutingTests(GatewayFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gateway_RoutesBillingRequests_ToBillingService()
|
||||
{
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync("/invoices/INV-001");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("INV-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gateway_RoutesInventoryRequests_ToInventoryService()
|
||||
{
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync("/items/SKU-001");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("SKU-001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gateway_HandlesSequentialRequestsToDifferentServices()
|
||||
{
|
||||
// Act - Send requests to both services
|
||||
var billingResponse = await _fixture.GatewayClient.GetAsync("/invoices/INV-001");
|
||||
var inventoryResponse = await _fixture.GatewayClient.GetAsync("/items/SKU-001");
|
||||
|
||||
// Assert - Both should succeed
|
||||
billingResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
inventoryResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gateway_HandlesConcurrentRequestsToDifferentServices()
|
||||
{
|
||||
// Act - Send requests to both services concurrently
|
||||
var billingTask = _fixture.GatewayClient.GetAsync("/invoices/INV-001");
|
||||
var inventoryTask = _fixture.GatewayClient.GetAsync("/items/SKU-001");
|
||||
|
||||
await Task.WhenAll(billingTask, inventoryTask);
|
||||
|
||||
// Assert - Both should succeed
|
||||
billingTask.Result.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
inventoryTask.Result.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gateway_ReturnsNotFound_ForUnknownRoute()
|
||||
{
|
||||
// Act
|
||||
var response = await _fixture.GatewayClient.GetAsync("/unknown/route");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
203
docs/modules/router/transports/README.md
Normal file
203
docs/modules/router/transports/README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Router Transport Plugins
|
||||
|
||||
StellaOps Router uses a **plugin-based transport architecture** that enables runtime loading of transport implementations. This allows operators to deploy only the transports they need and swap implementations without recompiling the Gateway.
|
||||
|
||||
## Available Transports
|
||||
|
||||
| Transport | Plugin Assembly | Use Case | Status |
|
||||
|-----------|-----------------|----------|--------|
|
||||
| [TCP](./tcp.md) | `StellaOps.Router.Transport.Tcp.dll` | Internal services, same datacenter | Stable |
|
||||
| [TLS](./tls.md) | `StellaOps.Router.Transport.Tls.dll` | Cross-datacenter, mTLS | Stable |
|
||||
| [UDP](./udp.md) | `StellaOps.Router.Transport.Udp.dll` | Fire-and-forget, broadcast | Stable |
|
||||
| [RabbitMQ](./rabbitmq.md) | `StellaOps.Router.Transport.RabbitMq.dll` | Async processing, fan-out | Stable |
|
||||
| [InMemory](./inmemory.md) | `StellaOps.Router.Transport.InMemory.dll` | Development, testing | Stable |
|
||||
| Valkey | `StellaOps.Messaging.Transport.Valkey.dll` | Distributed, pub/sub | Stable |
|
||||
| PostgreSQL | `StellaOps.Messaging.Transport.Postgres.dll` | Transactional, LISTEN/NOTIFY | Stable |
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
### Loading Model
|
||||
|
||||
Transport plugins are loaded at Gateway startup via `RouterTransportPluginLoader`:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Gateway Startup │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ RouterTransportPluginLoader │ │
|
||||
│ │ │ │
|
||||
│ │ 1. Scan plugins/router/transports/ │ │
|
||||
│ │ 2. Load assemblies in isolation (AssemblyLoadContext) │ │
|
||||
│ │ 3. Discover IRouterTransportPlugin implementations │ │
|
||||
│ │ 4. Register configured transport with DI │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Transport Plugin (e.g., TLS) │ │
|
||||
│ │ │ │
|
||||
│ │ - TransportName: "tls" │ │
|
||||
│ │ - DisplayName: "TLS Transport" │ │
|
||||
│ │ - IsAvailable(): Check dependencies │ │
|
||||
│ │ - Register(): Wire up ITransportServer/ITransportClient │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
plugins/
|
||||
└── router/
|
||||
└── transports/
|
||||
├── StellaOps.Router.Transport.Tcp.dll
|
||||
├── StellaOps.Router.Transport.Tls.dll
|
||||
├── StellaOps.Router.Transport.Udp.dll
|
||||
├── StellaOps.Router.Transport.RabbitMq.dll
|
||||
└── StellaOps.Router.Transport.InMemory.dll
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Transport selection and options are configured in `router.yaml` or environment variables:
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: tls # Which transport plugin to use
|
||||
Tls: # Transport-specific options
|
||||
Port: 5101
|
||||
CertificatePath: /certs/server.pfx
|
||||
RequireClientCertificate: true
|
||||
Tcp:
|
||||
Port: 5100
|
||||
MaxConnections: 1000
|
||||
```
|
||||
|
||||
Environment override:
|
||||
```bash
|
||||
ROUTER__TRANSPORT__TYPE=tcp
|
||||
ROUTER__TRANSPORT__TCP__PORT=5100
|
||||
```
|
||||
|
||||
## Using Plugins in Gateway
|
||||
|
||||
### Programmatic Loading
|
||||
|
||||
```csharp
|
||||
using StellaOps.Router.Common.Plugins;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Load transport plugins from directory
|
||||
var pluginLoader = new RouterTransportPluginLoader(
|
||||
builder.Services.BuildServiceProvider().GetService<ILogger<RouterTransportPluginLoader>>());
|
||||
|
||||
var pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins", "router", "transports");
|
||||
pluginLoader.LoadFromDirectory(pluginsPath);
|
||||
|
||||
// Register the configured transport (reads Router:Transport:Type from config)
|
||||
pluginLoader.RegisterConfiguredTransport(
|
||||
builder.Services,
|
||||
builder.Configuration,
|
||||
RouterTransportMode.Both); // Register both server and client
|
||||
|
||||
var app = builder.Build();
|
||||
// ...
|
||||
```
|
||||
|
||||
### Gateway Integration
|
||||
|
||||
The Gateway automatically loads transport plugins during startup. Configure in `router.yaml`:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
name: api-gateway
|
||||
plugins:
|
||||
transports:
|
||||
directory: plugins/router/transports
|
||||
searchPattern: "StellaOps.Router.Transport.*.dll"
|
||||
```
|
||||
|
||||
## Creating Custom Transports
|
||||
|
||||
See the [Transport Plugin Development Guide](./development.md) for creating custom transport implementations.
|
||||
|
||||
### Minimal Plugin Example
|
||||
|
||||
```csharp
|
||||
using StellaOps.Router.Common.Plugins;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MyCompany.Router.Transport.Custom;
|
||||
|
||||
public sealed class CustomTransportPlugin : IRouterTransportPlugin
|
||||
{
|
||||
public string TransportName => "custom";
|
||||
public string DisplayName => "Custom Transport";
|
||||
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
public void Register(RouterTransportRegistrationContext context)
|
||||
{
|
||||
var configSection = context.Configuration.GetSection("Router:Transport:Custom");
|
||||
|
||||
context.Services.Configure<CustomTransportOptions>(options =>
|
||||
{
|
||||
configSection.Bind(options);
|
||||
});
|
||||
|
||||
if (context.Mode.HasFlag(RouterTransportMode.Server))
|
||||
{
|
||||
context.Services.AddSingleton<CustomTransportServer>();
|
||||
context.Services.AddSingleton<ITransportServer>(sp =>
|
||||
sp.GetRequiredService<CustomTransportServer>());
|
||||
}
|
||||
|
||||
if (context.Mode.HasFlag(RouterTransportMode.Client))
|
||||
{
|
||||
context.Services.AddSingleton<CustomTransportClient>();
|
||||
context.Services.AddSingleton<ITransportClient>(sp =>
|
||||
sp.GetRequiredService<CustomTransportClient>());
|
||||
context.Services.AddSingleton<IMicroserviceTransport>(sp =>
|
||||
sp.GetRequiredService<CustomTransportClient>());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Transport Selection Guide
|
||||
|
||||
| Scenario | Recommended Transport | Configuration |
|
||||
|----------|----------------------|---------------|
|
||||
| Development/Testing | InMemory | `Type: inmemory` |
|
||||
| Same-datacenter | TCP | `Type: tcp` |
|
||||
| Cross-datacenter secure | TLS | `Type: tls` with mTLS |
|
||||
| High-volume async | RabbitMQ | `Type: rabbitmq` |
|
||||
| Broadcast/fire-and-forget | UDP | `Type: udp` |
|
||||
| Distributed with replay | Valkey | Via Messaging plugins |
|
||||
| Transactional messaging | PostgreSQL | Via Messaging plugins |
|
||||
|
||||
## Air-Gap Deployment
|
||||
|
||||
For offline/air-gapped deployments:
|
||||
|
||||
1. Pre-package transport plugins with your deployment
|
||||
2. Configure the plugin directory path
|
||||
3. No external network access required
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
plugins:
|
||||
transports:
|
||||
directory: /opt/stellaops/plugins/router/transports
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Router Architecture](../ARCHITECTURE.md)
|
||||
- [Plugin SDK Guide](../../10_PLUGIN_SDK_GUIDE.md)
|
||||
- [Unified Plugin System](../../plugins/README.md)
|
||||
534
docs/modules/router/transports/development.md
Normal file
534
docs/modules/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)
|
||||
233
docs/modules/router/transports/inmemory.md
Normal file
233
docs/modules/router/transports/inmemory.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# InMemory Transport
|
||||
|
||||
The InMemory transport provides zero-latency, in-process communication for development, testing, and scenarios where services run in the same process.
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Plugin Assembly | `StellaOps.Router.Transport.InMemory.dll` |
|
||||
| Transport Name | `inmemory` |
|
||||
| Latency | Sub-microsecond |
|
||||
| Use Case | Development, testing, embedded scenarios |
|
||||
|
||||
## Configuration
|
||||
|
||||
### router.yaml
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: inmemory
|
||||
InMemory:
|
||||
MaxPendingMessages: 10000
|
||||
MessageTimeout: "00:01:00"
|
||||
```
|
||||
|
||||
### microservice.yaml
|
||||
|
||||
```yaml
|
||||
routers:
|
||||
- host: localhost
|
||||
port: 0 # Port ignored for InMemory
|
||||
transportType: InMemory
|
||||
priority: 1
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
ROUTER__TRANSPORT__TYPE=inmemory
|
||||
```
|
||||
|
||||
## Options Reference
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `MaxPendingMessages` | int | `10000` | Maximum queued messages before backpressure |
|
||||
| `MessageTimeout` | TimeSpan | `00:01:00` | Timeout for pending messages |
|
||||
| `PreserveMessageOrder` | bool | `true` | Guarantee message ordering |
|
||||
|
||||
## Architecture
|
||||
|
||||
The InMemory transport uses a shared `InMemoryConnectionRegistry` singleton that enables direct in-process communication:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Single Process │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ InMemoryConnectionRegistry ││
|
||||
│ │ (Singleton) ││
|
||||
│ │ ││
|
||||
│ │ ┌────────────────┐ ┌─────────────────────────────┐ ││
|
||||
│ │ │ Service A │ │ Service B │ ││
|
||||
│ │ │ (InMemoryClient│◄────►│ (InMemoryServer) │ ││
|
||||
│ │ │ endpoints) │ │ │ ││
|
||||
│ │ └────────────────┘ └─────────────────────────────┘ ││
|
||||
│ │ ││
|
||||
│ │ Messages passed directly via ││
|
||||
│ │ ConcurrentQueue<T> - no serialization ││
|
||||
│ └──────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Development
|
||||
|
||||
Run Gateway and microservices in the same process:
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register InMemory transport (shared between gateway and services)
|
||||
builder.Services.AddInMemoryTransport();
|
||||
|
||||
// Add gateway
|
||||
builder.Services.AddRouterGateway(builder.Configuration);
|
||||
|
||||
// Add microservice in same process
|
||||
builder.Services.AddStellaMicroservice(options =>
|
||||
{
|
||||
options.ServiceName = "order-service";
|
||||
options.Version = "1.0.0";
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseRouterGateway();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
```csharp
|
||||
public class OrderServiceTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public OrderServiceTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Use InMemory transport for tests
|
||||
services.Configure<RouterOptions>(opts =>
|
||||
opts.Transport.Type = "inmemory");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateOrder_ReturnsOrderId()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
var response = await client.PostAsJsonAsync("/orders", new
|
||||
{
|
||||
CustomerId = "CUST-001",
|
||||
Amount = 99.99m
|
||||
});
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<OrderResponse>();
|
||||
Assert.NotNull(result?.OrderId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Embedded Scenarios
|
||||
|
||||
For single-binary deployments:
|
||||
|
||||
```csharp
|
||||
// All services compiled into one executable
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddInMemoryTransport();
|
||||
|
||||
// Register all microservice handlers
|
||||
services.AddScoped<CreateOrderEndpoint>();
|
||||
services.AddScoped<GetOrderEndpoint>();
|
||||
services.AddScoped<CreateInvoiceEndpoint>();
|
||||
})
|
||||
.Build();
|
||||
```
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Metric | Typical Value |
|
||||
|--------|---------------|
|
||||
| Latency (p50) | < 0.1ms |
|
||||
| Latency (p99) | < 0.5ms |
|
||||
| Throughput | 500,000+ rps |
|
||||
| Memory overhead | Minimal |
|
||||
|
||||
*Zero serialization, direct object passing*
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **Single process only**: Cannot communicate across process boundaries
|
||||
2. **No persistence**: Messages lost on process termination
|
||||
3. **No distribution**: Cannot scale to multiple nodes
|
||||
4. **Shared memory**: Large messages consume process memory
|
||||
|
||||
## When to Use InMemory
|
||||
|
||||
| Scenario | Use InMemory? |
|
||||
|----------|---------------|
|
||||
| Local development | Yes |
|
||||
| Unit testing | Yes |
|
||||
| Integration testing | Yes |
|
||||
| Single-binary deployment | Yes |
|
||||
| Multi-node deployment | No - use TCP/TLS |
|
||||
| Production load testing | No - use production transport |
|
||||
|
||||
## Transitioning to Production
|
||||
|
||||
When moving from development to production:
|
||||
|
||||
```yaml
|
||||
# Development (appsettings.Development.json)
|
||||
Router:
|
||||
Transport:
|
||||
Type: inmemory
|
||||
|
||||
# Production (appsettings.Production.json)
|
||||
Router:
|
||||
Transport:
|
||||
Type: tls
|
||||
Tls:
|
||||
Port: 5101
|
||||
CertificatePath: /certs/server.pfx
|
||||
```
|
||||
|
||||
No code changes required - just configuration.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Messages Not Being Delivered
|
||||
|
||||
1. Verify both client and server use InMemory transport
|
||||
2. Check `InMemoryConnectionRegistry` is registered as singleton
|
||||
3. Ensure services are registered in same DI container
|
||||
|
||||
### Memory Growing
|
||||
|
||||
1. Check `MaxPendingMessages` limit
|
||||
2. Verify consumers are processing messages
|
||||
3. Monitor for message timeout (messages queued too long)
|
||||
|
||||
### Order Not Preserved
|
||||
|
||||
1. Set `PreserveMessageOrder: true`
|
||||
2. Ensure single consumer per endpoint
|
||||
3. Don't use parallel processing in handlers
|
||||
|
||||
## See Also
|
||||
|
||||
- [TCP Transport](./tcp.md) - For multi-process development
|
||||
- [Transport Overview](./README.md)
|
||||
- [Testing Guide](../../19_TEST_SUITE_OVERVIEW.md)
|
||||
241
docs/modules/router/transports/rabbitmq.md
Normal file
241
docs/modules/router/transports/rabbitmq.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# RabbitMQ Transport
|
||||
|
||||
The RabbitMQ transport provides durable, asynchronous message delivery using AMQP 0.9.1 protocol. Ideal for high-volume async processing, fan-out patterns, and scenarios requiring message persistence.
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Plugin Assembly | `StellaOps.Router.Transport.RabbitMq.dll` |
|
||||
| Transport Name | `rabbitmq` |
|
||||
| Protocol | AMQP 0.9.1 (RabbitMQ.Client 7.x) |
|
||||
| Security | TLS, SASL authentication |
|
||||
| Use Case | Async processing, fan-out, durable messaging |
|
||||
|
||||
## Configuration
|
||||
|
||||
### router.yaml
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: rabbitmq
|
||||
RabbitMq:
|
||||
HostName: rabbitmq.internal
|
||||
Port: 5672
|
||||
VirtualHost: /stellaops
|
||||
UserName: stellaops
|
||||
Password: ${RABBITMQ_PASSWORD:-}
|
||||
Ssl:
|
||||
Enabled: true
|
||||
ServerName: rabbitmq.internal
|
||||
CertPath: /certs/client.pfx
|
||||
Exchange:
|
||||
Name: stellaops.router
|
||||
Type: topic
|
||||
Durable: true
|
||||
Queue:
|
||||
Durable: true
|
||||
AutoDelete: false
|
||||
PrefetchCount: 100
|
||||
```
|
||||
|
||||
### microservice.yaml
|
||||
|
||||
```yaml
|
||||
routers:
|
||||
- host: rabbitmq.internal
|
||||
port: 5672
|
||||
transportType: RabbitMq
|
||||
priority: 1
|
||||
rabbitmq:
|
||||
virtualHost: /stellaops
|
||||
exchange: stellaops.router
|
||||
routingKeyPrefix: orders
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
ROUTER__TRANSPORT__TYPE=rabbitmq
|
||||
ROUTER__TRANSPORT__RABBITMQ__HOSTNAME=rabbitmq.internal
|
||||
ROUTER__TRANSPORT__RABBITMQ__PORT=5672
|
||||
ROUTER__TRANSPORT__RABBITMQ__USERNAME=stellaops
|
||||
ROUTER__TRANSPORT__RABBITMQ__PASSWORD=secret
|
||||
```
|
||||
|
||||
## Options Reference
|
||||
|
||||
### Connection Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `HostName` | string | `localhost` | RabbitMQ server hostname |
|
||||
| `Port` | int | `5672` | AMQP port (5671 for TLS) |
|
||||
| `VirtualHost` | string | `/` | RabbitMQ virtual host |
|
||||
| `UserName` | string | `guest` | Authentication username |
|
||||
| `Password` | string | `guest` | Authentication password |
|
||||
| `ConnectionTimeout` | TimeSpan | `00:00:30` | Connection timeout |
|
||||
| `RequestedHeartbeat` | TimeSpan | `00:01:00` | Heartbeat interval |
|
||||
|
||||
### SSL Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Ssl:Enabled` | bool | `false` | Enable TLS |
|
||||
| `Ssl:ServerName` | string | - | Expected server certificate name |
|
||||
| `Ssl:CertPath` | string | - | Client certificate path |
|
||||
| `Ssl:CertPassphrase` | string | - | Client certificate password |
|
||||
| `Ssl:Version` | SslProtocols | `Tls13` | TLS version |
|
||||
|
||||
### Exchange Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Exchange:Name` | string | `stellaops.router` | Exchange name |
|
||||
| `Exchange:Type` | string | `topic` | Exchange type (direct, topic, fanout, headers) |
|
||||
| `Exchange:Durable` | bool | `true` | Survive broker restart |
|
||||
| `Exchange:AutoDelete` | bool | `false` | Delete when unused |
|
||||
|
||||
### Queue Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Queue:Durable` | bool | `true` | Persist messages to disk |
|
||||
| `Queue:AutoDelete` | bool | `false` | Delete when all consumers disconnect |
|
||||
| `Queue:Exclusive` | bool | `false` | Single-consumer queue |
|
||||
| `Queue:PrefetchCount` | int | `100` | Prefetch limit per consumer |
|
||||
|
||||
## Message Flow
|
||||
|
||||
```
|
||||
┌───────────────┐ ┌─────────────────────────────────────────────┐
|
||||
│ Gateway │ │ RabbitMQ Broker │
|
||||
│ │ │ │
|
||||
│ ─────────────►│ AMQP │ ┌───────────────┐ ┌──────────────────┐ │
|
||||
│ Publish │────────►│ │ Exchange │───►│ Service Queue │ │
|
||||
│ │ │ │ (topic/fanout)│ │ (orders.*) │ │
|
||||
└───────────────┘ │ └───────────────┘ └────────┬─────────┘ │
|
||||
│ │ │
|
||||
└─────────────────────────────────│──────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ Microservices │
|
||||
│ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ OrderCreate │ │ OrderNotification │ │
|
||||
│ │ Consumer │ │ Consumer │ │
|
||||
│ └─────────────┘ └─────────────────────┘ │
|
||||
└───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Routing Patterns
|
||||
|
||||
### Topic Routing
|
||||
|
||||
```yaml
|
||||
Exchange:
|
||||
Type: topic
|
||||
|
||||
# Microservice A binds to: orders.create.#
|
||||
# Microservice B binds to: orders.*.notify
|
||||
# Gateway publishes to: orders.create.premium → matches A only
|
||||
# Gateway publishes to: orders.cancel.notify → matches B only
|
||||
```
|
||||
|
||||
### Fan-Out Pattern
|
||||
|
||||
```yaml
|
||||
Exchange:
|
||||
Type: fanout
|
||||
|
||||
# All bound queues receive every message
|
||||
# Good for broadcasting events to all services
|
||||
```
|
||||
|
||||
### Direct Routing
|
||||
|
||||
```yaml
|
||||
Exchange:
|
||||
Type: direct
|
||||
|
||||
# Exact routing key match required
|
||||
# Gateway publishes to: order-service
|
||||
# Only queue bound with key "order-service" receives
|
||||
```
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Metric | Typical Value |
|
||||
|--------|---------------|
|
||||
| Latency (p50) | < 5ms |
|
||||
| Latency (p99) | < 20ms |
|
||||
| Throughput | 50,000+ mps |
|
||||
| Memory per connection | ~16KB |
|
||||
|
||||
*Persistent messages with acknowledgments on dedicated broker*
|
||||
|
||||
## High Availability
|
||||
|
||||
### Clustered RabbitMQ
|
||||
|
||||
```yaml
|
||||
RabbitMq:
|
||||
Endpoints:
|
||||
- Host: rabbit1.internal
|
||||
Port: 5672
|
||||
- Host: rabbit2.internal
|
||||
Port: 5672
|
||||
- Host: rabbit3.internal
|
||||
Port: 5672
|
||||
Queue:
|
||||
Arguments:
|
||||
x-ha-policy: all # Mirror to all nodes
|
||||
x-queue-type: quorum # Quorum queue (RabbitMQ 3.8+)
|
||||
```
|
||||
|
||||
### Dead Letter Handling
|
||||
|
||||
```yaml
|
||||
Queue:
|
||||
Arguments:
|
||||
x-dead-letter-exchange: stellaops.dlx
|
||||
x-dead-letter-routing-key: failed
|
||||
x-message-ttl: 3600000 # 1 hour TTL
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Refused
|
||||
|
||||
```
|
||||
Error: Failed to connect to rabbitmq.internal:5672
|
||||
```
|
||||
|
||||
1. Verify RabbitMQ is running: `rabbitmqctl status`
|
||||
2. Check firewall allows AMQP port
|
||||
3. Verify virtual host exists: `rabbitmqctl list_vhosts`
|
||||
4. Confirm user has permissions: `rabbitmqctl list_user_permissions stellaops`
|
||||
|
||||
### Authentication Failed
|
||||
|
||||
```
|
||||
Error: ACCESS_REFUSED - Login was refused
|
||||
```
|
||||
|
||||
1. Check username/password are correct
|
||||
2. Verify user exists: `rabbitmqctl list_users`
|
||||
3. Grant permissions: `rabbitmqctl set_permissions -p /stellaops stellaops ".*" ".*" ".*"`
|
||||
|
||||
### Messages Not Being Consumed
|
||||
|
||||
1. Check queue exists: `rabbitmqctl list_queues`
|
||||
2. Verify binding: `rabbitmqctl list_bindings -p /stellaops`
|
||||
3. Check consumer is connected: `rabbitmqctl list_consumers`
|
||||
4. Monitor unacked messages: `rabbitmqctl list_queues messages_unacknowledged`
|
||||
|
||||
## See Also
|
||||
|
||||
- [RabbitMQ Documentation](https://www.rabbitmq.com/documentation.html)
|
||||
- [Transport Overview](./README.md)
|
||||
- [Messaging Transports](../../messaging/)
|
||||
135
docs/modules/router/transports/tcp.md
Normal file
135
docs/modules/router/transports/tcp.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# TCP Transport
|
||||
|
||||
The TCP transport provides high-performance binary communication for internal microservices within the same datacenter or trusted network.
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Plugin Assembly | `StellaOps.Router.Transport.Tcp.dll` |
|
||||
| Transport Name | `tcp` |
|
||||
| Default Port | 5100 |
|
||||
| Security | Network isolation (no encryption) |
|
||||
| Use Case | Internal services, low-latency communication |
|
||||
|
||||
## Configuration
|
||||
|
||||
### router.yaml
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: tcp
|
||||
Tcp:
|
||||
Host: "0.0.0.0"
|
||||
Port: 5100
|
||||
MaxConnections: 1000
|
||||
ReceiveBufferSize: 65536
|
||||
SendBufferSize: 65536
|
||||
KeepAlive: true
|
||||
NoDelay: true
|
||||
```
|
||||
|
||||
### microservice.yaml
|
||||
|
||||
```yaml
|
||||
routers:
|
||||
- host: gateway.internal
|
||||
port: 5100
|
||||
transportType: Tcp
|
||||
priority: 1
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
ROUTER__TRANSPORT__TYPE=tcp
|
||||
ROUTER__TRANSPORT__TCP__HOST=0.0.0.0
|
||||
ROUTER__TRANSPORT__TCP__PORT=5100
|
||||
ROUTER__TRANSPORT__TCP__MAXCONNECTIONS=1000
|
||||
```
|
||||
|
||||
## Options Reference
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Host` | string | `0.0.0.0` | Bind address for server |
|
||||
| `Port` | int | `5100` | TCP port number |
|
||||
| `MaxConnections` | int | `1000` | Maximum concurrent connections |
|
||||
| `ReceiveBufferSize` | int | `65536` | Socket receive buffer size in bytes |
|
||||
| `SendBufferSize` | int | `65536` | Socket send buffer size in bytes |
|
||||
| `KeepAlive` | bool | `true` | Enable TCP keep-alive probes |
|
||||
| `NoDelay` | bool | `true` | Disable Nagle's algorithm (lower latency) |
|
||||
| `ConnectTimeout` | TimeSpan | `00:00:30` | Connection timeout |
|
||||
| `ReadTimeout` | TimeSpan | `00:02:00` | Socket read timeout |
|
||||
| `WriteTimeout` | TimeSpan | `00:02:00` | Socket write timeout |
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Metric | Typical Value |
|
||||
|--------|---------------|
|
||||
| Latency (p50) | < 1ms |
|
||||
| Latency (p99) | < 5ms |
|
||||
| Throughput | 100,000+ rps |
|
||||
| Memory per connection | ~2KB |
|
||||
|
||||
*Benchmarks on 10Gbps network with small payloads (<1KB)*
|
||||
|
||||
## Security Considerations
|
||||
|
||||
TCP transport does **not** provide encryption. Use only in:
|
||||
- Private networks with proper network segmentation
|
||||
- Same-datacenter deployments with firewalled traffic
|
||||
- Container orchestration networks (Kubernetes pod network)
|
||||
|
||||
For encrypted communication, use [TLS transport](./tls.md).
|
||||
|
||||
## Framing Protocol
|
||||
|
||||
The TCP transport uses the standard Router binary framing protocol:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Frame Header (24 bytes) │
|
||||
├────────────┬────────────┬────────────┬────────────┬────────────┤
|
||||
│ Magic (4) │ Version(2) │ Type (2) │ Flags (4) │ Length (8) │
|
||||
├────────────┴────────────┴────────────┴────────────┴────────────┤
|
||||
│ Correlation ID (4) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Frame Payload (variable) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Refused
|
||||
|
||||
```
|
||||
Error: Connection refused to gateway.internal:5100
|
||||
```
|
||||
|
||||
1. Verify Gateway is running and listening on port 5100
|
||||
2. Check firewall rules allow traffic on port 5100
|
||||
3. Verify DNS resolution of hostname
|
||||
|
||||
### Connection Timeout
|
||||
|
||||
```
|
||||
Error: Connection to gateway.internal:5100 timed out
|
||||
```
|
||||
|
||||
1. Increase `ConnectTimeout` value
|
||||
2. Check network connectivity between services
|
||||
3. Verify no network segmentation blocking traffic
|
||||
|
||||
### Performance Issues
|
||||
|
||||
1. Enable `NoDelay: true` for latency-sensitive workloads
|
||||
2. Tune buffer sizes based on payload sizes
|
||||
3. Monitor connection pool exhaustion
|
||||
|
||||
## See Also
|
||||
|
||||
- [TLS Transport](./tls.md) - Encrypted variant
|
||||
- [Transport Overview](./README.md)
|
||||
- [Router Architecture](../ARCHITECTURE.md)
|
||||
223
docs/modules/router/transports/tls.md
Normal file
223
docs/modules/router/transports/tls.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# TLS Transport
|
||||
|
||||
The TLS transport provides encrypted communication with optional mutual TLS (mTLS) authentication for secure cross-datacenter and external service communication.
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Plugin Assembly | `StellaOps.Router.Transport.Tls.dll` |
|
||||
| Transport Name | `tls` |
|
||||
| Default Port | 5101 |
|
||||
| Security | TLS 1.3, optional mTLS |
|
||||
| Use Case | Cross-datacenter, external services, compliance-required environments |
|
||||
|
||||
## Configuration
|
||||
|
||||
### router.yaml (Gateway/Server)
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: tls
|
||||
Tls:
|
||||
Host: "0.0.0.0"
|
||||
Port: 5101
|
||||
CertificatePath: /certs/server.pfx
|
||||
CertificatePassword: ${TLS_CERT_PASSWORD:-}
|
||||
RequireClientCertificate: true # Enable mTLS
|
||||
AllowedClientCertificates:
|
||||
- /certs/trusted/client1.cer
|
||||
- /certs/trusted/client2.cer
|
||||
TlsProtocols: Tls13 # TLS 1.3 only
|
||||
CheckCertificateRevocation: true
|
||||
```
|
||||
|
||||
### microservice.yaml (Client)
|
||||
|
||||
```yaml
|
||||
routers:
|
||||
- host: gateway.external.company.com
|
||||
port: 5101
|
||||
transportType: Tls
|
||||
priority: 1
|
||||
tls:
|
||||
clientCertificatePath: /certs/client.pfx
|
||||
clientCertificatePassword: ${CLIENT_CERT_PASSWORD:-}
|
||||
validateServerCertificate: true
|
||||
serverCertificateThumbprints:
|
||||
- "A1B2C3D4E5F6..."
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
ROUTER__TRANSPORT__TYPE=tls
|
||||
ROUTER__TRANSPORT__TLS__PORT=5101
|
||||
ROUTER__TRANSPORT__TLS__CERTIFICATEPATH=/certs/server.pfx
|
||||
ROUTER__TRANSPORT__TLS__CERTIFICATEPASSWORD=secret
|
||||
ROUTER__TRANSPORT__TLS__REQUIRECLIENTCERTIFICATE=true
|
||||
```
|
||||
|
||||
## Options Reference
|
||||
|
||||
### Server Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Host` | string | `0.0.0.0` | Bind address |
|
||||
| `Port` | int | `5101` | TLS port number |
|
||||
| `CertificatePath` | string | - | Path to server certificate (PFX/P12) |
|
||||
| `CertificatePassword` | string | - | Password for certificate file |
|
||||
| `RequireClientCertificate` | bool | `false` | Enable mutual TLS |
|
||||
| `AllowedClientCertificates` | string[] | - | Paths to trusted client certs |
|
||||
| `TlsProtocols` | TlsProtocols | `Tls12,Tls13` | Allowed TLS versions |
|
||||
| `CheckCertificateRevocation` | bool | `true` | Check CRL/OCSP |
|
||||
| `CipherSuites` | string[] | - | Allowed cipher suites (TLS 1.3) |
|
||||
|
||||
### Client Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `ClientCertificatePath` | string | - | Path to client certificate (PFX/P12) |
|
||||
| `ClientCertificatePassword` | string | - | Password for client certificate |
|
||||
| `ValidateServerCertificate` | bool | `true` | Validate server certificate |
|
||||
| `ServerCertificateThumbprints` | string[] | - | Pinned server cert thumbprints |
|
||||
| `AllowUntrustedCertificates` | bool | `false` | Allow self-signed certs (dev only) |
|
||||
|
||||
## Mutual TLS (mTLS)
|
||||
|
||||
For zero-trust environments, enable mutual TLS authentication:
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ Microservice │ │ Gateway │
|
||||
│ │ │ │
|
||||
│ Client Cert │◄───── TLS ────────►│ Server Cert │
|
||||
│ (identity) │ Handshake │ (identity) │
|
||||
│ │ │ │
|
||||
│ Validates: │ │ Validates: │
|
||||
│ - Server cert │ │ - Client cert │
|
||||
│ - Thumbprint │ │ - Allowlist │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
### Certificate Requirements
|
||||
|
||||
**Server Certificate:**
|
||||
- Extended Key Usage: `Server Authentication (1.3.6.1.5.5.7.3.1)`
|
||||
- Subject Alternative Name: Include all DNS names clients will connect to
|
||||
|
||||
**Client Certificate:**
|
||||
- Extended Key Usage: `Client Authentication (1.3.6.1.5.5.7.3.2)`
|
||||
- Common Name or SAN identifying the service
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Metric | Typical Value |
|
||||
|--------|---------------|
|
||||
| Latency (p50) | < 2ms |
|
||||
| Latency (p99) | < 10ms |
|
||||
| Throughput | 80,000+ rps |
|
||||
| Memory per connection | ~8KB |
|
||||
|
||||
*TLS 1.3 with session resumption on 10Gbps network*
|
||||
|
||||
## Certificate Management
|
||||
|
||||
### Generating Certificates
|
||||
|
||||
```bash
|
||||
# Generate CA
|
||||
openssl genrsa -out ca.key 4096
|
||||
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
|
||||
-subj "/CN=StellaOps Internal CA"
|
||||
|
||||
# Generate server certificate
|
||||
openssl genrsa -out server.key 2048
|
||||
openssl req -new -key server.key -out server.csr \
|
||||
-subj "/CN=gateway.internal" \
|
||||
-addext "subjectAltName=DNS:gateway.internal,DNS:localhost"
|
||||
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
|
||||
-CAcreateserial -out server.crt \
|
||||
-extfile <(echo "subjectAltName=DNS:gateway.internal,DNS:localhost")
|
||||
|
||||
# Package as PFX
|
||||
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt \
|
||||
-certfile ca.crt -passout pass:changeit
|
||||
```
|
||||
|
||||
### Certificate Rotation
|
||||
|
||||
1. Generate new certificate before expiry
|
||||
2. Update `CertificatePath` in configuration
|
||||
3. Restart Gateway (no connection interruption with graceful shutdown)
|
||||
4. Update client thumbprint pins if using certificate pinning
|
||||
|
||||
## Air-Gap Deployment
|
||||
|
||||
For offline environments:
|
||||
|
||||
1. Pre-provision all certificates
|
||||
2. Disable CRL/OCSP checks: `CheckCertificateRevocation: false`
|
||||
3. Use certificate pinning instead of chain validation
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: tls
|
||||
Tls:
|
||||
CheckCertificateRevocation: false
|
||||
AllowedClientCertificates:
|
||||
- /certs/trusted/client1.cer
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Certificate Validation Failed
|
||||
|
||||
```
|
||||
Error: The remote certificate is invalid according to the validation procedure
|
||||
```
|
||||
|
||||
1. Verify certificate is not expired: `openssl x509 -in cert.pem -noout -dates`
|
||||
2. Check certificate chain is complete
|
||||
3. Verify CA is trusted by the system or explicitly configured
|
||||
|
||||
### mTLS Handshake Failed
|
||||
|
||||
```
|
||||
Error: The client certificate is not provided
|
||||
```
|
||||
|
||||
1. Ensure client certificate is configured with correct path
|
||||
2. Verify certificate has Client Authentication EKU
|
||||
3. Check certificate is in Gateway's allowlist
|
||||
|
||||
### TLS Protocol Mismatch
|
||||
|
||||
```
|
||||
Error: A call to SSPI failed, TLS version mismatch
|
||||
```
|
||||
|
||||
1. Ensure both sides support compatible TLS versions
|
||||
2. Update `TlsProtocols` to include common version
|
||||
3. TLS 1.3 recommended for new deployments
|
||||
|
||||
## Compliance
|
||||
|
||||
The TLS transport supports compliance requirements:
|
||||
|
||||
| Standard | Configuration |
|
||||
|----------|---------------|
|
||||
| PCI-DSS | TLS 1.2+, strong ciphers, certificate validation |
|
||||
| HIPAA | TLS 1.2+, mTLS for service-to-service |
|
||||
| FedRAMP | TLS 1.3, FIPS-validated crypto modules |
|
||||
|
||||
For FIPS mode, ensure .NET is configured for FIPS compliance and use FIPS-approved cipher suites.
|
||||
|
||||
## See Also
|
||||
|
||||
- [TCP Transport](./tcp.md) - Unencrypted variant for internal use
|
||||
- [Transport Overview](./README.md)
|
||||
- [Security Hardening Guide](../../SECURITY_HARDENING_GUIDE.md)
|
||||
173
docs/modules/router/transports/udp.md
Normal file
173
docs/modules/router/transports/udp.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# UDP Transport
|
||||
|
||||
The UDP transport provides connectionless, fire-and-forget messaging suitable for broadcast notifications and scenarios where delivery guarantees are not critical.
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Plugin Assembly | `StellaOps.Router.Transport.Udp.dll` |
|
||||
| Transport Name | `udp` |
|
||||
| Default Port | 5102 |
|
||||
| Security | Network isolation (no encryption) |
|
||||
| Use Case | Broadcast, metrics, fire-and-forget events |
|
||||
|
||||
## Configuration
|
||||
|
||||
### router.yaml
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: udp
|
||||
Udp:
|
||||
Host: "0.0.0.0"
|
||||
Port: 5102
|
||||
ReceiveBufferSize: 65536
|
||||
SendBufferSize: 65536
|
||||
MulticastGroup: null # Optional multicast group
|
||||
MulticastTtl: 1
|
||||
EnableBroadcast: false
|
||||
```
|
||||
|
||||
### microservice.yaml
|
||||
|
||||
```yaml
|
||||
routers:
|
||||
- host: gateway.internal
|
||||
port: 5102
|
||||
transportType: Udp
|
||||
priority: 1
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
ROUTER__TRANSPORT__TYPE=udp
|
||||
ROUTER__TRANSPORT__UDP__HOST=0.0.0.0
|
||||
ROUTER__TRANSPORT__UDP__PORT=5102
|
||||
```
|
||||
|
||||
## Options Reference
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Host` | string | `0.0.0.0` | Bind address |
|
||||
| `Port` | int | `5102` | UDP port number |
|
||||
| `ReceiveBufferSize` | int | `65536` | Socket receive buffer size |
|
||||
| `SendBufferSize` | int | `65536` | Socket send buffer size |
|
||||
| `MulticastGroup` | string | `null` | Multicast group address (e.g., `239.1.2.3`) |
|
||||
| `MulticastTtl` | int | `1` | Multicast time-to-live |
|
||||
| `EnableBroadcast` | bool | `false` | Allow broadcast to `255.255.255.255` |
|
||||
| `MaxDatagramSize` | int | `65507` | Maximum UDP datagram size |
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Fire-and-Forget Events
|
||||
|
||||
For events where acknowledgment is not required:
|
||||
|
||||
```csharp
|
||||
[StellaEndpoint("POST", "/events/metrics", FireAndForget = true)]
|
||||
public sealed class MetricsEndpoint : IStellaEndpoint<MetricsBatch, EmptyResponse>
|
||||
{
|
||||
public Task<EmptyResponse> HandleAsync(MetricsBatch request, CancellationToken ct)
|
||||
{
|
||||
// Process metrics - no response expected
|
||||
return Task.FromResult(new EmptyResponse());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multicast Broadcasting
|
||||
|
||||
For broadcasting to multiple listeners:
|
||||
|
||||
```yaml
|
||||
Router:
|
||||
Transport:
|
||||
Type: udp
|
||||
Udp:
|
||||
MulticastGroup: "239.0.1.1"
|
||||
MulticastTtl: 2
|
||||
```
|
||||
|
||||
Services join the multicast group to receive broadcasts:
|
||||
|
||||
```csharp
|
||||
// All services with this config receive broadcasts
|
||||
routers:
|
||||
- host: "239.0.1.1"
|
||||
port: 5102
|
||||
transportType: Udp
|
||||
```
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Metric | Typical Value |
|
||||
|--------|---------------|
|
||||
| Latency (p50) | < 0.5ms |
|
||||
| Latency (p99) | < 2ms |
|
||||
| Throughput | 150,000+ pps |
|
||||
| Memory per socket | ~1KB |
|
||||
|
||||
*Note: No delivery guarantees; packets may be lost under load*
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **No delivery guarantees**: Packets may be dropped
|
||||
2. **No ordering guarantees**: Packets may arrive out of order
|
||||
3. **Size limits**: Maximum datagram size ~65KB (64KB practical limit)
|
||||
4. **No acknowledgments**: Sender doesn't know if message was received
|
||||
5. **No fragmentation handling**: Large messages must fit in single datagram
|
||||
|
||||
## When to Use UDP
|
||||
|
||||
**Good fit:**
|
||||
- Real-time metrics and telemetry
|
||||
- Service discovery announcements
|
||||
- Health check broadcasts
|
||||
- Low-latency gaming or streaming metadata
|
||||
|
||||
**Not recommended:**
|
||||
- Transaction processing
|
||||
- Data that must not be lost
|
||||
- Large payloads (use TCP/TLS instead)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
UDP transport does **not** provide encryption or authentication. Consider:
|
||||
|
||||
- Network segmentation
|
||||
- Firewall rules to restrict UDP traffic
|
||||
- Application-level message signing if integrity is needed
|
||||
- DTLS wrapper for encrypted UDP (not built-in)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Messages Not Received
|
||||
|
||||
1. Check firewall allows UDP traffic on the configured port
|
||||
2. Verify receiver is bound to correct address/port
|
||||
3. Check for buffer overflow (increase `ReceiveBufferSize`)
|
||||
4. Monitor for packet loss with network tools
|
||||
|
||||
### Multicast Not Working
|
||||
|
||||
1. Verify multicast routing is enabled on network
|
||||
2. Check `MulticastTtl` is sufficient for network topology
|
||||
3. Ensure IGMP snooping is properly configured
|
||||
4. Verify all receivers joined the multicast group
|
||||
|
||||
### High Packet Loss
|
||||
|
||||
1. Reduce message rate
|
||||
2. Increase buffer sizes
|
||||
3. Check for network congestion
|
||||
4. Consider switching to TCP for reliable delivery
|
||||
|
||||
## See Also
|
||||
|
||||
- [TCP Transport](./tcp.md) - Reliable delivery
|
||||
- [Transport Overview](./README.md)
|
||||
- [Router Architecture](../ARCHITECTURE.md)
|
||||
Reference in New Issue
Block a user