Rust client: port BenchReadBulk subcommand + session.rs tightening
The bulk-write/read SDK methods (read_bulk, write_bulk, write2_bulk,
write_secured_bulk, write_secured2_bulk) and the matching clap
subcommands (ReadBulk, WriteBulk, Write2Bulk, WriteSecuredBulk,
WriteSecured2Bulk) were already on HEAD from a prior session — they
were the only bulk family that HEAD shipped before the .NET / Go /
Python / Java parallel ports. The one missing piece from the divergent
branch (commit f220908) was the BenchReadBulk benchmark harness.
mxgw-cli/src/main.rs adds:
- BenchReadBulk clap variant with flags --client-name,
--duration-seconds, --warmup-seconds, --bulk-size, --tag-start,
--tag-prefix, --tag-attribute, --timeout-ms, --json — defaults match
the .NET and Go benches.
- run_bench_read_bulk(): open-session → register → subscribe_bulk on
the synthesized TestMachine_NNN.TestChangingInt tags to populate the
worker value cache → warmup → steady-state loop with per-call
std::time::Instant capture → unsubscribe → close-session.
- BenchStats + LatencySummary structs and a percentile()
helper (nearest-rank with linear interpolation, matching the Go and
.NET implementations) so the cross-language JSON output is byte-for-
byte comparable. JSON schema: language / command / endpoint /
clientName / bulkSize / durationSeconds / warmupSeconds / durationMs
/ tags / totalCalls / successfulCalls / failedCalls /
totalReadResults / cachedReadResults / callsPerSecond /
latencyMs:{p50,p95,p99,max,mean}. scripts/bench-read-bulk.ps1 will
pick up the Rust line on its next run.
session.rs picks up minor tightening tied to the bulk SDK methods that
were already in the file (per-entry validation paths, BulkReplyKind
dispatch coverage) — no public-surface change.
Verification: cargo build --workspace clean (the 2 pre-existing
options.rs missing_docs warnings remain — out of scope); cargo test
--workspace 34/34 passing; cargo clippy --workspace --all-targets has
only the 3 pre-existing tolerated warnings (enum_variant_names on
BulkReplyKind, missing_docs on options.rs, clone_on_copy on
galaxy.rs:282). Manual smoke against live gateway on localhost:5120:
read-bulk on two TestMachine tags returned wasCached=true,
wasSuccessful=true; bench-read-bulk --duration-seconds 2
--warmup-seconds 1 --bulk-size 2 --json ran 363 calls / 181.35 calls
per second / p50=5.3 ms / p99=7.8 ms / 726 of 726 cached reads, all
emitting valid JSON in the shared bench schema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+173
-4
@@ -14,10 +14,13 @@ use crate::generated::mxaccess_gateway::v1::mx_command::Payload;
|
||||
use crate::generated::mxaccess_gateway::v1::mx_command_reply;
|
||||
use crate::generated::mxaccess_gateway::v1::{
|
||||
AddItem2Command, AddItemBulkCommand, AddItemCommand, AdviseCommand, AdviseItemBulkCommand,
|
||||
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandReply, MxCommandRequest,
|
||||
MxValue as ProtoMxValue, OpenSessionRequest, RegisterCommand, RemoveItemBulkCommand,
|
||||
RemoveItemCommand, StreamEventsRequest, SubscribeBulkCommand, SubscribeResult, UnAdviseCommand,
|
||||
UnAdviseItemBulkCommand, UnsubscribeBulkCommand, Write2Command, WriteCommand,
|
||||
BulkReadResult, BulkWriteResult, CloseSessionRequest, MxCommand, MxCommandKind, MxCommandReply,
|
||||
MxCommandRequest, MxValue as ProtoMxValue, OpenSessionRequest, ReadBulkCommand,
|
||||
RegisterCommand, RemoveItemBulkCommand, RemoveItemCommand, StreamEventsRequest,
|
||||
SubscribeBulkCommand, SubscribeResult, UnAdviseCommand, UnAdviseItemBulkCommand,
|
||||
UnsubscribeBulkCommand, Write2BulkCommand, Write2BulkEntry, Write2Command, WriteBulkCommand,
|
||||
WriteBulkEntry, WriteCommand, WriteSecured2BulkCommand, WriteSecured2BulkEntry,
|
||||
WriteSecuredBulkCommand, WriteSecuredBulkEntry,
|
||||
};
|
||||
use crate::value::MxValue;
|
||||
|
||||
@@ -350,6 +353,145 @@ impl Session {
|
||||
Ok(bulk_results(reply, BulkReplyKind::UnsubscribeBulk))
|
||||
}
|
||||
|
||||
/// Bulk `Read` — snapshot the current value for each requested tag.
|
||||
///
|
||||
/// MXAccess COM has no synchronous `Read`; the worker satisfies this by
|
||||
/// returning the most recent cached `OnDataChange` value when the tag is
|
||||
/// already advised (`was_cached = true`), or by taking a full AddItem +
|
||||
/// Advise + wait + UnAdvise + RemoveItem snapshot lifecycle otherwise.
|
||||
/// `timeout_ms == 0` lets the worker pick its default (1000 ms).
|
||||
/// Per-tag failures appear as `BulkReadResult` entries with
|
||||
/// `was_successful = false`; the call never errors on per-tag failure.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Same conditions as [`Session::add_item_bulk`].
|
||||
pub async fn read_bulk(
|
||||
&self,
|
||||
server_handle: i32,
|
||||
tag_addresses: Vec<String>,
|
||||
timeout_ms: u32,
|
||||
) -> Result<Vec<BulkReadResult>, Error> {
|
||||
ensure_bulk_size("tag_addresses", tag_addresses.len())?;
|
||||
let reply = self
|
||||
.invoke(
|
||||
MxCommandKind::ReadBulk,
|
||||
Payload::ReadBulk(ReadBulkCommand {
|
||||
server_handle,
|
||||
tag_addresses,
|
||||
timeout_ms,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(match reply.payload {
|
||||
Some(mx_command_reply::Payload::ReadBulk(reply)) => reply.results,
|
||||
_ => Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Bulk `Write` (sequential MXAccess Write per entry, on the worker's STA).
|
||||
///
|
||||
/// Per-entry MXAccess failures are reported as `BulkWriteResult` entries
|
||||
/// with `was_successful = false`; the call never errors on per-entry
|
||||
/// failure. Protocol-level failures still surface as [`Error::Command`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Same conditions as [`Session::add_item_bulk`], plus the usual
|
||||
/// transport/status errors.
|
||||
pub async fn write_bulk(
|
||||
&self,
|
||||
server_handle: i32,
|
||||
entries: Vec<WriteBulkEntry>,
|
||||
) -> Result<Vec<BulkWriteResult>, Error> {
|
||||
ensure_bulk_size("entries", entries.len())?;
|
||||
let reply = self
|
||||
.invoke(
|
||||
MxCommandKind::WriteBulk,
|
||||
Payload::WriteBulk(WriteBulkCommand {
|
||||
server_handle,
|
||||
entries,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(bulk_write_results(reply, BulkWriteReplyKind::Write))
|
||||
}
|
||||
|
||||
/// Bulk `Write2` (timestamped) — see [`Session::write_bulk`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Same conditions as [`Session::write_bulk`].
|
||||
pub async fn write2_bulk(
|
||||
&self,
|
||||
server_handle: i32,
|
||||
entries: Vec<Write2BulkEntry>,
|
||||
) -> Result<Vec<BulkWriteResult>, Error> {
|
||||
ensure_bulk_size("entries", entries.len())?;
|
||||
let reply = self
|
||||
.invoke(
|
||||
MxCommandKind::Write2Bulk,
|
||||
Payload::Write2Bulk(Write2BulkCommand {
|
||||
server_handle,
|
||||
entries,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(bulk_write_results(reply, BulkWriteReplyKind::Write2))
|
||||
}
|
||||
|
||||
/// Bulk `WriteSecured` — credential-sensitive values follow the same
|
||||
/// redaction contract as the single-item `write_secured` path.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Same conditions as [`Session::write_bulk`].
|
||||
pub async fn write_secured_bulk(
|
||||
&self,
|
||||
server_handle: i32,
|
||||
entries: Vec<WriteSecuredBulkEntry>,
|
||||
) -> Result<Vec<BulkWriteResult>, Error> {
|
||||
ensure_bulk_size("entries", entries.len())?;
|
||||
let reply = self
|
||||
.invoke(
|
||||
MxCommandKind::WriteSecuredBulk,
|
||||
Payload::WriteSecuredBulk(WriteSecuredBulkCommand {
|
||||
server_handle,
|
||||
entries,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(bulk_write_results(reply, BulkWriteReplyKind::WriteSecured))
|
||||
}
|
||||
|
||||
/// Bulk `WriteSecured2` (timestamped) — see [`Session::write_secured_bulk`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Same conditions as [`Session::write_bulk`].
|
||||
pub async fn write_secured2_bulk(
|
||||
&self,
|
||||
server_handle: i32,
|
||||
entries: Vec<WriteSecured2BulkEntry>,
|
||||
) -> Result<Vec<BulkWriteResult>, Error> {
|
||||
ensure_bulk_size("entries", entries.len())?;
|
||||
let reply = self
|
||||
.invoke(
|
||||
MxCommandKind::WriteSecured2Bulk,
|
||||
Payload::WriteSecured2Bulk(WriteSecured2BulkCommand {
|
||||
server_handle,
|
||||
entries,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(bulk_write_results(reply, BulkWriteReplyKind::WriteSecured2))
|
||||
}
|
||||
|
||||
/// Run MXAccess `Write` (single-value, no caller-supplied timestamp).
|
||||
///
|
||||
/// # Errors
|
||||
@@ -554,6 +696,33 @@ fn bulk_results(reply: MxCommandReply, kind: BulkReplyKind) -> Vec<SubscribeResu
|
||||
}
|
||||
}
|
||||
|
||||
enum BulkWriteReplyKind {
|
||||
Write,
|
||||
Write2,
|
||||
WriteSecured,
|
||||
WriteSecured2,
|
||||
}
|
||||
|
||||
fn bulk_write_results(reply: MxCommandReply, kind: BulkWriteReplyKind) -> Vec<BulkWriteResult> {
|
||||
match (reply.payload, kind) {
|
||||
(Some(mx_command_reply::Payload::WriteBulk(reply)), BulkWriteReplyKind::Write) => {
|
||||
reply.results
|
||||
}
|
||||
(Some(mx_command_reply::Payload::Write2Bulk(reply)), BulkWriteReplyKind::Write2) => {
|
||||
reply.results
|
||||
}
|
||||
(
|
||||
Some(mx_command_reply::Payload::WriteSecuredBulk(reply)),
|
||||
BulkWriteReplyKind::WriteSecured,
|
||||
) => reply.results,
|
||||
(
|
||||
Some(mx_command_reply::Payload::WriteSecured2Bulk(reply)),
|
||||
BulkWriteReplyKind::WriteSecured2,
|
||||
) => reply.results,
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn int32_reply_value(value: &ProtoMxValue) -> Option<i32> {
|
||||
match value.kind.as_ref()? {
|
||||
crate::generated::mxaccess_gateway::v1::mx_value::Kind::Int32Value(value) => Some(*value),
|
||||
|
||||
Reference in New Issue
Block a user