fix(modbus): exception-safe batch wrapper; driver batch wiring consistency
This commit is contained in:
@@ -24,7 +24,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// (called in <see cref="DeviceState.DisposeHandles"/>); the library's own finalizer
|
||||
/// handles GC-collected tags.</para>
|
||||
/// </remarks>
|
||||
public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IBatchSubscribable,
|
||||
public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, IBatchSubscribable,
|
||||
IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, ISkippedTagReporter, IDisposable, IAsyncDisposable
|
||||
{
|
||||
/// <summary>Authored tags this driver could not build into its poll table (Gitea #541).</summary>
|
||||
@@ -114,9 +114,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// <summary>Occurs when a subscribed tag's value changes.</summary>
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
|
||||
/// <summary>Occurs once per poll cycle carrying every tag that changed in that cycle. The driver raises
|
||||
/// BOTH events for every burst — this one once, and <see cref="OnDataChange"/> once per changed tag — so
|
||||
/// a consumer must subscribe to EXACTLY ONE of the two or it double-delivers.</summary>
|
||||
/// <summary>
|
||||
/// Occurs once per poll cycle that produced at least one change, carrying that cycle's
|
||||
/// changed tags. Both data-change events fire by design — this one once per cycle,
|
||||
/// <see cref="OnDataChange"/> once per changed tag — so a consumer subscribes to EXACTLY
|
||||
/// ONE of the two; subscribing both double-delivers.
|
||||
/// </summary>
|
||||
public event EventHandler<DataChangeBatchEventArgs>? OnDataChangeBatch;
|
||||
|
||||
/// <summary>Occurs when a device's host connectivity status changes.</summary>
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
/// + the default <see cref="UnimplementedFocasClientFactory"/> makes misconfigured servers
|
||||
/// fail fast.
|
||||
/// </remarks>
|
||||
public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IBatchSubscribable,
|
||||
public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, IBatchSubscribable,
|
||||
IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, ISkippedTagReporter, IDisposable, IAsyncDisposable
|
||||
{
|
||||
/// <summary>Authored tags this driver could not build into its poll table (Gitea #541). FOCAS has three
|
||||
@@ -80,9 +80,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
/// <summary>Occurs when data changes on a subscribed tag.</summary>
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
/// <summary>Occurs once per poll cycle carrying every tag that changed in that cycle. The driver raises
|
||||
/// BOTH events for every burst — this one once, and <see cref="OnDataChange"/> once per changed tag — so
|
||||
/// a consumer must subscribe to EXACTLY ONE of the two or it double-delivers.</summary>
|
||||
/// <summary>
|
||||
/// Occurs once per poll cycle that produced at least one change, carrying that cycle's
|
||||
/// changed tags. Both data-change events fire by design — this one once per cycle,
|
||||
/// <see cref="OnDataChange"/> once per changed tag — so a consumer subscribes to EXACTLY
|
||||
/// ONE of the two; subscribing both double-delivers.
|
||||
/// </summary>
|
||||
public event EventHandler<DataChangeBatchEventArgs>? OnDataChangeBatch;
|
||||
/// <summary>Occurs when a device host connection status changes.</summary>
|
||||
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
|
||||
|
||||
@@ -141,9 +141,19 @@ public sealed class ModbusDriver
|
||||
{
|
||||
if (!ShouldPublish(tagRef, snapshot)) return;
|
||||
// Buffer what we publish so the cycle-end batch carries exactly the same items.
|
||||
// The append stays OUTSIDE the catch below on purpose: as far as the batch contract
|
||||
// is concerned the item WAS published — only the per-tag subscriber faulted — so it
|
||||
// must still appear in this cycle's batch, keeping the two events content-identical.
|
||||
_cycleBatchItems.GetOrAdd(handle, _ => new List<DataChangeItem>())
|
||||
.Add(new DataChangeItem(tagRef, snapshot));
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot));
|
||||
// Defensive, mirroring PollGroupEngine.ReportError: the engine invokes this bridge
|
||||
// UNGUARDED, so a throwing external subscriber would unwind PollOnceAsync mid-loop
|
||||
// and the cycle would never reach onBatch. Because the buffer above persists across
|
||||
// cycles (it is drained only by onBatch), those items would then silently MERGE into
|
||||
// a later cycle's batch — cross-cycle contamination unique to this driver. Swallowing
|
||||
// is what keeps the cycle running so onBatch drains the buffer on schedule.
|
||||
try { OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)); }
|
||||
catch { /* a buggy subscriber must not corrupt the poll cycle */ }
|
||||
},
|
||||
onError: HandlePollError,
|
||||
backoffCap: PollBackoffCap,
|
||||
|
||||
Reference in New Issue
Block a user