Files
mxaccessgw/clients/java
Joseph Doherty 1aafd6bde4 Code-review 2026-05-20 sweep #2: re-review at a020350, resolve 48 findings
Second re-review pass at commit a020350 caught 48 new findings — including
one High-severity regression I introduced in the prior sweep — and fixed
them all in one parallel wave.

High (1)
- Client.Python-018: prior sweep set `license = "Proprietary"` in
  pyproject.toml. setuptools >= 77 enforces PEP 639 and rejects the
  string (it must be a valid SPDX expression), so `pip wheel .` and
  `pip install -e .` both fail before any source compiles. Tests
  still pass because pytest bypasses the build backend via
  `pythonpath`. Dropped the invalid license string, kept the
  `License :: Other/Proprietary License` classifier, and added
  `tests/test_packaging.py` so a future regression of the same shape
  is caught in CI.

Mediums (6)
- Worker-023: `HeartbeatStuckCeiling` (default 75s = 5x HeartbeatGrace)
  on WorkerPipeSessionOptions bounds the in-flight-command watchdog
  suppression so a truly stuck COM call still triggers StaHung
  instead of permanently defeating the watchdog.
- Client.Rust-018: reverted Rust's `latencyMs` split so the
  cross-language bench comparison is apples-to-apples again;
  `failureLatencyMs` kept as Rust-only enrichment.
- Client.Java-021: applied Client.Java-002's terminal-state
  serialisation pattern to DeployEventStream so close() arriving
  after queue-overflow can't erase the overflow exception.
- IntegrationTests-017: teardown-parity test now uses a two-window
  stability check after UnAdvise instead of strict equality against
  the pre-UnAdvise count (which raced against in-flight events).
- IntegrationTests-019: new RecordingTestOutputHelper wraps every
  log sink the WriteSecured live test owns (worker stdout/stderr,
  gateway logs, direct WriteLine) so the credential is proven
  absent from the full output buffer, not just the diagnostic
  message.
- Tests-020: added MxAccessGatewayServiceConstraintTests coverage
  for the previously-uncovered Write2Bulk and WriteSecured2Bulk
  arms of WriteBulkConstraintPlan.SetPayload.

Lows (41 — highlights)
- Server: Galaxy glob cache eviction is race-free (Server-024);
  GalaxyRepositoryGrpcService takes IGalaxyRepository (Server-025);
  AlarmsOptions validated at startup (Server-026); Authorization.md
  Constraint Enforcement snippet/prose enumerate the bulk write/read
  family (Server-027); bulk-read-commands and bulk-write-commands
  capability tokens added to OpenSession (Server-029);
  NotWiredAlarmRpcDispatcher XML doc and missing scope-resolver and
  state-machine tests cleaned up (023, 028).
- Worker: AlarmCommandHandler now invokes the same STA-affinity
  guard the poll path uses, at every command entry (Worker-024);
  RunAsync null-checks the runtime-session factory result
  (Worker-025).
- Worker.Tests: shared LiveMxAccessOptInVariableName lives on
  GatewayContractInfo (Worker.Tests-025); MxAccessSession.CreateForTesting
  rejects production sinks (Worker.Tests-026); FakeRuntimeSession's
  CancelCommandReturnValue serialised under lock (Worker.Tests-027);
  Probes namespace lifted to MxGateway.Worker.Tests.Probes
  (Worker.Tests-029); cancel-envelope sequence numbers monotonised
  (Worker.Tests-030); docs/GatewayTesting.md gains a "Dev-rig Probes"
  section (Worker.Tests-028).
- Tests: ManualTimeProvider consolidated into one TestSupport/ copy
  (Tests-021); SessionManagerBulkTests adds a mid-flight cancellation
  test backed by a TaskCompletionSource fake (Tests-022); companion
  FakeWorkerProcess.WaitForExitAsync no longer fakes its exit signal
  (Tests-023); constraint plan reply-count divergence pinned
  (Tests-024).
