0868613890
The DL205/DL260 ECOM emits no TCP keepalives, so an idle backend socket can be silently dropped by a middlebox (switch, firewall, NAT) after 2-5 minutes. Enable OS SO_KEEPALIVE on backend and accepted upstream sockets, and drive a periodic synthetic FC03 heartbeat on each idle backend socket so a dead path is detected before a real client request hits it. Controlled by Connection.Keepalive (ON by default). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
4.9 KiB
C#
139 lines
4.9 KiB
C#
using System.Text.Json.Serialization;
|
||
|
||
namespace Mbproxy.Admin;
|
||
|
||
// ── Wire DTOs for GET /status.json ───────────────────────────────────────────
|
||
// Field names must match docs/Operations/StatusPage.md 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>
|
||
/// Count of BCD-rewriter slot decisions where the wire value was not a valid BCD
|
||
/// nibble pattern (e.g. <c>0xABCD</c> at a tag address). The slot passes through
|
||
/// unrewritten and this counter increments.
|
||
/// </summary>
|
||
long InvalidBcdWarnings);
|
||
|
||
/// <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, including the in-flight
|
||
/// multiplexer fields (<c>InFlight</c>, <c>MaxInFlight</c>, <c>TxIdWraps</c>,
|
||
/// <c>DisconnectCascades</c>, <c>QueueDepth</c>), the read-coalescing counters
|
||
/// (<c>CoalescedHitCount</c>, <c>CoalescedMissCount</c>, <c>CoalescedResponseToDeadUpstream</c>),
|
||
/// and the response-cache counters (<c>CacheHitCount</c>, <c>CacheMissCount</c>,
|
||
/// <c>CacheInvalidations</c>, <c>CacheEntryCount</c>, <c>CacheBytes</c>).
|
||
///
|
||
/// <para>The dashboard-side derived ratios <c>coalescingRatio</c> and <c>cacheHitRatio</c>
|
||
/// are intentionally NOT carried on the wire — consumers compute <c>Hit / (Hit + Miss)</c>
|
||
/// from the raw counters.</para>
|
||
/// </summary>
|
||
public sealed record PlcBackendStatus(
|
||
long ConnectsSuccess,
|
||
long ConnectsFailed,
|
||
ExceptionCounts ExceptionsByCode,
|
||
double LastRoundTripMs,
|
||
long InFlight,
|
||
long MaxInFlight,
|
||
long TxIdWraps,
|
||
long DisconnectCascades,
|
||
long QueueDepth,
|
||
long CoalescedHitCount,
|
||
long CoalescedMissCount,
|
||
long CoalescedResponseToDeadUpstream,
|
||
long CacheHitCount,
|
||
long CacheMissCount,
|
||
long CacheInvalidations,
|
||
long CacheEntryCount,
|
||
long CacheBytes,
|
||
/// <summary>Backend keepalive heartbeat probes issued on idle backend sockets.</summary>
|
||
long BackendHeartbeatsSent,
|
||
/// <summary>Keepalive heartbeat probes that timed out (backend not answering).</summary>
|
||
long BackendHeartbeatsFailed,
|
||
/// <summary>Backend teardowns triggered by a failed keepalive heartbeat.</summary>
|
||
long BackendIdleDisconnects);
|
||
|
||
/// <summary>Modbus exception counts by code.</summary>
|
||
public sealed record ExceptionCounts(
|
||
long Code01,
|
||
long Code02,
|
||
long Code03,
|
||
long Code04,
|
||
/// <summary>
|
||
/// Backend exceptions whose response code is not 01–04 (e.g. 0x06 Server Device
|
||
/// Busy, 0x0B Gateway Target Failed To Respond, vendor-specific codes).
|
||
/// </summary>
|
||
long CodeOther);
|
||
|
||
/// <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;
|