5e375f6d3d
Adds five new MXAccess command kinds (WriteBulk, Write2Bulk,
WriteSecuredBulk, WriteSecured2Bulk, ReadBulk) that ride the existing
"one round-trip, per-entry results" bulk shape used by AddItemBulk and
SubscribeBulk today. MXAccess COM has no native bulk API; the worker
runs each bulk operation as a sequential loop on its STA, returning
one BulkWriteResult / BulkReadResult per requested entry so per-item
MXAccess failures surface as was_successful=false rather than throwing.
ReadBulk has no MXAccess analogue. The worker satisfies it by:
- Returning the last cached OnDataChange payload (was_cached=true)
when the requested tag is already in the session''s item registry
AND advised — the existing subscription is NOT touched, since the
caller did not create it.
- Otherwise taking the AddItem + Advise + wait-for-OnDataChange +
UnAdvise + RemoveItem snapshot lifecycle itself (was_cached=false)
and leaving the session exactly as it was. The wait pumps Windows
messages on the STA so the inbound MXAccess event can dispatch
while the executor still holds the thread.
The new MxAccessValueCache lives on each MxAccessSession, shared with
MxAccessBaseEventSink which populates it on every OnDataChange after
the event clears the outbound queue. Eviction on RemoveItem keeps
reused MXAccess handles from serving stale values from a previous
lifetime.
Gateway-side authorization wires WriteBulk/Write2Bulk to invoke:write,
WriteSecuredBulk/WriteSecured2Bulk to invoke:secure, ReadBulk to
invoke:read. The constraint-filter pipeline is refactored from a single
BulkConstraintPlan record into an abstract base plus three concretes
(SubscribeBulk, WriteBulk, ReadBulk), each owning its own denied-entry
merge so the dispatch site never branches on reply shape. A new
FilterWriteBulkAsync<TEntry> generic over the four write-entry shapes
runs CheckWriteHandleAsync per entry; denied entries surface as the
BulkWriteResult shape, preserving original-index order.
All five language clients (.NET, Go, Rust, Python, Java) gained the
five new methods following their existing bulk pattern, with regenerated
protobufs.
Tests added:
- MxAccessValueCacheTests (6 cases) — Set/TryGet, Remove resets the
version, TryWaitForUpdate signals on Set, pump step fires each poll.
- MxAccessBaseEventSinkTests — OnDataChange populates the cache,
ValueCache property exposes the bound instance.
- MxAccessCommandExecutorTests — four bulk-write variants (per-entry
success/failure, value+timestamp forwarding, secured user ids),
ReadBulk snapshot lifecycle on uncached tag (timeout surfaces as
was_successful=false), invalid-payload reply.
- GatewayGrpcScopeResolverTests — five new MxCommandKind cases.
- SessionManagerTests — WriteBulk and ReadBulk forwarding through
FakeWorkerHarness; ReadBulk forwards timeout_ms.
- Per-client (.NET, Go, Rust, Python, Java) — WriteBulk builds the
right command and returns per-entry results, ReadBulk forwards the
timeout and unpacks the was_cached flag.
Cross-language e2e CLI subcommands for the new bulks are deliberately
scoped out of this change (each of the five client CLIs would need
five new subcommands plus matching phases in
scripts/run-client-e2e-tests.ps1); coverage equivalent to the existing
bulk-subscribe coverage is provided by worker + gateway + per-client
unit tests.
Docs updated in the same commit: gateway.md (Public MXAccess Command
Surface), docs/DesignDecisions.md (new "Bulk Command Family" section
with the ReadBulk cache-then-snapshot rationale), and every client
README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
200 lines
8.0 KiB
Markdown
200 lines
8.0 KiB
Markdown
# 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
|
|
|
|
```text
|
|
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`:
|
|
|
|
```powershell
|
|
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`:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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.
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```rust
|
|
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`](src/galaxy.rs) wraps the generated Galaxy bindings the same
|
|
way [`GatewayClient`](src/client.rs) wraps the gateway bindings:
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```powershell
|
|
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`](src/galaxy.rs) 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.
|
|
|
|
```rust
|
|
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.
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
$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
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [Rust Client Detailed Design](./RustClientDesign.md)
|
|
- [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)
|