- IntegrationTests: TryGetSession chain carries [MaybeNullWhen(false)]
  end-to-end (IntegrationTests-018); abnormal-exit keyword set
  tightened to pipe-disconnected/end-of-stream and the test now
  asserts streamTask.IsFaulted (020, 021).
- Client.Dotnet: bench commands added to isLongRunning so the
  default 30s wall-clock budget doesn't kill them (015);
  BenchStreamEventsAsync observes the inner stream task on every
  exit path (016).
- Client.Go: parseValue wraps strconv errors with flag context and
  %w (017); bench loops honour ctx.Done() (018); galaxy-watch parses
  RFC3339Nano with fractional seconds (019); runStreamEvents installs
  signal.NotifyContext like runGalaxyWatch (020); five new CLI-level
  table-driven tests cover the bulk/bench subcommands (021).
- Client.Java: toCompletable Javadoc rewritten to match the actual
  cancellation contract Client.Java-015 established (022); stream-events
  text path uses Long.toUnsignedString for worker_sequence (023);
  bench-read-bulk no longer pollutes success-latency histogram with
  failure durations (024); --shutdown-timeout CLI option propagates
  through to ClientOptions (025); seven new MxGatewayCliTests cover
  the bulk and bench commands (026).
- Client.Python: mxgateway_cli ships its own py.typed marker (019);
  wheel-build smoke test added under tests/test_packaging.py (020);
  README documents the Galaxy CLI parity gap explicitly (021).
- Client.Rust: RustClientDesign.md signatures match session.rs and
  document the AsRef<str> read_bulk genericism (019);
  next_correlation_id re-exported at the crate root, with a
  property-style doc contract and an explicit disclaimer that the
  literal textual format is not part of the contract (020).
- Contracts: BulkWriteResult comment names the actual
  IConstraintEnforcer mechanism instead of "tag-allowlist filter"
  (014); BulkReadResult gains explicit per-arm payload-population
  documentation for the success vs failure cases (015).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:28:54 -04:00
..

Java Client

The Java client workspace contains the MXAccess Gateway client library, generated protobuf/gRPC bindings, a Picocli test CLI project, and JUnit tests.

Layout

clients/java/
  settings.gradle
  build.gradle
  src/main/generated/
  mxgateway-client/
  mxgateway-cli/

mxgateway-client generates Java protobuf and gRPC sources from ../../src/MxGateway.Contracts/Protos. The Gradle protobuf plugin writes those generated sources under src/main/generated, which matches the client proto manifest in ../proto/proto-inputs.json. Do not edit generated files by hand.

mxgateway-client exposes MxGatewayClientOptions, MxGatewayClient, MxGatewaySession, value/status helpers, typed gateway exceptions, raw generated stubs, and generated protobuf messages for parity tests.

mxgateway-cli depends on mxgateway-client and provides the mxgw-java application entry point. The CLI supports version, session, command, event streaming, write, and smoke-test commands with deterministic JSON output.

Regenerating Protobuf Bindings

Run generation from clients/java after the shared .proto files or Java output path changes:

gradle :mxgateway-client:generateProto

Client Usage

Create a client with explicit transport and auth options:

MxGatewayClientOptions options = MxGatewayClientOptions.builder()
        .endpoint("localhost:5000")
        .apiKey(System.getenv("MXGATEWAY_API_KEY"))
        .plaintext(true)
        .build();

try (MxGatewayClient client = MxGatewayClient.connect(options);
        MxGatewaySession session = client.openSession("java-client")) {
    int serverHandle = session.register("java-client");
    int itemHandle = session.addItem(serverHandle, "TestObject.TestInt");
    session.advise(serverHandle, itemHandle);
    session.write(serverHandle, itemHandle, MxValues.int32Value(123), 0);
}

Use rawBlockingStub, rawFutureStub, rawAsyncStub, openSessionRaw, closeSessionRaw, invoke, and raw session helper methods when tests need the underlying protobuf messages. MxGatewayCommandException and MxAccessException preserve the raw MxCommandReply when the gateway returns a data-bearing MXAccess failure.

