diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a644bbe..072b06f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -60,7 +60,19 @@ jobs: dotnet tool install --global PowerShell echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" - # IPC-01 / IPC-19 / IPC-20: descriptor set + Contracts/Generated must match the current protos. + # IPC-25 Check 4 regenerates the Go and Python client bindings and diffs them, so the pinned + # generators must be present. protoc 34.1 is already installed above; Go and Python are set up + # above. Pin protoc-gen-go / protoc-gen-go-grpc to match the committed header stamps and grpcio + # -tools to match the committed _pb2 stamp, or Check 4 false-fails (or masks drift) under churn. + - name: Install pinned client codegen generators (Check 4) + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2 + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + python -m pip install 'grpcio-tools==1.80.0' + + # IPC-01 / IPC-19 / IPC-20 / IPC-25: descriptor set + Contracts/Generated + Go/Python bindings + # must match the current protos. - name: Codegen / descriptor freshness shell: pwsh run: ./scripts/check-codegen.ps1 @@ -93,10 +105,12 @@ jobs: python -m pytest java: - # Java client runs on a JDK-17 Linux runner (the macOS dev box has no JRE). The protobuf gradle - # plugin rewrites MxaccessGateway.java with spurious protobuf-runtime-version churn on every - # build; when no .proto changed, revert that one file so checkGeneratedClean / a dirty tree does - # not fail the build (repo memory project_java_generated_churn). + # Java client runs on a JDK-17 Linux runner (the macOS dev box has no JRE). The grpc/protobuf + # toolchain is fully pinned (clients/java/build.gradle: grpcVersion 1.76.0 / protobufVersion + # 4.33.1), so a regeneration is byte-identical to the committed aggregates modulo real .proto + # changes — `Verify generated tree is clean` (git diff) is the true drift gate (IPC-24). The + # single-file Java aggregates are where message-level proto drift lands, so this job now catches + # a .proto edited without regenerating and committing the Java client. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -115,13 +129,10 @@ jobs: - name: Gradle test working-directory: clients/java run: gradle test - - name: Revert spurious protobuf-version churn (no .proto changed) - # Both generated aggregates can pick up protobuf-runtime-version churn on regen; revert - # both so verify-clean still catches a real, uncommitted proto/codegen change elsewhere. - run: | - git checkout -- clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java || true - git checkout -- clients/java/src/main/generated/main/java/mxaccess_worker/v1/MxaccessWorker.java || true - name: Verify generated tree is clean + # IPC-24: the pinned grpc/protobuf toolchain regenerates byte-identical output, so this + # git-diff gate now catches message-level proto drift in the single-file Java aggregates + # (the old unconditional churn-revert step masked exactly that class and was deleted). run: git diff --exit-code -- clients/java/src/main/generated windows-x86: diff --git a/archreview/2026-07-12/remediation/00-tracking.md b/archreview/2026-07-12/remediation/00-tracking.md index 6911b42..aeb4c3a 100644 --- a/archreview/2026-07-12/remediation/00-tracking.md +++ b/archreview/2026-07-12/remediation/00-tracking.md @@ -44,12 +44,12 @@ Sequenced by cluster; a cluster is one change set. | CLI-35 | Medium | S | GWC-25 (coord) | Done | Python CLI `stream-events` crashes on a ReplayGap | | CLI-36 | Medium | S | GWC-25 (coord) | Done | Go CLI `stream-events` silently destroys the ReplayGap signal | | WRK-21 | Medium | M | owns IPC-23 fix; WRK-28 same batch | Done | DrainEvents bound is count-based only; oversized reply kills the session and loses the drained events | -| IPC-23 | Medium | S | WRK-21 | In progress — mechanics landed with WRK-21; proto-comment/doc wave pending | DrainEvents contract requirements (reply fits negotiated max, no event loss, drain-until-empty) + proto-comment/doc wave | +| IPC-23 | Medium | S | WRK-21 | Done | DrainEvents contract requirements (reply fits negotiated max, no event loss, drain-until-empty) + proto-comment/doc wave | | IPC-30 | Low | M | WRK-21 (same batch) | Done | Oversized event frame stays session-fatal by design, but the death becomes structured (fault frame + logged identity) | | SEC-31 | Medium | M | — | Done | Failure limiter partitions on attacker-controlled key id and blocks before verification (lockout DoS) | | SEC-32 | Low | S | SEC-31 | Done | Failure-limiter LRU flushable by junk-token spray; token prefix never validated | -| IPC-24 | Medium | S | — | Not started | CI's unconditional Java churn-revert masks real drift | -| IPC-25 | Medium | M | — | Not started | Regenerate stale Go/Python worker bindings + add binding-freshness guard (Check 4) to check-codegen.ps1 | +| IPC-24 | Medium | S | — | Done | CI's unconditional Java churn-revert masks real drift | +| IPC-25 | Medium | M | — | Done | Regenerate stale Go/Python worker bindings + add binding-freshness guard (Check 4) to check-codegen.ps1 | ## Finding registers by domain @@ -93,7 +93,7 @@ Full design + implementation for each row lives in the linked domain doc under i | IPC-29 | Low | — | S | WRK-26 (discharged by) | Done (discharged by WRK-26) | WorkerFrameProtocol.md missing write-scheduling/sequencing section | | IPC-30 | Low | P0 | M | WRK-21 (same batch) | Done | Oversized event frame: keep session-fatal, make the death structured | | IPC-31 | Info | — | — | — | N/A | Gateway creation-time sequence stamping accepted; diagnostic-only, decision recorded | -| IPC-32 | Info | — | S | IPC-25 (folded in) | Not started | check-codegen banner relabel 1/4…4/4 | +| IPC-32 | Info | — | S | IPC-25 (folded in) | Done | check-codegen banner relabel 1/4…4/4 | ### Security & dashboard — [40-security-dashboard.md](40-security-dashboard.md) @@ -175,3 +175,4 @@ Sequence these together rather than piecemeal — several are one change set spa | 2026-08-07 | **WRK-21 + WRK-28 + WRK-23 + IPC-30 → `Done`** (branch `fix/wrk-21-drain-cluster`, commits `33ba612` + test-fixture follow-ups `7c2eaf0`/`a256560`). WRK-21: `MxAccessEventQueue` gains a byte-budgeted `Drain(maxEvents, maxTotalBytes)` returning the new `WorkerEventDrainResult`, sizing inside the queue lock so an event that will not fit is never dequeued; `CreateDrainEventsReply` budgets against the negotiated frame max less a 64 KiB wrapper reserve and reports truncation through the existing `DiagnosticMessage` (no proto change), satisfying IPC-23 R1–R3; both reply-write seams (`HandleControlCommandAsync`, `ProcessCommandAsync`) now catch `MessageTooLarge` and answer the correlation with an `InvalidRequest` reply instead of unwinding/faulting the session. WRK-28: the 10,000 ceiling moved to `GatewayContractInfo.MaxDrainEventsPerCommand`, referenced by the gateway validator and the worker clamp (C# const, no `.proto` change). WRK-23: `WorkerFrameWriter` peek-stamps then commits `Sequence` only immediately before the stream write, so rejections leave no wire gap. IPC-30: an oversized event frame stays session-fatal but writes a `PROTOCOL_VIOLATION` `WorkerFault` with `command_method = EventDrain` naming family/handles/sequence/sizes (never the value) before exiting. Docs same commit: `MxAccessWorkerInstanceDesign.md`, `WorkerFrameProtocol.md`, `gateway.md`. **IPC-23 → `In progress`** — mechanics landed here; the proto-comment/doc wave (and its regen fan-out) is still pending and must not be folded into this branch. **Evidence** — macOS: `dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx` 0 warnings/0 errors, `dotnet test …MxGateway.Tests --filter FullyQualifiedName~MxAccessGrpcRequestValidator` 4/4 passed. windev (`scripts/ci/windev-worker-ci.ps1 -Sha a2565604 -Mode test`, 2026-08-07 06:47): x86 Worker build 0 warnings/0 errors, `Worker.Tests` **367 passed / 0 failed / 11 skipped** (skips are the live-MXAccess/dev-rig opt-ins), script exit 0. **Harness note:** `PipePair` runs both pipe ends in one process with blocking `FlushFileBuffers` per frame, so it wedges on multi-MB frames or after ~85 large round trips; the pipe tests therefore negotiate a 128 KiB frame maximum and walk 1,000 events to empty, while the full 10,000-event drain-to-empty no-loss proof runs at the queue layer (`MxAccessEventQueueTests`). | | 2026-08-07 | Code-review follow-ups on the same branch (commit `6bc3f9b`). (1) **Important** — `ResolveDrainReplyByteBudget` was a step, not a floor: just above the 64 KiB reserve the budget collapsed to a few bytes (exactly 1024 at the validator floor `MaxMessageBytes = 1024 + 64 KiB`), so a byte-heavy `DrainEvents` truncated on every call and the drain-until-empty loop never terminated. Now `Math.Max(frameMax - reserve, frameMax / 2)` — monotonic, never below half the frame max. New test `WorkerPipeSessionTests.DrainEvents_AtValidatorFloorFrameMax_MakesProgressAndTerminates` drives a byte-heavy queue at the exact validator floor and asserts drain-to-empty with no head reported oversized. (2) **Hardening** — the reply-too-large fallback write is now itself size-guarded (`WriteReplyTooLargeFallbackAsync`, shared by the control and STA reply seams) so a pathologically tiny negotiated max below the gateway floor (the WRK-24 gap) cannot make even the backstop session-fatal; log-and-swallow, comment points at WRK-24. (3) **Comment** — corrected the `RepeatedFieldOverheadBytes` docs: `WorkerEvent.CalculateSize()` already includes the event's tag+length, so the 8 bytes is pure slack, not wrapper compensation. **Evidence** — macOS build 0/0, validator filter 4/4. windev (`windev-worker-ci.ps1 -Sha 6bc3f9b -Mode test`, 07:07): x86 Worker build 0/0, `Worker.Tests` **368 passed / 0 failed / 11 skipped**, script exit 0. (An earlier run of the same SHA flaked on the pre-existing `RunAsync_WhenStaActivityIsStale_WritesWatchdogFault` — a 5 s CTS timeout under first-run load, untouched by this change; it passed on the clean re-run and in both prior full runs.) | | 2026-08-07 | **P1 doc-drift batch → `Done`: TST-27, WRK-26 (discharges IPC-29), CLI-42, CLI-43, IPC-28** (branch `fix/doc-drift-batch`). Doc-only; no source, proto, or test changes — cross-checked against HEAD in this worktree. **TST-27**: `docs/GatewayConfiguration.md`'s `ShowTagValues` row no longer says "Reserved" — it now states what `false` (default) does (`DashboardEventBroadcaster` blanks tag values from a deep-cloned `MxEvent` before the SignalR events-hub mirror, metadata still renders), the security relevance (the per-session hub ACL, SEC-25 roadmap item 12, still does not exist, so this redaction is the only thing between a low-trust Viewer and other sessions' tag values), and the honest scope limit (the flag does **not** cover `/browse`). **WRK-26** (discharges **IPC-29**): `docs/MxAccessWorkerInstanceDesign.md`'s "Outbound Queues" section rewritten from the stale five-level priority list to the two-class `Control`/`Event` scheduler actually shipped (`WorkerFrameWriter`/`WorkerFrameWritePriority.cs`), with the collapsed-decision rationale recorded, and the overflow paragraph rewritten to the implemented fail-fast (`WorkerFault` category `QueueOverflow` → fault frame written → `RunAsync` unwinds → generic `WorkerExitCode.UnexpectedFailure`, dedicated code still open). `docs/WorkerFrameProtocol.md` gained a new "Write Scheduling And Sequencing" section: the two priority classes, enqueue-then-contend/single-lock-holder-drains-all, write-time peek-stamp-commit sequencing, per-frame-rejection vs. stream-failure semantics, and flush coalescing — stated truthfully as landed (WRK-23's peek-stamp-commit is live at HEAD) or not (the drain loop still awaits each event `WriteAsync` individually, so WRK-25's N-events-one-flush batching has **not** landed and the section says so explicitly). Cancellation is deliberately **not** documented as a firm contract — a one-paragraph placeholder notes it is pending WRK-22, which has not landed (confirmed by reading `WorkerFrameWriter.cs`: no `Claimed`/tombstone machinery exists yet). `gateway.md:328-330` was cross-checked and left unchanged — its sequence prose (both sides stamp at write, per GWC-28) already reads true. **CLI-42**: `clients/rust/README.md` and `docs/ClientPackaging.md`'s Rust section now document the vendored proto layout matching `clients/rust/build.rs` exactly — repo-path-first resolution (`../../src/ZB.MOM.WW.MxGateway.Contracts/Protos`) falling back to `clients/rust/protos/` when the canonical path is absent (published-tarball case), the same-commit refresh rule enforced by `scripts/check-codegen.ps1` Check 3, and why `cargo package`/`cargo publish` run without `--no-verify` (matches `scripts/pack-clients.ps1:190-192`). **CLI-43**: `docs/style-guides/JavaStyleGuide.md` line 8 now says "Target Java 17 (the Ignition 8.3 baseline...)" mirroring the CLI-12 wording, matching the shipped `clients/java/build.gradle` toolchain-17 build. **IPC-28**: `docs/Grpc.md`'s exception-mapping prose gained `CommandTooLarge` → `ResourceExhausted` (verified against the live `switch` in `Grpc/MxAccessGatewayService.cs:950-960`), and the `Invoke` section gained one sentence on the oversized-payload path (`WorkerClient.InvokeAsync` rejects at the enqueue boundary per-correlation, session not faulted — verified against `WorkerClient.cs:220-234`), cross-referencing the headroom rule already documented in `docs/GatewayConfiguration.md:120-129`. Did not touch the DrainEvents-truncation row or the proto/`Generated/` trees — those belong to a parallel codegen task per the handoff note. **Source files cross-read for accuracy** (no edits): `src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs`, `.../WorkerFrameWritePriority.cs`, `.../WorkerPipeSession.cs` (confirmed two-class scheduler, WRK-21/23/28/30 landed, WRK-25/WRK-22 not landed), `src/ZB.MOM.WW.MxGateway.Worker/WorkerApplication.cs` (exit-code mapping), `src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventQueue.cs` (overflow fault path), `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardEventBroadcaster.cs` + `Configuration/DashboardOptions.cs` + `docs/GatewayDashboardDesign.md:170` (ShowTagValues), `src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs:940-963` + `Workers/WorkerClient.cs:205-244` + `Workers/WorkerClientErrorCode.cs` (CommandTooLarge mapping), `clients/rust/build.rs`, `clients/rust/Cargo.toml`, `scripts/check-codegen.ps1`, `scripts/pack-clients.ps1` (Rust vendoring), `gateway.md:326-360` (sequence-prose cross-check). Verification (greps, doc-only — no build required): `grep -n 'Reserved' docs/GatewayConfiguration.md` no longer matches the `ShowTagValues` row; `grep -n 'faults' docs/MxAccessWorkerInstanceDesign.md` shows no remaining five-level list; `grep -n 'scheduling' docs/WorkerFrameProtocol.md` finds the new section; `grep -rn 'Java 21' docs/style-guides/` empty; `grep -i vendored docs/ClientPackaging.md clients/rust/README.md` non-empty in both; `grep -n 'CommandTooLarge' docs/Grpc.md` shows the mapping. | +| 2026-08-07 | **IPC-23 + IPC-24 + IPC-25 + IPC-32 → `Done`; GWC-25 deferred proto-comment resolved** (branch `fix/ipc-24-25-codegen`). The proto-comment/doc + regen wave for the P0 codegen-freshness cluster. **Proto comments (comment-only):** `mxaccess_worker.proto` `GatewayHello.max_frame_bytes` gained the every-frame-must-fit / reply-builders-truncate sentence (IPC-23); `mxaccess_gateway.proto` `DrainEventsReply` gained the count-cap+byte-cap / drain-until-empty comment (IPC-23); `mxaccess_gateway.proto` `ReplayGap.oldest_available_sequence` gained the empty-ring value definition (`highest observed + 1`, `oldest − 1` formula stays valid) resolving GWC-25's deferred amendment. **Regen wave:** `Contracts/Generated/{MxaccessGateway,MxaccessWorker}.cs` (XML-doc only, no descriptor delta), `clients/rust/protos/{mxaccess_gateway,mxaccess_worker}.proto` byte-copied, Go `internal/generated/{mxaccess_gateway,mxaccess_worker}.pb.go` (worker binding was genuinely stale — it lacked the whole `MaxFrameBytes` field/accessor/rawDesc, now refreshed; gateway.pb.go got the new doc comments), Python `generated/mxaccess_worker_pb2.py` (real descriptor delta — `max_frame_bytes` field + cascaded offsets), Java aggregates `MxaccessGateway.java`/`MxaccessWorker.java` (javadoc from the new proto comments — **zero** protobuf-version churn under the pinned toolchain), and the client descriptor set `mxaccessgw-client-v1.protoset`. **IPC-24:** regenerating Java on the pinned toolchain (grpc 1.76.0 / protobuf 4.33.1, Homebrew JDK 17) produced only the legitimate javadoc delta with no `validateProtobufGencodeVersion`/`major=/minor=/patch=` churn — so the preferred path was taken: the unconditional churn-revert step and its comment were **deleted** from `.gitea/workflows/ci.yml` (the `git diff --exit-code` gate is now a true message-level drift gate for the single-file Java aggregates). **IPC-25:** pinned the Go generators in `clients/go/generate-proto.ps1` (`protoc-gen-go` v1.36.11 assert, `protoc-gen-go-grpc` 1.6.2 assert, protoc warn-only; also fixed a latent `(if …)`→`$(if …)` parse bug that broke the script under pwsh 7); added **Check 4** to `scripts/check-codegen.ps1` (regenerate Go+Python bindings, fail on any diff, tool-missing FAILS not skips) and relabeled the banners `1/4`…`4/4` + header comment (**IPC-32** folded in); added the pinned-generator installs to the `portable` CI job before the codegen step. **Docs same commit:** `ClientProtoGeneration.md` (pinned-versions table + Go/Python Check-4 guard + Java-revert-deleted note), `Contracts.md` (four-check enumeration), `GatewayTesting.md` (java job + four-check prose), `clients/java/zb-mom-ww-mxgateway-client/build.gradle` checkGeneratedClean caveat. **IPC-23 DrainEvents-truncation doc deviation:** `docs/Grpc.md` has no DrainEvents row (it documents only the seven public RPCs; DrainEvents is a worker diagnostic), and `docs/WorkerFrameProtocol.md` (owned by the parallel doc batch) already carries the byte-cap/drain-until-empty prose via WRK-21 — so no Grpc.md edit was made. **Evidence (macOS):** `pwsh scripts/check-codegen.ps1` all four checks green (banners `1/4`…`4/4`); `dotnet build …NonWindows.slnx` 0 warnings/0 errors; `dotnet test …Tests --filter ~ClientProtoInputTests` green; `grep -c max_frame_bytes` → Go worker 2, Python worker 1 (both non-zero, were 0); `clients/go` gofmt clean + `go build` + `go test` ok; `clients/python` `pytest` 163 passed/1 skipped; Java `gradle generateProto` clean under JDK 17. **Negative-path proof:** added a scratch field to `mxaccess_worker.proto`, regenerated only `Contracts/Generated/`, reran `check-codegen.ps1` and confirmed **Check 4 fails naming both `clients/go/internal/generated` and `clients/python/…/generated`**, then reverted. **Not published** (CLI-39 gates Go/Python republish). | diff --git a/archreview/2026-07-12/remediation/10-gateway-core.md b/archreview/2026-07-12/remediation/10-gateway-core.md index f35710c..5a04430 100644 --- a/archreview/2026-07-12/remediation/10-gateway-core.md +++ b/archreview/2026-07-12/remediation/10-gateway-core.md @@ -61,7 +61,7 @@ Coordinate with (do not block on) open GWC-21: if `EventChannelFullModeTimeout` The proto comment currently states "`oldest_available_sequence` itself IS still retained", which becomes false in the empty-ring case — per the docs-with-source rule, amend the field comment in the same commit to define the empty-ring value ("when nothing is retained, this is the next sequence that can be delivered — `highest observed + 1` — and the `oldest − 1` resume formula remains valid; the interval evicted is unchanged"). This is a comment-only proto change (no descriptor delta), but the repo's codegen rules still apply — see the steps. -**Implementation.** +**Implementation.** (Code + `docs/Sessions.md` landed 2026-08-07 on `fix/gwc-25-replaygap-trio`; the deferred proto-comment amendment below **landed 2026-08-07** with the IPC-23 codegen wave on `fix/ipc-24-25-codegen` — GWC-25 is fully resolved.) - `Sessions/SessionEventDistributor.cs:463-467`: replace `oldestAvailableSequence = 0;` with `oldestAvailableSequence = gap ? _highestSequenceSeen + 1 : 0;` plus a comment explaining the `oldest − 1` client formula this must keep valid (cite this finding). - `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto` (`ReplayGap.oldest_available_sequence`, ~line 759): append the empty-ring sentence above. Then regenerate per repo rules: delete `src/ZB.MOM.WW.MxGateway.Contracts/Generated/*.cs`, `dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj`, and **commit `Generated/`** (net48 worker builds break otherwise). Sync the vendored client copies of the proto byte-identical (`clients/*/`); a comment-only edit changes no descriptor, so: Python `*_pb2*` output is unchanged (comments are not embedded — regenerate with the pinned grpcio-tools only if the files actually differ), Go/C#/Rust generated doc comments will churn — regenerate those per each client README, and revert spurious Java aggregate-file churn if no message-level delta appears (per the established Java convention). - `docs/Sessions.md` (~lines 228-234, ReplayGap section): document the empty-ring sentinel value and that `after_worker_sequence = oldest_available_sequence − 1` is the universal resume formula in both the retained and fully-evicted cases. diff --git a/archreview/2026-07-12/remediation/30-contracts-ipc.md b/archreview/2026-07-12/remediation/30-contracts-ipc.md index ce367d9..7b66eda 100644 --- a/archreview/2026-07-12/remediation/30-contracts-ipc.md +++ b/archreview/2026-07-12/remediation/30-contracts-ipc.md @@ -12,16 +12,16 @@ All `path:line` citations were re-verified against the working tree at `4f5371f` | ID | Sev | Tier | Eff | Dep | Status | Title | |----|-----|------|-----|-----|--------|-------| -| IPC-23 | Medium | P0 | S¹ | WRK-21 | In progress — mechanics landed with WRK-21; proto-comment/doc wave pending | DrainEvents bound is count-based only; byte-heavy queue still builds a session-killing reply frame (contract requirements here; fix mechanics in WRK-21) | -| IPC-24 | Medium | P0 | S | — | Not started | CI's unconditional Java churn-revert masks real generated-code drift for message-level proto changes | -| IPC-25 | Medium | P0 | M | — | Not started | Committed Go/Python worker bindings are stale at HEAD; no guard covers them | +| IPC-23 | Medium | P0 | S¹ | WRK-21 | Done | DrainEvents bound is count-based only; byte-heavy queue still builds a session-killing reply frame (contract requirements here; fix mechanics in WRK-21) | +| IPC-24 | Medium | P0 | S | — | Done | CI's unconditional Java churn-revert masks real generated-code drift for message-level proto changes | +| IPC-25 | Medium | P0 | M | — | Done | Committed Go/Python worker bindings are stale at HEAD; no guard covers them | | IPC-26 | Low | P2 | S¹ | WRK-22 | Not started | Cancelled write leaves a ghost frame that is still written (contract requirement here; fix mechanics in WRK-22) | | IPC-27 | Low | P2 | S | — | Not started | Descriptor freshness test blind to enums, enum values, services/methods, and the Galaxy contract | | IPC-28 | Low | — | S | — | Done | `docs/Grpc.md` omits the `CommandTooLarge` → `ResourceExhausted` mapping | | IPC-29 | Low | — | S | — | Done (discharged by WRK-26) | Worker writer priority scheduling and write-time sequence stamping undocumented in the frame-protocol doc | | IPC-30 | Low | P0 | M | WRK-21 (same file/batch) | Done | Oversized worker→gateway event frame is session-fatal — make the death deliberate, structured, and diagnosable | | IPC-31 | Info | — | — | — | N/A | Gateway stamps sequence at creation, worker at write — accepted divergence; sequence is documented diagnostic-only (`gateway.md:328-330`); revisit only if sequence ever becomes load-bearing | -| IPC-32 | Info | — | S | IPC-25 | Not started | `check-codegen.ps1` check labels miscounted (folded into the IPC-25 script edit) | +| IPC-32 | Info | — | S | IPC-25 | Done | `check-codegen.ps1` check labels miscounted (folded into the IPC-25 script edit) | ¹ Effort for the work owned by *this* plan (proto comments + docs + acceptance criteria). The code mechanics are M and are tracked under WRK-21 / WRK-22 in the worker plan. diff --git a/clients/go/generate-proto.ps1 b/clients/go/generate-proto.ps1 index 9c807e6..85489b0 100644 --- a/clients/go/generate-proto.ps1 +++ b/clients/go/generate-proto.ps1 @@ -1,6 +1,16 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Pinned generator baseline. The committed Go bindings stamp these plugin versions in their +# headers (protoc-gen-go v1.36.11 / protoc-gen-go-grpc v1.6.2). Plugin-version drift rewrites +# those header stamps, so a regeneration on an off-pin machine would churn the tree and make +# check-codegen Check 4 false-fail (or mask real drift under churn). Assert the exact versions +# so a regen is deterministic. protoc itself is warn-only (source_code_info is normalized out of +# the committed bindings), matching publish-client-proto-inputs.ps1. +$PinnedProtocGenGoVersion = 'protoc-gen-go v1.36.11' +$PinnedProtocGenGoGrpcVersion = 'protoc-gen-go-grpc 1.6.2' +$PinnedProtocVersion = 'libprotoc 34.1' + $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..') $protoRoot = Join-Path $repoRoot 'src\ZB.MOM.WW.MxGateway.Contracts\Protos' $outputRoot = Join-Path $PSScriptRoot 'internal\generated' @@ -36,8 +46,25 @@ $wingetProtoc = if ($env:LOCALAPPDATA) { $goBin = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE 'go\bin' } elseif ($env:HOME) { Join-Path $env:HOME 'go/bin' } else { $null } $protoc = Resolve-Tool -Names @('protoc', 'protoc.exe') -FallbackPaths @($wingetProtoc) -$protocGenGo = Resolve-Tool -Names @('protoc-gen-go', 'protoc-gen-go.exe') -FallbackPaths @((if ($goBin) { Join-Path $goBin 'protoc-gen-go.exe' }), (if ($goBin) { Join-Path $goBin 'protoc-gen-go' })) -$protocGenGoGrpc = Resolve-Tool -Names @('protoc-gen-go-grpc', 'protoc-gen-go-grpc.exe') -FallbackPaths @((if ($goBin) { Join-Path $goBin 'protoc-gen-go-grpc.exe' }), (if ($goBin) { Join-Path $goBin 'protoc-gen-go-grpc' })) +$protocGenGo = Resolve-Tool -Names @('protoc-gen-go', 'protoc-gen-go.exe') -FallbackPaths @(($(if ($goBin) { Join-Path $goBin 'protoc-gen-go.exe' })), ($(if ($goBin) { Join-Path $goBin 'protoc-gen-go' }))) +$protocGenGoGrpc = Resolve-Tool -Names @('protoc-gen-go-grpc', 'protoc-gen-go-grpc.exe') -FallbackPaths @(($(if ($goBin) { Join-Path $goBin 'protoc-gen-go-grpc.exe' })), ($(if ($goBin) { Join-Path $goBin 'protoc-gen-go-grpc' }))) + +# Assert the pinned plugin versions before generating so Check 4 cannot false-fail (or mask drift) +# on an off-pin machine. protoc is warn-only. +$protocGenGoVersion = (& $protocGenGo --version 2>&1 | Out-String).Trim() +if ($protocGenGoVersion -ne $PinnedProtocGenGoVersion) { + throw "protoc-gen-go reports '$protocGenGoVersion', but regeneration is pinned to '$PinnedProtocGenGoVersion'. " + + "Install the pin: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11" +} +$protocGenGoGrpcVersion = (& $protocGenGoGrpc --version 2>&1 | Out-String).Trim() +if ($protocGenGoGrpcVersion -ne $PinnedProtocGenGoGrpcVersion) { + throw "protoc-gen-go-grpc reports '$protocGenGoGrpcVersion', but regeneration is pinned to '$PinnedProtocGenGoGrpcVersion'. " + + "Install the pin: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2" +} +$protocVersion = (& $protoc --version 2>&1 | Out-String).Trim() +if ($protocVersion -ne $PinnedProtocVersion) { + Write-Warning "protoc reports '$protocVersion', pin is '$PinnedProtocVersion'. Descriptor comments are normalized out of the committed Go bindings, so patch drift is tolerated; keep CI on the pin." +} # protoc discovers the plugins on PATH; prepend the directories the resolved plugins live in. $env:Path = (Split-Path $protocGenGo -Parent) + [System.IO.Path]::PathSeparator + (Split-Path $protocGenGoGrpc -Parent) + [System.IO.Path]::PathSeparator + $env:Path diff --git a/clients/go/internal/generated/mxaccess_gateway.pb.go b/clients/go/internal/generated/mxaccess_gateway.pb.go index 2932241..d3733d6 100644 --- a/clients/go/internal/generated/mxaccess_gateway.pb.go +++ b/clients/go/internal/generated/mxaccess_gateway.pb.go @@ -5974,8 +5974,12 @@ func (x *WorkerInfoReply) GetMxaccessClsid() string { } type DrainEventsReply struct { - state protoimpl.MessageState `protogen:"open.v1"` - Events []*MxEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The reply is bounded by both a server-side count cap and the negotiated + // worker-frame byte cap; a reply may therefore carry fewer events than + // `max_events` and fewer than are queued. Callers drain iteratively until an + // empty reply. + Events []*MxEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -6411,6 +6415,11 @@ type ReplayGap struct { // after_worker_sequence = oldest_available_sequence - 1 in the next // StreamEventsRequest, which will cause the server to replay starting at // oldest_available_sequence (the first retained event). + // When nothing is retained (the replay ring is empty), this is the next sequence + // that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + // formula remains valid: it resolves to the highest sequence already seen, so the + // follow-up resume replays nothing, reports no gap, and every newer live event + // passes. The interval evicted is unchanged. OldestAvailableSequence uint64 `protobuf:"varint,2,opt,name=oldest_available_sequence,json=oldestAvailableSequence,proto3" json:"oldest_available_sequence,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/clients/go/internal/generated/mxaccess_worker.pb.go b/clients/go/internal/generated/mxaccess_worker.pb.go index 1ecea8a..1bdfe6e 100644 --- a/clients/go/internal/generated/mxaccess_worker.pb.go +++ b/clients/go/internal/generated/mxaccess_worker.pb.go @@ -431,8 +431,17 @@ type GatewayHello struct { SupportedProtocolVersion uint32 `protobuf:"varint,1,opt,name=supported_protocol_version,json=supportedProtocolVersion,proto3" json:"supported_protocol_version,omitempty"` Nonce string `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"` GatewayVersion string `protobuf:"bytes,3,opt,name=gateway_version,json=gatewayVersion,proto3" json:"gateway_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Maximum worker-frame payload size, in bytes, negotiated by the gateway from its + // configured pipe limit. The worker adopts this as its frame-protocol MaxMessageBytes + // instead of a hard-coded default; 0 (an older gateway that never set the field) means + // "use the worker's built-in default". Sits above the public gRPC cap by an + // envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + // Every worker->gateway frame — events, heartbeats, faults, and control replies + // including DrainEvents — must serialize within this limit; reply builders truncate + // to fit rather than emit an oversized frame. + MaxFrameBytes uint32 `protobuf:"varint,4,opt,name=max_frame_bytes,json=maxFrameBytes,proto3" json:"max_frame_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GatewayHello) Reset() { @@ -486,6 +495,13 @@ func (x *GatewayHello) GetGatewayVersion() string { return "" } +func (x *GatewayHello) GetMaxFrameBytes() uint32 { + if x != nil { + return x.MaxFrameBytes + } + return 0 +} + type WorkerHello struct { state protoimpl.MessageState `protogen:"open.v1"` ProtocolVersion uint32 `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"` @@ -1109,11 +1125,12 @@ const file_mxaccess_worker_proto_rawDesc = "" + "\fworker_event\x18\x12 \x01(\v2\x1f.mxaccess_worker.v1.WorkerEventH\x00R\vworkerEvent\x12P\n" + "\x10worker_heartbeat\x18\x13 \x01(\v2#.mxaccess_worker.v1.WorkerHeartbeatH\x00R\x0fworkerHeartbeat\x12D\n" + "\fworker_fault\x18\x14 \x01(\v2\x1f.mxaccess_worker.v1.WorkerFaultH\x00R\vworkerFaultB\x06\n" + - "\x04body\"\x8b\x01\n" + + "\x04body\"\xb3\x01\n" + "\fGatewayHello\x12<\n" + "\x1asupported_protocol_version\x18\x01 \x01(\rR\x18supportedProtocolVersion\x12\x14\n" + "\x05nonce\x18\x02 \x01(\tR\x05nonce\x12'\n" + - "\x0fgateway_version\x18\x03 \x01(\tR\x0egatewayVersion\"\xa1\x01\n" + + "\x0fgateway_version\x18\x03 \x01(\tR\x0egatewayVersion\x12&\n" + + "\x0fmax_frame_bytes\x18\x04 \x01(\rR\rmaxFrameBytes\"\xa1\x01\n" + "\vWorkerHello\x12)\n" + "\x10protocol_version\x18\x01 \x01(\rR\x0fprotocolVersion\x12\x14\n" + "\x05nonce\x18\x02 \x01(\tR\x05nonce\x12*\n" + diff --git a/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java b/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java index bfee30d..7271063 100644 --- a/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java +++ b/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java @@ -69110,24 +69110,59 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { com.google.protobuf.MessageOrBuilder { /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ java.util.List getEventsList(); /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ mxaccess_gateway.v1.MxaccessGateway.MxEvent getEvents(int index); /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ int getEventsCount(); /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ java.util.List getEventsOrBuilderList(); /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ mxaccess_gateway.v1.MxaccessGateway.MxEventOrBuilder getEventsOrBuilder( @@ -69175,6 +69210,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { @SuppressWarnings("serial") private java.util.List events_; /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ @java.lang.Override @@ -69182,6 +69224,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return events_; } /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ @java.lang.Override @@ -69190,6 +69239,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return events_; } /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ @java.lang.Override @@ -69197,6 +69253,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return events_.size(); } /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ @java.lang.Override @@ -69204,6 +69267,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return events_.get(index); } /** + *
+     * The reply is bounded by both a server-side count cap and the negotiated
+     * worker-frame byte cap; a reply may therefore carry fewer events than
+     * `max_events` and fewer than are queued. Callers drain iteratively until an
+     * empty reply.
+     * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ @java.lang.Override @@ -69567,6 +69637,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { mxaccess_gateway.v1.MxaccessGateway.MxEvent, mxaccess_gateway.v1.MxaccessGateway.MxEvent.Builder, mxaccess_gateway.v1.MxaccessGateway.MxEventOrBuilder> eventsBuilder_; /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public java.util.List getEventsList() { @@ -69577,6 +69654,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public int getEventsCount() { @@ -69587,6 +69671,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public mxaccess_gateway.v1.MxaccessGateway.MxEvent getEvents(int index) { @@ -69597,6 +69688,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder setEvents( @@ -69614,6 +69712,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder setEvents( @@ -69628,6 +69733,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder addEvents(mxaccess_gateway.v1.MxaccessGateway.MxEvent value) { @@ -69644,6 +69756,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder addEvents( @@ -69661,6 +69780,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder addEvents( @@ -69675,6 +69801,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder addEvents( @@ -69689,6 +69822,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder addAllEvents( @@ -69704,6 +69844,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder clearEvents() { @@ -69717,6 +69864,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public Builder removeEvents(int index) { @@ -69730,6 +69884,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return this; } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public mxaccess_gateway.v1.MxaccessGateway.MxEvent.Builder getEventsBuilder( @@ -69737,6 +69898,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { return internalGetEventsFieldBuilder().getBuilder(index); } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public mxaccess_gateway.v1.MxaccessGateway.MxEventOrBuilder getEventsOrBuilder( @@ -69747,6 +69915,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public java.util.List @@ -69758,6 +69933,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public mxaccess_gateway.v1.MxaccessGateway.MxEvent.Builder addEventsBuilder() { @@ -69765,6 +69947,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { mxaccess_gateway.v1.MxaccessGateway.MxEvent.getDefaultInstance()); } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public mxaccess_gateway.v1.MxaccessGateway.MxEvent.Builder addEventsBuilder( @@ -69773,6 +69962,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { index, mxaccess_gateway.v1.MxaccessGateway.MxEvent.getDefaultInstance()); } /** + *
+       * The reply is bounded by both a server-side count cap and the negotiated
+       * worker-frame byte cap; a reply may therefore carry fewer events than
+       * `max_events` and fewer than are queued. Callers drain iteratively until an
+       * empty reply.
+       * 
+ * * repeated .mxaccess_gateway.v1.MxEvent events = 1; */ public java.util.List @@ -75305,6 +75501,11 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { * after_worker_sequence = oldest_available_sequence - 1 in the next * StreamEventsRequest, which will cause the server to replay starting at * oldest_available_sequence (the first retained event). + * When nothing is retained (the replay ring is empty), this is the next sequence + * that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + * formula remains valid: it resolves to the highest sequence already seen, so the + * follow-up resume replays nothing, reports no gap, and every newer live event + * passes. The interval evicted is unchanged. * * * uint64 oldest_available_sequence = 2; @@ -75386,6 +75587,11 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { * after_worker_sequence = oldest_available_sequence - 1 in the next * StreamEventsRequest, which will cause the server to replay starting at * oldest_available_sequence (the first retained event). + * When nothing is retained (the replay ring is empty), this is the next sequence + * that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + * formula remains valid: it resolves to the highest sequence already seen, so the + * follow-up resume replays nothing, reports no gap, and every newer live event + * passes. The interval evicted is unchanged. * * * uint64 oldest_available_sequence = 2; @@ -75781,6 +75987,11 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { * after_worker_sequence = oldest_available_sequence - 1 in the next * StreamEventsRequest, which will cause the server to replay starting at * oldest_available_sequence (the first retained event). + * When nothing is retained (the replay ring is empty), this is the next sequence + * that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + * formula remains valid: it resolves to the highest sequence already seen, so the + * follow-up resume replays nothing, reports no gap, and every newer live event + * passes. The interval evicted is unchanged. * * * uint64 oldest_available_sequence = 2; @@ -75800,6 +76011,11 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { * after_worker_sequence = oldest_available_sequence - 1 in the next * StreamEventsRequest, which will cause the server to replay starting at * oldest_available_sequence (the first retained event). + * When nothing is retained (the replay ring is empty), this is the next sequence + * that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + * formula remains valid: it resolves to the highest sequence already seen, so the + * follow-up resume replays nothing, reports no gap, and every newer live event + * passes. The interval evicted is unchanged. * * * uint64 oldest_available_sequence = 2; @@ -75823,6 +76039,11 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { * after_worker_sequence = oldest_available_sequence - 1 in the next * StreamEventsRequest, which will cause the server to replay starting at * oldest_available_sequence (the first retained event). + * When nothing is retained (the replay ring is empty), this is the next sequence + * that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + * formula remains valid: it resolves to the highest sequence already seen, so the + * follow-up resume replays nothing, reports no gap, and every newer live event + * passes. The interval evicted is unchanged. * * * uint64 oldest_available_sequence = 2; diff --git a/clients/java/src/main/generated/main/java/mxaccess_worker/v1/MxaccessWorker.java b/clients/java/src/main/generated/main/java/mxaccess_worker/v1/MxaccessWorker.java index 3303cfc..5b3819b 100644 --- a/clients/java/src/main/generated/main/java/mxaccess_worker/v1/MxaccessWorker.java +++ b/clients/java/src/main/generated/main/java/mxaccess_worker/v1/MxaccessWorker.java @@ -3797,6 +3797,9 @@ public final class MxaccessWorker extends com.google.protobuf.GeneratedFile { * instead of a hard-coded default; 0 (an older gateway that never set the field) means * "use the worker's built-in default". Sits above the public gRPC cap by an * envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + * Every worker->gateway frame — events, heartbeats, faults, and control replies + * including DrainEvents — must serialize within this limit; reply builders truncate + * to fit rather than emit an oversized frame. * * * uint32 max_frame_bytes = 4; @@ -3941,6 +3944,9 @@ public final class MxaccessWorker extends com.google.protobuf.GeneratedFile { * instead of a hard-coded default; 0 (an older gateway that never set the field) means * "use the worker's built-in default". Sits above the public gRPC cap by an * envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + * Every worker->gateway frame — events, heartbeats, faults, and control replies + * including DrainEvents — must serialize within this limit; reply builders truncate + * to fit rather than emit an oversized frame. * * * uint32 max_frame_bytes = 4; @@ -4499,6 +4505,9 @@ public final class MxaccessWorker extends com.google.protobuf.GeneratedFile { * instead of a hard-coded default; 0 (an older gateway that never set the field) means * "use the worker's built-in default". Sits above the public gRPC cap by an * envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + * Every worker->gateway frame — events, heartbeats, faults, and control replies + * including DrainEvents — must serialize within this limit; reply builders truncate + * to fit rather than emit an oversized frame. * * * uint32 max_frame_bytes = 4; @@ -4515,6 +4524,9 @@ public final class MxaccessWorker extends com.google.protobuf.GeneratedFile { * instead of a hard-coded default; 0 (an older gateway that never set the field) means * "use the worker's built-in default". Sits above the public gRPC cap by an * envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + * Every worker->gateway frame — events, heartbeats, faults, and control replies + * including DrainEvents — must serialize within this limit; reply builders truncate + * to fit rather than emit an oversized frame. * * * uint32 max_frame_bytes = 4; @@ -4535,6 +4547,9 @@ public final class MxaccessWorker extends com.google.protobuf.GeneratedFile { * instead of a hard-coded default; 0 (an older gateway that never set the field) means * "use the worker's built-in default". Sits above the public gRPC cap by an * envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + * Every worker->gateway frame — events, heartbeats, faults, and control replies + * including DrainEvents — must serialize within this limit; reply builders truncate + * to fit rather than emit an oversized frame. * * * uint32 max_frame_bytes = 4; diff --git a/clients/java/zb-mom-ww-mxgateway-client/build.gradle b/clients/java/zb-mom-ww-mxgateway-client/build.gradle index cdb2b4a..4fe4351 100644 --- a/clients/java/zb-mom-ww-mxgateway-client/build.gradle +++ b/clients/java/zb-mom-ww-mxgateway-client/build.gradle @@ -63,10 +63,11 @@ protobuf { // or a plugin/protobuf version bump, silently drifts the committed output. checkGeneratedClean // fails when the regenerated tree differs from what is committed. // -// Caveat (repo memory project_java_generated_churn): the protobuf gradle plugin also rewrites -// MxaccessGateway.java with a spurious protobuf-runtime-version delta on every build even when no -// .proto changed. CI reverts that one file (git checkout) before invoking this task; locally, do the -// same when you did not touch a .proto. See docs/GatewayTesting.md "Continuous Integration". +// The grpc/protobuf toolchain is pinned (build.gradle: grpcVersion / protobufVersion), so a +// regeneration is byte-identical to the committed single-file aggregates modulo real .proto +// changes — no spurious protobuf-runtime-version churn (IPC-24 verified this and deleted the old +// unconditional CI churn-revert step, which masked message-level drift). Regenerate and commit +// after any .proto change. See docs/GatewayTesting.md "Continuous Integration". tasks.register('checkGeneratedClean') { group = 'verification' description = 'Fails if the committed generated Java tree differs from a fresh regeneration.' @@ -83,9 +84,9 @@ tasks.register('checkGeneratedClean') { def dirty = stdout.toString().trim() if (!dirty.isEmpty()) { throw new GradleException( - "Generated Java is stale or churned:\n${dirty}\n" + - "Regenerate and commit after a .proto change, or 'git checkout' the spurious " + - "MxaccessGateway.java protobuf-version churn when no .proto changed.") + "Generated Java is stale:\n${dirty}\n" + + "Regenerate and commit the Java client after a .proto change " + + "(gradle :zb-mom-ww-mxgateway-client:generateProto).") } } } diff --git a/clients/proto/descriptors/mxaccessgw-client-v1.protoset b/clients/proto/descriptors/mxaccessgw-client-v1.protoset index b42b792..c3d6da8 100644 Binary files a/clients/proto/descriptors/mxaccessgw-client-v1.protoset and b/clients/proto/descriptors/mxaccessgw-client-v1.protoset differ diff --git a/clients/python/src/zb_mom_ww_mxgateway/generated/mxaccess_worker_pb2.py b/clients/python/src/zb_mom_ww_mxgateway/generated/mxaccess_worker_pb2.py index 9344395..98db30a 100644 --- a/clients/python/src/zb_mom_ww_mxgateway/generated/mxaccess_worker_pb2.py +++ b/clients/python/src/zb_mom_ww_mxgateway/generated/mxaccess_worker_pb2.py @@ -27,7 +27,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__ import mxaccess_gateway_pb2 as mxaccess__gateway__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15mxaccess_worker.proto\x12\x12mxaccess_worker.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x16mxaccess_gateway.proto\"\x95\x06\n\x0eWorkerEnvelope\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x10\n\x08sequence\x18\x03 \x01(\x04\x12\x16\n\x0e\x63orrelation_id\x18\x04 \x01(\t\x12\x39\n\rgateway_hello\x18\n \x01(\x0b\x32 .mxaccess_worker.v1.GatewayHelloH\x00\x12\x37\n\x0cworker_hello\x18\x0b \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerHelloH\x00\x12\x37\n\x0cworker_ready\x18\x0c \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerReadyH\x00\x12;\n\x0eworker_command\x18\r \x01(\x0b\x32!.mxaccess_worker.v1.WorkerCommandH\x00\x12\x46\n\x14worker_command_reply\x18\x0e \x01(\x0b\x32&.mxaccess_worker.v1.WorkerCommandReplyH\x00\x12\x39\n\rworker_cancel\x18\x0f \x01(\x0b\x32 .mxaccess_worker.v1.WorkerCancelH\x00\x12=\n\x0fworker_shutdown\x18\x10 \x01(\x0b\x32\".mxaccess_worker.v1.WorkerShutdownH\x00\x12\x44\n\x13worker_shutdown_ack\x18\x11 \x01(\x0b\x32%.mxaccess_worker.v1.WorkerShutdownAckH\x00\x12\x37\n\x0cworker_event\x18\x12 \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerEventH\x00\x12?\n\x10worker_heartbeat\x18\x13 \x01(\x0b\x32#.mxaccess_worker.v1.WorkerHeartbeatH\x00\x12\x37\n\x0cworker_fault\x18\x14 \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerFaultH\x00\x42\x06\n\x04\x62ody\"Z\n\x0cGatewayHello\x12\"\n\x1asupported_protocol_version\x18\x01 \x01(\r\x12\r\n\x05nonce\x18\x02 \x01(\t\x12\x17\n\x0fgateway_version\x18\x03 \x01(\t\"i\n\x0bWorkerHello\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\r\n\x05nonce\x18\x02 \x01(\t\x12\x19\n\x11worker_process_id\x18\x03 \x01(\x05\x12\x16\n\x0eworker_version\x18\x04 \x01(\t\"\x8e\x01\n\x0bWorkerReady\x12\x19\n\x11worker_process_id\x18\x01 \x01(\x05\x12\x17\n\x0fmxaccess_progid\x18\x02 \x01(\t\x12\x16\n\x0emxaccess_clsid\x18\x03 \x01(\t\x12\x33\n\x0fready_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"w\n\rWorkerCommand\x12/\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x1e.mxaccess_gateway.v1.MxCommand\x12\x35\n\x11\x65nqueue_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x81\x01\n\x12WorkerCommandReply\x12\x32\n\x05reply\x18\x01 \x01(\x0b\x32#.mxaccess_gateway.v1.MxCommandReply\x12\x37\n\x13\x63ompleted_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x1e\n\x0cWorkerCancel\x12\x0e\n\x06reason\x18\x01 \x01(\t\"Q\n\x0eWorkerShutdown\x12/\n\x0cgrace_period\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x0e\n\x06reason\x18\x02 \x01(\t\"H\n\x11WorkerShutdownAck\x12\x33\n\x06status\x18\x01 \x01(\x0b\x32#.mxaccess_gateway.v1.ProtocolStatus\":\n\x0bWorkerEvent\x12+\n\x05\x65vent\x18\x01 \x01(\x0b\x32\x1c.mxaccess_gateway.v1.MxEvent\"\xa5\x02\n\x0fWorkerHeartbeat\x12\x19\n\x11worker_process_id\x18\x01 \x01(\x05\x12.\n\x05state\x18\x02 \x01(\x0e\x32\x1f.mxaccess_worker.v1.WorkerState\x12?\n\x1blast_sta_activity_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1d\n\x15pending_command_count\x18\x04 \x01(\r\x12\"\n\x1aoutbound_event_queue_depth\x18\x05 \x01(\r\x12\x1b\n\x13last_event_sequence\x18\x06 \x01(\x04\x12&\n\x1e\x63urrent_command_correlation_id\x18\x07 \x01(\t\"\xf4\x01\n\x0bWorkerFault\x12\x39\n\x08\x63\x61tegory\x18\x01 \x01(\x0e\x32\'.mxaccess_worker.v1.WorkerFaultCategory\x12\x16\n\x0e\x63ommand_method\x18\x02 \x01(\t\x12\x14\n\x07hresult\x18\x03 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\x0e\x65xception_type\x18\x04 \x01(\t\x12\x1a\n\x12\x64iagnostic_message\x18\x05 \x01(\t\x12<\n\x0fprotocol_status\x18\x06 \x01(\x0b\x32#.mxaccess_gateway.v1.ProtocolStatusB\n\n\x08_hresult*\x97\x02\n\x0bWorkerState\x12\x1c\n\x18WORKER_STATE_UNSPECIFIED\x10\x00\x12\x19\n\x15WORKER_STATE_STARTING\x10\x01\x12\x1c\n\x18WORKER_STATE_HANDSHAKING\x10\x02\x12!\n\x1dWORKER_STATE_INITIALIZING_STA\x10\x03\x12\x16\n\x12WORKER_STATE_READY\x10\x04\x12\"\n\x1eWORKER_STATE_EXECUTING_COMMAND\x10\x05\x12\x1e\n\x1aWORKER_STATE_SHUTTING_DOWN\x10\x06\x12\x18\n\x14WORKER_STATE_STOPPED\x10\x07\x12\x18\n\x14WORKER_STATE_FAULTED\x10\x08*\xc7\x04\n\x13WorkerFaultCategory\x12%\n!WORKER_FAULT_CATEGORY_UNSPECIFIED\x10\x00\x12+\n\'WORKER_FAULT_CATEGORY_INVALID_ARGUMENTS\x10\x01\x12\x37\n3WORKER_FAULT_CATEGORY_GATEWAY_AUTHENTICATION_FAILED\x10\x02\x12+\n\'WORKER_FAULT_CATEGORY_PROTOCOL_MISMATCH\x10\x03\x12,\n(WORKER_FAULT_CATEGORY_PROTOCOL_VIOLATION\x10\x04\x12+\n\'WORKER_FAULT_CATEGORY_PIPE_DISCONNECTED\x10\x05\x12\x32\n.WORKER_FAULT_CATEGORY_MXACCESS_CREATION_FAILED\x10\x06\x12\x31\n-WORKER_FAULT_CATEGORY_MXACCESS_COMMAND_FAILED\x10\x07\x12:\n6WORKER_FAULT_CATEGORY_MXACCESS_EVENT_CONVERSION_FAILED\x10\x08\x12\"\n\x1eWORKER_FAULT_CATEGORY_STA_HUNG\x10\t\x12(\n$WORKER_FAULT_CATEGORY_QUEUE_OVERFLOW\x10\n\x12*\n&WORKER_FAULT_CATEGORY_SHUTDOWN_TIMEOUT\x10\x0b\x42&\xaa\x02#ZB.MOM.WW.MxGateway.Contracts.Protob\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15mxaccess_worker.proto\x12\x12mxaccess_worker.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x16mxaccess_gateway.proto\"\x95\x06\n\x0eWorkerEnvelope\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\x12\n\nsession_id\x18\x02 \x01(\t\x12\x10\n\x08sequence\x18\x03 \x01(\x04\x12\x16\n\x0e\x63orrelation_id\x18\x04 \x01(\t\x12\x39\n\rgateway_hello\x18\n \x01(\x0b\x32 .mxaccess_worker.v1.GatewayHelloH\x00\x12\x37\n\x0cworker_hello\x18\x0b \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerHelloH\x00\x12\x37\n\x0cworker_ready\x18\x0c \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerReadyH\x00\x12;\n\x0eworker_command\x18\r \x01(\x0b\x32!.mxaccess_worker.v1.WorkerCommandH\x00\x12\x46\n\x14worker_command_reply\x18\x0e \x01(\x0b\x32&.mxaccess_worker.v1.WorkerCommandReplyH\x00\x12\x39\n\rworker_cancel\x18\x0f \x01(\x0b\x32 .mxaccess_worker.v1.WorkerCancelH\x00\x12=\n\x0fworker_shutdown\x18\x10 \x01(\x0b\x32\".mxaccess_worker.v1.WorkerShutdownH\x00\x12\x44\n\x13worker_shutdown_ack\x18\x11 \x01(\x0b\x32%.mxaccess_worker.v1.WorkerShutdownAckH\x00\x12\x37\n\x0cworker_event\x18\x12 \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerEventH\x00\x12?\n\x10worker_heartbeat\x18\x13 \x01(\x0b\x32#.mxaccess_worker.v1.WorkerHeartbeatH\x00\x12\x37\n\x0cworker_fault\x18\x14 \x01(\x0b\x32\x1f.mxaccess_worker.v1.WorkerFaultH\x00\x42\x06\n\x04\x62ody\"s\n\x0cGatewayHello\x12\"\n\x1asupported_protocol_version\x18\x01 \x01(\r\x12\r\n\x05nonce\x18\x02 \x01(\t\x12\x17\n\x0fgateway_version\x18\x03 \x01(\t\x12\x17\n\x0fmax_frame_bytes\x18\x04 \x01(\r\"i\n\x0bWorkerHello\x12\x18\n\x10protocol_version\x18\x01 \x01(\r\x12\r\n\x05nonce\x18\x02 \x01(\t\x12\x19\n\x11worker_process_id\x18\x03 \x01(\x05\x12\x16\n\x0eworker_version\x18\x04 \x01(\t\"\x8e\x01\n\x0bWorkerReady\x12\x19\n\x11worker_process_id\x18\x01 \x01(\x05\x12\x17\n\x0fmxaccess_progid\x18\x02 \x01(\t\x12\x16\n\x0emxaccess_clsid\x18\x03 \x01(\t\x12\x33\n\x0fready_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"w\n\rWorkerCommand\x12/\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x1e.mxaccess_gateway.v1.MxCommand\x12\x35\n\x11\x65nqueue_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x81\x01\n\x12WorkerCommandReply\x12\x32\n\x05reply\x18\x01 \x01(\x0b\x32#.mxaccess_gateway.v1.MxCommandReply\x12\x37\n\x13\x63ompleted_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x1e\n\x0cWorkerCancel\x12\x0e\n\x06reason\x18\x01 \x01(\t\"Q\n\x0eWorkerShutdown\x12/\n\x0cgrace_period\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x0e\n\x06reason\x18\x02 \x01(\t\"H\n\x11WorkerShutdownAck\x12\x33\n\x06status\x18\x01 \x01(\x0b\x32#.mxaccess_gateway.v1.ProtocolStatus\":\n\x0bWorkerEvent\x12+\n\x05\x65vent\x18\x01 \x01(\x0b\x32\x1c.mxaccess_gateway.v1.MxEvent\"\xa5\x02\n\x0fWorkerHeartbeat\x12\x19\n\x11worker_process_id\x18\x01 \x01(\x05\x12.\n\x05state\x18\x02 \x01(\x0e\x32\x1f.mxaccess_worker.v1.WorkerState\x12?\n\x1blast_sta_activity_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1d\n\x15pending_command_count\x18\x04 \x01(\r\x12\"\n\x1aoutbound_event_queue_depth\x18\x05 \x01(\r\x12\x1b\n\x13last_event_sequence\x18\x06 \x01(\x04\x12&\n\x1e\x63urrent_command_correlation_id\x18\x07 \x01(\t\"\xf4\x01\n\x0bWorkerFault\x12\x39\n\x08\x63\x61tegory\x18\x01 \x01(\x0e\x32\'.mxaccess_worker.v1.WorkerFaultCategory\x12\x16\n\x0e\x63ommand_method\x18\x02 \x01(\t\x12\x14\n\x07hresult\x18\x03 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\x0e\x65xception_type\x18\x04 \x01(\t\x12\x1a\n\x12\x64iagnostic_message\x18\x05 \x01(\t\x12<\n\x0fprotocol_status\x18\x06 \x01(\x0b\x32#.mxaccess_gateway.v1.ProtocolStatusB\n\n\x08_hresult*\x97\x02\n\x0bWorkerState\x12\x1c\n\x18WORKER_STATE_UNSPECIFIED\x10\x00\x12\x19\n\x15WORKER_STATE_STARTING\x10\x01\x12\x1c\n\x18WORKER_STATE_HANDSHAKING\x10\x02\x12!\n\x1dWORKER_STATE_INITIALIZING_STA\x10\x03\x12\x16\n\x12WORKER_STATE_READY\x10\x04\x12\"\n\x1eWORKER_STATE_EXECUTING_COMMAND\x10\x05\x12\x1e\n\x1aWORKER_STATE_SHUTTING_DOWN\x10\x06\x12\x18\n\x14WORKER_STATE_STOPPED\x10\x07\x12\x18\n\x14WORKER_STATE_FAULTED\x10\x08*\xc7\x04\n\x13WorkerFaultCategory\x12%\n!WORKER_FAULT_CATEGORY_UNSPECIFIED\x10\x00\x12+\n\'WORKER_FAULT_CATEGORY_INVALID_ARGUMENTS\x10\x01\x12\x37\n3WORKER_FAULT_CATEGORY_GATEWAY_AUTHENTICATION_FAILED\x10\x02\x12+\n\'WORKER_FAULT_CATEGORY_PROTOCOL_MISMATCH\x10\x03\x12,\n(WORKER_FAULT_CATEGORY_PROTOCOL_VIOLATION\x10\x04\x12+\n\'WORKER_FAULT_CATEGORY_PIPE_DISCONNECTED\x10\x05\x12\x32\n.WORKER_FAULT_CATEGORY_MXACCESS_CREATION_FAILED\x10\x06\x12\x31\n-WORKER_FAULT_CATEGORY_MXACCESS_COMMAND_FAILED\x10\x07\x12:\n6WORKER_FAULT_CATEGORY_MXACCESS_EVENT_CONVERSION_FAILED\x10\x08\x12\"\n\x1eWORKER_FAULT_CATEGORY_STA_HUNG\x10\t\x12(\n$WORKER_FAULT_CATEGORY_QUEUE_OVERFLOW\x10\n\x12*\n&WORKER_FAULT_CATEGORY_SHUTDOWN_TIMEOUT\x10\x0b\x42&\xaa\x02#ZB.MOM.WW.MxGateway.Contracts.Protob\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -35,32 +35,32 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mxaccess_worker_pb2', _glob if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\252\002#ZB.MOM.WW.MxGateway.Contracts.Proto' - _globals['_WORKERSTATE']._serialized_start=2316 - _globals['_WORKERSTATE']._serialized_end=2595 - _globals['_WORKERFAULTCATEGORY']._serialized_start=2598 - _globals['_WORKERFAULTCATEGORY']._serialized_end=3181 + _globals['_WORKERSTATE']._serialized_start=2341 + _globals['_WORKERSTATE']._serialized_end=2620 + _globals['_WORKERFAULTCATEGORY']._serialized_start=2623 + _globals['_WORKERFAULTCATEGORY']._serialized_end=3206 _globals['_WORKERENVELOPE']._serialized_start=135 _globals['_WORKERENVELOPE']._serialized_end=924 _globals['_GATEWAYHELLO']._serialized_start=926 - _globals['_GATEWAYHELLO']._serialized_end=1016 - _globals['_WORKERHELLO']._serialized_start=1018 - _globals['_WORKERHELLO']._serialized_end=1123 - _globals['_WORKERREADY']._serialized_start=1126 - _globals['_WORKERREADY']._serialized_end=1268 - _globals['_WORKERCOMMAND']._serialized_start=1270 - _globals['_WORKERCOMMAND']._serialized_end=1389 - _globals['_WORKERCOMMANDREPLY']._serialized_start=1392 - _globals['_WORKERCOMMANDREPLY']._serialized_end=1521 - _globals['_WORKERCANCEL']._serialized_start=1523 - _globals['_WORKERCANCEL']._serialized_end=1553 - _globals['_WORKERSHUTDOWN']._serialized_start=1555 - _globals['_WORKERSHUTDOWN']._serialized_end=1636 - _globals['_WORKERSHUTDOWNACK']._serialized_start=1638 - _globals['_WORKERSHUTDOWNACK']._serialized_end=1710 - _globals['_WORKEREVENT']._serialized_start=1712 - _globals['_WORKEREVENT']._serialized_end=1770 - _globals['_WORKERHEARTBEAT']._serialized_start=1773 - _globals['_WORKERHEARTBEAT']._serialized_end=2066 - _globals['_WORKERFAULT']._serialized_start=2069 - _globals['_WORKERFAULT']._serialized_end=2313 + _globals['_GATEWAYHELLO']._serialized_end=1041 + _globals['_WORKERHELLO']._serialized_start=1043 + _globals['_WORKERHELLO']._serialized_end=1148 + _globals['_WORKERREADY']._serialized_start=1151 + _globals['_WORKERREADY']._serialized_end=1293 + _globals['_WORKERCOMMAND']._serialized_start=1295 + _globals['_WORKERCOMMAND']._serialized_end=1414 + _globals['_WORKERCOMMANDREPLY']._serialized_start=1417 + _globals['_WORKERCOMMANDREPLY']._serialized_end=1546 + _globals['_WORKERCANCEL']._serialized_start=1548 + _globals['_WORKERCANCEL']._serialized_end=1578 + _globals['_WORKERSHUTDOWN']._serialized_start=1580 + _globals['_WORKERSHUTDOWN']._serialized_end=1661 + _globals['_WORKERSHUTDOWNACK']._serialized_start=1663 + _globals['_WORKERSHUTDOWNACK']._serialized_end=1735 + _globals['_WORKEREVENT']._serialized_start=1737 + _globals['_WORKEREVENT']._serialized_end=1795 + _globals['_WORKERHEARTBEAT']._serialized_start=1798 + _globals['_WORKERHEARTBEAT']._serialized_end=2091 + _globals['_WORKERFAULT']._serialized_start=2094 + _globals['_WORKERFAULT']._serialized_end=2338 # @@protoc_insertion_point(module_scope) diff --git a/clients/rust/protos/mxaccess_gateway.proto b/clients/rust/protos/mxaccess_gateway.proto index d3e3a53..be96d5a 100644 --- a/clients/rust/protos/mxaccess_gateway.proto +++ b/clients/rust/protos/mxaccess_gateway.proto @@ -676,6 +676,10 @@ message WorkerInfoReply { } message DrainEventsReply { + // The reply is bounded by both a server-side count cap and the negotiated + // worker-frame byte cap; a reply may therefore carry fewer events than + // `max_events` and fewer than are queued. Callers drain iteratively until an + // empty reply. repeated MxEvent events = 1; } @@ -760,6 +764,11 @@ message ReplayGap { // after_worker_sequence = oldest_available_sequence - 1 in the next // StreamEventsRequest, which will cause the server to replay starting at // oldest_available_sequence (the first retained event). + // When nothing is retained (the replay ring is empty), this is the next sequence + // that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + // formula remains valid: it resolves to the highest sequence already seen, so the + // follow-up resume replays nothing, reports no gap, and every newer live event + // passes. The interval evicted is unchanged. uint64 oldest_available_sequence = 2; } diff --git a/clients/rust/protos/mxaccess_worker.proto b/clients/rust/protos/mxaccess_worker.proto index e50c4ff..7a8a5e7 100644 --- a/clients/rust/protos/mxaccess_worker.proto +++ b/clients/rust/protos/mxaccess_worker.proto @@ -47,6 +47,9 @@ message GatewayHello { // instead of a hard-coded default; 0 (an older gateway that never set the field) means // "use the worker's built-in default". Sits above the public gRPC cap by an // envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + // Every worker->gateway frame — events, heartbeats, faults, and control replies + // including DrainEvents — must serialize within this limit; reply builders truncate + // to fit rather than emit an oversized frame. uint32 max_frame_bytes = 4; } diff --git a/docs/ClientProtoGeneration.md b/docs/ClientProtoGeneration.md index fa21cb7..270ccf5 100644 --- a/docs/ClientProtoGeneration.md +++ b/docs/ClientProtoGeneration.md @@ -88,15 +88,23 @@ scripts assert the pin and resolve tools from `PATH`: | Generator | Pinned version | Guard | |-----------|----------------|-------| | protoc (descriptor set) | 34.1 | version assertion in `scripts/publish-client-proto-inputs.ps1` | -| `Grpc.Tools` (C# `Generated/`) | 2.80.0 (contracts csproj) | `scripts/check-codegen.ps1` git-diff of `Generated/` | -| `grpcio-tools` (Python) | 1.80.0 (protobuf runtime 6.31.1) | version assertion in `clients/python/generate-proto.ps1` | -| protobuf / grpc-java (Java) | `protobufVersion` / `grpcVersion` in `clients/java/build.gradle` | `checkGeneratedClean` gradle task | +| `Grpc.Tools` (C# `Generated/`) | 2.80.0 (contracts csproj) | `scripts/check-codegen.ps1` git-diff of `Generated/` (Check 2) | +| `protoc-gen-go` (Go) | v1.36.11 | version assertion in `clients/go/generate-proto.ps1`; `check-codegen.ps1` Check 4 | +| `protoc-gen-go-grpc` (Go) | 1.6.2 | version assertion in `clients/go/generate-proto.ps1`; `check-codegen.ps1` Check 4 | +| `grpcio-tools` (Python) | 1.80.0 (protobuf runtime 6.31.1) | version assertion in `clients/python/generate-proto.ps1`; `check-codegen.ps1` Check 4 | +| protobuf / grpc-java (Java) | `protobufVersion` / `grpcVersion` in `clients/java/build.gradle` | CI `git diff --exit-code` over `clients/java/src/main/generated` after `gradle test` (the `checkGeneratedClean` gradle task is the equivalent local check) | A newer `grpcio-tools` stamps a `GRPC_GENERATED_VERSION` above the pinned grpcio -runtime and breaks Python `pytest`; the Java protobuf plugin rewrites -`MxaccessGateway.java` with spurious protobuf-runtime-version churn on every build -(revert that one file when no `.proto` changed — see -[Gateway Testing](./GatewayTesting.md) "Continuous Integration"). +runtime and breaks Python `pytest`, so the Python and Go scripts assert their +generator pins before regenerating. Under the pinned grpc/protobuf toolchain the +Java protobuf plugin regenerates byte-identical output (modulo real `.proto` +changes), so CI enforces Java freshness with a direct `git diff --exit-code -- clients/java/src/main/generated` +step after `gradle test` (which transitively regenerates via `generateProto`); the +`checkGeneratedClean` gradle task is the equivalent check for local/manual use. The +old unconditional churn-revert CI step (which masked message-level drift in the +single-file Java aggregates) was deleted (IPC-24). Go and Python committed +bindings are guarded by `check-codegen.ps1` **Check 4**, which regenerates both +and fails on any diff. ## Output Directories diff --git a/docs/Contracts.md b/docs/Contracts.md index f6ce3db..fc53b25 100644 --- a/docs/Contracts.md +++ b/docs/Contracts.md @@ -114,7 +114,11 @@ dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csp `scripts/check-codegen.ps1` enforces this in CI (it force-regenerates and fails on any `git diff` against the committed `Generated/`) — that regeneration diff in the `portable` job is the primary guard. The SSH-driven `windows-x86` job's net48 worker build is the -secondary guard (a stale `Generated/` also breaks the x86 build with `CS0246`). +secondary guard (a stale `Generated/` also breaks the x86 build with `CS0246`). The same +script runs four checks in total: the committed client descriptor set (Check 1), the C# +`Generated/` (Check 2), the Rust vendored protos (Check 3), and the Go/Python client bindings +(Check 4) each regenerate and fail on any diff. See +[Client Proto Generation](./ClientProtoGeneration.md) for the pinned generator versions. Client generation inputs are published through `clients/proto/proto-inputs.json` and the descriptor set under diff --git a/docs/GatewayTesting.md b/docs/GatewayTesting.md index 074a20f..f55b9aa 100644 --- a/docs/GatewayTesting.md +++ b/docs/GatewayTesting.md @@ -428,10 +428,13 @@ runtime because the x86 Worker cannot build on Linux: gateway fake-worker tests, and builds/tests the clients that run on Linux: .NET client build, Go (`gofmt` + `go build` + `go test`), Rust (`cargo fmt --check` + `cargo test` + `cargo clippy -D warnings`), and Python (`pytest`). -- **`java`** (Linux, JDK 17) — `gradle test`. The protobuf gradle plugin rewrites - `MxaccessGateway.java` with spurious protobuf-runtime-version churn on every build, so - when no `.proto` changed the job reverts that one file (`git checkout`) before asserting - the generated tree is clean. The dev Mac has no JRE, so Java verification is CI-only. +- **`java`** (Linux, JDK 17) — `gradle test`, then `git diff --exit-code` over the generated + tree. The grpc/protobuf toolchain is pinned (`clients/java/build.gradle`), so a regeneration + is byte-identical to the committed single-file aggregates modulo real `.proto` changes; the + git-diff is therefore a true drift gate that now catches message-level proto drift in the Java + client (IPC-24 deleted the old unconditional churn-revert step, which masked exactly that + class). The dev Mac has a Homebrew JDK 17, so `gradle generateProto` can be run there to refresh + the Java aggregates when a `.proto` changes. - **`windows-x86`** (Linux runner, per push/PR) — builds the **x86 / net48 Worker and Worker.Tests**, which are Windows-only and out of scope for the Linux jobs. It runs on a Linux runner that always schedules and SSHes to windev (`10.100.0.48`), where @@ -449,10 +452,14 @@ runtime because the x86 Worker cannot build on Linux: `xUnit1030`). Gated `if: github.event_name == 'schedule'`, so it never gates a push. On failure it opens a Gitea issue via the Actions token, since nobody watches the Actions page. -The freshness guard `scripts/check-codegen.ps1` fails the build when the committed -client descriptor set or the C# `Generated/` no longer matches the current `.proto` -sources — the codegen drift class this repo has hit repeatedly (stale client -descriptors, net48 `CS0246` on unregenerated protos). The **primary** guard for the +The freshness guard `scripts/check-codegen.ps1` runs four checks and fails the build when the +committed client descriptor set (Check 1), the C# `Generated/` (Check 2), the Rust vendored +protos (Check 3), or the Go/Python client bindings (Check 4, IPC-25) no longer match the current +`.proto` sources — the codegen drift class this repo has hit repeatedly (stale client +descriptors, net48 `CS0246` on unregenerated protos, silently stale Go/Python worker bindings). +Check 4 regenerates the Go and Python bindings with their pinned generators (`protoc-gen-go` +v1.36.11 / `protoc-gen-go-grpc` 1.6.2, `grpcio-tools` 1.80.0) and fails on any diff; a missing +generator fails the check rather than skipping it. The **primary** guard for the "regenerate and commit `Generated/`" rule is that regeneration diff in the `portable` job; the `windows-x86` net48 compile is the **secondary** guard (a stale `Generated/` also breaks the x86 build with `CS0246`). See [Client Proto Generation](./ClientProtoGeneration.md) and diff --git a/scripts/check-codegen.ps1 b/scripts/check-codegen.ps1 index d7e4177..ab19901 100644 --- a/scripts/check-codegen.ps1 +++ b/scripts/check-codegen.ps1 @@ -1,7 +1,7 @@ #!/usr/bin/env pwsh -# Codegen freshness guard for CI (IPC-01, IPC-19, IPC-20, CLI-02). +# Codegen freshness guard for CI (IPC-01, IPC-19, IPC-20, IPC-25, CLI-02). # -# Three checks, all Linux/macOS-runnable (no Server build, no x86 worker): +# Four checks, all Linux/macOS-runnable (no Server build, no x86 worker): # 1. Published client descriptor set matches the current .proto sources (delegates to # publish-client-proto-inputs.ps1 -Check, which normalizes source_code_info so it is # protoc-version tolerant). @@ -13,6 +13,12 @@ # crate buildable outside the repo, CLI-02) are byte-identical to the canonical Contracts # protos. A drift means a .proto was edited without refreshing the vendored copies, which would # publish a stale wire contract to crate consumers while the in-repo build stays correct. +# 4. The committed Go and Python client bindings match a fresh regeneration (IPC-25). The two +# per-client generate-proto.ps1 scripts pin their generators (protoc-gen-go v1.36.11 / +# protoc-gen-go-grpc v1.6.2 for Go; grpcio-tools 1.80.0 for Python), so a clean checkout +# regenerates deterministic output; a non-empty git diff means a .proto was edited without +# regenerating and committing those bindings. A missing generator FAILS the check (a skipped +# guard is the exact silent-drift hole IPC-25 closes), never skips it. # # The x86 Worker + Worker.Tests are Windows-only and are guarded by the SSH-driven `windows-x86` # CI job (see docs/GatewayTesting.md, Continuous Integration), not here. @@ -28,7 +34,7 @@ $generatedDir = Join-Path $repoRoot 'src/ZB.MOM.WW.MxGateway.Contracts/Generated $contractsProject = Join-Path $repoRoot 'src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj' $failures = New-Object System.Collections.Generic.List[string] -Write-Host '== Check 1/2: client descriptor set freshness ==' +Write-Host '== Check 1/4: client descriptor set freshness ==' try { & (Join-Path $PSScriptRoot 'publish-client-proto-inputs.ps1') -Check if ($LASTEXITCODE -ne 0) { @@ -40,7 +46,7 @@ catch { } Write-Host '' -Write-Host '== Check 2/2: Contracts/Generated matches a fresh regeneration ==' +Write-Host '== Check 2/4: Contracts/Generated matches a fresh regeneration ==' try { # Force a full regeneration: Grpc.Tools skips regen when the committed .cs look up to date, so # remove them first (the documented "del Generated/*.cs to force regen" trick). @@ -66,7 +72,7 @@ catch { } Write-Host '' -Write-Host '== Check 3/3: Rust vendored protos match canonical Contracts protos ==' +Write-Host '== Check 3/4: Rust vendored protos match canonical Contracts protos ==' try { $canonicalProtoDir = Join-Path $repoRoot 'src/ZB.MOM.WW.MxGateway.Contracts/Protos' $vendoredProtoDir = Join-Path $repoRoot 'clients/rust/protos' @@ -87,6 +93,28 @@ catch { $failures.Add("Rust vendored proto check failed: $($_.Exception.Message)") } +Write-Host '' +Write-Host '== Check 4/4: Go and Python client bindings match a fresh regeneration ==' +try { + # Regenerate both binding sets with their pinned generators, then diff. The per-client scripts + # throw on a missing or off-pin generator, so any failure here FAILS the check rather than + # skipping it (a skipped guard is exactly the silent-drift hole IPC-25 closes). + $goBindingDir = 'clients/go/internal/generated' + $pyBindingDir = 'clients/python/src/zb_mom_ww_mxgateway/generated' + + & (Join-Path $repoRoot 'clients/go/generate-proto.ps1') | Out-Host + & (Join-Path $repoRoot 'clients/python/generate-proto.ps1') | Out-Host + + $bindingDiff = (& git -C $repoRoot status --porcelain -- $goBindingDir $pyBindingDir | Out-String).Trim() + if (-not [string]::IsNullOrEmpty($bindingDiff)) { + Write-Host $bindingDiff + $failures.Add("Go/Python client bindings differ from a fresh regeneration. Run clients/go/generate-proto.ps1 and clients/python/generate-proto.ps1 with the pinned generators and commit $goBindingDir and $pyBindingDir.") + } +} +catch { + $failures.Add("Go/Python codegen check failed (tool missing or regeneration error): $($_.Exception.Message)") +} + Write-Host '' if ($failures.Count -gt 0) { Write-Host 'Codegen freshness check FAILED:' -ForegroundColor Red diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs index fe182a4..f78dc5b 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs @@ -22796,6 +22796,12 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto { private static readonly pb::FieldCodec _repeated_events_codec = pb::FieldCodec.ForMessage(10, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEvent.Parser); private readonly pbc::RepeatedField events_ = new pbc::RepeatedField(); + /// + /// The reply is bounded by both a server-side count cap and the negotiated + /// worker-frame byte cap; a reply may therefore carry fewer events than + /// `max_events` and fewer than are queued. Callers drain iteratively until an + /// empty reply. + /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public pbc::RepeatedField Events { @@ -24510,6 +24516,11 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto { /// after_worker_sequence = oldest_available_sequence - 1 in the next /// StreamEventsRequest, which will cause the server to replay starting at /// oldest_available_sequence (the first retained event). + /// When nothing is retained (the replay ring is empty), this is the next sequence + /// that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + /// formula remains valid: it resolves to the highest sequence already seen, so the + /// follow-up resume replays nothing, reports no gap, and every newer live event + /// passes. The interval evicted is unchanged. /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessWorker.cs b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessWorker.cs index d8e1444..892348a 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessWorker.cs +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessWorker.cs @@ -1164,6 +1164,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto { /// instead of a hard-coded default; 0 (an older gateway that never set the field) means /// "use the worker's built-in default". Sits above the public gRPC cap by an /// envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + /// Every worker->gateway frame — events, heartbeats, faults, and control replies + /// including DrainEvents — must serialize within this limit; reply builders truncate + /// to fit rather than emit an oversized frame. /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto index d3e3a53..be96d5a 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -676,6 +676,10 @@ message WorkerInfoReply { } message DrainEventsReply { + // The reply is bounded by both a server-side count cap and the negotiated + // worker-frame byte cap; a reply may therefore carry fewer events than + // `max_events` and fewer than are queued. Callers drain iteratively until an + // empty reply. repeated MxEvent events = 1; } @@ -760,6 +764,11 @@ message ReplayGap { // after_worker_sequence = oldest_available_sequence - 1 in the next // StreamEventsRequest, which will cause the server to replay starting at // oldest_available_sequence (the first retained event). + // When nothing is retained (the replay ring is empty), this is the next sequence + // that can be delivered — `highest observed + 1` — and the `oldest - 1` resume + // formula remains valid: it resolves to the highest sequence already seen, so the + // follow-up resume replays nothing, reports no gap, and every newer live event + // passes. The interval evicted is unchanged. uint64 oldest_available_sequence = 2; } diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_worker.proto b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_worker.proto index e50c4ff..7a8a5e7 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_worker.proto +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_worker.proto @@ -47,6 +47,9 @@ message GatewayHello { // instead of a hard-coded default; 0 (an older gateway that never set the field) means // "use the worker's built-in default". Sits above the public gRPC cap by an // envelope-overhead margin so an accepted gRPC payload always fits one worker frame. + // Every worker->gateway frame — events, heartbeats, faults, and control replies + // including DrainEvents — must serialize within this limit; reply builders truncate + // to fit rather than emit an oversized frame. uint32 max_frame_bytes = 4; }