Portal Community

IServerNode Interface

public interface IServerNode : IHostedService
{
    string NodeId { get; }
    string DisplayName { get; }

    Task<ServerNodeResponse> HandleRequestAsync(
        ServerNodeRequest request,
        CancellationToken ct);

    ServerNodeHealth GetHealth();
}

public record ServerNodeRequest
{
    public string ExecutionId  { get; init; } = default!;
    public string TenantId     { get; init; } = default!;
    public string NodeId       { get; init; } = default!;
    public JsonDocument Payload { get; init; } = default!;
}

public record ServerNodeResponse
{
    public bool Success        { get; init; }
    public JsonDocument? Data  { get; init; }
    public string? ErrorMessage { get; init; }
}

public record ServerNodeHealth
{
    public bool IsReady        { get; init; }
    public string Status       { get; init; } = "unknown";
    public DateTimeOffset CheckedAt { get; init; }
    public Dictionary<string, object> Diagnostics { get; init; } = [];
}

Lifecycle Methods

MethodCalled ByPurpose
StartAsync(ct)ASP.NET Core host at startupLoad model, open connections, warm up resources, register with ServerNodeRegistry
HandleRequestAsync(req, ct)ServerNodeCallExecutor during workflow executionProcess one workflow node call — must be thread-safe
StopAsync(ct)ASP.NET Core host on shutdownDrain in-flight requests, close connections, release resources
GetHealth()Health check endpoint GET /health/server-nodes/{nodeId}Report readiness status and diagnostics

Base Class Implementation

public abstract class BaseServerNode : IServerNode
{
    private readonly IServerNodeRegistry _registry;
    private readonly ILogger _logger;

    protected BaseServerNode(IServerNodeRegistry registry, ILogger logger)
    {
        _registry = registry;
        _logger   = logger;
    }

    public abstract string NodeId       { get; }
    public abstract string DisplayName  { get; }

    public virtual async Task StartAsync(CancellationToken ct)
    {
        _logger.LogInformation("ServerNode {NodeId} starting...", NodeId);
        await InitialiseAsync(ct);
        _registry.Register(NodeId, this);
        _logger.LogInformation("ServerNode {NodeId} ready.", NodeId);
    }

    public virtual async Task StopAsync(CancellationToken ct)
    {
        _logger.LogInformation("ServerNode {NodeId} stopping...", NodeId);
        _registry.Deregister(NodeId);
        await DisposeResourcesAsync(ct);
    }

    protected abstract Task InitialiseAsync(CancellationToken ct);
    protected abstract Task DisposeResourcesAsync(CancellationToken ct);

    public abstract Task<ServerNodeResponse> HandleRequestAsync(
        ServerNodeRequest request, CancellationToken ct);

    public abstract ServerNodeHealth GetHealth();
}
Register after initialisation, not before: Always call _registry.Register(NodeId, this) at the end of StartAsync, after all resources are loaded. If the node registers before it is ready, workflow calls may arrive before the model is loaded or connections are established.