MxGatewaySession exposes the full bulk family — addItemBulk, adviseItemBulk, removeItemBulk, unAdviseItemBulk, subscribeBulk, unsubscribeBulk, writeBulk, write2Bulk, writeSecuredBulk, writeSecured2Bulk, and readBulk. Each carries one round-trip with a List<*Entry> (or List<String> / List<Integer> for the legacy bulk shapes) and returns List<SubscribeResult> / List<BulkWriteResult> / List<BulkReadResult>; per-entry MXAccess failures populate wasSuccessful == false and never throw. readBulk takes a per-tag timeoutMs (0 = worker default) and returns cached OnDataChange values when the tag is already advised (wasCached == true) without touching the existing subscription.

openSession verifies the gateway's reported gateway_protocol_version against the version this client was generated for and throws MxGatewayException on a mismatch, so an incompatible client fails fast with a clear message instead of issuing commands that fail downstream. A gateway that does not populate the field is accepted unchanged.

MxGatewaySession implements AutoCloseable. The try-with-resources close() performs a CloseSession network RPC but swallows (and logs) any failure of that RPC so a close-time error never replaces the exception a try-with-resources body is already propagating. Call closeRaw() explicitly when you need to observe the close result or handle a close-time failure.

MxGatewayClient and GalaxyRepositoryClient implement AutoCloseable. For a client that owns its channel (built with connect), the try-with-resources close() shuts the channel down and waits up to the configured shutdownTimeout (default 10 s, independent of connectTimeout) for termination, forcibly shutting it down on timeout, so in-flight calls and Netty event-loop threads are not left running after the block exits. If the calling thread is interrupted while waiting, the channel is forcibly shut down and the interrupt flag is restored. closeAndAwaitTermination() does the same but throws InterruptedException for callers that want a checked, blocking-aware shutdown. close() is a no-op for a caller-managed channel.

MxEventStream implements Iterator<MxEvent> and AutoCloseable. Closing it cancels the underlying gRPC stream. Canceling or timing out a Java client call only stops the client from waiting; it does not abort an in-flight MXAccess COM call on the worker STA. Closing an MxEventStream before the gRPC call has attached its observer (a real race when callers cancel immediately after subscribing) is safe — the close is replayed in the observer's beforeStart and the underlying call is cancelled, matching DeployEventStream behaviour. The event stream uses gRPC's default auto-inbound flow control with a fixed 1024-element buffer and no client-side flow control: this is the gateway's documented fail-fast event-backpressure model, so a consumer that stalls long enough to fill the buffer triggers an overflow that cancels the subscription and surfaces an MxGatewayException from the next next() call. Drain events promptly and be prepared to resubscribe with a resume cursor.

Cancellation of CompletableFuture results from openSessionAsync, invokeAsync, acknowledgeAlarmAsync, getLastDeployTimeAsync, testConnectionAsync, and discoverHierarchyAsync forwards to the underlying gRPC call: calling cancel(true) on the returned future aborts the in-flight RPC instead of merely detaching the future from its result.

Galaxy Repository Browse

The Galaxy Repository service is a separate metadata-only gRPC service exposed by the gateway. It lets clients enumerate the deployed Galaxy object hierarchy and the dynamic attributes on each object so they know which tag references to subscribe to via the MXAccess Gateway service. It uses the same API-key auth as the gateway and requires the metadata:read scope.

GalaxyRepositoryClient mirrors the MxGatewayClient pattern (caller-managed or owned channel, MxGatewayClientOptions, blocking + async variants). Three RPCs are exposed:

MxGatewayClientOptions options = MxGatewayClientOptions.builder()
        .endpoint("localhost:5000")
        .apiKey(System.getenv("MXGATEWAY_API_KEY"))
        .plaintext(true)
        .build();

try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
    boolean ok = galaxy.testConnection();
    Optional<Instant> lastDeploy = galaxy.getLastDeployTime();
    List<GalaxyObject> hierarchy = galaxy.discoverHierarchy();
}

getLastDeployTime returns Optional.empty() when the server reports present=false. discoverHierarchy returns the generated GalaxyObject proto messages directly so callers can read all fields (including the nested GalaxyAttribute list) without an extra DTO layer.

