Merge branch 'grpc-event-batching' — additive site-stream event batching, negotiated, 100ev/25ms window (residual #3 / R2)

This commit is contained in:
Joseph Doherty
2026-08-15 04:13:52 -04:00
16 changed files with 2232 additions and 134 deletions
@@ -93,6 +93,20 @@ public static class ScadaBridgeTelemetry
Meter.CreateCounter<long>("scadabridge.site.stream.events_dropped", unit: "1",
description: "Events evicted from a site gRPC stream's bounded send channel, tagged by stream kind.");
// ---------------- Histograms ----------------
/// <summary>
/// Distribution of how many events each site→central stream frame carried (R2 — gRPC
/// event batching), tagged by stream kind. Recorded ONLY for subscriptions that
/// negotiated batching, so the series' very existence says "this central speaks the
/// batched wire". A distribution pinned at 1 means the coalescing window never sees a
/// backlog (the site is quiet, or the window is too small to be earning anything);
/// mass at the size cap means the cap, not the window, is the binding constraint.
/// </summary>
private static readonly Histogram<int> _siteStreamBatchSize =
Meter.CreateHistogram<int>("scadabridge.site.stream.batch_size", unit: "1",
description: "Events per site gRPC stream frame (1 = unbatched frame), tagged by stream kind.");
// ---------------- Observable gauges ----------------
/// <summary>Current count of open site connections, mutated via <see cref="Interlocked"/>.</summary>
@@ -178,6 +192,16 @@ public static class ScadaBridgeTelemetry
public static void RecordSiteStreamEventDropped(string streamKind) =>
_siteStreamEventDrops.Add(1, new KeyValuePair<string, object?>("stream", streamKind));
/// <summary>
/// Records how many events one site gRPC stream frame carried. Called once per emitted
/// frame on a batching-negotiated subscription (never on an un-negotiated one, where it
/// would degenerate into a per-event instrument on the hottest path in the product).
/// </summary>
/// <param name="streamKind">Stream kind tag (<c>instance</c> or <c>site-alarms</c>).</param>
/// <param name="events">Events packed into the frame; 1 for a plain unbatched frame.</param>
public static void RecordSiteStreamBatchSize(string streamKind, int events) =>
_siteStreamBatchSize.Record(events, new KeyValuePair<string, object?>("stream", streamKind));
/// <summary>
/// Registers the provider the StoreAndForward queue-depth gauge reads on each observation.
/// A later task supplies a provider that reads the real StoreAndForward depth. A null
@@ -152,6 +152,26 @@ public class CommunicationOptions
/// </summary>
public int GrpcSiteAlarmStreamChannelCapacity { get; set; } = 20_000;
/// <summary>
/// Maximum events coalesced into one site→central stream frame (R2 — gRPC event
/// batching). At target scale a site emits ~37.5k events/s and every one of them used to
/// cost its own gRPC message; batching amortises that framing overhead. Set to 1 to
/// disable batching on this node without a wire change (every frame then carries exactly
/// one event, which is also what an un-negotiated subscription gets).
/// </summary>
public int GrpcStreamBatchMaxEvents { get; set; } = 100;
/// <summary>
/// Maximum time the site lingers accumulating a stream batch once a backlog has been
/// observed. Deliberately small: the target-scale load test measures end-to-end event
/// latency against a 250 ms P99 threshold (measured P99 4.57 ms), and this window is the
/// only latency batching can add — so it is validated strictly below that threshold.
/// A lone event on a quiet stream is never delayed by it (see
/// <c>SiteStreamEventBatcher</c>); the window applies only after a backlog is proven.
/// <see cref="TimeSpan.Zero"/> means "pack only what is already queued, never wait".
/// </summary>
public TimeSpan GrpcStreamBatchWindow { get; set; } = TimeSpan.FromMilliseconds(25);
/// <summary>Akka.Remote transport heartbeat interval.</summary>
public TimeSpan TransportHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(5);
@@ -12,6 +12,14 @@ namespace ZB.MOM.WW.ScadaBridge.Communication;
/// </summary>
public sealed class CommunicationOptionsValidator : OptionsValidatorBase<CommunicationOptions>
{
/// <summary>
/// Exclusive upper bound on <see cref="CommunicationOptions.GrpcStreamBatchWindow"/> —
/// the end-to-end site→central event latency budget the target-scale load test asserts
/// a P99 against. The coalescing window is the only latency batching introduces, so it
/// must stay strictly inside that budget rather than consuming it whole.
/// </summary>
internal static readonly TimeSpan StreamBatchWindowCeiling = TimeSpan.FromMilliseconds(250);
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, CommunicationOptions options)
{
@@ -75,6 +83,23 @@ public sealed class CommunicationOptionsValidator : OptionsValidatorBase<Communi
builder.RequireThat(options.GrpcMaxConcurrentStreams > 0,
$"ScadaBridge:Communication:GrpcMaxConcurrentStreams must be positive (was {options.GrpcMaxConcurrentStreams}).");
// ── Site→central stream event batching (R2) ─────────────────────────────
// 1 is legal and means "disabled" (one event per frame, the un-negotiated shape).
builder.RequireThat(options.GrpcStreamBatchMaxEvents > 0,
$"ScadaBridge:Communication:GrpcStreamBatchMaxEvents must be positive — 1 disables "
+ $"batching (was {options.GrpcStreamBatchMaxEvents}).");
// The coalescing window is the ONLY latency batching can add, and the target-scale
// load test holds end-to-end event latency to a 250 ms P99. Validate it strictly
// below that so a misconfigured window cannot silently spend the entire budget.
builder.RequireThat(
options.GrpcStreamBatchWindow >= TimeSpan.Zero
&& options.GrpcStreamBatchWindow < StreamBatchWindowCeiling,
$"ScadaBridge:Communication:GrpcStreamBatchWindow must be non-negative and strictly "
+ $"below {StreamBatchWindowCeiling.TotalMilliseconds:0} ms (the end-to-end stream "
+ $"latency budget the coalescing window spends from); zero means \"pack only what is "
+ $"already queued\" (was {options.GrpcStreamBatchWindow}).");
// The gRPC site→central transport needs at least one central endpoint to dial. gRPC is now
// the only site→central transport (ClusterClient was removed in the migration's Phase 4), so
// every site node must declare its central endpoints — there is no Akka fallback to ignore
@@ -0,0 +1,225 @@
using System.Diagnostics;
using System.Threading.Channels;
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <summary>
/// Per-subscriber coalescing pump that drains a stream's bounded send channel and writes
/// it to the gRPC response stream, optionally packing several consecutive events into one
/// <see cref="SiteStreamEventBatch"/> frame (R2 — gRPC event batching).
///
/// <para>
/// <b>Where this sits.</b> Strictly DOWNSTREAM of <c>StreamRelayActor</c>'s bounded
/// <c>DropOldest</c> channel, and one instance per subscriber. It therefore changes only
/// how many gRPC frames a given set of events costs — it does <em>not</em> change the
/// site's burst ceiling, which is set by the shared publish stage upstream of the
/// BroadcastHub (deferred-work register row 31) and by the per-subscriber channel capacity.
/// Events dropped by that channel are dropped before the batcher ever sees them.
/// </para>
///
/// <para>
/// <b>Latency contract.</b> The pump never delays a lone event. It takes the first event,
/// drains whatever is <em>already queued</em> behind it (which costs no time at all), and
/// only then — having proven a backlog exists — lingers up to
/// <paramref name="maxWindow"/> for more. A quiet stream is therefore byte-identical and
/// latency-identical to the pre-batching wire: one plain
/// <c>attribute_changed</c>/<c>alarm_changed</c> frame, emitted immediately. A saturated
/// stream pays at most one window per batch, which is why the window is validated well
/// under the 250 ms end-to-end latency threshold the target-scale load test measures
/// against.
/// </para>
///
/// <para>
/// <b>Ordering.</b> Events are emitted in the exact order they were read; a batch preserves
/// that order inside <c>SiteStreamEventBatch.events</c>, and the client unpacks in order.
/// Nothing is reordered or coalesced away — batching is purely a framing change.
/// </para>
/// </summary>
internal static class SiteStreamEventBatcher
{
/// <summary>
/// Drains <paramref name="reader"/> until it completes or <paramref name="ct"/> is
/// cancelled, writing frames through <paramref name="writeAsync"/>.
///
/// <para>
/// Pass <paramref name="maxBatchEvents"/> = 1 to get the exact pre-batching behaviour
/// (one frame per event, no window, no metric) — that is what an un-negotiated
/// subscription uses, so an old central never sees a frame shape it cannot parse.
/// </para>
///
/// <para>
/// When the channel writer completes with events still buffered, the buffered events
/// are flushed as a final frame before the pump returns. Cancellation is deliberately
/// NOT flushed: the token is cancelled precisely when the client is gone or the site is
/// shutting down, so the write would fail anyway; the
/// <see cref="OperationCanceledException"/> propagates to the caller exactly as the
/// pre-batching <c>await foreach</c> did.
/// </para>
/// </summary>
/// <param name="reader">The subscriber's bounded send-channel reader.</param>
/// <param name="correlationId">Correlation id stamped on an emitted batch frame.</param>
/// <param name="maxBatchEvents">Hard cap on events per frame; 1 disables batching entirely.</param>
/// <param name="maxWindow">Maximum time to linger accumulating a batch once a backlog is observed.</param>
/// <param name="writeAsync">Writes one frame to the gRPC response stream.</param>
/// <param name="onFrameEmitted">Optional observer of each emitted frame's event count (the batch-size histogram).</param>
/// <param name="ct">Cancels the pump (client disconnect, duplicate replacement, shutdown, stream lifetime).</param>
/// <returns>A task that completes when the channel is drained and closed.</returns>
internal static async Task PumpAsync(
ChannelReader<SiteStreamEvent> reader,
string correlationId,
int maxBatchEvents,
TimeSpan maxWindow,
Func<SiteStreamEvent, CancellationToken, Task> writeAsync,
Action<int>? onFrameEmitted,
CancellationToken ct)
{
var buffer = new List<SiteStreamEvent>(Math.Max(1, Math.Min(maxBatchEvents, 256)));
while (true)
{
if (!await reader.WaitToReadAsync(ct).ConfigureAwait(false))
{
// Writer completed and the channel is empty — normal end of stream.
return;
}
if (!reader.TryRead(out var first))
{
// Raced another reader (there is only one, but WaitToReadAsync can also
// wake on completion); loop round and re-evaluate.
continue;
}
buffer.Clear();
buffer.Add(first);
var writerCompleted = false;
if (maxBatchEvents > 1)
{
// Free drain: everything already sitting in the channel costs no latency.
while (buffer.Count < maxBatchEvents && reader.TryRead(out var queued))
{
buffer.Add(queued);
}
// Linger ONLY when a real backlog was observed. A single event on an
// otherwise idle stream is emitted immediately — the window must never
// become a floor on latency for the quiet case.
if (buffer.Count > 1 && buffer.Count < maxBatchEvents && maxWindow > TimeSpan.Zero)
{
writerCompleted = await LingerAsync(
reader, buffer, maxBatchEvents, maxWindow, ct).ConfigureAwait(false);
}
}
await EmitAsync(buffer, correlationId, writeAsync, onFrameEmitted, ct).ConfigureAwait(false);
if (writerCompleted)
{
// Flush-on-close: the buffered events above were the tail of the stream.
return;
}
}
}
/// <summary>
/// Accumulates further events into <paramref name="buffer"/> for at most
/// <paramref name="maxWindow"/> from the moment the backlog was observed, or until the
/// size cap is reached.
/// </summary>
/// <param name="reader">The subscriber's send-channel reader.</param>
/// <param name="buffer">Batch under construction; appended to in arrival order.</param>
/// <param name="maxBatchEvents">Hard cap on events per frame.</param>
/// <param name="maxWindow">Maximum lingering time for this batch.</param>
/// <param name="ct">Cancels the pump.</param>
/// <returns>
/// <see langword="true"/> when the channel writer completed while lingering (the caller
/// must emit the buffer and then stop), otherwise <see langword="false"/>.
/// </returns>
private static async Task<bool> LingerAsync(
ChannelReader<SiteStreamEvent> reader,
List<SiteStreamEvent> buffer,
int maxBatchEvents,
TimeSpan maxWindow,
CancellationToken ct)
{
var started = Stopwatch.GetTimestamp();
while (buffer.Count < maxBatchEvents)
{
var remaining = maxWindow - Stopwatch.GetElapsedTime(started);
if (remaining <= TimeSpan.Zero)
{
return false;
}
using var lingerCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
lingerCts.CancelAfter(remaining);
bool more;
try
{
more = await reader.WaitToReadAsync(lingerCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
// The window elapsed — close the batch with what we have.
return false;
}
if (!more)
{
// Writer completed with the buffer non-empty: flush it, then stop.
return true;
}
while (buffer.Count < maxBatchEvents && reader.TryRead(out var queued))
{
buffer.Add(queued);
}
}
return false;
}
/// <summary>
/// Writes <paramref name="buffer"/> as a single frame: a plain event frame when the
/// buffer holds exactly one event (identical to the pre-batching wire), otherwise a
/// <see cref="SiteStreamEventBatch"/> frame carrying them in order.
/// </summary>
/// <param name="buffer">Events to emit, in arrival order. Never empty.</param>
/// <param name="correlationId">Correlation id for the enclosing batch frame.</param>
/// <param name="writeAsync">Writes one frame to the gRPC response stream.</param>
/// <param name="onFrameEmitted">Optional observer of the emitted frame's event count.</param>
/// <param name="ct">Cancels the write.</param>
/// <returns>A task that completes when the frame has been written.</returns>
private static async Task EmitAsync(
List<SiteStreamEvent> buffer,
string correlationId,
Func<SiteStreamEvent, CancellationToken, Task> writeAsync,
Action<int>? onFrameEmitted,
CancellationToken ct)
{
if (buffer.Count == 1)
{
await writeAsync(buffer[0], ct).ConfigureAwait(false);
onFrameEmitted?.Invoke(1);
return;
}
var batch = new SiteStreamEventBatch();
foreach (var evt in buffer)
{
// The enclosing frame carries the correlation id once for the whole batch;
// clearing it on the inner events is the byte saving batching exists for.
// No consumer reads the inner value (see SiteStreamGrpcClient.ForEachEvent).
evt.CorrelationId = string.Empty;
batch.Events.Add(evt);
}
await writeAsync(
new SiteStreamEvent { CorrelationId = correlationId, Batch = batch }, ct).ConfigureAwait(false);
onFrameEmitted?.Invoke(buffer.Count);
}
}
@@ -199,19 +199,25 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
var request = new InstanceStreamRequest
{
CorrelationId = correlationId,
InstanceUniqueName = instanceUniqueName
InstanceUniqueName = instanceUniqueName,
// R2 batch negotiation. Declaring support is safe against ANY site: one that
// predates batching ignores the unknown field and keeps sending per-event
// frames, which ForEachEvent handles as the single-event case.
BatchingSupported = true
};
void Deliver(SiteStreamEvent single)
{
var domainEvent = ConvertToDomainEvent(single);
if (domainEvent != null)
onEvent(domainEvent);
}
await ConsumeStreamAsync(
correlationId,
cts,
() => _client.SubscribeInstance(request, cancellationToken: cts.Token),
evt =>
{
var domainEvent = ConvertToDomainEvent(evt);
if (domainEvent != null)
onEvent(domainEvent);
},
frame => ForEachEvent(frame, Deliver),
onError,
onCompleted);
}
@@ -270,19 +276,23 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
var request = new SiteStreamRequest
{
CorrelationId = correlationId
CorrelationId = correlationId,
// R2 batch negotiation — see SubscribeAsync.
BatchingSupported = true
};
void Deliver(SiteStreamEvent single)
{
// Site-wide stream is alarm-only by contract; defensively ignore anything else.
if (ConvertToAlarmEvent(single) is { } alarm)
onAlarmEvent(alarm);
}
await ConsumeStreamAsync(
correlationId,
cts,
() => _client.SubscribeSite(request, cancellationToken: cts.Token),
evt =>
{
// Site-wide stream is alarm-only by contract; defensively ignore anything else.
if (ConvertToAlarmEvent(evt) is { } alarm)
onAlarmEvent(alarm);
},
frame => ForEachEvent(frame, Deliver),
onError,
onCompleted,
onConnected);
@@ -451,6 +461,39 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
}
}
/// <summary>
/// Unpacks one wire frame into the per-event pipeline (R2 — gRPC event batching).
/// A plain <c>attribute_changed</c>/<c>alarm_changed</c> frame is delivered as-is; a
/// <see cref="SiteStreamEventBatch"/> frame is unpacked <b>in order</b> into the same
/// callback, so every downstream consumer (<c>SiteAlarmAggregatorActor</c>,
/// <c>DebugStreamBridgeActor</c>, the consumer-keepalive/orphan logic, per-event
/// <c>Timestamp</c> fidelity) sees no difference between a batched and an unbatched site.
/// <para>
/// Unpacking is deliberately NON-RECURSIVE: the server never nests a batch inside a
/// batch, and a nested or unknown inner case from a malformed peer is skipped rather
/// than followed. Internal for testability.
/// </para>
/// </summary>
/// <param name="frame">The wire frame received from the site.</param>
/// <param name="handler">Invoked once per contained event, in arrival order.</param>
internal static void ForEachEvent(SiteStreamEvent frame, Action<SiteStreamEvent> handler)
{
if (frame.EventCase != SiteStreamEvent.EventOneofCase.Batch)
{
handler(frame);
return;
}
foreach (var inner in frame.Batch.Events)
{
if (inner.EventCase is SiteStreamEvent.EventOneofCase.AttributeChanged
or SiteStreamEvent.EventOneofCase.AlarmChanged)
{
handler(inner);
}
}
}
/// <summary>
/// Converts a proto SiteStreamEvent to the corresponding domain message.
/// Internal for testability.
@@ -29,6 +29,8 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
private readonly TimeSpan _maxStreamLifetime;
private readonly int _instanceChannelCapacity;
private readonly int _siteAlarmChannelCapacity;
private readonly int _streamBatchMaxEvents;
private readonly TimeSpan _streamBatchWindow;
private volatile bool _ready;
// Flipped by CancelAllStreams() when the host enters
// CoordinatedShutdown so SubscribeInstance refuses new streams with
@@ -75,7 +77,8 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
ILogger<SiteStreamGrpcServer> logger,
int maxConcurrentStreams = 100)
: this(streamSubscriber, logger, maxConcurrentStreams, TimeSpan.FromHours(4),
DefaultInstanceChannelCapacity, DefaultSiteAlarmChannelCapacity)
DefaultInstanceChannelCapacity, DefaultSiteAlarmChannelCapacity,
DefaultStreamBatchMaxEvents, DefaultStreamBatchWindow)
{
}
@@ -85,6 +88,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
/// <summary>Fallback site-wide alarm send-channel capacity when no options are bound.</summary>
internal const int DefaultSiteAlarmChannelCapacity = 20_000;
/// <summary>Fallback stream-batch size cap when no options are bound (R2).</summary>
internal const int DefaultStreamBatchMaxEvents = 100;
/// <summary>Fallback stream-batch coalescing window when no options are bound (R2).</summary>
internal static readonly TimeSpan DefaultStreamBatchWindow = TimeSpan.FromMilliseconds(25);
/// <summary>
/// DI constructor — binds <see cref="CommunicationOptions.GrpcMaxConcurrentStreams"/>
/// and <see cref="CommunicationOptions.GrpcMaxStreamLifetime"/> so the documented
@@ -102,7 +111,9 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
options.Value.GrpcMaxConcurrentStreams,
options.Value.GrpcMaxStreamLifetime,
options.Value.GrpcInstanceStreamChannelCapacity,
options.Value.GrpcSiteAlarmStreamChannelCapacity)
options.Value.GrpcSiteAlarmStreamChannelCapacity,
options.Value.GrpcStreamBatchMaxEvents,
options.Value.GrpcStreamBatchWindow)
{
}
@@ -112,7 +123,9 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
int maxConcurrentStreams,
TimeSpan maxStreamLifetime,
int instanceChannelCapacity,
int siteAlarmChannelCapacity)
int siteAlarmChannelCapacity,
int streamBatchMaxEvents,
TimeSpan streamBatchWindow)
{
_streamSubscriber = streamSubscriber;
_logger = logger;
@@ -120,6 +133,11 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
_maxStreamLifetime = maxStreamLifetime;
_instanceChannelCapacity = Math.Max(1, instanceChannelCapacity);
_siteAlarmChannelCapacity = Math.Max(1, siteAlarmChannelCapacity);
// Floored/clamped rather than thrown on: CommunicationOptionsValidator already
// fails the boot on a bad value, and a degenerate one must not blow up deep
// inside a live RPC on a host composed without validation (tests, embedded use).
_streamBatchMaxEvents = Math.Max(1, streamBatchMaxEvents);
_streamBatchWindow = streamBatchWindow < TimeSpan.Zero ? TimeSpan.Zero : streamBatchWindow;
}
/// <summary>
@@ -230,6 +248,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
/// <summary>Effective site-wide alarm send-channel capacity. Exposed for tests.</summary>
internal int SiteAlarmChannelCapacity => _siteAlarmChannelCapacity;
/// <summary>Effective stream-batch size cap (R2). Exposed for tests.</summary>
internal int StreamBatchMaxEvents => _streamBatchMaxEvents;
/// <summary>Effective stream-batch coalescing window (R2). Exposed for tests.</summary>
internal TimeSpan StreamBatchWindow => _streamBatchWindow;
/// <summary>
/// Total events evicted from stream send channels on this node since start (both stream
/// kinds). Exported as <c>scadabridge.site.stream.events_dropped</c>; exposed here so a
@@ -251,7 +275,10 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
relay => _streamSubscriber.Subscribe(request.InstanceUniqueName, relay),
request.InstanceUniqueName,
_instanceChannelCapacity,
streamKind: "instance");
streamKind: "instance",
// R2 batch negotiation: proto3 defaults this to false, so a central built
// before batching existed keeps getting one frame per event.
batchingSupported: request.BatchingSupported);
/// <inheritdoc />
public override Task SubscribeSite(
@@ -271,7 +298,8 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
// DropOldest meant an alarm burst during a WAN stall silently evicted operator-
// visible transitions to make room for diagnostics traffic.
_siteAlarmChannelCapacity,
streamKind: "site-alarms");
streamKind: "site-alarms",
batchingSupported: request.BatchingSupported);
/// <summary>
/// Shared streaming pipeline behind <see cref="SubscribeInstance"/> and
@@ -289,6 +317,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
/// <param name="description">Human-readable subscription description for logging.</param>
/// <param name="channelCapacity">Send-channel capacity for this stream kind.</param>
/// <param name="streamKind">Telemetry tag for this stream kind (<c>instance</c>/<c>site-alarms</c>).</param>
/// <param name="batchingSupported">
/// Whether the SUBSCRIBING CLIENT declared it understands the <c>SiteStreamEventBatch</c>
/// oneof case (R2). False — the proto3 default an older central necessarily sends —
/// pins this stream to one event per frame, so a peer that predates batching can never
/// receive a frame case its generated code drops on the floor.
/// </param>
private async Task RunSubscriptionStreamAsync(
string correlationId,
IServerStreamWriter<SiteStreamEvent> responseStream,
@@ -296,7 +330,8 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
Func<IActorRef, string> subscribe,
string description,
int channelCapacity,
string streamKind)
string streamKind,
bool batchingSupported)
{
if (!_ready)
throw new RpcException(new GrpcStatus(StatusCode.Unavailable, "Server not ready"));
@@ -318,11 +353,29 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
StatusCode.InvalidArgument, "correlation_id is missing or not a valid identifier"));
}
// Duplicate prevention -- cancel existing stream for this correlationId
// Duplicate prevention -- cancel existing stream for this correlationId.
//
// CANCEL ONLY, never Dispose. The replaced stream's CTS belongs to its own
// handler's `using var streamCts`, which is still running and still reads
// `streamCts.Token` (at the pump call below, and previously at the
// `ReadAllAsync(streamCts.Token)` it replaced). Disposing it from here raced that
// read and surfaced as an unhandled ObjectDisposedException escaping the RPC —
// observed as a full-suite-load-only failure of
// GrpcStreamIntegrationTests.Pipeline_DuplicateCorrelationId_ReplacesStream, and a
// pre-existing hazard (the same Dispose + the same first-token-read relationship
// exist unchanged before R2). Cancellation alone is what replacement needs; the
// owning handler's `using` still disposes it exactly once on every exit path.
if (_activeStreams.TryRemove(correlationId, out var existingEntry))
{
existingEntry.Cts.Cancel();
existingEntry.Cts.Dispose();
try
{
existingEntry.Cts.Cancel();
}
catch (ObjectDisposedException)
{
// Its owner finished and disposed it between the TryRemove and here —
// already terminal, nothing to cancel. Mirrors CancelAllStreams().
}
}
// Check max concurrent streams after duplicate removal.
@@ -422,10 +475,24 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
ScadaBridgeTelemetry.SiteConnectionOpened();
try
{
await foreach (var evt in channel.Reader.ReadAllAsync(streamCts.Token))
{
await responseStream.WriteAsync(evt, streamCts.Token);
}
// R2 — event batching. The pump replaces the old per-event
// `await foreach (…) WriteAsync(evt)` loop and is byte-for-byte identical to
// it when maxBatchEvents == 1, which is exactly what an un-negotiated
// subscription gets. It sits DOWNSTREAM of the bounded DropOldest channel
// above, so it changes framing only — never the site's burst ceiling.
await SiteStreamEventBatcher.PumpAsync(
channel.Reader,
correlationId,
maxBatchEvents: batchingSupported ? _streamBatchMaxEvents : 1,
maxWindow: _streamBatchWindow,
(evt, token) => responseStream.WriteAsync(evt, token),
// Recorded only on a negotiated stream: on an un-negotiated one every
// frame carries exactly one event, so the histogram would degenerate
// into a per-event instrument on the product's hottest path.
batchingSupported
? size => ScadaBridgeTelemetry.RecordSiteStreamBatchSize(streamKind, size)
: null,
streamCts.Token);
}
catch (OperationCanceledException)
{
@@ -20,6 +20,14 @@ service SiteStreamService {
message InstanceStreamRequest {
string correlation_id = 1;
string instance_unique_name = 2;
// Client-declared BATCH NEGOTIATION (R2, event batching). When true the client
// understands the SiteStreamEventBatch oneof case and the server may coalesce
// consecutive events into one frame. proto3 defaults this to false, so an OLD
// central that never sets it keeps receiving one frame per event — that default
// IS the negotiation, and it is what makes new-site↔old-central safe. A NEW
// central sets it against an OLD site, which ignores the unknown field and
// keeps sending per-event frames the new client also accepts. Additive-only.
bool batching_supported = 3;
}
// Request for the site-wide, alarm-only SubscribeSite stream. Unlike
@@ -27,6 +35,8 @@ message InstanceStreamRequest {
// transitions for every instance on the site.
message SiteStreamRequest {
string correlation_id = 1;
// See InstanceStreamRequest.batching_supported. Additive-only.
bool batching_supported = 2;
}
message SiteStreamEvent {
@@ -34,9 +44,26 @@ message SiteStreamEvent {
oneof event {
AttributeValueUpdate attribute_changed = 2;
AlarmStateUpdate alarm_changed = 3;
// Coalesced frame (R2). Emitted ONLY when the subscription request set
// batching_supported = true. A batch is never nested inside a batch, and a
// single event is always sent as a plain attribute_changed/alarm_changed
// frame — so a quiet stream's wire shape is byte-identical to before.
SiteStreamEventBatch batch = 4;
}
}
// Coalesced carrier for several consecutive stream events (R2). Ordering is
// significant: events appear in the exact order the site produced them, and the
// client unpacks them in order into the same per-event pipeline, so per-event
// Timestamp fidelity and downstream sequencing are unchanged.
//
// The inner events deliberately leave correlation_id EMPTY — the enclosing
// SiteStreamEvent carries it once for the whole frame, which is the byte saving
// batching exists for. No consumer reads the inner correlation_id.
message SiteStreamEventBatch {
repeated SiteStreamEvent events = 1;
}
enum Quality {
QUALITY_UNSPECIFIED = 0;
QUALITY_GOOD = 1;
@@ -26,97 +26,102 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
string.Concat(
"ChdQcm90b3Mvc2l0ZXN0cmVhbS5wcm90bxIKc2l0ZXN0cmVhbRofZ29vZ2xl",
"L3Byb3RvYnVmL3RpbWVzdGFtcC5wcm90bxoeZ29vZ2xlL3Byb3RvYnVmL3dy",
"YXBwZXJzLnByb3RvIk0KFUluc3RhbmNlU3RyZWFtUmVxdWVzdBIWCg5jb3Jy",
"YXBwZXJzLnByb3RvImkKFUluc3RhbmNlU3RyZWFtUmVxdWVzdBIWCg5jb3Jy",
"ZWxhdGlvbl9pZBgBIAEoCRIcChRpbnN0YW5jZV91bmlxdWVfbmFtZRgCIAEo",
"CSIrChFTaXRlU3RyZWFtUmVxdWVzdBIWCg5jb3JyZWxhdGlvbl9pZBgBIAEo",
"CSKoAQoPU2l0ZVN0cmVhbUV2ZW50EhYKDmNvcnJlbGF0aW9uX2lkGAEgASgJ",
"Ej0KEWF0dHJpYnV0ZV9jaGFuZ2VkGAIgASgLMiAuc2l0ZXN0cmVhbS5BdHRy",
"aWJ1dGVWYWx1ZVVwZGF0ZUgAEjUKDWFsYXJtX2NoYW5nZWQYAyABKAsyHC5z",
"aXRlc3RyZWFtLkFsYXJtU3RhdGVVcGRhdGVIAEIHCgVldmVudCLIAQoUQXR0",
"cmlidXRlVmFsdWVVcGRhdGUSHAoUaW5zdGFuY2VfdW5pcXVlX25hbWUYASAB",
"KAkSFgoOYXR0cmlidXRlX3BhdGgYAiABKAkSFgoOYXR0cmlidXRlX25hbWUY",
"AyABKAkSDQoFdmFsdWUYBCABKAkSJAoHcXVhbGl0eRgFIAEoDjITLnNpdGVz",
"dHJlYW0uUXVhbGl0eRItCgl0aW1lc3RhbXAYBiABKAsyGi5nb29nbGUucHJv",
"dG9idWYuVGltZXN0YW1wIq8FChBBbGFybVN0YXRlVXBkYXRlEhwKFGluc3Rh",
"bmNlX3VuaXF1ZV9uYW1lGAEgASgJEhIKCmFsYXJtX25hbWUYAiABKAkSKQoF",
"c3RhdGUYAyABKA4yGi5zaXRlc3RyZWFtLkFsYXJtU3RhdGVFbnVtEhAKCHBy",
"aW9yaXR5GAQgASgFEi0KCXRpbWVzdGFtcBgFIAEoCzIaLmdvb2dsZS5wcm90",
"b2J1Zi5UaW1lc3RhbXASKQoFbGV2ZWwYBiABKA4yGi5zaXRlc3RyZWFtLkFs",
"YXJtTGV2ZWxFbnVtEg8KB21lc3NhZ2UYByABKAkSDAoEa2luZBgIIAEoCRIO",
"CgZhY3RpdmUYCSABKAgSFAoMYWNrbm93bGVkZ2VkGAogASgIEhEKCWNvbmZp",
"cm1lZBgLIAEoCBIUCgxzaGVsdmVfc3RhdGUYDCABKAkSEgoKc3VwcHJlc3Nl",
"ZBgNIAEoCBIYChBzb3VyY2VfcmVmZXJlbmNlGA4gASgJEhcKD2FsYXJtX3R5",
"cGVfbmFtZRgPIAEoCRIQCghjYXRlZ29yeRgQIAEoCRIVCg1vcGVyYXRvcl91",
"c2VyGBEgASgJEhgKEG9wZXJhdG9yX2NvbW1lbnQYEiABKAkSNwoTb3JpZ2lu",
"YWxfcmFpc2VfdGltZRgTIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3Rh",
"bXASFQoNY3VycmVudF92YWx1ZRgUIAEoCRITCgtsaW1pdF92YWx1ZRgVIAEo",
"CRIkChxuYXRpdmVfc291cmNlX2Nhbm9uaWNhbF9uYW1lGBYgASgJEiEKGWlz",
"X2NvbmZpZ3VyZWRfcGxhY2Vob2xkZXIYFyABKAgSLAoIYWNrX3RpbWUYGCAB",
"KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIr0ECg1BdWRpdEV2ZW50",
"RHRvEhAKCGV2ZW50X2lkGAEgASgJEjMKD29jY3VycmVkX2F0X3V0YxgCIAEo",
"CzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASDwoHY2hhbm5lbBgDIAEo",
"CRIMCgRraW5kGAQgASgJEhYKDmNvcnJlbGF0aW9uX2lkGAUgASgJEhYKDnNv",
"dXJjZV9zaXRlX2lkGAYgASgJEhoKEnNvdXJjZV9pbnN0YW5jZV9pZBgHIAEo",
"CRIVCg1zb3VyY2Vfc2NyaXB0GAggASgJEg0KBWFjdG9yGAkgASgJEg4KBnRh",
"cmdldBgKIAEoCRIOCgZzdGF0dXMYCyABKAkSMAoLaHR0cF9zdGF0dXMYDCAB",
"KAsyGy5nb29nbGUucHJvdG9idWYuSW50MzJWYWx1ZRIwCgtkdXJhdGlvbl9t",
"cxgNIAEoCzIbLmdvb2dsZS5wcm90b2J1Zi5JbnQzMlZhbHVlEhUKDWVycm9y",
"X21lc3NhZ2UYDiABKAkSFAoMZXJyb3JfZGV0YWlsGA8gASgJEhcKD3JlcXVl",
"c3Rfc3VtbWFyeRgQIAEoCRIYChByZXNwb25zZV9zdW1tYXJ5GBEgASgJEhkK",
"EXBheWxvYWRfdHJ1bmNhdGVkGBIgASgIEg0KBWV4dHJhGBMgASgJEhQKDGV4",
"ZWN1dGlvbl9pZBgUIAEoCRIbChNwYXJlbnRfZXhlY3V0aW9uX2lkGBUgASgJ",
"EhMKC3NvdXJjZV9ub2RlGBYgASgJIjwKD0F1ZGl0RXZlbnRCYXRjaBIpCgZl",
"dmVudHMYASADKAsyGS5zaXRlc3RyZWFtLkF1ZGl0RXZlbnREdG8iJwoJSW5n",
"ZXN0QWNrEhoKEmFjY2VwdGVkX2V2ZW50X2lkcxgBIAMoCSKJAwoWU2l0ZUNh",
"bGxPcGVyYXRpb25hbER0bxIcChR0cmFja2VkX29wZXJhdGlvbl9pZBgBIAEo",
"CRIPCgdjaGFubmVsGAIgASgJEg4KBnRhcmdldBgDIAEoCRITCgtzb3VyY2Vf",
"c2l0ZRgEIAEoCRIOCgZzdGF0dXMYBSABKAkSEwoLcmV0cnlfY291bnQYBiAB",
"KAUSEgoKbGFzdF9lcnJvchgHIAEoCRIwCgtodHRwX3N0YXR1cxgIIAEoCzIb",
"Lmdvb2dsZS5wcm90b2J1Zi5JbnQzMlZhbHVlEjIKDmNyZWF0ZWRfYXRfdXRj",
"GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCg51cGRhdGVk",
"X2F0X3V0YxgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMwoP",
"dGVybWluYWxfYXRfdXRjGAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVz",
"dGFtcBITCgtzb3VyY2Vfbm9kZRgMIAEoCSKAAQoVQ2FjaGVkVGVsZW1ldHJ5",
"UGFja2V0Ei4KC2F1ZGl0X2V2ZW50GAEgASgLMhkuc2l0ZXN0cmVhbS5BdWRp",
"dEV2ZW50RHRvEjcKC29wZXJhdGlvbmFsGAIgASgLMiIuc2l0ZXN0cmVhbS5T",
"aXRlQ2FsbE9wZXJhdGlvbmFsRHRvIkoKFENhY2hlZFRlbGVtZXRyeUJhdGNo",
"EjIKB3BhY2tldHMYASADKAsyIS5zaXRlc3RyZWFtLkNhY2hlZFRlbGVtZXRy",
"eVBhY2tldCJtChZQdWxsQXVkaXRFdmVudHNSZXF1ZXN0Ei0KCXNpbmNlX3V0",
"YxgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEgoKYmF0Y2hf",
"c2l6ZRgCIAEoBRIQCghhZnRlcl9pZBgDIAEoCSJcChdQdWxsQXVkaXRFdmVu",
"dHNSZXNwb25zZRIpCgZldmVudHMYASADKAsyGS5zaXRlc3RyZWFtLkF1ZGl0",
"RXZlbnREdG8SFgoObW9yZV9hdmFpbGFibGUYAiABKAgiawoUUHVsbFNpdGVD",
"YWxsc1JlcXVlc3QSLQoJc2luY2VfdXRjGAEgASgLMhouZ29vZ2xlLnByb3Rv",
"YnVmLlRpbWVzdGFtcBISCgpiYXRjaF9zaXplGAIgASgFEhAKCGFmdGVyX2lk",
"GAMgASgJImkKFVB1bGxTaXRlQ2FsbHNSZXNwb25zZRI4CgxvcGVyYXRpb25h",
"bHMYASADKAsyIi5zaXRlc3RyZWFtLlNpdGVDYWxsT3BlcmF0aW9uYWxEdG8S",
"FgoObW9yZV9hdmFpbGFibGUYAiABKAgqXAoHUXVhbGl0eRIXChNRVUFMSVRZ",
"X1VOU1BFQ0lGSUVEEAASEAoMUVVBTElUWV9HT09EEAESFQoRUVVBTElUWV9V",
"TkNFUlRBSU4QAhIPCgtRVUFMSVRZX0JBRBADKl0KDkFsYXJtU3RhdGVFbnVt",
"EhsKF0FMQVJNX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSQUxBUk1fU1RBVEVf",
"Tk9STUFMEAESFgoSQUxBUk1fU1RBVEVfQUNUSVZFEAIqhQEKDkFsYXJtTGV2",
"ZWxFbnVtEhQKEEFMQVJNX0xFVkVMX05PTkUQABITCg9BTEFSTV9MRVZFTF9M",
"T1cQARIXChNBTEFSTV9MRVZFTF9MT1dfTE9XEAISFAoQQUxBUk1fTEVWRUxf",
"SElHSBADEhkKFUFMQVJNX0xFVkVMX0hJR0hfSElHSBAEMoYEChFTaXRlU3Ry",
"ZWFtU2VydmljZRJVChFTdWJzY3JpYmVJbnN0YW5jZRIhLnNpdGVzdHJlYW0u",
"SW5zdGFuY2VTdHJlYW1SZXF1ZXN0Ghsuc2l0ZXN0cmVhbS5TaXRlU3RyZWFt",
"RXZlbnQwARJNCg1TdWJzY3JpYmVTaXRlEh0uc2l0ZXN0cmVhbS5TaXRlU3Ry",
"ZWFtUmVxdWVzdBobLnNpdGVzdHJlYW0uU2l0ZVN0cmVhbUV2ZW50MAESRwoR",
"SW5nZXN0QXVkaXRFdmVudHMSGy5zaXRlc3RyZWFtLkF1ZGl0RXZlbnRCYXRj",
"aBoVLnNpdGVzdHJlYW0uSW5nZXN0QWNrElAKFUluZ2VzdENhY2hlZFRlbGVt",
"ZXRyeRIgLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gaFS5zaXRl",
"c3RyZWFtLkluZ2VzdEFjaxJaCg9QdWxsQXVkaXRFdmVudHMSIi5zaXRlc3Ry",
"ZWFtLlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QaIy5zaXRlc3RyZWFtLlB1bGxB",
"dWRpdEV2ZW50c1Jlc3BvbnNlElQKDVB1bGxTaXRlQ2FsbHMSIC5zaXRlc3Ry",
"ZWFtLlB1bGxTaXRlQ2FsbHNSZXF1ZXN0GiEuc2l0ZXN0cmVhbS5QdWxsU2l0",
"ZUNhbGxzUmVzcG9uc2VCK6oCKFpCLk1PTS5XVy5TY2FkYUJyaWRnZS5Db21t",
"dW5pY2F0aW9uLkdycGNiBnByb3RvMw=="));
"CRIaChJiYXRjaGluZ19zdXBwb3J0ZWQYAyABKAgiRwoRU2l0ZVN0cmVhbVJl",
"cXVlc3QSFgoOY29ycmVsYXRpb25faWQYASABKAkSGgoSYmF0Y2hpbmdfc3Vw",
"cG9ydGVkGAIgASgIItsBCg9TaXRlU3RyZWFtRXZlbnQSFgoOY29ycmVsYXRp",
"b25faWQYASABKAkSPQoRYXR0cmlidXRlX2NoYW5nZWQYAiABKAsyIC5zaXRl",
"c3RyZWFtLkF0dHJpYnV0ZVZhbHVlVXBkYXRlSAASNQoNYWxhcm1fY2hhbmdl",
"ZBgDIAEoCzIcLnNpdGVzdHJlYW0uQWxhcm1TdGF0ZVVwZGF0ZUgAEjEKBWJh",
"dGNoGAQgASgLMiAuc2l0ZXN0cmVhbS5TaXRlU3RyZWFtRXZlbnRCYXRjaEgA",
"QgcKBWV2ZW50IkMKFFNpdGVTdHJlYW1FdmVudEJhdGNoEisKBmV2ZW50cxgB",
"IAMoCzIbLnNpdGVzdHJlYW0uU2l0ZVN0cmVhbUV2ZW50IsgBChRBdHRyaWJ1",
"dGVWYWx1ZVVwZGF0ZRIcChRpbnN0YW5jZV91bmlxdWVfbmFtZRgBIAEoCRIW",
"Cg5hdHRyaWJ1dGVfcGF0aBgCIAEoCRIWCg5hdHRyaWJ1dGVfbmFtZRgDIAEo",
"CRINCgV2YWx1ZRgEIAEoCRIkCgdxdWFsaXR5GAUgASgOMhMuc2l0ZXN0cmVh",
"bS5RdWFsaXR5Ei0KCXRpbWVzdGFtcBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1",
"Zi5UaW1lc3RhbXAirwUKEEFsYXJtU3RhdGVVcGRhdGUSHAoUaW5zdGFuY2Vf",
"dW5pcXVlX25hbWUYASABKAkSEgoKYWxhcm1fbmFtZRgCIAEoCRIpCgVzdGF0",
"ZRgDIAEoDjIaLnNpdGVzdHJlYW0uQWxhcm1TdGF0ZUVudW0SEAoIcHJpb3Jp",
"dHkYBCABKAUSLQoJdGltZXN0YW1wGAUgASgLMhouZ29vZ2xlLnByb3RvYnVm",
"LlRpbWVzdGFtcBIpCgVsZXZlbBgGIAEoDjIaLnNpdGVzdHJlYW0uQWxhcm1M",
"ZXZlbEVudW0SDwoHbWVzc2FnZRgHIAEoCRIMCgRraW5kGAggASgJEg4KBmFj",
"dGl2ZRgJIAEoCBIUCgxhY2tub3dsZWRnZWQYCiABKAgSEQoJY29uZmlybWVk",
"GAsgASgIEhQKDHNoZWx2ZV9zdGF0ZRgMIAEoCRISCgpzdXBwcmVzc2VkGA0g",
"ASgIEhgKEHNvdXJjZV9yZWZlcmVuY2UYDiABKAkSFwoPYWxhcm1fdHlwZV9u",
"YW1lGA8gASgJEhAKCGNhdGVnb3J5GBAgASgJEhUKDW9wZXJhdG9yX3VzZXIY",
"ESABKAkSGAoQb3BlcmF0b3JfY29tbWVudBgSIAEoCRI3ChNvcmlnaW5hbF9y",
"YWlzZV90aW1lGBMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIV",
"Cg1jdXJyZW50X3ZhbHVlGBQgASgJEhMKC2xpbWl0X3ZhbHVlGBUgASgJEiQK",
"HG5hdGl2ZV9zb3VyY2VfY2Fub25pY2FsX25hbWUYFiABKAkSIQoZaXNfY29u",
"ZmlndXJlZF9wbGFjZWhvbGRlchgXIAEoCBIsCghhY2tfdGltZRgYIAEoCzIa",
"Lmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAivQQKDUF1ZGl0RXZlbnREdG8S",
"EAoIZXZlbnRfaWQYASABKAkSMwoPb2NjdXJyZWRfYXRfdXRjGAIgASgLMhou",
"Z29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdjaGFubmVsGAMgASgJEgwK",
"BGtpbmQYBCABKAkSFgoOY29ycmVsYXRpb25faWQYBSABKAkSFgoOc291cmNl",
"X3NpdGVfaWQYBiABKAkSGgoSc291cmNlX2luc3RhbmNlX2lkGAcgASgJEhUK",
"DXNvdXJjZV9zY3JpcHQYCCABKAkSDQoFYWN0b3IYCSABKAkSDgoGdGFyZ2V0",
"GAogASgJEg4KBnN0YXR1cxgLIAEoCRIwCgtodHRwX3N0YXR1cxgMIAEoCzIb",
"Lmdvb2dsZS5wcm90b2J1Zi5JbnQzMlZhbHVlEjAKC2R1cmF0aW9uX21zGA0g",
"ASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMyVmFsdWUSFQoNZXJyb3JfbWVz",
"c2FnZRgOIAEoCRIUCgxlcnJvcl9kZXRhaWwYDyABKAkSFwoPcmVxdWVzdF9z",
"dW1tYXJ5GBAgASgJEhgKEHJlc3BvbnNlX3N1bW1hcnkYESABKAkSGQoRcGF5",
"bG9hZF90cnVuY2F0ZWQYEiABKAgSDQoFZXh0cmEYEyABKAkSFAoMZXhlY3V0",
"aW9uX2lkGBQgASgJEhsKE3BhcmVudF9leGVjdXRpb25faWQYFSABKAkSEwoL",
"c291cmNlX25vZGUYFiABKAkiPAoPQXVkaXRFdmVudEJhdGNoEikKBmV2ZW50",
"cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0byInCglJbmdlc3RB",
"Y2sSGgoSYWNjZXB0ZWRfZXZlbnRfaWRzGAEgAygJIokDChZTaXRlQ2FsbE9w",
"ZXJhdGlvbmFsRHRvEhwKFHRyYWNrZWRfb3BlcmF0aW9uX2lkGAEgASgJEg8K",
"B2NoYW5uZWwYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhMKC3NvdXJjZV9zaXRl",
"GAQgASgJEg4KBnN0YXR1cxgFIAEoCRITCgtyZXRyeV9jb3VudBgGIAEoBRIS",
"CgpsYXN0X2Vycm9yGAcgASgJEjAKC2h0dHBfc3RhdHVzGAggASgLMhsuZ29v",
"Z2xlLnByb3RvYnVmLkludDMyVmFsdWUSMgoOY3JlYXRlZF9hdF91dGMYCSAB",
"KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDnVwZGF0ZWRfYXRf",
"dXRjGAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCg90ZXJt",
"aW5hbF9hdF91dGMYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1w",
"EhMKC3NvdXJjZV9ub2RlGAwgASgJIoABChVDYWNoZWRUZWxlbWV0cnlQYWNr",
"ZXQSLgoLYXVkaXRfZXZlbnQYASABKAsyGS5zaXRlc3RyZWFtLkF1ZGl0RXZl",
"bnREdG8SNwoLb3BlcmF0aW9uYWwYAiABKAsyIi5zaXRlc3RyZWFtLlNpdGVD",
"YWxsT3BlcmF0aW9uYWxEdG8iSgoUQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gSMgoH",
"cGFja2V0cxgBIAMoCzIhLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5UGFj",
"a2V0Im0KFlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QSLQoJc2luY2VfdXRjGAEg",
"ASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpiYXRjaF9zaXpl",
"GAIgASgFEhAKCGFmdGVyX2lkGAMgASgJIlwKF1B1bGxBdWRpdEV2ZW50c1Jl",
"c3BvbnNlEikKBmV2ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVu",
"dER0bxIWCg5tb3JlX2F2YWlsYWJsZRgCIAEoCCJrChRQdWxsU2l0ZUNhbGxz",
"UmVxdWVzdBItCglzaW5jZV91dGMYASABKAsyGi5nb29nbGUucHJvdG9idWYu",
"VGltZXN0YW1wEhIKCmJhdGNoX3NpemUYAiABKAUSEAoIYWZ0ZXJfaWQYAyAB",
"KAkiaQoVUHVsbFNpdGVDYWxsc1Jlc3BvbnNlEjgKDG9wZXJhdGlvbmFscxgB",
"IAMoCzIiLnNpdGVzdHJlYW0uU2l0ZUNhbGxPcGVyYXRpb25hbER0bxIWCg5t",
"b3JlX2F2YWlsYWJsZRgCIAEoCCpcCgdRdWFsaXR5EhcKE1FVQUxJVFlfVU5T",
"UEVDSUZJRUQQABIQCgxRVUFMSVRZX0dPT0QQARIVChFRVUFMSVRZX1VOQ0VS",
"VEFJThACEg8KC1FVQUxJVFlfQkFEEAMqXQoOQWxhcm1TdGF0ZUVudW0SGwoX",
"QUxBUk1fU1RBVEVfVU5TUEVDSUZJRUQQABIWChJBTEFSTV9TVEFURV9OT1JN",
"QUwQARIWChJBTEFSTV9TVEFURV9BQ1RJVkUQAiqFAQoOQWxhcm1MZXZlbEVu",
"dW0SFAoQQUxBUk1fTEVWRUxfTk9ORRAAEhMKD0FMQVJNX0xFVkVMX0xPVxAB",
"EhcKE0FMQVJNX0xFVkVMX0xPV19MT1cQAhIUChBBTEFSTV9MRVZFTF9ISUdI",
"EAMSGQoVQUxBUk1fTEVWRUxfSElHSF9ISUdIEAQyhgQKEVNpdGVTdHJlYW1T",
"ZXJ2aWNlElUKEVN1YnNjcmliZUluc3RhbmNlEiEuc2l0ZXN0cmVhbS5JbnN0",
"YW5jZVN0cmVhbVJlcXVlc3QaGy5zaXRlc3RyZWFtLlNpdGVTdHJlYW1FdmVu",
"dDABEk0KDVN1YnNjcmliZVNpdGUSHS5zaXRlc3RyZWFtLlNpdGVTdHJlYW1S",
"ZXF1ZXN0Ghsuc2l0ZXN0cmVhbS5TaXRlU3RyZWFtRXZlbnQwARJHChFJbmdl",
"c3RBdWRpdEV2ZW50cxIbLnNpdGVzdHJlYW0uQXVkaXRFdmVudEJhdGNoGhUu",
"c2l0ZXN0cmVhbS5Jbmdlc3RBY2sSUAoVSW5nZXN0Q2FjaGVkVGVsZW1ldHJ5",
"EiAuc2l0ZXN0cmVhbS5DYWNoZWRUZWxlbWV0cnlCYXRjaBoVLnNpdGVzdHJl",
"YW0uSW5nZXN0QWNrEloKD1B1bGxBdWRpdEV2ZW50cxIiLnNpdGVzdHJlYW0u",
"UHVsbEF1ZGl0RXZlbnRzUmVxdWVzdBojLnNpdGVzdHJlYW0uUHVsbEF1ZGl0",
"RXZlbnRzUmVzcG9uc2USVAoNUHVsbFNpdGVDYWxscxIgLnNpdGVzdHJlYW0u",
"UHVsbFNpdGVDYWxsc1JlcXVlc3QaIS5zaXRlc3RyZWFtLlB1bGxTaXRlQ2Fs",
"bHNSZXNwb25zZUIrqgIoWkIuTU9NLldXLlNjYWRhQnJpZGdlLkNvbW11bmlj",
"YXRpb24uR3JwY2IGcHJvdG8z"));
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[] {
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest.Parser, new[]{ "CorrelationId", "InstanceUniqueName" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamRequest), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamRequest.Parser, new[]{ "CorrelationId" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent.Parser, new[]{ "CorrelationId", "AttributeChanged", "AlarmChanged" }, new[]{ "Event" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest.Parser, new[]{ "CorrelationId", "InstanceUniqueName", "BatchingSupported" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamRequest), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamRequest.Parser, new[]{ "CorrelationId", "BatchingSupported" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent.Parser, new[]{ "CorrelationId", "AttributeChanged", "AlarmChanged", "Batch" }, new[]{ "Event" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch.Parser, new[]{ "Events" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AttributeValueUpdate), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AttributeValueUpdate.Parser, new[]{ "InstanceUniqueName", "AttributePath", "AttributeName", "Value", "Quality", "Timestamp" }, null, null, null, null),
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),
@@ -201,6 +206,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
public InstanceStreamRequest(InstanceStreamRequest other) : this() {
correlationId_ = other.correlationId_;
instanceUniqueName_ = other.instanceUniqueName_;
batchingSupported_ = other.batchingSupported_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
@@ -234,6 +240,27 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
}
}
/// <summary>Field number for the "batching_supported" field.</summary>
public const int BatchingSupportedFieldNumber = 3;
private bool batchingSupported_;
/// <summary>
/// Client-declared BATCH NEGOTIATION (R2, event batching). When true the client
/// understands the SiteStreamEventBatch oneof case and the server may coalesce
/// consecutive events into one frame. proto3 defaults this to false, so an OLD
/// central that never sets it keeps receiving one frame per event — that default
/// IS the negotiation, and it is what makes new-site↔old-central safe. A NEW
/// central sets it against an OLD site, which ignores the unknown field and
/// keeps sending per-event frames the new client also accepts. Additive-only.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool BatchingSupported {
get { return batchingSupported_; }
set {
batchingSupported_ = value;
}
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
@@ -251,6 +278,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
}
if (CorrelationId != other.CorrelationId) return false;
if (InstanceUniqueName != other.InstanceUniqueName) return false;
if (BatchingSupported != other.BatchingSupported) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -260,6 +288,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
int hash = 1;
if (CorrelationId.Length != 0) hash ^= CorrelationId.GetHashCode();
if (InstanceUniqueName.Length != 0) hash ^= InstanceUniqueName.GetHashCode();
if (BatchingSupported != false) hash ^= BatchingSupported.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
@@ -286,6 +315,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
output.WriteRawTag(18);
output.WriteString(InstanceUniqueName);
}
if (BatchingSupported != false) {
output.WriteRawTag(24);
output.WriteBool(BatchingSupported);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -304,6 +337,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
output.WriteRawTag(18);
output.WriteString(InstanceUniqueName);
}
if (BatchingSupported != false) {
output.WriteRawTag(24);
output.WriteBool(BatchingSupported);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -320,6 +357,9 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
if (InstanceUniqueName.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(InstanceUniqueName);
}
if (BatchingSupported != false) {
size += 1 + 1;
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -338,6 +378,9 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
if (other.InstanceUniqueName.Length != 0) {
InstanceUniqueName = other.InstanceUniqueName;
}
if (other.BatchingSupported != false) {
BatchingSupported = other.BatchingSupported;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
@@ -365,6 +408,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
InstanceUniqueName = input.ReadString();
break;
}
case 24: {
BatchingSupported = input.ReadBool();
break;
}
}
}
#endif
@@ -392,6 +439,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
InstanceUniqueName = input.ReadString();
break;
}
case 24: {
BatchingSupported = input.ReadBool();
break;
}
}
}
}
@@ -440,6 +491,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public SiteStreamRequest(SiteStreamRequest other) : this() {
correlationId_ = other.correlationId_;
batchingSupported_ = other.batchingSupported_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
@@ -461,6 +513,21 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
}
}
/// <summary>Field number for the "batching_supported" field.</summary>
public const int BatchingSupportedFieldNumber = 2;
private bool batchingSupported_;
/// <summary>
/// See InstanceStreamRequest.batching_supported. Additive-only.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool BatchingSupported {
get { return batchingSupported_; }
set {
batchingSupported_ = value;
}
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
@@ -477,6 +544,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
return true;
}
if (CorrelationId != other.CorrelationId) return false;
if (BatchingSupported != other.BatchingSupported) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -485,6 +553,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
public override int GetHashCode() {
int hash = 1;
if (CorrelationId.Length != 0) hash ^= CorrelationId.GetHashCode();
if (BatchingSupported != false) hash ^= BatchingSupported.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
@@ -507,6 +576,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
output.WriteRawTag(10);
output.WriteString(CorrelationId);
}
if (BatchingSupported != false) {
output.WriteRawTag(16);
output.WriteBool(BatchingSupported);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -521,6 +594,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
output.WriteRawTag(10);
output.WriteString(CorrelationId);
}
if (BatchingSupported != false) {
output.WriteRawTag(16);
output.WriteBool(BatchingSupported);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -534,6 +611,9 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
if (CorrelationId.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(CorrelationId);
}
if (BatchingSupported != false) {
size += 1 + 1;
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -549,6 +629,9 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
if (other.CorrelationId.Length != 0) {
CorrelationId = other.CorrelationId;
}
if (other.BatchingSupported != false) {
BatchingSupported = other.BatchingSupported;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
@@ -572,6 +655,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
CorrelationId = input.ReadString();
break;
}
case 16: {
BatchingSupported = input.ReadBool();
break;
}
}
}
#endif
@@ -595,6 +682,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
CorrelationId = input.ReadString();
break;
}
case 16: {
BatchingSupported = input.ReadBool();
break;
}
}
}
}
@@ -645,6 +736,9 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
case EventOneofCase.AlarmChanged:
AlarmChanged = other.AlarmChanged.Clone();
break;
case EventOneofCase.Batch:
Batch = other.Batch.Clone();
break;
}
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
@@ -692,12 +786,31 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
}
}
/// <summary>Field number for the "batch" field.</summary>
public const int BatchFieldNumber = 4;
/// <summary>
/// Coalesced frame (R2). Emitted ONLY when the subscription request set
/// batching_supported = true. A batch is never nested inside a batch, and a
/// single event is always sent as a plain attribute_changed/alarm_changed
/// frame — so a quiet stream's wire shape is byte-identical to before.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch Batch {
get { return eventCase_ == EventOneofCase.Batch ? (global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch) event_ : null; }
set {
event_ = value;
eventCase_ = value == null ? EventOneofCase.None : EventOneofCase.Batch;
}
}
private object event_;
/// <summary>Enum of possible cases for the "event" oneof.</summary>
public enum EventOneofCase {
None = 0,
AttributeChanged = 2,
AlarmChanged = 3,
Batch = 4,
}
private EventOneofCase eventCase_ = EventOneofCase.None;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -731,6 +844,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
if (CorrelationId != other.CorrelationId) return false;
if (!object.Equals(AttributeChanged, other.AttributeChanged)) return false;
if (!object.Equals(AlarmChanged, other.AlarmChanged)) return false;
if (!object.Equals(Batch, other.Batch)) return false;
if (EventCase != other.EventCase) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -742,6 +856,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
if (CorrelationId.Length != 0) hash ^= CorrelationId.GetHashCode();
if (eventCase_ == EventOneofCase.AttributeChanged) hash ^= AttributeChanged.GetHashCode();
if (eventCase_ == EventOneofCase.AlarmChanged) hash ^= AlarmChanged.GetHashCode();
if (eventCase_ == EventOneofCase.Batch) hash ^= Batch.GetHashCode();
hash ^= (int) eventCase_;
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
@@ -773,6 +888,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
output.WriteRawTag(26);
output.WriteMessage(AlarmChanged);
}
if (eventCase_ == EventOneofCase.Batch) {
output.WriteRawTag(34);
output.WriteMessage(Batch);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -795,6 +914,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
output.WriteRawTag(26);
output.WriteMessage(AlarmChanged);
}
if (eventCase_ == EventOneofCase.Batch) {
output.WriteRawTag(34);
output.WriteMessage(Batch);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -814,6 +937,9 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
if (eventCase_ == EventOneofCase.AlarmChanged) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(AlarmChanged);
}
if (eventCase_ == EventOneofCase.Batch) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(Batch);
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -842,6 +968,12 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
}
AlarmChanged.MergeFrom(other.AlarmChanged);
break;
case EventOneofCase.Batch:
if (Batch == null) {
Batch = new global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch();
}
Batch.MergeFrom(other.Batch);
break;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
@@ -885,6 +1017,15 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
AlarmChanged = subBuilder;
break;
}
case 34: {
global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch subBuilder = new global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch();
if (eventCase_ == EventOneofCase.Batch) {
subBuilder.MergeFrom(Batch);
}
input.ReadMessage(subBuilder);
Batch = subBuilder;
break;
}
}
}
#endif
@@ -926,6 +1067,212 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
AlarmChanged = subBuilder;
break;
}
case 34: {
global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch subBuilder = new global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEventBatch();
if (eventCase_ == EventOneofCase.Batch) {
subBuilder.MergeFrom(Batch);
}
input.ReadMessage(subBuilder);
Batch = subBuilder;
break;
}
}
}
}
#endif
}
/// <summary>
/// Coalesced carrier for several consecutive stream events (R2). Ordering is
/// significant: events appear in the exact order the site produced them, and the
/// client unpacks them in order into the same per-event pipeline, so per-event
/// Timestamp fidelity and downstream sequencing are unchanged.
///
/// The inner events deliberately leave correlation_id EMPTY — the enclosing
/// SiteStreamEvent carries it once for the whole frame, which is the byte saving
/// batching exists for. No consumer reads the inner correlation_id.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class SiteStreamEventBatch : pb::IMessage<SiteStreamEventBatch>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
, pb::IBufferMessage
#endif
{
private static readonly pb::MessageParser<SiteStreamEventBatch> _parser = new pb::MessageParser<SiteStreamEventBatch>(() => new SiteStreamEventBatch());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pb::MessageParser<SiteStreamEventBatch> Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[3]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
pbr::MessageDescriptor pb::IMessage.Descriptor {
get { return Descriptor; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public SiteStreamEventBatch() {
OnConstruction();
}
partial void OnConstruction();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public SiteStreamEventBatch(SiteStreamEventBatch other) : this() {
events_ = other.events_.Clone();
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public SiteStreamEventBatch Clone() {
return new SiteStreamEventBatch(this);
}
/// <summary>Field number for the "events" field.</summary>
public const int EventsFieldNumber = 1;
private static readonly pb::FieldCodec<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent> _repeated_events_codec
= pb::FieldCodec.ForMessage(10, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent.Parser);
private readonly pbc::RepeatedField<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent> events_ = new pbc::RepeatedField<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent>();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public pbc::RepeatedField<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent> Events {
get { return events_; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
return Equals(other as SiteStreamEventBatch);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Equals(SiteStreamEventBatch other) {
if (ReferenceEquals(other, null)) {
return false;
}
if (ReferenceEquals(other, this)) {
return true;
}
if(!events_.Equals(other.events_)) return false;
return Equals(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
hash ^= events_.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
return hash;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override string ToString() {
return pb::JsonFormatter.ToDiagnosticString(this);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void WriteTo(pb::CodedOutputStream output) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
events_.WriteTo(output, _repeated_events_codec);
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
events_.WriteTo(ref output, _repeated_events_codec);
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
}
#endif
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
size += events_.CalculateSize(_repeated_events_codec);
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
return size;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(SiteStreamEventBatch other) {
if (other == null) {
return;
}
events_.Add(other.events_);
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(pb::CodedInputStream input) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
input.ReadRawMessage(this);
#else
uint tag;
while ((tag = input.ReadTag()) != 0) {
if ((tag & 7) == 4) {
// Abort on any end group tag.
return;
}
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 10: {
events_.AddEntriesFrom(input, _repeated_events_codec);
break;
}
}
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
uint tag;
while ((tag = input.ReadTag()) != 0) {
if ((tag & 7) == 4) {
// Abort on any end group tag.
return;
}
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
case 10: {
events_.AddEntriesFrom(ref input, _repeated_events_codec);
break;
}
}
}
}
@@ -948,7 +1295,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[3]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[4]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -1340,7 +1687,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[4]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[5]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -2461,7 +2808,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[5]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[6]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -3476,7 +3823,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[6]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[7]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -3663,7 +4010,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[7]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[8]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -3856,7 +4203,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[8]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[9]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -4514,7 +4861,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[9]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[10]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -4767,7 +5114,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[10]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[11]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -4965,7 +5312,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[11]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[12]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -5257,7 +5604,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[12]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[13]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -5490,7 +5837,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[13]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[14]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -5780,7 +6127,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[14]; }
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[15]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]