fix(auditlog): explicit permanent rejections on the ingest ack (IngestAck.rejected_event_ids, field 2) with a dedicated counter; reconciler ignores transient faults for abandonment; persist the served high-water mark; prune only on a non-empty enumeration (review 2026-10-21 06/PERF-1, 06/IMPL-1 adversarial follow-up)
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using System.Data.Common;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||
|
||||
/// <summary>
|
||||
/// Classifies a central audit-insert failure (review 2026-10-21 06/IMPL-1 follow-up) so that a
|
||||
/// DATABASE problem — a maintenance window, a timeout, a dropped connection, an exhausted budget —
|
||||
/// is never mistaken for a verdict about the ROW. Two questions, two predicates:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="IsPermanentRowRejection"/> — narrow: the row itself can never be inserted
|
||||
/// (string truncation, NULL into NOT NULL, a conversion / overflow, a constraint). Only these ids
|
||||
/// go back to the site as <c>rejected_event_ids</c>, which is what lets the site give a row up as
|
||||
/// <c>Rejected</c>.</item>
|
||||
/// <item><see cref="IsTransient"/> — broad: anything that may clear on its own. The reconciliation
|
||||
/// puller never counts these toward permanently abandoning a row.</item>
|
||||
/// </list>
|
||||
/// The exception's whole <see cref="Exception.InnerException"/> chain is inspected, because EF Core
|
||||
/// and the execution strategy wrap the provider exception.
|
||||
/// </summary>
|
||||
internal static class AuditIngestErrorClassifier
|
||||
{
|
||||
/// <summary>
|
||||
/// SQL Server errors that describe the ROW, not the server: 8152 / 2628 string or binary data
|
||||
/// would be truncated, 515 NULL into a NOT NULL column, 245 / 241 / 242 / 8114 / 8115 / 220 / 232
|
||||
/// conversion and arithmetic overflow, 547 CHECK / FOREIGN KEY constraint.
|
||||
/// </summary>
|
||||
private static readonly HashSet<int> PermanentRowErrorNumbers = new()
|
||||
{
|
||||
8152, 2628, 515, 245, 241, 242, 8114, 8115, 220, 232, 547,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// SQL Server errors that may clear on their own (timeout, connection loss, deadlock, lock
|
||||
/// timeout, resource / Azure throttling, database unavailable or in a restricted state).
|
||||
/// </summary>
|
||||
private static readonly HashSet<int> TransientErrorNumbers = new()
|
||||
{
|
||||
-2, -1, 0, 2, 20, 53, 64, 121, 233, 1205, 1222, 3960, 4060, 4221, 8645, 8651,
|
||||
10053, 10054, 10060, 10928, 10929, 40143, 40197, 40501, 40540, 40613,
|
||||
49918, 49919, 49920, 942, 952, 983, 976, 978, 3906, 9001, 9002,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// True when the failure is a SQL Server error that says the row itself can never be inserted.
|
||||
/// Everything else — including every transient error and every non-SQL exception — is false,
|
||||
/// so the site keeps such rows Pending and simply retries them.
|
||||
/// </summary>
|
||||
/// <param name="ex">The insert failure.</param>
|
||||
/// <returns>Whether the row should be reported back to the site as permanently rejected.</returns>
|
||||
public static bool IsPermanentRowRejection(Exception ex)
|
||||
{
|
||||
if (IsTransient(ex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var e = ex; e is not null; e = e.InnerException)
|
||||
{
|
||||
if (e is SqlException sql)
|
||||
{
|
||||
foreach (SqlError error in sql.Errors)
|
||||
{
|
||||
if (PermanentRowErrorNumbers.Contains(error.Number))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return PermanentRowErrorNumbers.Contains(sql.Number);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when any exception in the chain is a timeout, cancellation, socket / IO / connection
|
||||
/// failure, an EF retry-limit exhaustion, a provider error flagged transient, or a SQL Server
|
||||
/// error number in the transient set.
|
||||
/// </summary>
|
||||
/// <param name="ex">The insert failure.</param>
|
||||
/// <returns>Whether the failure may clear on its own.</returns>
|
||||
public static bool IsTransient(Exception ex)
|
||||
{
|
||||
for (var e = ex; e is not null; e = e.InnerException)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case TimeoutException:
|
||||
case OperationCanceledException:
|
||||
case SocketException:
|
||||
case IOException:
|
||||
case HttpRequestException:
|
||||
return true;
|
||||
case SqlException sql:
|
||||
if (TransientErrorNumbers.Contains(sql.Number))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
foreach (SqlError error in sql.Errors)
|
||||
{
|
||||
if (TransientErrorNumbers.Contains(error.Number))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (sql.IsTransient)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case DbException db when db.IsTransient:
|
||||
return true;
|
||||
}
|
||||
|
||||
// EF Core's RetryLimitExceededException (an InvalidOperationException) wraps a
|
||||
// transient provider error once the execution strategy gives up.
|
||||
if (e.GetType().Name == "RetryLimitExceededException")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,7 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
var replyTo = Sender;
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
var accepted = new List<Guid>(cmd.Events.Count);
|
||||
var rejected = new List<Guid>();
|
||||
|
||||
// Resolve the repository for the whole batch — one DbContext per
|
||||
// message, mirroring NotificationOutboxActor.
|
||||
@@ -184,7 +185,7 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
// unregistered (test composition roots) means the per-row catch
|
||||
// simply logs without surfacing on the health dashboard.
|
||||
var failureCounter = scope.ServiceProvider.GetService<ICentralAuditWriteFailureCounter>();
|
||||
await IngestWithRepositoryAsync(repository, redactor, failureCounter, cmd, nowUtc, accepted)
|
||||
await IngestWithRepositoryAsync(repository, redactor, failureCounter, cmd, nowUtc, accepted, rejected)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -201,7 +202,7 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
accepted.Count);
|
||||
}
|
||||
|
||||
replyTo.Tell(new IngestAuditEventsReply(accepted));
|
||||
replyTo.Tell(new IngestAuditEventsReply(accepted) { RejectedEventIds = rejected });
|
||||
}
|
||||
|
||||
private async Task IngestWithRepositoryAsync(
|
||||
@@ -210,7 +211,8 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
ICentralAuditWriteFailureCounter? failureCounter,
|
||||
IngestAuditEventsCommand cmd,
|
||||
DateTime nowUtc,
|
||||
List<Guid> accepted)
|
||||
List<Guid> accepted,
|
||||
List<Guid> rejected)
|
||||
{
|
||||
// Stamp the ingest timestamp here, not at the site. Redact BEFORE the
|
||||
// IngestedAtUtc stamp so the redacted copy carries the central-side
|
||||
@@ -303,6 +305,15 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
catch { /* counter must never throw — defence in depth */ }
|
||||
}
|
||||
|
||||
// Review 2026-10-21 06/IMPL-1: tell the site ONLY about rows the server
|
||||
// refused for a reason intrinsic to the row (truncation, NOT NULL, conversion,
|
||||
// constraint). A timeout / connection loss / maintenance window leaves the id
|
||||
// in neither list, so the site retries it without counting a refusal.
|
||||
if (AuditIngestErrorClassifier.IsPermanentRowRejection(rowEx))
|
||||
{
|
||||
rejected.Add(cmd.Events[i].EventId);
|
||||
}
|
||||
|
||||
_logger.LogError(rowEx,
|
||||
"Failed to persist audit event {EventId} during batch ingest; row will be retried by the site.",
|
||||
cmd.Events[i].EventId);
|
||||
@@ -336,6 +347,7 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
{
|
||||
var replyTo = Sender;
|
||||
var accepted = new List<Guid>(cmd.Entries.Count);
|
||||
var rejected = new List<Guid>();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -370,7 +382,7 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
strategy, dbContext, auditRepo, siteCallRepo, redactor, cmd, accepted, budget.Token)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
replyTo.Tell(new IngestCachedTelemetryReply(accepted));
|
||||
replyTo.Tell(new IngestCachedTelemetryReply(accepted) { RejectedEventIds = rejected });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -445,6 +457,11 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
// sustained dual-write failure surfaces on the dashboard.
|
||||
try { failureCounter?.Increment(); }
|
||||
catch { /* counter must never throw — defence in depth */ }
|
||||
if (AuditIngestErrorClassifier.IsPermanentRowRejection(ex))
|
||||
{
|
||||
rejected.Add(entry.Audit.EventId);
|
||||
}
|
||||
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Combined telemetry dual-write failed for AuditEvent {EventId} / TrackedOperationId {TrackedOpId}; rolled back.",
|
||||
@@ -463,7 +480,7 @@ public class AuditLogIngestActor : ReceiveActor
|
||||
"Combined telemetry batch ingest failed before per-entry processing.");
|
||||
}
|
||||
|
||||
replyTo.Tell(new IngestCachedTelemetryReply(accepted));
|
||||
replyTo.Tell(new IngestCachedTelemetryReply(accepted) { RejectedEventIds = rejected });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -302,13 +302,15 @@ public class SiteAuditReconciliationActor : ReceiveActor
|
||||
return;
|
||||
}
|
||||
|
||||
PruneUnknownSites(sites, eventStream);
|
||||
|
||||
if (sites.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// After the empty guard: an empty enumeration is as likely a transient lookup glitch
|
||||
// as a fleet with no sites, and pruning on it would throw away every cursor.
|
||||
PruneUnknownSites(sites, eventStream);
|
||||
|
||||
// Use CreateAsyncScope + await using so scoped EF Core
|
||||
// services (IAsyncDisposable DbContexts) dispose asynchronously
|
||||
// without blocking on sync Dispose() of pending connection cleanup.
|
||||
@@ -489,34 +491,51 @@ public class SiteAuditReconciliationActor : ReceiveActor
|
||||
// is permanently abandoned (LogCritical + cursor advances past)
|
||||
// to keep a truly broken row from blocking all subsequent
|
||||
// progress for the site.
|
||||
var attempts = _failedInsertAttempts.GetValueOrDefault(evt.EventId) + 1;
|
||||
_failedInsertAttempts[evt.EventId] = attempts;
|
||||
|
||||
if (attempts >= MaxPermanentInsertAttempts)
|
||||
if (AuditIngestErrorClassifier.IsTransient(ex))
|
||||
{
|
||||
_logger.LogCritical(
|
||||
// A database problem (timeout, connection loss, deadlock, throttling, a
|
||||
// maintenance window) is not a verdict about this row: hold the cursor
|
||||
// and retry, but never count it toward permanent abandonment — a long
|
||||
// outage must not turn healthy rows into ReconciliationAbandoned losses
|
||||
// (review 2026-10-21 06/PERF-1 adversarial follow-up).
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Permanently abandoning AuditEvent {EventId} from site {SiteId} after {Attempts} consecutive insert failures; cursor will advance past it.",
|
||||
"Reconciliation ingest of AuditEvent {EventId} from site {SiteId} hit a transient database fault; cursor held back, not counted toward abandonment.",
|
||||
evt.EventId,
|
||||
site.SiteId,
|
||||
attempts);
|
||||
_failedInsertAttempts.Remove(evt.EventId);
|
||||
RememberAbandoned(evt.EventId);
|
||||
advanceForThisRow = true;
|
||||
|
||||
await EmitAbandonmentRecordAsync(repository, evt, site.SiteId, ex, nowUtc)
|
||||
.ConfigureAwait(false);
|
||||
site.SiteId);
|
||||
hasUnresolvedFailure = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Reconciliation ingest failed for AuditEvent {EventId} from site {SiteId} (attempt {Attempts}/{Max}); cursor held back for retry.",
|
||||
evt.EventId,
|
||||
site.SiteId,
|
||||
attempts,
|
||||
MaxPermanentInsertAttempts);
|
||||
hasUnresolvedFailure = true;
|
||||
var attempts = _failedInsertAttempts.GetValueOrDefault(evt.EventId) + 1;
|
||||
_failedInsertAttempts[evt.EventId] = attempts;
|
||||
|
||||
if (attempts >= MaxPermanentInsertAttempts)
|
||||
{
|
||||
_logger.LogCritical(
|
||||
ex,
|
||||
"Permanently abandoning AuditEvent {EventId} from site {SiteId} after {Attempts} consecutive insert failures; cursor will advance past it.",
|
||||
evt.EventId,
|
||||
site.SiteId,
|
||||
attempts);
|
||||
_failedInsertAttempts.Remove(evt.EventId);
|
||||
RememberAbandoned(evt.EventId);
|
||||
advanceForThisRow = true;
|
||||
|
||||
await EmitAbandonmentRecordAsync(repository, evt, site.SiteId, ex, nowUtc)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Reconciliation ingest failed for AuditEvent {EventId} from site {SiteId} (attempt {Attempts}/{Max}); cursor held back for retry.",
|
||||
evt.EventId,
|
||||
site.SiteId,
|
||||
attempts,
|
||||
MaxPermanentInsertAttempts);
|
||||
hasUnresolvedFailure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public static class SiteAuditForwardStates
|
||||
public const string PushAbandoned = "PushAbandoned";
|
||||
|
||||
/// <summary>
|
||||
/// A row central answered but refused (not in <c>AcceptedEventIds</c>) on
|
||||
/// A row central explicitly and permanently refused (listed in the ack's <c>rejected_event_ids</c>) on
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry.SiteAuditTelemetryOptions.MaxRejectedPushAttempts"/>
|
||||
/// consecutive drains. Moved out of the push queue so one poison row cannot wedge every push
|
||||
/// batch behind it; the pull then delivers it, or central's permanent-abandon path records the
|
||||
@@ -53,11 +53,11 @@ public interface ISiteAuditPushLifecycle
|
||||
Task MarkPushAbandonedAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Records one refused push attempt for each supplied <see cref="AuditForwardState.Pending"/>
|
||||
/// Records one explicitly-refused push attempt for each supplied <see cref="AuditForwardState.Pending"/>
|
||||
/// row (bumps <c>AttemptCount</c>, stamps <c>LastAttemptUtc</c>) and flips every row that has
|
||||
/// now reached <paramref name="maxAttempts"/> to <see cref="SiteAuditForwardStates.Rejected"/>.
|
||||
/// </summary>
|
||||
/// <param name="eventIds">Event IDs central answered for but did not accept.</param>
|
||||
/// <param name="eventIds">Event IDs central listed as permanently rejected.</param>
|
||||
/// <param name="maxAttempts">Refused attempts after which a row leaves the push queue.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that resolves to the number of rows flipped to Rejected by this call.</returns>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
|
||||
|
||||
/// <summary>
|
||||
/// Site audit forwarding instruments. Published on a meter named
|
||||
/// <see cref="ScadaBridgeTelemetry.MeterName"/> — OpenTelemetry matches meters by NAME, so these
|
||||
/// export through the existing <c>ObservedMeters</c> allowlist entry without a Host change.
|
||||
/// </summary>
|
||||
internal static class SiteAuditTelemetryMetrics
|
||||
{
|
||||
private static readonly Meter Meter = new(ScadaBridgeTelemetry.MeterName);
|
||||
|
||||
private static readonly Counter<long> RejectedRows =
|
||||
Meter.CreateCounter<long>("scadabridge.site.audit.rejected_rows", unit: "1",
|
||||
description: "Site audit rows central permanently refused MaxRejectedPushAttempts times, moved out of the push queue as Rejected (still served by the reconciliation pull).");
|
||||
|
||||
/// <summary>Records <paramref name="count"/> rows flipped to Rejected.</summary>
|
||||
/// <param name="count">Rows flipped.</param>
|
||||
public static void RecordRejectedRows(long count)
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
RejectedRows.Add(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,6 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
private readonly SqliteAuditWriterOptions _options;
|
||||
private readonly ILogger<SqliteAuditWriter> _logger;
|
||||
private readonly INodeIdentityProvider _nodeIdentity;
|
||||
private readonly IAuditWriteFailureCounter? _failureCounter;
|
||||
private readonly object _writeLock = new();
|
||||
private readonly Channel<PendingAuditEvent> _writeQueue;
|
||||
private readonly Task _writerLoop;
|
||||
@@ -89,17 +88,11 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="nodeIdentity">Node identity provider.</param>
|
||||
/// <param name="connectionStringOverride">Optional connection string override.</param>
|
||||
/// <param name="failureCounter">
|
||||
/// Optional <c>SiteAuditWriteFailures</c> sink, bumped once per row the push gives up on as
|
||||
/// <see cref="SiteAuditForwardStates.Rejected"/> (review 2026-10-21 06/IMPL-1). Null on
|
||||
/// composition roots without Site Health Monitoring.
|
||||
/// </param>
|
||||
public SqliteAuditWriter(
|
||||
IOptions<SqliteAuditWriterOptions> options,
|
||||
ILogger<SqliteAuditWriter> logger,
|
||||
INodeIdentityProvider nodeIdentity,
|
||||
string? connectionStringOverride = null,
|
||||
IAuditWriteFailureCounter? failureCounter = null)
|
||||
string? connectionStringOverride = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
@@ -108,7 +101,6 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
_nodeIdentity = nodeIdentity;
|
||||
_failureCounter = failureCounter;
|
||||
|
||||
var connectionString = connectionStringOverride
|
||||
?? $"Data Source={_options.DatabasePath};Cache=Shared";
|
||||
@@ -116,6 +108,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
_connection.Open();
|
||||
|
||||
InitializeSchema();
|
||||
LoadMaxServedRowId();
|
||||
|
||||
// Open a second connection for read-only callers
|
||||
// (GetBacklogStatsAsync, ReadPendingAsync, ReadPendingSinceAsync,
|
||||
@@ -275,6 +268,16 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
CREATE INDEX IF NOT EXISTS IX_fwd
|
||||
ON audit_forward_state (ForwardState, IsCachedKind, OccurredAtUtc);
|
||||
|
||||
-- Node-local key/value state that must survive a process restart. Holds the
|
||||
-- reconciliation pull's served-rows high-water mark ('max_served_rowid'), so a
|
||||
-- restart no longer resets the bound that lets a covering cursor retire rows the
|
||||
-- pull already delivered (review 2026-10-21 06/PERF-1 adversarial follow-up).
|
||||
-- Additive and idempotent: an older binary simply ignores the table.
|
||||
CREATE TABLE IF NOT EXISTS audit_site_state (
|
||||
Key TEXT NOT NULL PRIMARY KEY,
|
||||
Value INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Flow Debugger (design 2026-09-05 §5.4): PullTraceEvents reads audit_event by
|
||||
-- time window + json_extract on ExecutionId/ParentExecutionId; this bounds the scan
|
||||
-- to the window. Additive and idempotent.
|
||||
@@ -345,6 +348,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
// TryWrite can tell "disposed" from "full" without racing the channel's state.
|
||||
private bool _writerCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Rows moved to <see cref="SiteAuditForwardStates.Rejected"/> since construction (also exported
|
||||
/// as the <c>scadabridge.site.audit.rejected_rows</c> counter).
|
||||
/// </summary>
|
||||
public long RejectedPushRowCount => Interlocked.Read(ref _rejectedPushRows);
|
||||
private long _rejectedPushRows;
|
||||
|
||||
private async Task ProcessWriteQueueAsync()
|
||||
{
|
||||
var batch = new List<PendingAuditEvent>(_options.BatchSize);
|
||||
@@ -845,6 +855,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
// pull's 256-rows-per-tick budget was spent on acked rows, so any site above
|
||||
// ~0.85 audit rows/s fell permanently behind and latched the stalled signal.
|
||||
// A Forwarded row is acked by central; the pull has nothing to repair there.
|
||||
IReadOnlyList<AuditEvent> rows;
|
||||
var before = Volatile.Read(ref _maxServedRowId);
|
||||
lock (_readLock)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
@@ -884,8 +896,33 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
}
|
||||
cmd.Parameters.AddWithValue("$limit", batchSize);
|
||||
|
||||
return Task.FromResult(ReadServedRows(cmd, batchSize));
|
||||
rows = ReadServedRows(cmd, batchSize);
|
||||
}
|
||||
|
||||
// Persist the (possibly advanced) mark OUTSIDE _readLock so the two locks are never
|
||||
// nested in read→write order. Only when it moved — at most one tiny upsert per pull.
|
||||
var mark = Volatile.Read(ref _maxServedRowId);
|
||||
if (mark > before)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
PersistMaxServedRowId(mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"SqliteAuditWriter could not persist the served high-water mark {Mark}; a restart re-learns it from the next pull.",
|
||||
mark);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(rows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1005,11 +1042,11 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
"of the push queue as Rejected; the reconciliation pull still serves them (central records " +
|
||||
"a ReconciliationAbandoned row if they can never be inserted).",
|
||||
flipped, threshold);
|
||||
for (var i = 0; i < flipped; i++)
|
||||
{
|
||||
try { _failureCounter?.Increment(); }
|
||||
catch { /* counter must never throw — defence in depth */ }
|
||||
}
|
||||
// Its OWN counter, not SiteAuditWriteFailures: the local write succeeded, and
|
||||
// the refusal is central's verdict on the row — blaming the site's writer for it
|
||||
// would be wrong (review 2026-10-21 adversarial follow-up).
|
||||
Interlocked.Add(ref _rejectedPushRows, flipped);
|
||||
SiteAuditTelemetryMetrics.RecordRejectedRows(flipped);
|
||||
}
|
||||
|
||||
return Task.FromResult(flipped);
|
||||
@@ -1154,6 +1191,49 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
/// </summary>
|
||||
private long _maxServedRowId;
|
||||
|
||||
private const string MaxServedRowIdKey = "max_served_rowid";
|
||||
|
||||
/// <summary>
|
||||
/// Restores <see cref="_maxServedRowId"/> from <c>audit_site_state</c>, clamped to the live
|
||||
/// <c>MAX(rowid)</c> so a stale value can never vouch for a reused rowid. Before this the
|
||||
/// bound was per-process: after a site-node restart it was 0, and because a healthy site's
|
||||
/// pull now often serves nothing, owed rows central had already ingested (e.g. PushAbandoned)
|
||||
/// could stay unretired — and counted on the backlog — indefinitely.
|
||||
/// </summary>
|
||||
private void LoadMaxServedRowId()
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
SELECT MIN(
|
||||
IFNULL((SELECT Value FROM audit_site_state WHERE Key = $key), 0),
|
||||
IFNULL((SELECT MAX(rowid) FROM audit_forward_state), 0));
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("$key", MaxServedRowIdKey);
|
||||
var value = Convert.ToInt64(cmd.ExecuteScalar(), System.Globalization.CultureInfo.InvariantCulture);
|
||||
Volatile.Write(ref _maxServedRowId, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Durably records the served high-water mark. Runs on the WRITE connection under
|
||||
/// <c>_writeLock</c> (never nested inside <c>_readLock</c>). Best-effort: a failure leaves
|
||||
/// the in-memory bound intact and only means a restart re-learns it from the next pull.
|
||||
/// </summary>
|
||||
private void PersistMaxServedRowId(long value, SqliteTransaction? transaction = null)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.Transaction = transaction;
|
||||
cmd.CommandText = """
|
||||
INSERT INTO audit_site_state (Key, Value) VALUES ($key, $value)
|
||||
ON CONFLICT(Key) DO UPDATE SET Value = excluded.Value;
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("$key", MaxServedRowIdKey);
|
||||
cmd.Parameters.AddWithValue("$value", value);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
/// <summary>Test seam: the current in-memory served high-water mark.</summary>
|
||||
internal long MaxServedRowIdForTests => Volatile.Read(ref _maxServedRowId);
|
||||
|
||||
/// <summary>
|
||||
/// Executes a reconciliation-pull read whose projection carries <c>fs.rowid</c> as an
|
||||
/// 11th column, materialising the canonical rows while advancing
|
||||
@@ -1222,12 +1302,12 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
// acked them over the push path, they are already purge-eligible, and flipping
|
||||
// them was one UPDATE per pushed row for no benefit (review 2026-10-21 06/PERF-1).
|
||||
//
|
||||
// Bound state is per-process: after a restart it is 0 until this instance serves
|
||||
// a batch, so the first pull retires nothing and the pull after it
|
||||
// resumes normal retirement. Conservative in the safe direction (delays
|
||||
// retirement, never loses a row); the liveness note in MarkReconciledUpToAsync's
|
||||
// interface doc + Component-AuditLog.md covers what a permanently halted
|
||||
// reconciliation means for such rows.
|
||||
// The bound is persisted in audit_site_state (loaded at construction, clamped to
|
||||
// the live MAX(rowid)), so a site-node restart no longer resets it to 0 and strands
|
||||
// rows the pull had already served. Conservative in the safe direction (it can
|
||||
// only delay retirement, never lose a row); the liveness note in
|
||||
// MarkReconciledUpToAsync's interface doc + Component-AuditLog.md covers what a
|
||||
// permanently halted reconciliation means for such rows.
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = $"""
|
||||
UPDATE audit_forward_state AS fs
|
||||
@@ -1506,7 +1586,10 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, ISiteAuditPushLi
|
||||
var liveMax = Convert.ToInt64(maxCmd.ExecuteScalar(),
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (liveMax < Volatile.Read(ref _maxServedRowId))
|
||||
{
|
||||
Volatile.Write(ref _maxServedRowId, liveMax);
|
||||
PersistMaxServedRowId(liveMax, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
@@ -481,28 +481,30 @@ public class SiteAuditTelemetryActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a refused push attempt for every row that was SENT in an answered batch but is
|
||||
/// missing from the ack's <c>AcceptedEventIds</c>; rows refused
|
||||
/// <see cref="SiteAuditTelemetryOptions.MaxRejectedPushAttempts"/> times leave the push
|
||||
/// queue as <see cref="SiteAuditForwardStates.Rejected"/> (review 2026-10-21 06/IMPL-1).
|
||||
/// Records a refused push attempt for every SENT row central explicitly listed in the ack's
|
||||
/// <c>RejectedEventIds</c> — a permanent, row-intrinsic insert failure. Rows refused
|
||||
/// <see cref="SiteAuditTelemetryOptions.MaxRejectedPushAttempts"/> times leave the push queue as
|
||||
/// <see cref="SiteAuditForwardStates.Rejected"/> (review 2026-10-21 06/IMPL-1).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Called only after the push RETURNED — a transport fault is not a verdict about any row
|
||||
/// and never counts. Best-effort: a bookkeeping failure leaves the rows Pending, exactly as
|
||||
/// before this existed.
|
||||
/// "Not accepted" is NOT a refusal: central answers with an empty or partial ack during a SQL
|
||||
/// maintenance window, a timeout, or an exhausted ingest budget, and counting those would flip
|
||||
/// healthy rows to Rejected. Only the explicit list counts; an older central never sends it, so
|
||||
/// against it this degrades to the pre-IMPL-1 behaviour (rows stay Pending). Best-effort: a
|
||||
/// bookkeeping failure leaves the rows Pending.
|
||||
/// </remarks>
|
||||
private async Task RecordRefusalsAsync(IReadOnlyList<string> sentIds, IngestAck ack, CancellationToken ct)
|
||||
{
|
||||
if (_queue is not ISiteAuditPushLifecycle lifecycle || sentIds.Count == 0)
|
||||
if (_queue is not ISiteAuditPushLifecycle lifecycle || sentIds.Count == 0 || ack.RejectedEventIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var accepted = new HashSet<string>(ack.AcceptedEventIds, StringComparer.OrdinalIgnoreCase);
|
||||
var sent = new HashSet<string>(sentIds, StringComparer.OrdinalIgnoreCase);
|
||||
var refused = new List<Guid>();
|
||||
foreach (var raw in sentIds)
|
||||
foreach (var raw in ack.RejectedEventIds)
|
||||
{
|
||||
if (!accepted.Contains(raw) && Guid.TryParse(raw, out var id))
|
||||
if (sent.Contains(raw) && Guid.TryParse(raw, out var id))
|
||||
{
|
||||
refused.Add(id);
|
||||
}
|
||||
|
||||
@@ -57,18 +57,22 @@ public sealed class SiteAuditTelemetryOptions
|
||||
public int CachedTrackingGraceSeconds { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Number of drains in which central ANSWERED a push but refused a row (the row is
|
||||
/// missing from <c>AcceptedEventIds</c>) after which the row leaves the push queue as
|
||||
/// <c>Rejected</c>. Default: 10 (≈ 50 s at the busy cadence).
|
||||
/// Number of drains in which central EXPLICITLY refused a row (listed it in the ack's
|
||||
/// additive <c>rejected_event_ids</c> — a permanent, row-intrinsic insert failure such as an
|
||||
/// over-width value) after which the row leaves the push queue as <c>Rejected</c>.
|
||||
/// Default: 10 (≈ 50 s at the busy cadence).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The push reads oldest-first, so a row central can never insert (e.g. an over-width
|
||||
/// column) used to sit at the head of every batch forever, failing each set-based insert
|
||||
/// at central and dragging the batch into the per-row fallback (review 2026-10-21
|
||||
/// 06/IMPL-1). A transport fault (no answer) never counts. <c>Rejected</c> is NOT a drop:
|
||||
/// 06/IMPL-1). A transport fault, and a row merely ABSENT from the accepted list (a database
|
||||
/// outage, a timeout, an exhausted ingest budget), never counts; an older central that does not
|
||||
/// send the field therefore never triggers it. <c>Rejected</c> is NOT a drop:
|
||||
/// the reconciliation pull still serves the row, so a transient central refusal still
|
||||
/// lands, and a genuinely poison row is recorded centrally as <c>ReconciliationAbandoned</c>
|
||||
/// before it retires. Each flip bumps <c>SiteAuditWriteFailures</c> on the site health report.
|
||||
/// before it retires. Each flip is counted on <c>scadabridge.site.audit.rejected_rows</c> (NOT
|
||||
/// <c>SiteAuditWriteFailures</c> — the site's own write succeeded).
|
||||
/// </remarks>
|
||||
public int MaxRejectedPushAttempts { get; set; } = 10;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public sealed class SiteCommunicationAuditClient : ISiteStreamAuditClient
|
||||
.Ask<IngestAuditEventsReply>(new IngestAuditEventsCommand(events), _askTimeout, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return ToAck(reply.AcceptedEventIds);
|
||||
return ToAck(reply.AcceptedEventIds, reply.RejectedEventIds);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -105,16 +105,21 @@ public sealed class SiteCommunicationAuditClient : ISiteStreamAuditClient
|
||||
.Ask<IngestCachedTelemetryReply>(new IngestCachedTelemetryCommand(entries), _askTimeout, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return ToAck(reply.AcceptedEventIds);
|
||||
return ToAck(reply.AcceptedEventIds, reply.RejectedEventIds);
|
||||
}
|
||||
|
||||
private static IngestAck ToAck(IReadOnlyList<Guid> acceptedEventIds)
|
||||
private static IngestAck ToAck(IReadOnlyList<Guid> acceptedEventIds, IReadOnlyList<Guid> rejectedEventIds)
|
||||
{
|
||||
var ack = new IngestAck();
|
||||
foreach (var id in acceptedEventIds)
|
||||
{
|
||||
ack.AcceptedEventIds.Add(id.ToString());
|
||||
}
|
||||
// Explicit permanent refusals only (review 2026-10-21 06/IMPL-1); empty from an older central.
|
||||
foreach (var id in rejectedEventIds)
|
||||
{
|
||||
ack.RejectedEventIds.Add(id.ToString());
|
||||
}
|
||||
return ack;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,13 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
||||
/// The gRPC handler echoes these ids back over the wire as the <c>IngestAck</c>
|
||||
/// the site uses to flip rows to <c>Forwarded</c>.
|
||||
/// </summary>
|
||||
public sealed record IngestAuditEventsReply(IReadOnlyList<Guid> AcceptedEventIds);
|
||||
public sealed record IngestAuditEventsReply(IReadOnlyList<Guid> AcceptedEventIds)
|
||||
{
|
||||
/// <summary>
|
||||
/// Ids central PERMANENTLY refused — a non-transient per-row insert failure (e.g. an
|
||||
/// over-width value), never a database outage or an exhausted budget. Additive (review
|
||||
/// 2026-10-21 06/IMPL-1): the site counts only these toward giving a row up as
|
||||
/// <c>Rejected</c>; empty from an older central, which therefore never triggers it.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Guid> RejectedEventIds { get; init; } = Array.Empty<Guid>();
|
||||
}
|
||||
|
||||
@@ -7,4 +7,13 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
||||
/// UPSERT) committed; entries whose transaction rolled back are absent so the
|
||||
/// site can leave the row Pending and retry on the next drain.
|
||||
/// </summary>
|
||||
public sealed record IngestCachedTelemetryReply(IReadOnlyList<Guid> AcceptedEventIds);
|
||||
public sealed record IngestCachedTelemetryReply(IReadOnlyList<Guid> AcceptedEventIds)
|
||||
{
|
||||
/// <summary>
|
||||
/// Ids central PERMANENTLY refused — a non-transient per-row insert failure (e.g. an
|
||||
/// over-width value), never a database outage or an exhausted budget. Additive (review
|
||||
/// 2026-10-21 06/IMPL-1): the site counts only these toward giving a row up as
|
||||
/// <c>Rejected</c>; empty from an older central, which therefore never triggers it.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Guid> RejectedEventIds { get; init; } = Array.Empty<Guid>();
|
||||
}
|
||||
|
||||
@@ -314,7 +314,9 @@ public static class CentralControlDtoMapper
|
||||
/// </summary>
|
||||
/// <param name="acceptedEventIds">Ids central considers durably persisted.</param>
|
||||
/// <returns>The wire ack carrying the ids in "D" string form, in order.</returns>
|
||||
public static IngestAck ToIngestAck(IReadOnlyList<Guid> acceptedEventIds)
|
||||
/// <param name="rejectedEventIds">Ids central permanently refused (additive field 2); null = none.</param>
|
||||
public static IngestAck ToIngestAck(
|
||||
IReadOnlyList<Guid> acceptedEventIds, IReadOnlyList<Guid>? rejectedEventIds = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(acceptedEventIds);
|
||||
|
||||
@@ -324,9 +326,39 @@ public static class CentralControlDtoMapper
|
||||
ack.AcceptedEventIds.Add(id.ToString());
|
||||
}
|
||||
|
||||
if (rejectedEventIds is not null)
|
||||
{
|
||||
foreach (var id in rejectedEventIds)
|
||||
{
|
||||
ack.RejectedEventIds.Add(id.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return ack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the permanently-rejected id list (additive field 2) out of an <see cref="IngestAck"/>;
|
||||
/// empty from an older central. Unparseable entries are skipped.
|
||||
/// </summary>
|
||||
/// <param name="ack">The wire ack to read.</param>
|
||||
/// <returns>The rejected event ids, in wire order.</returns>
|
||||
public static IReadOnlyList<Guid> RejectedFromIngestAck(IngestAck ack)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ack);
|
||||
|
||||
var ids = new List<Guid>(ack.RejectedEventIds.Count);
|
||||
foreach (var id in ack.RejectedEventIds)
|
||||
{
|
||||
if (Guid.TryParse(id, out var g))
|
||||
{
|
||||
ids.Add(g);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the accepted-id list back out of an <see cref="IngestAck"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -198,7 +198,7 @@ public sealed class CentralControlGrpcService : CentralControlService.CentralCon
|
||||
SiteStreamGrpcServer.AuditIngestAskTimeout,
|
||||
context).ConfigureAwait(false);
|
||||
|
||||
return CentralControlDtoMapper.ToIngestAck(reply.AcceptedEventIds);
|
||||
return CentralControlDtoMapper.ToIngestAck(reply.AcceptedEventIds, reply.RejectedEventIds);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -223,7 +223,7 @@ public sealed class CentralControlGrpcService : CentralControlService.CentralCon
|
||||
SiteStreamGrpcServer.AuditIngestAskTimeout,
|
||||
context).ConfigureAwait(false);
|
||||
|
||||
return CentralControlDtoMapper.ToIngestAck(reply.AcceptedEventIds);
|
||||
return CentralControlDtoMapper.ToIngestAck(reply.AcceptedEventIds, reply.RejectedEventIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -88,7 +88,10 @@ public sealed class GrpcCentralTransport : ICentralTransport
|
||||
var batch = CentralControlDtoMapper.ToDto(message);
|
||||
Dispatch(replyTo, SiteStreamGrpcServer.AuditIngestAskTimeout,
|
||||
(c, o) => c.IngestAuditEventsAsync(batch, o),
|
||||
ack => new IngestAuditEventsReply(CentralControlDtoMapper.FromIngestAck(ack)), retrySafe: true);
|
||||
ack => new IngestAuditEventsReply(CentralControlDtoMapper.FromIngestAck(ack))
|
||||
{
|
||||
RejectedEventIds = CentralControlDtoMapper.RejectedFromIngestAck(ack),
|
||||
}, retrySafe: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -97,7 +100,10 @@ public sealed class GrpcCentralTransport : ICentralTransport
|
||||
var batch = CentralControlDtoMapper.ToDto(message);
|
||||
Dispatch(replyTo, SiteStreamGrpcServer.AuditIngestAskTimeout,
|
||||
(c, o) => c.IngestCachedTelemetryAsync(batch, o),
|
||||
ack => new IngestCachedTelemetryReply(CentralControlDtoMapper.FromIngestAck(ack)), retrySafe: true);
|
||||
ack => new IngestCachedTelemetryReply(CentralControlDtoMapper.FromIngestAck(ack))
|
||||
{
|
||||
RejectedEventIds = CentralControlDtoMapper.RejectedFromIngestAck(ack),
|
||||
}, retrySafe: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -172,7 +172,15 @@ message AuditEventDto {
|
||||
}
|
||||
|
||||
message AuditEventBatch { repeated AuditEventDto events = 1; }
|
||||
message IngestAck { repeated string accepted_event_ids = 1; }
|
||||
// rejected_event_ids (additive, field 2 — review 2026-10-21 06/IMPL-1): ids central
|
||||
// PERMANENTLY refused (a non-transient per-row SQL error such as an over-width
|
||||
// value). Only these count toward the site's MaxRejectedPushAttempts; ids that are
|
||||
// neither accepted nor rejected (DB outage, budget exhaustion) are simply retried.
|
||||
// An older central never sets it, so the site never counts a refusal.
|
||||
message IngestAck {
|
||||
repeated string accepted_event_ids = 1;
|
||||
repeated string rejected_event_ids = 2;
|
||||
}
|
||||
|
||||
// Audit Log (#23) M3 cached-call combined telemetry: a single packet carries
|
||||
// both the AuditEvent row to insert and the SiteCalls operational-state upsert
|
||||
|
||||
@@ -71,57 +71,58 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
"b2FkX3RydW5jYXRlZBgSIAEoCBINCgVleHRyYRgTIAEoCRIUCgxleGVjdXRp",
|
||||
"b25faWQYFCABKAkSGwoTcGFyZW50X2V4ZWN1dGlvbl9pZBgVIAEoCRITCgtz",
|
||||
"b3VyY2Vfbm9kZRgWIAEoCSI8Cg9BdWRpdEV2ZW50QmF0Y2gSKQoGZXZlbnRz",
|
||||
"GAEgAygLMhkuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50RHRvIicKCUluZ2VzdEFj",
|
||||
"axIaChJhY2NlcHRlZF9ldmVudF9pZHMYASADKAkiiQMKFlNpdGVDYWxsT3Bl",
|
||||
"cmF0aW9uYWxEdG8SHAoUdHJhY2tlZF9vcGVyYXRpb25faWQYASABKAkSDwoH",
|
||||
"Y2hhbm5lbBgCIAEoCRIOCgZ0YXJnZXQYAyABKAkSEwoLc291cmNlX3NpdGUY",
|
||||
"BCABKAkSDgoGc3RhdHVzGAUgASgJEhMKC3JldHJ5X2NvdW50GAYgASgFEhIK",
|
||||
"Cmxhc3RfZXJyb3IYByABKAkSMAoLaHR0cF9zdGF0dXMYCCABKAsyGy5nb29n",
|
||||
"bGUucHJvdG9idWYuSW50MzJWYWx1ZRIyCg5jcmVhdGVkX2F0X3V0YxgJIAEo",
|
||||
"CzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoOdXBkYXRlZF9hdF91",
|
||||
"dGMYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD3Rlcm1p",
|
||||
"bmFsX2F0X3V0YxgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAS",
|
||||
"EwoLc291cmNlX25vZGUYDCABKAkigAEKFUNhY2hlZFRlbGVtZXRyeVBhY2tl",
|
||||
"dBIuCgthdWRpdF9ldmVudBgBIAEoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVu",
|
||||
"dER0bxI3CgtvcGVyYXRpb25hbBgCIAEoCzIiLnNpdGVzdHJlYW0uU2l0ZUNh",
|
||||
"bGxPcGVyYXRpb25hbER0byJKChRDYWNoZWRUZWxlbWV0cnlCYXRjaBIyCgdw",
|
||||
"YWNrZXRzGAEgAygLMiEuc2l0ZXN0cmVhbS5DYWNoZWRUZWxlbWV0cnlQYWNr",
|
||||
"ZXQibQoWUHVsbEF1ZGl0RXZlbnRzUmVxdWVzdBItCglzaW5jZV91dGMYASAB",
|
||||
"KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhIKCmJhdGNoX3NpemUY",
|
||||
"AiABKAUSEAoIYWZ0ZXJfaWQYAyABKAkiXAoXUHVsbEF1ZGl0RXZlbnRzUmVz",
|
||||
"cG9uc2USKQoGZXZlbnRzGAEgAygLMhkuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50",
|
||||
"RHRvEhYKDm1vcmVfYXZhaWxhYmxlGAIgASgIIoQBChZQdWxsVHJhY2VFdmVu",
|
||||
"dHNSZXF1ZXN0EhAKCGFmdGVyX2lkGAQgASgJEhUKDWV4ZWN1dGlvbl9pZHMY",
|
||||
"ASADKAkSLQoJc2luY2VfdXRjGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRp",
|
||||
"bWVzdGFtcBISCgptYXhfZXZlbnRzGAMgASgFIkQKF1B1bGxUcmFjZUV2ZW50",
|
||||
"c1Jlc3BvbnNlEikKBmV2ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRF",
|
||||
"dmVudER0byJrChRQdWxsU2l0ZUNhbGxzUmVxdWVzdBItCglzaW5jZV91dGMY",
|
||||
"ASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhIKCmJhdGNoX3Np",
|
||||
"emUYAiABKAUSEAoIYWZ0ZXJfaWQYAyABKAkiaQoVUHVsbFNpdGVDYWxsc1Jl",
|
||||
"c3BvbnNlEjgKDG9wZXJhdGlvbmFscxgBIAMoCzIiLnNpdGVzdHJlYW0uU2l0",
|
||||
"ZUNhbGxPcGVyYXRpb25hbER0bxIWCg5tb3JlX2F2YWlsYWJsZRgCIAEoCCpc",
|
||||
"CgdRdWFsaXR5EhcKE1FVQUxJVFlfVU5TUEVDSUZJRUQQABIQCgxRVUFMSVRZ",
|
||||
"X0dPT0QQARIVChFRVUFMSVRZX1VOQ0VSVEFJThACEg8KC1FVQUxJVFlfQkFE",
|
||||
"EAMqXQoOQWxhcm1TdGF0ZUVudW0SGwoXQUxBUk1fU1RBVEVfVU5TUEVDSUZJ",
|
||||
"RUQQABIWChJBTEFSTV9TVEFURV9OT1JNQUwQARIWChJBTEFSTV9TVEFURV9B",
|
||||
"Q1RJVkUQAiqFAQoOQWxhcm1MZXZlbEVudW0SFAoQQUxBUk1fTEVWRUxfTk9O",
|
||||
"RRAAEhMKD0FMQVJNX0xFVkVMX0xPVxABEhcKE0FMQVJNX0xFVkVMX0xPV19M",
|
||||
"T1cQAhIUChBBTEFSTV9MRVZFTF9ISUdIEAMSGQoVQUxBUk1fTEVWRUxfSElH",
|
||||
"SF9ISUdIEAQy4gQKEVNpdGVTdHJlYW1TZXJ2aWNlElUKEVN1YnNjcmliZUlu",
|
||||
"c3RhbmNlEiEuc2l0ZXN0cmVhbS5JbnN0YW5jZVN0cmVhbVJlcXVlc3QaGy5z",
|
||||
"aXRlc3RyZWFtLlNpdGVTdHJlYW1FdmVudDABEk0KDVN1YnNjcmliZVNpdGUS",
|
||||
"HS5zaXRlc3RyZWFtLlNpdGVTdHJlYW1SZXF1ZXN0Ghsuc2l0ZXN0cmVhbS5T",
|
||||
"aXRlU3RyZWFtRXZlbnQwARJHChFJbmdlc3RBdWRpdEV2ZW50cxIbLnNpdGVz",
|
||||
"dHJlYW0uQXVkaXRFdmVudEJhdGNoGhUuc2l0ZXN0cmVhbS5Jbmdlc3RBY2sS",
|
||||
"UAoVSW5nZXN0Q2FjaGVkVGVsZW1ldHJ5EiAuc2l0ZXN0cmVhbS5DYWNoZWRU",
|
||||
"ZWxlbWV0cnlCYXRjaBoVLnNpdGVzdHJlYW0uSW5nZXN0QWNrEloKD1B1bGxB",
|
||||
"dWRpdEV2ZW50cxIiLnNpdGVzdHJlYW0uUHVsbEF1ZGl0RXZlbnRzUmVxdWVz",
|
||||
"dBojLnNpdGVzdHJlYW0uUHVsbEF1ZGl0RXZlbnRzUmVzcG9uc2USWgoPUHVs",
|
||||
"bFRyYWNlRXZlbnRzEiIuc2l0ZXN0cmVhbS5QdWxsVHJhY2VFdmVudHNSZXF1",
|
||||
"ZXN0GiMuc2l0ZXN0cmVhbS5QdWxsVHJhY2VFdmVudHNSZXNwb25zZRJUCg1Q",
|
||||
"dWxsU2l0ZUNhbGxzEiAuc2l0ZXN0cmVhbS5QdWxsU2l0ZUNhbGxzUmVxdWVz",
|
||||
"dBohLnNpdGVzdHJlYW0uUHVsbFNpdGVDYWxsc1Jlc3BvbnNlQiuqAihaQi5N",
|
||||
"T00uV1cuU2NhZGFCcmlkZ2UuQ29tbXVuaWNhdGlvbi5HcnBjYgZwcm90bzM="));
|
||||
"GAEgAygLMhkuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50RHRvIkMKCUluZ2VzdEFj",
|
||||
"axIaChJhY2NlcHRlZF9ldmVudF9pZHMYASADKAkSGgoScmVqZWN0ZWRfZXZl",
|
||||
"bnRfaWRzGAIgAygJIokDChZTaXRlQ2FsbE9wZXJhdGlvbmFsRHRvEhwKFHRy",
|
||||
"YWNrZWRfb3BlcmF0aW9uX2lkGAEgASgJEg8KB2NoYW5uZWwYAiABKAkSDgoG",
|
||||
"dGFyZ2V0GAMgASgJEhMKC3NvdXJjZV9zaXRlGAQgASgJEg4KBnN0YXR1cxgF",
|
||||
"IAEoCRITCgtyZXRyeV9jb3VudBgGIAEoBRISCgpsYXN0X2Vycm9yGAcgASgJ",
|
||||
"EjAKC2h0dHBfc3RhdHVzGAggASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMy",
|
||||
"VmFsdWUSMgoOY3JlYXRlZF9hdF91dGMYCSABKAsyGi5nb29nbGUucHJvdG9i",
|
||||
"dWYuVGltZXN0YW1wEjIKDnVwZGF0ZWRfYXRfdXRjGAogASgLMhouZ29vZ2xl",
|
||||
"LnByb3RvYnVmLlRpbWVzdGFtcBIzCg90ZXJtaW5hbF9hdF91dGMYCyABKAsy",
|
||||
"Gi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhMKC3NvdXJjZV9ub2RlGAwg",
|
||||
"ASgJIoABChVDYWNoZWRUZWxlbWV0cnlQYWNrZXQSLgoLYXVkaXRfZXZlbnQY",
|
||||
"ASABKAsyGS5zaXRlc3RyZWFtLkF1ZGl0RXZlbnREdG8SNwoLb3BlcmF0aW9u",
|
||||
"YWwYAiABKAsyIi5zaXRlc3RyZWFtLlNpdGVDYWxsT3BlcmF0aW9uYWxEdG8i",
|
||||
"SgoUQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gSMgoHcGFja2V0cxgBIAMoCzIhLnNp",
|
||||
"dGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5UGFja2V0Im0KFlB1bGxBdWRpdEV2",
|
||||
"ZW50c1JlcXVlc3QSLQoJc2luY2VfdXRjGAEgASgLMhouZ29vZ2xlLnByb3Rv",
|
||||
"YnVmLlRpbWVzdGFtcBISCgpiYXRjaF9zaXplGAIgASgFEhAKCGFmdGVyX2lk",
|
||||
"GAMgASgJIlwKF1B1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEikKBmV2ZW50cxgB",
|
||||
"IAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxIWCg5tb3JlX2F2YWls",
|
||||
"YWJsZRgCIAEoCCKEAQoWUHVsbFRyYWNlRXZlbnRzUmVxdWVzdBIQCghhZnRl",
|
||||
"cl9pZBgEIAEoCRIVCg1leGVjdXRpb25faWRzGAEgAygJEi0KCXNpbmNlX3V0",
|
||||
"YxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEgoKbWF4X2V2",
|
||||
"ZW50cxgDIAEoBSJEChdQdWxsVHJhY2VFdmVudHNSZXNwb25zZRIpCgZldmVu",
|
||||
"dHMYASADKAsyGS5zaXRlc3RyZWFtLkF1ZGl0RXZlbnREdG8iawoUUHVsbFNp",
|
||||
"dGVDYWxsc1JlcXVlc3QSLQoJc2luY2VfdXRjGAEgASgLMhouZ29vZ2xlLnBy",
|
||||
"b3RvYnVmLlRpbWVzdGFtcBISCgpiYXRjaF9zaXplGAIgASgFEhAKCGFmdGVy",
|
||||
"X2lkGAMgASgJImkKFVB1bGxTaXRlQ2FsbHNSZXNwb25zZRI4CgxvcGVyYXRp",
|
||||
"b25hbHMYASADKAsyIi5zaXRlc3RyZWFtLlNpdGVDYWxsT3BlcmF0aW9uYWxE",
|
||||
"dG8SFgoObW9yZV9hdmFpbGFibGUYAiABKAgqXAoHUXVhbGl0eRIXChNRVUFM",
|
||||
"SVRZX1VOU1BFQ0lGSUVEEAASEAoMUVVBTElUWV9HT09EEAESFQoRUVVBTElU",
|
||||
"WV9VTkNFUlRBSU4QAhIPCgtRVUFMSVRZX0JBRBADKl0KDkFsYXJtU3RhdGVF",
|
||||
"bnVtEhsKF0FMQVJNX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSQUxBUk1fU1RB",
|
||||
"VEVfTk9STUFMEAESFgoSQUxBUk1fU1RBVEVfQUNUSVZFEAIqhQEKDkFsYXJt",
|
||||
"TGV2ZWxFbnVtEhQKEEFMQVJNX0xFVkVMX05PTkUQABITCg9BTEFSTV9MRVZF",
|
||||
"TF9MT1cQARIXChNBTEFSTV9MRVZFTF9MT1dfTE9XEAISFAoQQUxBUk1fTEVW",
|
||||
"RUxfSElHSBADEhkKFUFMQVJNX0xFVkVMX0hJR0hfSElHSBAEMuIEChFTaXRl",
|
||||
"U3RyZWFtU2VydmljZRJVChFTdWJzY3JpYmVJbnN0YW5jZRIhLnNpdGVzdHJl",
|
||||
"YW0uSW5zdGFuY2VTdHJlYW1SZXF1ZXN0Ghsuc2l0ZXN0cmVhbS5TaXRlU3Ry",
|
||||
"ZWFtRXZlbnQwARJNCg1TdWJzY3JpYmVTaXRlEh0uc2l0ZXN0cmVhbS5TaXRl",
|
||||
"U3RyZWFtUmVxdWVzdBobLnNpdGVzdHJlYW0uU2l0ZVN0cmVhbUV2ZW50MAES",
|
||||
"RwoRSW5nZXN0QXVkaXRFdmVudHMSGy5zaXRlc3RyZWFtLkF1ZGl0RXZlbnRC",
|
||||
"YXRjaBoVLnNpdGVzdHJlYW0uSW5nZXN0QWNrElAKFUluZ2VzdENhY2hlZFRl",
|
||||
"bGVtZXRyeRIgLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gaFS5z",
|
||||
"aXRlc3RyZWFtLkluZ2VzdEFjaxJaCg9QdWxsQXVkaXRFdmVudHMSIi5zaXRl",
|
||||
"c3RyZWFtLlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QaIy5zaXRlc3RyZWFtLlB1",
|
||||
"bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEloKD1B1bGxUcmFjZUV2ZW50cxIiLnNp",
|
||||
"dGVzdHJlYW0uUHVsbFRyYWNlRXZlbnRzUmVxdWVzdBojLnNpdGVzdHJlYW0u",
|
||||
"UHVsbFRyYWNlRXZlbnRzUmVzcG9uc2USVAoNUHVsbFNpdGVDYWxscxIgLnNp",
|
||||
"dGVzdHJlYW0uUHVsbFNpdGVDYWxsc1JlcXVlc3QaIS5zaXRlc3RyZWFtLlB1",
|
||||
"bGxTaXRlQ2FsbHNSZXNwb25zZUIrqgIoWkIuTU9NLldXLlNjYWRhQnJpZGdl",
|
||||
"LkNvbW11bmljYXRpb24uR3JwY2IGcHJvdG8z"));
|
||||
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
|
||||
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.WrappersReflection.Descriptor, },
|
||||
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.Quality), typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateEnum), typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmLevelEnum), }, null, new pbr::GeneratedClrTypeInfo[] {
|
||||
@@ -133,7 +134,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateUpdate), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateUpdate.Parser, new[]{ "InstanceUniqueName", "AlarmName", "State", "Priority", "Timestamp", "Level", "Message", "Kind", "Active", "Acknowledged", "Confirmed", "ShelveState", "Suppressed", "SourceReference", "AlarmTypeName", "Category", "OperatorUser", "OperatorComment", "OriginalRaiseTime", "CurrentValue", "LimitValue", "NativeSourceCanonicalName", "IsConfiguredPlaceholder", "AckTime" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventDto), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventDto.Parser, new[]{ "EventId", "OccurredAtUtc", "Channel", "Kind", "CorrelationId", "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra", "ExecutionId", "ParentExecutionId", "SourceNode" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventBatch), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventBatch.Parser, new[]{ "Events" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck.Parser, new[]{ "AcceptedEventIds" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck.Parser, new[]{ "AcceptedEventIds", "RejectedEventIds" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteCallOperationalDto), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteCallOperationalDto.Parser, new[]{ "TrackedOperationId", "Channel", "Target", "SourceSite", "Status", "RetryCount", "LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc", "SourceNode" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryPacket), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryPacket.Parser, new[]{ "AuditEvent", "Operational" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryBatch), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryBatch.Parser, new[]{ "Packets" }, null, null, null, null),
|
||||
@@ -4085,6 +4086,13 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// rejected_event_ids (additive, field 2 — review 2026-10-21 06/IMPL-1): ids central
|
||||
/// PERMANENTLY refused (a non-transient per-row SQL error such as an over-width
|
||||
/// value). Only these count toward the site's MaxRejectedPushAttempts; ids that are
|
||||
/// neither accepted nor rejected (DB outage, budget exhaustion) are simply retried.
|
||||
/// An older central never sets it, so the site never counts a refusal.
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
|
||||
public sealed partial class IngestAck : pb::IMessage<IngestAck>
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
@@ -4121,6 +4129,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public IngestAck(IngestAck other) : this() {
|
||||
acceptedEventIds_ = other.acceptedEventIds_.Clone();
|
||||
rejectedEventIds_ = other.rejectedEventIds_.Clone();
|
||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||
}
|
||||
|
||||
@@ -4141,6 +4150,17 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
get { return acceptedEventIds_; }
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "rejected_event_ids" field.</summary>
|
||||
public const int RejectedEventIdsFieldNumber = 2;
|
||||
private static readonly pb::FieldCodec<string> _repeated_rejectedEventIds_codec
|
||||
= pb::FieldCodec.ForString(18);
|
||||
private readonly pbc::RepeatedField<string> rejectedEventIds_ = new pbc::RepeatedField<string>();
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public pbc::RepeatedField<string> RejectedEventIds {
|
||||
get { return rejectedEventIds_; }
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override bool Equals(object other) {
|
||||
@@ -4157,6 +4177,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
return true;
|
||||
}
|
||||
if(!acceptedEventIds_.Equals(other.acceptedEventIds_)) return false;
|
||||
if(!rejectedEventIds_.Equals(other.rejectedEventIds_)) return false;
|
||||
return Equals(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
@@ -4165,6 +4186,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
public override int GetHashCode() {
|
||||
int hash = 1;
|
||||
hash ^= acceptedEventIds_.GetHashCode();
|
||||
hash ^= rejectedEventIds_.GetHashCode();
|
||||
if (_unknownFields != null) {
|
||||
hash ^= _unknownFields.GetHashCode();
|
||||
}
|
||||
@@ -4184,6 +4206,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
output.WriteRawMessage(this);
|
||||
#else
|
||||
acceptedEventIds_.WriteTo(output, _repeated_acceptedEventIds_codec);
|
||||
rejectedEventIds_.WriteTo(output, _repeated_rejectedEventIds_codec);
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(output);
|
||||
}
|
||||
@@ -4195,6 +4218,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
|
||||
acceptedEventIds_.WriteTo(ref output, _repeated_acceptedEventIds_codec);
|
||||
rejectedEventIds_.WriteTo(ref output, _repeated_rejectedEventIds_codec);
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(ref output);
|
||||
}
|
||||
@@ -4206,6 +4230,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
public int CalculateSize() {
|
||||
int size = 0;
|
||||
size += acceptedEventIds_.CalculateSize(_repeated_acceptedEventIds_codec);
|
||||
size += rejectedEventIds_.CalculateSize(_repeated_rejectedEventIds_codec);
|
||||
if (_unknownFields != null) {
|
||||
size += _unknownFields.CalculateSize();
|
||||
}
|
||||
@@ -4219,6 +4244,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
return;
|
||||
}
|
||||
acceptedEventIds_.Add(other.acceptedEventIds_);
|
||||
rejectedEventIds_.Add(other.rejectedEventIds_);
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
@@ -4242,6 +4268,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
acceptedEventIds_.AddEntriesFrom(input, _repeated_acceptedEventIds_codec);
|
||||
break;
|
||||
}
|
||||
case 18: {
|
||||
rejectedEventIds_.AddEntriesFrom(input, _repeated_rejectedEventIds_codec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -4265,6 +4295,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
acceptedEventIds_.AddEntriesFrom(ref input, _repeated_acceptedEventIds_codec);
|
||||
break;
|
||||
}
|
||||
case 18: {
|
||||
rejectedEventIds_.AddEntriesFrom(ref input, _repeated_rejectedEventIds_codec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user