test(runtime): recording subscribable driver factory + enabled-driver v3 seed
This commit is contained in:
@@ -188,3 +188,196 @@ internal sealed class StubHandle : ISubscriptionHandle
|
||||
/// <summary>Gets the diagnostic ID of the subscription.</summary>
|
||||
public string DiagnosticId => "stub-sub";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A per-instance recording driver for the restart / re-subscribe suite (03/S16, U6): unlike
|
||||
/// <see cref="SubscribableStubDriver"/> it carries the <b>spec's</b> <see cref="DriverInstanceId"/>
|
||||
/// (<see cref="StubDriver"/> hard-codes a constant on a non-virtual member), and it counts and records the
|
||||
/// alarm subscription alongside the value one — the two travel in a single
|
||||
/// <c>SetDesiredSubscriptions</c> but through two different child code paths, so both need observing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>The falsification bar these counters exist for:</b> a restart test must assert a FUNCTION the new
|
||||
/// incarnation performs — it subscribed, it re-initialised — never that the incarnation exists.
|
||||
/// Existence is what
|
||||
/// <c>DriverHostActorReconcileTests.RestartDriver_respawns_the_child_without_an_actor_name_collision</c>
|
||||
/// asserts, and that test passes against the very defect 03/S16 names.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b><see cref="FireMalformedDataChange"/> is the supervision crash seam</b> — see
|
||||
/// <c>DriverHostActorRestartTests</c>'s file remarks for why the obvious alternatives do not work.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class RecordingSubscribableDriver : IDriver, ISubscribable, IAlarmSource
|
||||
{
|
||||
private readonly Lock _gate = new();
|
||||
private readonly StubHandle _handle = new();
|
||||
private readonly RecordingAlarmHandle _alarmHandle = new();
|
||||
private int _initializeCount;
|
||||
private int _shutdownCount;
|
||||
private int _subscribeCount;
|
||||
private int _subscribeAlarmsCount;
|
||||
private IReadOnlyList<string>? _lastSubscribedRefs;
|
||||
private IReadOnlyList<string>? _lastSubscribedAlarmRefs;
|
||||
|
||||
/// <summary>Initializes a new recording driver stamped with the spec's identity.</summary>
|
||||
/// <param name="driverInstanceId">The driver instance id from the spec that created it.</param>
|
||||
/// <param name="driverType">The driver type from the spec that created it.</param>
|
||||
public RecordingSubscribableDriver(string driverInstanceId, string driverType)
|
||||
{
|
||||
DriverInstanceId = driverInstanceId;
|
||||
DriverType = driverType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DriverType { get; }
|
||||
|
||||
/// <summary>Number of <see cref="InitializeAsync"/> calls. A supervision restart re-uses the SAME driver
|
||||
/// object (the Props closure captured it), so this going 1 to 2 is how the reprovision is observed.</summary>
|
||||
public int InitializeCount => Volatile.Read(ref _initializeCount);
|
||||
|
||||
/// <summary>Number of <see cref="ShutdownAsync"/> calls — a restart's <c>PostStop</c> raises it.</summary>
|
||||
public int ShutdownCount => Volatile.Read(ref _shutdownCount);
|
||||
|
||||
/// <summary>Number of <see cref="SubscribeAsync"/> calls.</summary>
|
||||
public int SubscribeCount => Volatile.Read(ref _subscribeCount);
|
||||
|
||||
/// <summary>Number of <see cref="SubscribeAlarmsAsync"/> calls.</summary>
|
||||
public int SubscribeAlarmsCount => Volatile.Read(ref _subscribeAlarmsCount);
|
||||
|
||||
/// <summary>The refs passed to the most recent <see cref="SubscribeAsync"/>, or null if never called.</summary>
|
||||
public IReadOnlyList<string>? LastSubscribedRefs
|
||||
{
|
||||
get { lock (_gate) return _lastSubscribedRefs; }
|
||||
}
|
||||
|
||||
/// <summary>The refs passed to the most recent <see cref="SubscribeAlarmsAsync"/>, or null if never called.</summary>
|
||||
public IReadOnlyList<string>? LastSubscribedAlarmRefs
|
||||
{
|
||||
get { lock (_gate) return _lastSubscribedAlarmRefs; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _initializeCount);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _shutdownCount);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ISubscriptionHandle> SubscribeAsync(
|
||||
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _subscribeCount);
|
||||
lock (_gate) _lastSubscribedRefs = fullReferences.ToArray();
|
||||
return Task.FromResult<ISubscriptionHandle>(_handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _subscribeAlarmsCount);
|
||||
lock (_gate) _lastSubscribedAlarmRefs = sourceNodeIds.ToArray();
|
||||
return Task.FromResult<IAlarmSubscriptionHandle>(_alarmHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Fires a well-formed data change (the ordinary production path).</summary>
|
||||
/// <param name="fullRef">The reference whose value changed.</param>
|
||||
/// <param name="value">The new value.</param>
|
||||
public void FireDataChange(string fullRef, object? value) =>
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(
|
||||
_handle, fullRef, new DataValueSnapshot(value, 0u, DateTime.UtcNow, DateTime.UtcNow)));
|
||||
|
||||
/// <summary>
|
||||
/// Raises <see cref="OnDataChange"/> with a NULL <c>Snapshot</c> — the supervision crash seam.
|
||||
/// <c>DataChangeEventArgs</c> is a positional record with no null validation, and the actor's
|
||||
/// <c>OnDataChangeForward</c> dereferences <c>msg.Snapshot</c> unguarded ON THE ACTOR THREAD, so this
|
||||
/// produces a genuine unhandled exception inside the child, which Akka's default decider turns into a
|
||||
/// Restart. The fault itself is arbitrary; what is under test is the RECOVERY.
|
||||
/// </summary>
|
||||
/// <param name="fullRef">The reference to name in the malformed event.</param>
|
||||
public void FireMalformedDataChange(string fullRef) =>
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(_handle, fullRef, null!));
|
||||
|
||||
/// <summary>Raises <see cref="OnAlarmEvent"/>; present so the event is not merely written-to-never-read.</summary>
|
||||
/// <param name="conditionId">The condition id to raise.</param>
|
||||
public void FireAlarm(string conditionId) =>
|
||||
OnAlarmEvent?.Invoke(this, new AlarmEventArgs(
|
||||
_alarmHandle, conditionId, conditionId, "AlarmCondition", "test", AlarmSeverity.High, DateTime.UtcNow));
|
||||
|
||||
private sealed class RecordingAlarmHandle : IAlarmSubscriptionHandle
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => "recording-alarm-sub";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IDriverFactory"/> that hands out <see cref="RecordingSubscribableDriver"/>s and KEEPS
|
||||
/// every one it created, in creation order. The list — not a single "the driver" reference — is the point:
|
||||
/// an operator Restart tears the child down and spawns a new one around a NEW driver object, so the
|
||||
/// assertion subject is <c>Created[1]</c>, while a SUPERVISION restart re-uses <c>Created[0]</c> (the Props
|
||||
/// closure captured it) and part of the assertion is that <c>Created.Count</c> stayed 1.
|
||||
/// </summary>
|
||||
internal sealed class RecordingSubscribableDriverFactory : IDriverFactory
|
||||
{
|
||||
private readonly string _supportedType;
|
||||
private readonly Lock _gate = new();
|
||||
private readonly List<RecordingSubscribableDriver> _created = new();
|
||||
|
||||
/// <summary>Initializes a factory that materialises exactly one driver type.</summary>
|
||||
/// <param name="supportedType">The driver type this factory answers for.</param>
|
||||
public RecordingSubscribableDriverFactory(string supportedType) => _supportedType = supportedType;
|
||||
|
||||
/// <summary>Every driver this factory created, in creation order.</summary>
|
||||
public IReadOnlyList<RecordingSubscribableDriver> Created
|
||||
{
|
||||
get { lock (_gate) return _created.ToArray(); }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
if (!string.Equals(driverType, _supportedType, StringComparison.Ordinal)) return null;
|
||||
var driver = new RecordingSubscribableDriver(driverInstanceId, driverType);
|
||||
lock (_gate) _created.Add(driver);
|
||||
return driver;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<string> SupportedTypes => new[] { _supportedType };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user