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,92 @@
|
||||
using Mbproxy.Admin;
|
||||
using Mbproxy.Configuration;
|
||||
using Mbproxy.Diagnostics;
|
||||
using Mbproxy.Options;
|
||||
using Mbproxy.Proxy;
|
||||
using Serilog;
|
||||
|
||||
namespace Mbproxy;
|
||||
|
||||
internal static class HostingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the <c>"Mbproxy"</c> configuration section, binds it to
|
||||
/// <see cref="MbproxyOptions"/> via <c>IOptionsMonitor</c>, and registers
|
||||
/// the schema-level <see cref="MbproxyOptionsValidator"/>.
|
||||
///
|
||||
/// Phase 06: also registers <see cref="ServiceCounters"/> (singleton) and
|
||||
/// <see cref="ConfigReconciler"/> (singleton) so they can be injected into
|
||||
/// <see cref="Proxy.ProxyWorker"/>.
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddMbproxyOptions(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.Services
|
||||
.AddOptions<MbproxyOptions>()
|
||||
.BindConfiguration("Mbproxy")
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Services.AddSingleton<
|
||||
Microsoft.Extensions.Options.IValidateOptions<MbproxyOptions>,
|
||||
MbproxyOptionsValidator>();
|
||||
|
||||
// Phase 06: service-wide counters (read by Phase 07 status page).
|
||||
builder.Services.AddSingleton<ServiceCounters>();
|
||||
|
||||
// Phase 06: hot-reload reconciler (singleton; subscribes to IOptionsMonitor.OnChange).
|
||||
builder.Services.AddSingleton<ConfigReconciler>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers Phase 07 admin endpoint services:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="AssemblyVersionAccessor"/> (singleton — reads version attribute once).</item>
|
||||
/// <item><see cref="StatusSnapshotBuilder"/> (singleton — pure orchestration).</item>
|
||||
/// <item><see cref="AdminEndpointHost"/> (hosted service — owns the Kestrel admin server).</item>
|
||||
/// </list>
|
||||
/// Must be called after <see cref="AddMbproxyOptions"/> and after
|
||||
/// <c>AddHostedService<ProxyWorker></c> (so ProxyWorker is available via DI).
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddMbproxyAdmin(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<AssemblyVersionAccessor>();
|
||||
builder.Services.AddSingleton<StatusSnapshotBuilder>();
|
||||
// Register AdminEndpointHost as a singleton so ShutdownCoordinator can inject it
|
||||
// directly without going through the IHostedService collection.
|
||||
builder.Services.AddSingleton<AdminEndpointHost>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<AdminEndpointHost>());
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures Serilog from the <c>"Serilog"</c> configuration section,
|
||||
/// with console and rolling-file sinks as defaults.
|
||||
///
|
||||
/// <para>Phase 08: when <paramref name="addEventLogBridge"/> is <c>true</c>, the
|
||||
/// <see cref="Diagnostics.EventLogBridge"/> is added as a sub-sink for events at
|
||||
/// <see cref="Serilog.Events.LogEventLevel.Error"/> and above. This flag should only be
|
||||
/// set when the service is running as a Windows Service — the bridge silently ignores
|
||||
/// events when the Event Log source is not registered.</para>
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddMbproxySerilog(
|
||||
this IHostApplicationBuilder builder,
|
||||
bool addEventLogBridge = false)
|
||||
{
|
||||
var cfg = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration);
|
||||
|
||||
if (addEventLogBridge && OperatingSystem.IsWindows())
|
||||
{
|
||||
cfg = cfg.WriteTo.Sink(
|
||||
new EventLogBridge(enabled: true),
|
||||
Serilog.Events.LogEventLevel.Error);
|
||||
}
|
||||
|
||||
Log.Logger = cfg.CreateLogger();
|
||||
|
||||
builder.Services.AddSerilog(dispose: true);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user