a0203503a7
Re-reviewed every module/client against the 10-category checklist
(REVIEW-PROCESS.md) at commit 1cd51bb, filed 72 new findings, and
fixed them in three priority waves (3 High, 17 Medium, 52 Low).
Highs
- Server-017: enumerate AcknowledgeAlarm / QueryActiveAlarms in
GatewayGrpcScopeResolver so non-admin keys can use them; document
the mapping in docs/Authorization.md; add interceptor tests.
- Client.Java-013: add the five missing bulk-method stubs to the
CLI FakeSession so the test module compiles on a clean tree.
- Client.Rust-013: fix the clippy::doc_lazy_continuation regression
in generated tonic code by reformatting the ReadBulkCommand proto
comment and scoping a #![allow(...)] to the generated submodules.
Mediums (highlights)
- Server: unify GatewaySession state-lock discipline (-015) and
make DisposeAsync race-safe against in-flight CloseAsync (-016);
add constraint-enforcement test coverage for the bulk-plan path
(-021).
- Worker: introduce StaRuntimeShutdownException so RunAlarmPollLoop
can distinguish graceful shutdown from a real STA-affinity
violation (-016); have the watchdog skip StaHung while
CurrentCommandCorrelationId is non-empty so a legitimate slow
ReadBulk no longer self-faults (-017).
- Tests: add per-method round-trip + cancellation coverage for the
11 GatewaySession bulk methods (-013); replace the real TCP probe
in GalaxyHierarchyCacheTests with an IGalaxyRepository fake
(-016).
- IntegrationTests: drive the StreamEvents writer in the live Write
test and assert OnWriteComplete (-012); add live tests for
Unadvise/RemoveItem/Unregister ordering, WriteSecured, and
abnormal worker exit (-014).
- Worker.Tests: replace MxAccessSession reflection with an internal
CreateForTesting factory (-016); cover WorkerCancel and
unexpected-body envelope branches (-017).
- Client.Java: cancel MxEventStream when close() races
beforeStart() (-014); return a CancellingCompletableFuture that
actually forwards cancellation through .thenApply chains (-015).
- Client.Python: drop the silent localhost-plaintext downgrade in
the CLI; require explicit --plaintext (-013).
- Client.Rust: stop bench-read-bulk from polluting success-latency
histograms with failed-call durations (-015); add coverage for
the five MalformedReply paths, the bulk-write helpers, the
Error::Unavailable mapping, and the unary-fault path (-016).
- Contracts: extend docs/Contracts.md with the bulk read/write
command family (-009).
Lows (highlights)
- Server: cap GalaxyGlobMatcher.RegexCache; align
WorkerAlarmRpcDispatcher missing-session handling; drop the
duplicate dashboard @page routes; refresh IAlarmRpcDispatcher
XML doc.
- Worker: surface SetXmlAlarmQuery COM failures; remove dead
subscriptionExpression / ExecutingCommand arms; preserve
factory-supplied runtime sessions; split MxAlarmSnapshot.cs into
three files.
- Tests: dispose the WebApplication in seven test classes; rebuild
FakeWorkerProcess.WaitForExitAsync against a real TaskCompletion
source; switch the heartbeat-expires test to ManualTimeProvider;
add InvariantCulture to the remaining DateTimeOffset.Parse sites;
document GalaxyFilterInputSafetyTests in GatewayTesting.md.
- IntegrationTests: comment fixes, RecordingServerStreamWriter
IDisposable, class-level [Trait], single-source ZB default
connection string.
- Worker.Tests: replace silent-return gating with LiveMxAccessFact
so absent env vars SKIP not pass; PascalCase rename of probe
[Fact]s; deterministic deadline test; new frame-protocol error
tests; ComputeTransitions diff-coverage; relocate dev-rig probes
to Probes/.
- Contracts: add round-trip coverage and per-field redaction /
Galaxy-identifier comments to the protos.
- Client.Dotnet: introduce clients/dotnet/Directory.Build.props so
TreatWarningsAsErrors / analysers apply; document
DiscoverHierarchyOptions and IMxGatewayCliClient; require typed
bulk-read handles in CLI; surface AcknowledgeAlarm transport
faults through Translate().
- Client.Go: kill dead code in alarms_test / fakeGalaxyServer /
runWriteBulkVariant; document the six new subcommands in
writeUsage; drain galaxy-watch events on limit; switch io.EOF
comparisons to errors.Is.
- Client.Java: shared shutdown helpers + new shutdownTimeout
option; regex-based credential redaction; Long.toUnsignedString
for uint64 sequence; doc fixes.
- Client.Python: combine duplicate imports; add coverage for
_percentile / bench-read-bulk / MAX_AGGREGATE_EVENTS /
_api_key_from_env; populate pyproject metadata and ship py.typed.
- Client.Rust: expose next_correlation_id() so CLI ping/close
stop hard-coding correlation IDs; resync RustClientDesign.md
with the current Session / Error surface and CLI subcommand set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
279 lines
13 KiB
Markdown
279 lines
13 KiB
Markdown
# 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
|
|
|
|
```text
|
|
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:
|
|
|
|
```powershell
|
|
gradle :mxgateway-client:generateProto
|
|
```
|
|
|
|
## Client Usage
|
|
|
|
Create a client with explicit transport and auth options:
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
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.
|
|
|
|
```powershell
|
|
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`.
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
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`:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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`:
|
|
|
|
```powershell
|
|
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`:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
$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"
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [Java Client Detailed Design](./JavaClientDesign.md)
|
|
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|