mbproxy: initial commit through Phase 9 (TxId multiplexing)
Adds the mbproxy service end-to-end. Phases 00-08 implement the production-ready single-listener / 1:1-backend transparent Modbus TCP proxy with bidirectional BCD rewriting for the ~54-PLC DL205/DL260 fleet. Phase 9 replaces the connection layer with a single backend socket per PLC plus MBAP TxId rewriting, lifting the H2-ECOM100's 4-concurrent-client cap as an operational ceiling. Phase 9 additions of note: - PlcMultiplexer + UpstreamPipe + TxIdAllocator + CorrelationMap - InFlightRequest with IReadOnlyList<InterestedParty> (load-bearing for Phase 10 read coalescing — do not collapse to a single field) - Per-request watchdog: surfaces Modbus exception 0x0B to upstream on BackendRequestTimeoutMs, defending against lost responses, dead-PLC paths, and pymodbus 3.13.0's concurrent-multiplexed- request bug (its ServerRequestHandler.last_pdu state race) - Status DTO + HTML gain inFlight / maxInFlight / txIdWraps / disconnectCascades / queueDepth (Tier 1.6 in docs/kpi.md) Tests: 263 unit + 38 E2E. Multiplexer correctness under truly concurrent backend traffic is proved against a stub backend in PlcMultiplexerTests; MultiplexerE2ETests paces requests so pymodbus 3.13's single-PDU framer stays in known-good mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Mbproxy.Admin;
|
||||
|
||||
// ── Wire DTOs for GET /status.json ───────────────────────────────────────────
|
||||
// Field names must match design.md "Status page" tables EXACTLY (camelCase via
|
||||
// JsonKnownNamingPolicy.CamelCase on the source-gen context).
|
||||
|
||||
/// <summary>
|
||||
/// Top-level response envelope for <c>GET /status.json</c>.
|
||||
/// </summary>
|
||||
public sealed record StatusResponse(
|
||||
ServiceFields Service,
|
||||
ListenersAggregate Listeners,
|
||||
IReadOnlyList<PlcStatus> Plcs);
|
||||
|
||||
/// <summary>Service-wide identity and reload counters.</summary>
|
||||
public sealed record ServiceFields(
|
||||
long UptimeSeconds,
|
||||
string Version,
|
||||
DateTimeOffset? ConfigLastReloadUtc,
|
||||
int ConfigReloadCount,
|
||||
int ConfigReloadRejectedCount);
|
||||
|
||||
/// <summary>Aggregate listener state across all configured PLCs.</summary>
|
||||
public sealed record ListenersAggregate(int Bound, int Configured);
|
||||
|
||||
/// <summary>Per-PLC status row.</summary>
|
||||
public sealed record PlcStatus(
|
||||
string Name,
|
||||
string Host,
|
||||
int ListenPort,
|
||||
PlcListenerStatus Listener,
|
||||
PlcClientsStatus Clients,
|
||||
PlcPdusStatus Pdus,
|
||||
PlcBackendStatus Backend,
|
||||
PlcBytesStatus Bytes);
|
||||
|
||||
/// <summary>Listener state sub-object.</summary>
|
||||
public sealed record PlcListenerStatus(
|
||||
string State,
|
||||
string? LastBindError,
|
||||
int RecoveryAttempts);
|
||||
|
||||
/// <summary>Connected-clients sub-object.</summary>
|
||||
public sealed record PlcClientsStatus(
|
||||
int Connected,
|
||||
IReadOnlyList<ClientSnapshot> RemoteEndpoints);
|
||||
|
||||
/// <summary>Per-connection-pair snapshot for the status page.</summary>
|
||||
public sealed record ClientSnapshot(
|
||||
string Remote,
|
||||
DateTimeOffset ConnectedAtUtc,
|
||||
long PdusForwarded);
|
||||
|
||||
/// <summary>PDU counters sub-object.</summary>
|
||||
public sealed record PlcPdusStatus(
|
||||
long Forwarded,
|
||||
FcCounts ByFc,
|
||||
long RewrittenSlots,
|
||||
long PartialBcdWarnings);
|
||||
|
||||
/// <summary>Per-function-code request counts.</summary>
|
||||
public sealed record FcCounts(
|
||||
long Fc03,
|
||||
long Fc04,
|
||||
long Fc06,
|
||||
long Fc16,
|
||||
long Other);
|
||||
|
||||
/// <summary>
|
||||
/// Backend connect, exception, and multiplexer telemetry. Phase 9 added
|
||||
/// <c>InFlight</c>, <c>MaxInFlight</c>, <c>TxIdWraps</c>, <c>DisconnectCascades</c>, and
|
||||
/// <c>QueueDepth</c> to surface the live state of the per-PLC TxId-multiplexed connection.
|
||||
/// </summary>
|
||||
public sealed record PlcBackendStatus(
|
||||
long ConnectsSuccess,
|
||||
long ConnectsFailed,
|
||||
ExceptionCounts ExceptionsByCode,
|
||||
double LastRoundTripMs,
|
||||
long InFlight,
|
||||
long MaxInFlight,
|
||||
long TxIdWraps,
|
||||
long DisconnectCascades,
|
||||
long QueueDepth);
|
||||
|
||||
/// <summary>Modbus exception counts by code.</summary>
|
||||
public sealed record ExceptionCounts(
|
||||
long Code01,
|
||||
long Code02,
|
||||
long Code03,
|
||||
long Code04);
|
||||
|
||||
/// <summary>Byte-transfer counters.</summary>
|
||||
public sealed record PlcBytesStatus(
|
||||
long UpstreamIn,
|
||||
long UpstreamOut);
|
||||
|
||||
// ── Source-generation context ─────────────────────────────────────────────────
|
||||
// TreatWarningsAsErrors is on, so the context must include every reachable type.
|
||||
|
||||
[JsonSerializable(typeof(StatusResponse))]
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
internal partial class StatusJsonContext : JsonSerializerContext;
|
||||
Reference in New Issue
Block a user