Files
mxaccessgw/clients/java/README.md
T
Joseph Doherty 6eb9ea9105 Resolve Client.Java-006..012 code-review findings
Client.Java-006: close() on both clients only called shutdown(). It now
awaits termination up to the connect timeout and shutdownNow()s on timeout.

Client.Java-007: added MxGatewayLowFindingsTests covering the alarm surface,
async streaming, MxEventStream overflow, and TLS channel construction. A
latent bug surfaced: a missing CA file throws IllegalArgumentException, not
SSLException — the channel-builder catch was broadened accordingly.

Client.Java-008: async thenApply sites now route stray RuntimeExceptions
through MxGatewayErrors.fromGrpc via a normalising validator.

Client.Java-009: extracted ~80 duplicated lines (createChannel, withDeadline,
toCompletable, ...) into a shared MxGatewayChannels; both clients delegate.

Client.Java-010 (re-triaged): the README's metadata:read scope was correct;
the acknowledgeAlarm Javadoc's invoke:alarm-ack was wrong — corrected to the
admin scope.

Client.Java-011: documented the intentional fail-fast event-stream
backpressure in Javadoc and the README.

Client.Java-012: replaced CommonOptions.resolved()'s mutate-and-return-this
with side-effect-free resolvedApiKey()/resolvedTimeout() accessors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:42:51 -04:00

256 lines
11 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.
`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 connect timeout
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. The event stream uses gRPC's default auto-inbound flow
control with a fixed 16-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.
## 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)