The CLI exposes matching subcommands: galaxy-test, galaxy-deploy-time, galaxy-discover, and galaxy-watch. They take the same --endpoint, --api-key-env, --plaintext, --ca-file, --server-name-override, --timeout, and --json options as the gateway commands.

gradle :mxgateway-cli:run --args="galaxy-test --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :mxgateway-cli:run --args="galaxy-deploy-time --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :mxgateway-cli:run --args="galaxy-discover --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"

Watching deploy events

GalaxyRepository.WatchDeployEvents is a server-streaming RPC: the gateway sends a bootstrap DeployEvent immediately on subscribe and then one event each time it observes a new galaxy.time_of_last_deploy. The sequence field is monotonic per server start; gaps mean the per-subscriber buffer dropped older events because the consumer was too slow.

The client exposes both an iterator-style adaptor over the async stub and an observer-callback variant. Both honour the channel-level streamTimeout.

try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options);
        DeployEventStream events = galaxy.watchDeployEvents(/* lastSeenDeployTime */ null)) {
    while (events.hasNext()) {
        DeployEvent event = events.next();
        // event.getSequence(), event.getObservedAt(),
        // event.getTimeOfLastDeploy() / getTimeOfLastDeployPresent(),
        // event.getObjectCount(), event.getAttributeCount()
    }
}

Pass an Instant for lastSeenDeployTime to suppress the bootstrap event when the cached deploy time matches what the caller already has. DeployEventStream implements Iterator<DeployEvent> and AutoCloseable; closing it cancels the underlying gRPC call.

For callback delivery (e.g. when the consumer wants to drive a queue or reactive pipeline), use the async variant:

DeployEventSubscription subscription = galaxy.watchDeployEventsAsync(
        lastSeen,
        new StreamObserver<>() {
            @Override public void onNext(DeployEvent value) { /* ... */ }
            @Override public void onError(Throwable t) { /* ... */ }
            @Override public void onCompleted() { /* ... */ }
        });
// later:
subscription.cancel(); // or subscription.close()

The matching CLI subcommand streams events until cancelled (Ctrl+C) and prints one line per event in text mode or one JSON object per event with --json:

gradle :mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --json"
gradle :mxgateway-cli:run --args="galaxy-watch --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --last-seen-deploy-time 2026-04-28T18:30:00Z --limit 5"

CLI Usage

Run the CLI through Gradle:

gradle :mxgateway-cli:run --args="version --json"
gradle :mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
gradle :mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
gradle :mxgateway-cli:run --args="add-item --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item TestObject.TestInt --json"
gradle :mxgateway-cli:run --args="advise --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --json"
gradle :mxgateway-cli:run --args="write --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json"
gradle :mxgateway-cli:run --args="stream-events --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --limit 1 --json"
gradle :mxgateway-cli:run --args="smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestObject.TestInt --json"

The CLI accepts --api-key, --api-key-env, --plaintext, --ca-file, --server-name-override, --timeout, and --json on gateway commands. JSON output redacts API keys.

Use TLS options for a secured gateway:

gradle :mxgateway-cli:run --args="smoke --endpoint mxgateway.example.local:5001 --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestObject.TestInt --json"

Build And Test

Run the Java checks from clients/java:

gradle test

The build uses the Java 21 Gradle toolchain, compiles generated protobuf/gRPC code, and runs JUnit 5 tests for the client wrapper, shared behavior fixtures, in-process gRPC behavior, stream cancellation, and CLI parser/output behavior.

Packaging

Create local library and CLI artifacts from clients/java:

gradle :mxgateway-client:jar :mxgateway-cli:installDist

The library jar is under mxgateway-client/build/libs. The installed CLI distribution is under mxgateway-cli/build/install/mxgateway-cli.

Integration Checks

Run live checks only when a gateway and MXAccess-backed worker are available:

$env:MXGATEWAY_INTEGRATION = '1'
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'TestObject.TestInt'
gradle :mxgateway-cli:run --args="smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json"