376 lines
10 KiB
Markdown
376 lines
10 KiB
Markdown
For this step, the goal is: make `StellaOps.Router.Common` the single, stable contract layer that everything else can depend on, with **no behavior** yet, just shapes. After this, gateway, microservice SDK, transports, and config can all compile against it.
|
||
|
||
Think of this as “lock down the domain vocabulary”.
|
||
|
||
---
|
||
|
||
## 0. Pre-work
|
||
|
||
**All devs touching Common:**
|
||
|
||
1. Read `docs/router/specs.md`, specifically:
|
||
|
||
* The sections describing:
|
||
|
||
* Enums (`TransportType`, `FrameType`, `InstanceHealthStatus`, etc.).
|
||
* Endpoint/instance/routing models.
|
||
* Frames and request/response correlation.
|
||
* Routing state and routing plugin.
|
||
2. Agree that no class/interface will be added to Common if it isn’t in the spec (or discussed with you and then added to the spec).
|
||
|
||
---
|
||
|
||
## 1. Inventory and file layout
|
||
|
||
**Owner: “Common” lead**
|
||
|
||
1. From `specs.md`, extract a **type inventory** for `StellaOps.Router.Common`:
|
||
|
||
Enumerations:
|
||
|
||
* `TransportType`
|
||
* `FrameType`
|
||
* `InstanceHealthStatus`
|
||
|
||
Core value objects:
|
||
|
||
* `ClaimRequirement`
|
||
* `EndpointDescriptor`
|
||
* `InstanceDescriptor`
|
||
* `ConnectionState`
|
||
* `PayloadLimits` (if used from Common; otherwise keep in Config only)
|
||
* Any small value types you’ve defined (e.g. cancel payload, ping metrics etc. if present in specs).
|
||
|
||
Routing:
|
||
|
||
* `RoutingContext`
|
||
* `RoutingDecision`
|
||
|
||
Frames:
|
||
|
||
* `Frame` (type + correlation id + payload)
|
||
* Optional payload contracts for HELLO, HEARTBEAT, ENDPOINTS_UPDATE, etc., if you’ve specified them explicitly.
|
||
|
||
Abstractions/interfaces:
|
||
|
||
* `IGlobalRoutingState`
|
||
* `IRoutingPlugin`
|
||
* `ITransportServer`
|
||
* `ITransportClient`
|
||
* Optional: `IRegionProvider` if you kept it in the spec.
|
||
|
||
2. Propose a file layout inside `src/__Libraries/StellaOps.Router.Common`:
|
||
|
||
Example:
|
||
|
||
```text
|
||
/StellaOps.Router.Common
|
||
/Enums
|
||
TransportType.cs
|
||
FrameType.cs
|
||
InstanceHealthStatus.cs
|
||
/Models
|
||
ClaimRequirement.cs
|
||
EndpointDescriptor.cs
|
||
InstanceDescriptor.cs
|
||
ConnectionState.cs
|
||
RoutingContext.cs
|
||
RoutingDecision.cs
|
||
Frame.cs
|
||
/Abstractions
|
||
IGlobalRoutingState.cs
|
||
IRoutingPlugin.cs
|
||
ITransportClient.cs
|
||
ITransportServer.cs
|
||
IRegionProvider.cs (if used)
|
||
```
|
||
|
||
3. Get a quick 👍/👎 from you on the layout (no code yet, just file names and namespaces).
|
||
|
||
---
|
||
|
||
## 2. Implement enums and basic models
|
||
|
||
**Owner: Common dev**
|
||
|
||
Scope: simple, immutable models, no methods.
|
||
|
||
1. **Enums**
|
||
|
||
Implement:
|
||
|
||
* `TransportType` with `[Udp, Tcp, Certificate, RabbitMq]`.
|
||
* `FrameType` with:
|
||
|
||
* `Hello`, `Heartbeat`, `EndpointsUpdate`, `Request`, `RequestStreamData`, `Response`, `ResponseStreamData`, `Cancel` (and any others in specs).
|
||
* `InstanceHealthStatus` with:
|
||
|
||
* `Unknown`, `Healthy`, `Degraded`, `Draining`, `Unhealthy`.
|
||
|
||
All enums live under `namespace StellaOps.Router.Common;`.
|
||
|
||
2. **Value models**
|
||
|
||
Implement as plain classes/records with auto-properties:
|
||
|
||
* `ClaimRequirement`:
|
||
|
||
* `string Type` (required).
|
||
* `string? Value` (optional).
|
||
* `EndpointDescriptor`:
|
||
|
||
* `string ServiceName`
|
||
* `string Version`
|
||
* `string Method`
|
||
* `string Path`
|
||
* `TimeSpan DefaultTimeout`
|
||
* `bool SupportsStreaming`
|
||
* `IReadOnlyList<ClaimRequirement> RequiringClaims`
|
||
* `InstanceDescriptor`:
|
||
|
||
* `string InstanceId`
|
||
* `string ServiceName`
|
||
* `string Version`
|
||
* `string Region`
|
||
* `ConnectionState`:
|
||
|
||
* `string ConnectionId`
|
||
* `InstanceDescriptor Instance`
|
||
* `InstanceHealthStatus Status`
|
||
* `DateTime LastHeartbeatUtc`
|
||
* `double AveragePingMs`
|
||
* `TransportType TransportType`
|
||
* `IReadOnlyDictionary<(string Method, string Path), EndpointDescriptor> Endpoints`
|
||
|
||
Design choices:
|
||
|
||
* Make constructors minimal (empty constructors okay for now).
|
||
* Use `init` where reasonable to encourage immutability for descriptors; `ConnectionState` can have mutable health fields.
|
||
|
||
3. **PayloadLimits (if in Common)**
|
||
|
||
If the spec places `PayloadLimits` in Common (versus Config), implement:
|
||
|
||
```csharp
|
||
public sealed class PayloadLimits
|
||
{
|
||
public long MaxRequestBytesPerCall { get; set; }
|
||
public long MaxRequestBytesPerConnection { get; set; }
|
||
public long MaxAggregateInflightBytes { get; set; }
|
||
}
|
||
```
|
||
|
||
If it’s defined in Config only, leave it there and avoid duplication.
|
||
|
||
---
|
||
|
||
## 3. Implement frame & correlation model
|
||
|
||
**Owner: Common dev**
|
||
|
||
1. Implement `Frame`:
|
||
|
||
```csharp
|
||
public sealed class Frame
|
||
{
|
||
public FrameType Type { get; init; }
|
||
public Guid CorrelationId { get; init; }
|
||
public byte[] Payload { get; init; } = Array.Empty<byte>();
|
||
}
|
||
```
|
||
|
||
2. If `specs.md` defines specific payload DTOs (e.g. `HelloPayload`, `HeartbeatPayload`, `CancelPayload`), define them too:
|
||
|
||
* `HelloPayload`:
|
||
|
||
* `InstanceDescriptor` and list of `EndpointDescriptor`s, or the equivalent properties.
|
||
* `HeartbeatPayload`:
|
||
|
||
* `InstanceId`, `Status`, metrics.
|
||
* `CancelPayload`:
|
||
|
||
* `string Reason` or similar.
|
||
|
||
Keep them as simple DTOs with no logic.
|
||
|
||
3. Do **not** implement serialization yet (no JSON/MessagePack references here); Common should only define shapes.
|
||
|
||
---
|
||
|
||
## 4. Routing abstractions
|
||
|
||
**Owner: Common dev**
|
||
|
||
Implement the routing interface + context & decision types.
|
||
|
||
1. `RoutingContext`:
|
||
|
||
* Match the spec. If your `specs.md` version includes `HttpContext`, follow it; if you intentionally kept Common free of ASP.NET types, use a neutral context (e.g. method/path/headers/principal).
|
||
* For now, if `HttpContext` is included in spec, define:
|
||
|
||
```csharp
|
||
public sealed class RoutingContext
|
||
{
|
||
public object HttpContext { get; init; } = default!; // or Microsoft.AspNetCore.Http.HttpContext if allowed
|
||
public EndpointDescriptor Endpoint { get; init; } = default!;
|
||
public string GatewayRegion { get; init; } = string.Empty;
|
||
}
|
||
```
|
||
|
||
Then you can refine the type once you finalize whether Common can reference ASP.NET packages. If you want to avoid that now, define your own lightweight context model and let gateway adapt.
|
||
|
||
2. `RoutingDecision`:
|
||
|
||
* Must include:
|
||
|
||
* `EndpointDescriptor Endpoint`
|
||
* `ConnectionState Connection`
|
||
* `TransportType TransportType`
|
||
* `TimeSpan EffectiveTimeout`
|
||
|
||
3. `IGlobalRoutingState`:
|
||
|
||
Interface only, no implementation:
|
||
|
||
```csharp
|
||
public interface IGlobalRoutingState
|
||
{
|
||
EndpointDescriptor? ResolveEndpoint(string method, string path);
|
||
|
||
IReadOnlyList<ConnectionState> GetConnectionsFor(
|
||
string serviceName,
|
||
string version,
|
||
string method,
|
||
string path);
|
||
}
|
||
```
|
||
|
||
4. `IRoutingPlugin`:
|
||
|
||
* Single method:
|
||
|
||
```csharp
|
||
public interface IRoutingPlugin
|
||
{
|
||
Task<RoutingDecision?> ChooseInstanceAsync(
|
||
RoutingContext context,
|
||
CancellationToken cancellationToken);
|
||
}
|
||
```
|
||
|
||
* No logic; just interface.
|
||
|
||
---
|
||
|
||
## 5. Transport abstractions
|
||
|
||
**Owner: Common dev**
|
||
|
||
Implement the shared transport contracts.
|
||
|
||
1. `ITransportServer`:
|
||
|
||
```csharp
|
||
public interface ITransportServer
|
||
{
|
||
Task StartAsync(CancellationToken cancellationToken);
|
||
Task StopAsync(CancellationToken cancellationToken);
|
||
}
|
||
```
|
||
|
||
2. `ITransportClient`:
|
||
|
||
Per spec, you need:
|
||
|
||
* A buffered call (request → response).
|
||
* A streaming call.
|
||
* A cancel call.
|
||
|
||
Interfaces only; content roughly:
|
||
|
||
```csharp
|
||
public interface ITransportClient
|
||
{
|
||
Task<Frame> SendRequestAsync(
|
||
ConnectionState connection,
|
||
Frame requestFrame,
|
||
TimeSpan timeout,
|
||
CancellationToken cancellationToken);
|
||
|
||
Task SendCancelAsync(
|
||
ConnectionState connection,
|
||
Guid correlationId,
|
||
string? reason = null);
|
||
|
||
Task SendStreamingAsync(
|
||
ConnectionState connection,
|
||
Frame requestHeader,
|
||
Stream requestBody,
|
||
Func<Stream, Task> readResponseBody,
|
||
PayloadLimits limits,
|
||
CancellationToken cancellationToken);
|
||
}
|
||
```
|
||
|
||
No implementation or transport-specific logic here. No network types beyond `Stream` and `Task`.
|
||
|
||
3. `IRegionProvider` (if you decided to keep it):
|
||
|
||
```csharp
|
||
public interface IRegionProvider
|
||
{
|
||
string Region { get; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Wire Common into tests (sanity checks only)
|
||
|
||
**Owner: Common tests dev**
|
||
|
||
Create a few very simple unit tests in `StellaOps.Router.Common.Tests`:
|
||
|
||
1. **Shape tests** (these are mostly compile-time):
|
||
|
||
* That `EndpointDescriptor` has the expected properties and default values can be set.
|
||
* That `ConnectionState` can be constructed and that its `Endpoints` dictionary handles `(Method, Path)` keys.
|
||
|
||
2. **Enum completeness tests**:
|
||
|
||
* Assert that `Enum.GetValues(typeof(FrameType))` contains all expected values. This catches accidental changes.
|
||
|
||
3. **No behavior yet**:
|
||
|
||
* No routing algorithms or transport behavior tests here; just that model contracts behave like dumb DTOs (e.g. property assignment, default value semantics).
|
||
|
||
This is mostly to lock in the shape and catch accidental refactors later.
|
||
|
||
---
|
||
|
||
## 7. Cleanliness & review checklist
|
||
|
||
Before you move on to the in-memory transport and gateway/microservice wiring, check:
|
||
|
||
1. `StellaOps.Router.Common`:
|
||
|
||
* Compiles with zero warnings (nullable enabled).
|
||
* Only references BCL; no ASP.NET or serializer packages unless intentionally agreed in the spec.
|
||
|
||
2. All types listed in `specs.md` under the Common section exist and match names & property sets.
|
||
|
||
3. No behavior/logic:
|
||
|
||
* No LINQ-heavy methods.
|
||
* No routing algorithm code.
|
||
* No network code.
|
||
* No YAML/JSON or serialization.
|
||
|
||
4. `StellaOps.Router.Common.Tests` runs and passes.
|
||
|
||
5. `docs/router/specs.md` is updated if there was any discrepancy (or the code is updated to match the spec, not the other way around).
|
||
|
||
---
|
||
|
||
If you want the next step, I can outline “3. Build in-memory transport + minimal HELLO/REQUEST/RESPONSE wiring” in the same style, so agents can move from contracts to a working vertical slice.
|