Files
git.stella-ops.org/docs/router/02-Step.md
2025-12-02 18:38:32 +02:00

10 KiB
Raw Blame History

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 isnt 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 youve 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 youve 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:

    /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:

    public sealed class PayloadLimits
    {
        public long MaxRequestBytesPerCall { get; set; }
        public long MaxRequestBytesPerConnection { get; set; }
        public long MaxAggregateInflightBytes { get; set; }
    }
    

    If its defined in Config only, leave it there and avoid duplication.


3. Implement frame & correlation model

Owner: Common dev

  1. Implement Frame:

    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 EndpointDescriptors, 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:

      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:

    public interface IGlobalRoutingState
    {
        EndpointDescriptor? ResolveEndpoint(string method, string path);
    
        IReadOnlyList<ConnectionState> GetConnectionsFor(
            string serviceName,
            string version,
            string method,
            string path);
    }
    
  4. IRoutingPlugin:

    • Single method:

      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:

    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:

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

    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.