Files
mxaccessgw/clients/rust
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
..
2026-04-29 07:27:00 -04:00

Rust Client Workspace

The Rust client workspace contains the MXAccess Gateway client library, a test CLI, and tests for generated contract wiring plus wrapper behavior. The library uses the shared protobuf inputs documented in ../../docs/ClientProtoGeneration.md so the Rust bindings compile against the same public gateway and worker contracts as the server.

Layout

clients/rust/
  Cargo.toml
  build.rs
  src/
  tests/
  crates/mxgw-cli/

build.rs reads the .proto files from ../../src/MxGateway.Contracts/Protos and generates tonic/prost bindings into Cargo build output. src/generated.rs declares the Rust modules that include those generated files. src/generated remains reserved for checked-in generator output if the crate later changes to source-tree generation.

Build And Test

Run the Rust workspace checks from clients/rust:

cargo fmt --all --check
cargo test --workspace
cargo check --workspace
cargo clippy --workspace --all-targets -- -D warnings

The build script uses protoc from PATH or the Windows path recorded in ../../docs/ToolchainLinks.md.

Packaging

Create local release artifacts from clients/rust:

cargo build --workspace --release
cargo install --path crates/mxgw-cli --locked --force

cargo check --workspace regenerates the tonic and prost modules into Cargo build output through build.rs.

CLI

The CLI exposes version, session, command, event stream, write, and smoke commands over the same client wrapper used by tests:

cargo run -p mxgw-cli -- version --json
cargo run -p mxgw-cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- register --session-id <session-id> --client-name mxgw-rust-cli --json
cargo run -p mxgw-cli -- add-item --session-id <session-id> --server-handle 1 --item TestChildObject.TestInt --json
cargo run -p mxgw-cli -- advise --session-id <session-id> --server-handle 1 --item-handle 1 --json
cargo run -p mxgw-cli -- stream-events --session-id <session-id> --max-events 1 --json
cargo run -p mxgw-cli -- write --session-id <session-id> --server-handle 1 --item-handle 1 --value-type int32 --value 123 --json

Use --tls, --ca-file, and --server-name-override for TLS endpoints. The CLI reads the API key from --api-key or from --api-key-env, which defaults to MXGATEWAY_API_KEY. API keys are redacted by the library option and secret types.

cargo run -p mxgw-cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt --json

Library Surface

ClientOptions configures endpoint, API key, plaintext or TLS transport, timeouts, custom CA files, and server name override. GatewayClient::connect creates an authenticated tonic client and attaches authorization: Bearer <api-key> metadata to unary and streaming calls.

GatewayClient exposes raw generated calls through open_session_raw, close_session_raw, invoke_raw, stream_events, and raw_client. The session helpers keep MXAccess handles visible:

let session = client.open_session(request).await?;
let server_handle = session.register("mxgw-rust").await?;
let item_handle = session.add_item(server_handle, "TestChildObject.TestInt").await?;
session.advise(server_handle, item_handle).await?;
let mut events = session.events().await?;
session.close().await?;

MxValue, MxArrayValue, and MxStatus wrap generated protobuf messages while preserving the raw message for parity diagnostics. Command replies whose protocol status is not PROTOCOL_STATUS_CODE_OK become Error::Command and retain the raw MxCommandReply.

The session also exposes the full bulk family — add_item_bulk, advise_item_bulk, remove_item_bulk, un_advise_item_bulk, subscribe_bulk, unsubscribe_bulk, write_bulk, write2_bulk, write_secured_bulk, write_secured2_bulk, and read_bulk. Each carries a Vec of entries in one round-trip and returns one result per entry; per-entry MXAccess failures populate was_successful = false and never raise. read_bulk takes a per-tag timeout (u32 milliseconds, 0 = worker default) and returns the cached OnDataChange value when the tag is already advised (was_cached = true) without touching the existing subscription.

Galaxy Repository browse

The Galaxy Repository service exposes a read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL database). It uses the same API-key auth as the gateway service but requires the metadata:read scope on the server.

GalaxyClient wraps the generated Galaxy bindings the same way GatewayClient wraps the gateway bindings:

let mut galaxy = GalaxyClient::connect(
    ClientOptions::new("http://localhost:5000")
        .with_api_key(ApiKey::new(api_key)),
).await?;

let ok = galaxy.test_connection().await?;
let last_deploy = galaxy.get_last_deploy_time().await?; // Option<prost_types::Timestamp>
let objects = galaxy.discover_hierarchy().await?;       // Vec<GalaxyObject>

get_last_deploy_time returns None when the server reports present = false. discover_hierarchy returns the generated GalaxyObject proto type (re-exported via mxgateway_client::generated::galaxy_repository::v1) with all attributes attached.

The CLI ships matching subcommands under galaxy:

cargo run -p mxgw-cli -- galaxy test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy last-deploy-time --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy discover-hierarchy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json

Watching deploy events

watch_deploy_events opens the WatchDeployEvents server stream. The server emits a bootstrap DeployEvent describing the current cache state on subscribe, then one event each time the cached galaxy.time_of_last_deploy changes. sequence is monotonic per server start; gaps signal that the per-subscriber buffer dropped older events. Pass last_seen_deploy_time to suppress the bootstrap event when the client's cached deploy time matches the server's.

use futures_util::StreamExt;

let mut stream = galaxy.watch_deploy_events(None).await?;
while let Some(event) = stream.next().await {
    let event = event?;
    println!(
        "seq={} objects={} attributes={}",
        event.sequence, event.object_count, event.attribute_count,
    );
}
// Drop the stream to cancel the gRPC call.

The matching CLI subcommand prints one line per event (--json switches to one JSON object per event). --last-seen-deploy-time accepts an RFC3339 timestamp and is forwarded to the server. --max-events (default 0 = no cap) lets you stop after a fixed number of events; otherwise the command runs until the stream ends or Ctrl+C is pressed.

cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T15:30:00Z

Integration Checks

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

$env:MXGATEWAY_INTEGRATION = '1'
$env:MXGATEWAY_ENDPOINT = 'http://127.0.0.1:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'TestChildObject.TestInt'
cargo run -p mxgw-cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json