bd6568bcbd
Task #125 / #137. The hosted service + scheduler classes already shipped; this commit connects them to the published-generation driver list so a Tier C driver with `RecycleIntervalSeconds` in its `ResilienceConfig` actually gets an armed scheduler at bootstrap. Wiring: - `DriverFactoryRegistry.Register` gains an optional `DriverTier` parameter (default Tier.A). Existing call sites unchanged — `GalaxyProxyDriverFactoryExtensions.Register` explicitly passes Tier.C so the bootstrapper can identify out-of-process drivers without a per-driver-type allow-list. - `DriverResilienceOptions` + parser grow `RecycleIntervalSeconds`. Tier A/B values are rejected with a diagnostic (decision #74 — recycling an in-process driver would kill every OPC UA session). Non-positive values are rejected the same way. - `DriverInstanceBootstrapper` auto-arms a `ScheduledRecycleScheduler` after a successful driver register when: (1) the registered tier is C, (2) the row's ResilienceConfig carries a positive recycle interval, (3) DI has an `IDriverSupervisor` keyed by that `DriverInstanceId`. Missing supervisor → warn + skip (no crash). That keeps the wiring harmless by default: no driver ships a supervisor today, so the hosted service runs with zero schedulers out of the box. - `Program.cs` registers `ScheduledRecycleHostedService` as singleton (shared with `DriverInstanceBootstrapper`) + hosted service (drives the tick loop). Constructor changes on the bootstrapper ripple into DI resolution automatically. Tests: 4 new parser tests covering RecycleIntervalSeconds on Tier C happy path, null default, Tier A/B rejection, non-positive rejection. Existing 283 Server.Tests + 200 Core.Tests all still green. No behavioural change for existing deployments: Galaxy driver + any future Tier C driver gain the opt-in automatically; Tier A/B drivers (FOCAS, Modbus, S7, AB CIP, AB Legacy, TwinCAT) are structurally excluded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.9 KiB
C#
87 lines
3.9 KiB
C#
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
|
|
|
/// <summary>
|
|
/// Process-singleton registry of <see cref="IDriver"/> factories keyed by
|
|
/// <c>DriverInstance.DriverType</c> string. Each driver project ships a DI
|
|
/// extension (e.g. <c>services.AddGalaxyProxyDriverFactory()</c>) that registers
|
|
/// its factory at startup; the bootstrapper looks up the factory by
|
|
/// <c>DriverInstance.DriverType</c> + invokes it with the row's
|
|
/// <c>DriverInstanceId</c> + <c>DriverConfig</c> JSON.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Closes the gap surfaced by task #240 live smoke — DriverInstance rows in
|
|
/// the central config DB had no path to materialise as registered <see cref="IDriver"/>
|
|
/// instances. The factory registry is the seam.
|
|
/// </remarks>
|
|
public sealed class DriverFactoryRegistry
|
|
{
|
|
private readonly Dictionary<string, Func<string, string, IDriver>> _factories
|
|
= new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly Dictionary<string, DriverTier> _tiers
|
|
= new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly object _lock = new();
|
|
|
|
/// <summary>
|
|
/// Register a factory for <paramref name="driverType"/>. Throws if a factory is
|
|
/// already registered for that type — drivers are singletons by type-name in
|
|
/// this process.
|
|
/// </summary>
|
|
/// <param name="driverType">Matches <c>DriverInstance.DriverType</c>.</param>
|
|
/// <param name="factory">
|
|
/// Receives <c>(driverInstanceId, driverConfigJson)</c>; returns a new
|
|
/// <see cref="IDriver"/>. Must NOT call <see cref="IDriver.InitializeAsync"/>
|
|
/// itself — the bootstrapper calls it via <see cref="DriverHost.RegisterAsync"/>
|
|
/// so the host's per-driver retry semantics apply uniformly.
|
|
/// </param>
|
|
/// <param name="tier">
|
|
/// Stability tier per <c>docs/v2/driver-stability.md</c>. Defaults to
|
|
/// <see cref="DriverTier.A"/> (in-process, lowest recycle footprint); Tier C
|
|
/// drivers SHOULD pass this explicitly so the scheduled-recycle wiring in
|
|
/// <c>DriverInstanceBootstrapper</c> can skip in-process drivers by tier check
|
|
/// rather than by driver-type allow-list.
|
|
/// </param>
|
|
public void Register(string driverType, Func<string, string, IDriver> factory,
|
|
DriverTier tier = DriverTier.A)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
|
ArgumentNullException.ThrowIfNull(factory);
|
|
lock (_lock)
|
|
{
|
|
if (_factories.ContainsKey(driverType))
|
|
throw new InvalidOperationException(
|
|
$"DriverType '{driverType}' factory already registered for this process");
|
|
_factories[driverType] = factory;
|
|
_tiers[driverType] = tier;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to look up the factory for <paramref name="driverType"/>. Returns null
|
|
/// if no driver assembly registered one — bootstrapper logs + skips so a
|
|
/// missing-assembly deployment doesn't take down the whole server.
|
|
/// </summary>
|
|
public Func<string, string, IDriver>? TryGet(string driverType)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
|
lock (_lock) return _factories.GetValueOrDefault(driverType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Look up the tier recorded alongside the factory. Returns <see cref="DriverTier.A"/>
|
|
/// for unknown driver types — a missing registration is already a skipped-bootstrap
|
|
/// case upstream; we don't double-surface that failure here.
|
|
/// </summary>
|
|
public DriverTier GetTier(string driverType)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
|
lock (_lock) return _tiers.GetValueOrDefault(driverType, DriverTier.A);
|
|
}
|
|
|
|
public IReadOnlyCollection<string> RegisteredTypes
|
|
{
|
|
get { lock (_lock) return [.. _factories.Keys]; }
|
|
}
|
|
}
|