fix(historian-gateway): dispose alarm-write channel at shutdown + ServerHistorian startup diagnostic
Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -732,12 +732,13 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
|||||||
/// <summary>Gets the current exponential backoff delay for retry operations.</summary>
|
/// <summary>Gets the current exponential backoff delay for retry operations.</summary>
|
||||||
public TimeSpan CurrentBackoff => BackoffLadder[_backoffIndex];
|
public TimeSpan CurrentBackoff => BackoffLadder[_backoffIndex];
|
||||||
|
|
||||||
/// <summary>Disposes the sink and releases all held resources including the drain timer.</summary>
|
/// <summary>Disposes the sink and releases all held resources including the drain timer and the writer.</summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_drainTimer?.Dispose();
|
_drainTimer?.Dispose();
|
||||||
_drainGate.Dispose();
|
_drainGate.Dispose();
|
||||||
|
if (_writer is IDisposable writerDisposable) writerDisposable.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -36,7 +36,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
|
|||||||
/// event forever.
|
/// event forever.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class GatewayAlarmHistorianWriter : IAlarmHistorianWriter
|
public sealed class GatewayAlarmHistorianWriter : IAlarmHistorianWriter, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IHistorianGatewayClient _client;
|
private readonly IHistorianGatewayClient _client;
|
||||||
private readonly ILogger<GatewayAlarmHistorianWriter> _logger;
|
private readonly ILogger<GatewayAlarmHistorianWriter> _logger;
|
||||||
@@ -154,4 +154,11 @@ public sealed class GatewayAlarmHistorianWriter : IAlarmHistorianWriter
|
|||||||
// Unknown/unclassified gRPC code → dead-letter to avoid an infinite drain loop.
|
// Unknown/unclassified gRPC code → dead-letter to avoid an infinite drain loop.
|
||||||
_ => HistorianWriteOutcome.PermanentFail,
|
_ => HistorianWriteOutcome.PermanentFail,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the underlying gateway client and its gRPC channel. The concrete
|
||||||
|
/// <see cref="HistorianGatewayClientAdapter"/> implements <see cref="IDisposable"/>; test doubles
|
||||||
|
/// that only implement <see cref="IAsyncDisposable"/> are safely no-opped by the cast guard.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose() => (_client as IDisposable)?.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ if (hasDriver)
|
|||||||
var serverHistorianOptions = builder.Configuration
|
var serverHistorianOptions = builder.Configuration
|
||||||
.GetSection(ServerHistorianOptions.SectionName).Get<ServerHistorianOptions>()
|
.GetSection(ServerHistorianOptions.SectionName).Get<ServerHistorianOptions>()
|
||||||
?? new ServerHistorianOptions();
|
?? new ServerHistorianOptions();
|
||||||
|
foreach (var warning in serverHistorianOptions.Validate())
|
||||||
|
Log.Warning("ServerHistorian misconfiguration detected at startup: {Warning}", warning);
|
||||||
builder.Services.AddAlarmHistorian(
|
builder.Services.AddAlarmHistorian(
|
||||||
builder.Configuration,
|
builder.Configuration,
|
||||||
(_, sp) => GatewayHistorian.CreateAlarmWriter(serverHistorianOptions, sp));
|
(_, sp) => GatewayHistorian.CreateAlarmWriter(serverHistorianOptions, sp));
|
||||||
|
|||||||
+16
@@ -199,4 +199,20 @@ public sealed class GatewayAlarmHistorianWriterTests
|
|||||||
|
|
||||||
Assert.Empty(outcomes);
|
Assert.Empty(outcomes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispose_with_async_only_client_does_not_throw()
|
||||||
|
{
|
||||||
|
// FakeHistorianGatewayClient implements IAsyncDisposable only — not IDisposable.
|
||||||
|
// The `as IDisposable` guard in GatewayAlarmHistorianWriter.Dispose() must safely
|
||||||
|
// no-op rather than throw when the client cannot be cast to IDisposable.
|
||||||
|
var fake = new FakeHistorianGatewayClient();
|
||||||
|
var writer = Writer(fake);
|
||||||
|
|
||||||
|
var ex = Record.Exception(() => ((IDisposable)writer).Dispose());
|
||||||
|
|
||||||
|
Assert.Null(ex);
|
||||||
|
// The Fake is IAsyncDisposable only; the sync Dispose must not call DisposeAsync.
|
||||||
|
Assert.Equal(0, fake.DisposeCallCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user