Phase 6.1 Stream B.4 — wire ScheduledRecycleHostedService into bootstrap
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>
This commit is contained in:
@@ -19,6 +19,8 @@ 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>
|
||||
@@ -33,7 +35,15 @@ public sealed class DriverFactoryRegistry
|
||||
/// itself — the bootstrapper calls it via <see cref="DriverHost.RegisterAsync"/>
|
||||
/// so the host's per-driver retry semantics apply uniformly.
|
||||
/// </param>
|
||||
public void Register(string driverType, Func<string, string, IDriver> factory)
|
||||
/// <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);
|
||||
@@ -43,6 +53,7 @@ public sealed class DriverFactoryRegistry
|
||||
throw new InvalidOperationException(
|
||||
$"DriverType '{driverType}' factory already registered for this process");
|
||||
_factories[driverType] = factory;
|
||||
_tiers[driverType] = tier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +68,17 @@ public sealed class DriverFactoryRegistry
|
||||
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]; }
|
||||
|
||||
Reference in New Issue
Block a user