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:
@@ -26,8 +26,8 @@ use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||
WriteBulkEntry, WriteSecured2BulkEntry, WriteSecuredBulkEntry,
|
||||
};
|
||||
use zb_mom_ww_mxgateway_client::{
|
||||
ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue, MxValueProjection,
|
||||
CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION,
|
||||
next_correlation_id, ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue,
|
||||
MxValueProjection, CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
const MAX_AGGREGATE_EVENTS: usize = 10_000;
|
||||
@@ -359,8 +359,9 @@ enum Command {
|
||||
/// write `__MXGW_BATCH_EOR__` to stdout after every result. Errors are
|
||||
/// written as `{"error":"…","type":"error"}` JSON to stdout (not stderr)
|
||||
/// so the harness can parse them without interleaving stderr. The loop
|
||||
/// never terminates on command error; only stdin EOF (or an empty line)
|
||||
/// ends the session.
|
||||
/// never terminates on command error or accidental blank lines; only
|
||||
/// stdin EOF ends the session — empty lines log an empty-EOR-bracketed
|
||||
/// result and continue, matching the other four language CLIs.
|
||||
Batch,
|
||||
#[command(subcommand)]
|
||||
Galaxy(GalaxyCommand),
|
||||
@@ -503,7 +504,7 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
||||
let client = connect(connection).await?;
|
||||
let reply = client
|
||||
.invoke(MxCommandRequest {
|
||||
client_correlation_id: "rust-cli-ping".to_owned(),
|
||||
client_correlation_id: next_correlation_id("cli-ping"),
|
||||
command: Some(MxCommand {
|
||||
kind: MxCommandKind::Ping as i32,
|
||||
payload: Some(zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::mx_command::Payload::Ping(
|
||||
@@ -550,7 +551,7 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
||||
let reply = client
|
||||
.close_session_raw(CloseSessionRequest {
|
||||
session_id,
|
||||
client_correlation_id: "rust-cli-close-session".to_owned(),
|
||||
client_correlation_id: next_correlation_id("cli-close-session"),
|
||||
})
|
||||
.await?;
|
||||
if json {
|
||||
@@ -624,7 +625,7 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
||||
json,
|
||||
} => {
|
||||
let session = session_for(connection, session_id).await?;
|
||||
let results = session.read_bulk(server_handle, items, timeout_ms).await?;
|
||||
let results = session.read_bulk(server_handle, &items, timeout_ms).await?;
|
||||
print_read_bulk_results("read-bulk", &results, json);
|
||||
}
|
||||
Command::WriteBulk {
|
||||
@@ -832,7 +833,7 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
||||
let client = connect(connection).await?;
|
||||
let mut stream = client
|
||||
.stream_alarms(StreamAlarmsRequest {
|
||||
client_correlation_id: "rust-cli-stream-alarms".to_owned(),
|
||||
client_correlation_id: next_correlation_id("cli-stream-alarms"),
|
||||
alarm_filter_prefix: filter_prefix.unwrap_or_default(),
|
||||
})
|
||||
.await?;
|
||||
@@ -869,7 +870,7 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
||||
let client = connect(connection).await?;
|
||||
let reply = client
|
||||
.acknowledge_alarm(AcknowledgeAlarmRequest {
|
||||
client_correlation_id: "rust-cli-acknowledge-alarm".to_owned(),
|
||||
client_correlation_id: next_correlation_id("cli-acknowledge-alarm"),
|
||||
alarm_full_reference: reference,
|
||||
comment,
|
||||
operator_user: operator,
|
||||
@@ -1113,8 +1114,15 @@ const BATCH_EOR: &str = "__MXGW_BATCH_EOR__";
|
||||
/// each through the normal [`dispatch`] path, and write [`BATCH_EOR`] to
|
||||
/// stdout after every result. Errors are serialised as JSON to stdout so
|
||||
/// the harness can parse them without interleaving stderr. The loop never
|
||||
/// terminates on command error; only stdin EOF or an empty line ends the
|
||||
/// session.
|
||||
/// terminates on command error or accidental blank lines; only stdin EOF
|
||||
/// ends the session — empty lines log an empty-EOR-bracketed result and
|
||||
/// continue.
|
||||
///
|
||||
/// `std::io::Stdin::lock().lines()` is a blocking iterator and the dispatch
|
||||
/// future is spawned on a separate tokio task so the runtime's main worker
|
||||
/// stays free. When the runtime is multi-threaded the blocking read keeps
|
||||
/// one worker parked on `ReadFile`; that is acceptable here because no other
|
||||
/// future on the main task needs to run while we wait for the next command.
|
||||
async fn run_batch() -> Result<(), Error> {
|
||||
let stdin = io::stdin();
|
||||
let stdout = io::stdout();
|
||||
@@ -1125,12 +1133,11 @@ async fn run_batch() -> Result<(), Error> {
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split_ascii_whitespace().collect();
|
||||
if parts.is_empty() {
|
||||
// Empty / whitespace-only line: log an empty-EOR-bracketed
|
||||
// result and continue so accidental blank lines from the
|
||||
// PowerShell e2e harness do not silently end the session.
|
||||
println!("{BATCH_EOR}");
|
||||
stdout.lock().flush().ok();
|
||||
continue;
|
||||
@@ -1388,6 +1395,7 @@ async fn run_bench_read_bulk(
|
||||
let bench_outcome = async {
|
||||
let server_handle = session.register(&client_name).await?;
|
||||
let subscribe_results = session.subscribe_bulk(server_handle, tags.clone()).await?;
|
||||
let tags_ref: &[String] = &tags;
|
||||
let item_handles: Vec<i32> = subscribe_results
|
||||
.iter()
|
||||
.filter(|r| r.was_successful)
|
||||
@@ -1401,7 +1409,7 @@ async fn run_bench_read_bulk(
|
||||
let warmup_deadline = Instant::now() + Duration::from_secs(warmup_seconds);
|
||||
while Instant::now() < warmup_deadline {
|
||||
let _ = session
|
||||
.read_bulk(server_handle, tags.clone(), timeout_ms_param)
|
||||
.read_bulk(server_handle, tags_ref, timeout_ms_param)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1419,7 +1427,7 @@ async fn run_bench_read_bulk(
|
||||
while Instant::now() < steady_deadline {
|
||||
let call_start = Instant::now();
|
||||
let result = session
|
||||
.read_bulk(server_handle, tags.clone(), timeout_ms_param)
|
||||
.read_bulk(server_handle, tags_ref, timeout_ms_param)
|
||||
.await;
|
||||
let elapsed = call_start.elapsed();
|
||||
latencies_ms.push(elapsed.as_secs_f64() * 1000.0);
|
||||
@@ -1473,7 +1481,7 @@ async fn run_bench_read_bulk(
|
||||
let close_result = client
|
||||
.close_session_raw(CloseSessionRequest {
|
||||
session_id: session_id.clone(),
|
||||
client_correlation_id: "rust-cli-bench-read-bulk-close".to_owned(),
|
||||
client_correlation_id: next_correlation_id("cli-bench-read-bulk-close"),
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -2100,6 +2108,43 @@ mod tests {
|
||||
assert_eq!(super::BATCH_EOR, "__MXGW_BATCH_EOR__");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_percentile_summary_matches_hand_built_sample() {
|
||||
// Hand-built sample with 5 values: 1, 2, 3, 4, 5.
|
||||
let sample: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let summary = super::percentile_summary(&sample);
|
||||
|
||||
assert_eq!(summary.max, 5.0);
|
||||
// Mean = 15/5 = 3.0
|
||||
assert!((summary.mean - 3.0).abs() < f64::EPSILON);
|
||||
// p50: rank = 0.5 * 4 = 2 -> sorted[2] = 3.0
|
||||
assert!((summary.p50 - 3.0).abs() < f64::EPSILON);
|
||||
// p95: rank = 0.95 * 4 = 3.8 -> 4.0 + 0.8 * (5.0 - 4.0) = 4.8
|
||||
assert!((summary.p95 - 4.8).abs() < f64::EPSILON);
|
||||
// p99: rank = 0.99 * 4 = 3.96 -> 4.0 + 0.96 * 1.0 = 4.96
|
||||
assert!((summary.p99 - 4.96).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_percentile_summary_handles_empty_sample() {
|
||||
let summary = super::percentile_summary(&[]);
|
||||
assert_eq!(summary.p50, 0.0);
|
||||
assert_eq!(summary.p95, 0.0);
|
||||
assert_eq!(summary.p99, 0.0);
|
||||
assert_eq!(summary.max, 0.0);
|
||||
assert_eq!(summary.mean, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_percentile_summary_handles_single_value_sample() {
|
||||
let summary = super::percentile_summary(&[42.0]);
|
||||
assert_eq!(summary.p50, 42.0);
|
||||
assert_eq!(summary.p95, 42.0);
|
||||
assert_eq!(summary.p99, 42.0);
|
||||
assert_eq!(summary.max, 42.0);
|
||||
assert_eq!(summary.mean, 42.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rfc3339_parser_round_trips_z_and_offset_inputs() {
|
||||
// 2026-04-28T15:30:00Z = 1_777_995_000 (sanity-checked once below)
|
||||
|
||||
Reference in New Issue
Block a user