Resolve Client.Rust-022..029: MalformedReply, correlation ids, clippy
Client.Rust-022 Restored Error::MalformedReply for register / add_item /
add_item2 and the bulk-subscribe / read-bulk / write-bulk
dispatch arms so malformed-but-OK replies fail loudly
instead of returning Vec::new().
Client.Rust-023 Restored next_correlation_id and routed every CLI close /
stream-alarms / acknowledge-alarm / bench-read-bulk call
through it so each call carries a unique opaque token.
Client.Rust-024 Added round-trip tests for read_bulk / write_bulk /
write2_bulk / write_secured_bulk / write_secured2_bulk
plus stream_alarms and percentile_summary unit tests.
Client.Rust-025 RustClientDesign.md re-synced — new bulk SDK, alarms
surface, Error variants, CLI command list, and the
Windows stack workaround.
Client.Rust-026 Session::read_bulk now borrows a tag slice; bench-read-
bulk binds tags once outside the warm-up / steady-state
loops.
Client.Rust-027 .cargo/config.toml selector tightened to
cfg(all(windows, target_env = "msvc")) and comment
rewritten to match reality (release + debug ship the
8 MB reservation).
Client.Rust-028 run_batch removed the empty-line break; stdin EOF is
the only terminator.
Client.Rust-029 Re-applied Client.Rust-001 / 002 / 012 — added the
missing doc comments, renamed BulkReplyKind variants,
and replaced the clone-on-copy with a deref under lock
so cargo clippy -D warnings is clean.
All resolved at 2026-05-24; cargo fmt + check + clippy + test all green
(55 tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,23 @@ Expected dependencies:
|
||||
- `clap`
|
||||
- `serde`
|
||||
- `serde_json`
|
||||
- `tracing`
|
||||
|
||||
## Windows Build Notes
|
||||
|
||||
`clients/rust/.cargo/config.toml` carries an MSVC-scoped rustflag that bumps
|
||||
the default 1 MB Windows main-thread stack to 8 MB
|
||||
(`-C link-arg=/STACK:8388608`, under `cfg(all(windows, target_env = "msvc"))`).
|
||||
The setting is required because clap-derive materialises a large `Command`
|
||||
enum (one variant per CLI subcommand, each carrying its flag args) on the
|
||||
main task's stack in debug builds, before any user code runs; the default 1
|
||||
MB stack overflows during enum construction. The `/STACK:` link-arg writes
|
||||
into the PE header's `IMAGE_OPTIONAL_HEADER.SizeOfStackReserve` at link
|
||||
time, so both debug and release artifacts ship with the same 8 MB stack
|
||||
reservation. Release builds would not overflow without it (the optimizer
|
||||
elides the enum from the stack frame), but the setting is kept on for
|
||||
release too so both build flavours produce binaries with identical stack
|
||||
metadata. The MSVC-only selector keeps `x86_64-pc-windows-gnu` (mingw)
|
||||
builds unaffected, since the GNU linker rejects `/STACK:`.
|
||||
|
||||
## Library API
|
||||
|
||||
@@ -94,18 +110,65 @@ impl Session {
|
||||
pub async fn add_item(&self, server_handle: i32, item: &str) -> Result<i32, Error>;
|
||||
pub async fn add_item2(&self, server_handle: i32, item: &str, context: &str) -> Result<i32, Error>;
|
||||
pub async fn advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error>;
|
||||
pub async fn un_advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error>;
|
||||
pub async fn remove_item(&self, server_handle: i32, item_handle: i32) -> Result<(), Error>;
|
||||
pub async fn add_item_bulk(&self, server_handle: i32, tag_addresses: Vec<String>) -> Result<Vec<SubscribeResult>, Error>;
|
||||
pub async fn advise_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
|
||||
pub async fn remove_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
|
||||
pub async fn un_advise_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
|
||||
pub async fn subscribe_bulk(&self, server_handle: i32, tag_addresses: Vec<String>) -> Result<Vec<SubscribeResult>, Error>;
|
||||
pub async fn unsubscribe_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
|
||||
pub async fn read_bulk<S: AsRef<str>>(&self, server_handle: i32, tag_addresses: &[S], timeout_ms: u32) -> Result<Vec<BulkReadResult>, Error>;
|
||||
pub async fn write(&self, server_handle: i32, item_handle: i32, value: MxValue, user_id: i32) -> Result<(), Error>;
|
||||
pub async fn write2(&self, server_handle: i32, item_handle: i32, value: MxValue, timestamp_value: MxValue, user_id: i32) -> Result<(), Error>;
|
||||
pub async fn write_bulk(&self, server_handle: i32, entries: Vec<WriteBulkEntry>) -> Result<Vec<BulkWriteResult>, Error>;
|
||||
pub async fn write2_bulk(&self, server_handle: i32, entries: Vec<Write2BulkEntry>) -> Result<Vec<BulkWriteResult>, Error>;
|
||||
pub async fn write_secured_bulk(&self, server_handle: i32, entries: Vec<WriteSecuredBulkEntry>) -> Result<Vec<BulkWriteResult>, Error>;
|
||||
pub async fn write_secured2_bulk(&self, server_handle: i32, entries: Vec<WriteSecured2BulkEntry>) -> Result<Vec<BulkWriteResult>, Error>;
|
||||
pub async fn events(&self) -> Result<impl Stream<Item = Result<MxEvent, Error>>, Error>;
|
||||
pub async fn close(&self) -> Result<(), Error>;
|
||||
}
|
||||
```
|
||||
|
||||
The per-entry credentials and timestamps (`user_id`, `timestamp_value`,
|
||||
`current_user_id`, `verifier_user_id`) live on the `WriteBulkEntry` /
|
||||
`Write2BulkEntry` / `WriteSecuredBulkEntry` / `WriteSecured2BulkEntry`
|
||||
structs rather than as trailing positional arguments on the bulk-write
|
||||
helpers, matching the protobuf shapes in `mxaccess_gateway.proto`.
|
||||
`read_bulk` is generic over `AsRef<str>` so callers can pass `&[String]` or
|
||||
`&[&str]` without cloning at the call site (the cross-language bench-read-bulk
|
||||
hot loop relies on this).
|
||||
|
||||
The `session::next_correlation_id` helper is `pub` and re-exported at the
|
||||
crate root (`zb_mom_ww_mxgateway_client::next_correlation_id`); raw-RPC
|
||||
consumers like the `mxgw` CLI's `Ping`, `CloseSession`, `StreamAlarms`,
|
||||
`AcknowledgeAlarm`, and `BenchReadBulk` paths call it so every request
|
||||
carries a unique correlation id that gateway logs can tell apart from
|
||||
concurrent CLI smokes. The textual format is intentionally not part of the
|
||||
public contract.
|
||||
|
||||
## Alarms
|
||||
|
||||
`GatewayClient` exposes the gateway's session-less central alarm surface:
|
||||
|
||||
```rust
|
||||
pub type AlarmFeedStream = Pin<Box<dyn Stream<Item = Result<AlarmFeedMessage, Error>> + Send + 'static>>;
|
||||
|
||||
impl GatewayClient {
|
||||
pub async fn stream_alarms(&self, request: StreamAlarmsRequest) -> Result<AlarmFeedStream, Error>;
|
||||
pub async fn acknowledge_alarm(&self, request: AcknowledgeAlarmRequest) -> Result<AcknowledgeAlarmReply, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
`stream_alarms` opens with one `active_alarm` per currently-active alarm
|
||||
(the ConditionRefresh snapshot), then a single `snapshot_complete`, then a
|
||||
`transition` for every subsequent raise / acknowledge / clear. The feed is
|
||||
served by the gateway's always-on alarm monitor — no worker session is
|
||||
opened — so any number of clients may attach. Dropping the stream cancels
|
||||
the gRPC call cooperatively. `acknowledge_alarm` is idempotent at the
|
||||
MxAccess layer; the returned `AcknowledgeAlarmReply` carries the native
|
||||
MxStatus from the worker.
|
||||
|
||||
## Authentication
|
||||
|
||||
Use a `tonic` interceptor or request extension layer to add:
|
||||
@@ -140,19 +203,31 @@ Use `thiserror`:
|
||||
|
||||
```rust
|
||||
pub enum Error {
|
||||
InvalidEndpoint { endpoint: String, detail: String },
|
||||
InvalidArgument { name: String, detail: String },
|
||||
Transport(tonic::transport::Error),
|
||||
Status(tonic::Status),
|
||||
Authentication(String),
|
||||
Authorization(String),
|
||||
Session(SessionError),
|
||||
Worker(WorkerError),
|
||||
Command(CommandError),
|
||||
MxAccess(MxAccessError),
|
||||
Timeout,
|
||||
Cancelled,
|
||||
Authentication { message: String, status: Box<tonic::Status> },
|
||||
Authorization { message: String, status: Box<tonic::Status> },
|
||||
Timeout { message: String, status: Box<tonic::Status> },
|
||||
Cancelled { message: String, status: Box<tonic::Status> },
|
||||
Unavailable { message: String, status: Box<tonic::Status> },
|
||||
Status(Box<tonic::Status>),
|
||||
Command(Box<CommandError>),
|
||||
ProtocolStatus { operation: &'static str, code: ProtocolStatusCode, message: String },
|
||||
MalformedReply { detail: String },
|
||||
}
|
||||
```
|
||||
|
||||
- `Unavailable` classifies `Code::Unavailable` / `Code::ResourceExhausted`
|
||||
so callers can distinguish transient failures from permanent ones.
|
||||
- `MalformedReply` surfaces the rare case where the gateway returned a
|
||||
protocol-level `Ok` envelope but the typed payload arm was missing or did
|
||||
not match the command kind (e.g. an `AddItemReply` body on a `WriteBulk`
|
||||
reply). This is distinct from `ProtocolStatus` because the protocol-level
|
||||
envelope itself succeeded; the corruption is in the payload shape.
|
||||
- `InvalidEndpoint` is raised before any RPC dispatch when the endpoint URL
|
||||
or CA file fails to parse / load.
|
||||
|
||||
Preserve raw command replies in `CommandError` where applicable.
|
||||
|
||||
## Test CLI
|
||||
@@ -165,11 +240,39 @@ Commands:
|
||||
|
||||
```text
|
||||
mxgw version
|
||||
mxgw smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt
|
||||
mxgw ping
|
||||
mxgw open-session
|
||||
mxgw close-session --session-id <id>
|
||||
mxgw register --session-id <id>
|
||||
mxgw add-item --session-id <id> --server-handle <h> --item <tag>
|
||||
mxgw advise --session-id <id> --server-handle <h> --item-handle <h>
|
||||
mxgw subscribe-bulk --session-id <id> --server-handle <h> --items <csv>
|
||||
mxgw unsubscribe-bulk --session-id <id> --server-handle <h> --item-handles <csv>
|
||||
mxgw read-bulk --session-id <id> --server-handle <h> --items <csv> [--timeout-ms <ms>]
|
||||
mxgw write --session-id <id> --server-handle 1 --item-handle 1 --value-type int32 --value 123
|
||||
mxgw write2 --session-id <id> --server-handle 1 --item-handle 1 --value-type int32 --value 123 --timestamp <iso>
|
||||
mxgw write-bulk --session-id <id> --server-handle <h> --item-handles <csv> --value-type <t> --values <csv>
|
||||
mxgw write2-bulk ...
|
||||
mxgw write-secured-bulk ...
|
||||
mxgw write-secured2-bulk ...
|
||||
mxgw stream-events --session-id <id> --json
|
||||
mxgw write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
|
||||
mxgw stream-alarms [--filter-prefix <prefix>] [--max-events <n>]
|
||||
mxgw acknowledge-alarm --reference <full-ref> [--comment <txt>] [--operator <user>]
|
||||
mxgw bench-read-bulk [--duration-seconds <n>] [--warmup-seconds <n>] [--bulk-size <n>]
|
||||
mxgw smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt
|
||||
mxgw batch
|
||||
mxgw galaxy {test-connection,last-deploy-time,discover-hierarchy,watch}
|
||||
```
|
||||
|
||||
`batch` reads commands from stdin one per line and dispatches each through
|
||||
the normal subcommand path; the loop terminates only on stdin EOF (blank
|
||||
lines log an empty-EOR-bracketed result and continue) so accidental empty
|
||||
lines from the PowerShell e2e harness do not silently end the session.
|
||||
`bench-read-bulk` opens its own session, subscribes to `--bulk-size` tags so
|
||||
the worker's value cache populates from OnDataChange events, hammers
|
||||
`read_bulk` in a tight loop for `--duration-seconds`, and emits the
|
||||
cross-language JSON shape that `scripts/bench-read-bulk.ps1` collates.
|
||||
|
||||
JSON output should use `serde_json`.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
Reference in New Issue
Block a user