clients/rust: SDK methods for AcknowledgeAlarm + QueryActiveAlarms (PR E.6)

Eleventh PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Mirrors PR E.2's .NET surface
on the Rust async SDK. Depends on PR E.1 (regen, merged).

- GatewayClient::acknowledge_alarm — async unary call. Uses the
  existing unary_request helper (call timeout) and routes failures
  through Error mapping; non-OK protocol status promotes to
  Error::ProtocolStatus via ensure_protocol_success.
- GatewayClient::query_active_alarms — async server-streaming call
  returning a new ActiveAlarmStream type alias (parallel to
  EventStream). Errors are pre-mapped from tonic::Status; dropping
  the stream cancels the call cooperatively.
- GATEWAY_PROTOCOL_VERSION bumped 2 → 3 to match the .NET contract.
- FakeGateway test impl extends to satisfy the new trait methods so
  client_behavior.rs builds. Two new integration tests cover the
  new SDK methods.

Tests:
- 12 unit + 10 client_behavior + 4 proto_fixtures = 26 tests, all
  pass under cargo test (Rust 1.x via existing toolchain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 17:06:13 -04:00
parent d0bc78cd43
commit fe19c478c0
3 changed files with 149 additions and 7 deletions
+60 -2
View File
@@ -16,8 +16,9 @@ use crate::auth::AuthInterceptor;
use crate::error::{ensure_command_success, ensure_protocol_success, Error};
use crate::generated::mxaccess_gateway::v1::mx_access_gateway_client::MxAccessGatewayClient;
use crate::generated::mxaccess_gateway::v1::{
CloseSessionReply, CloseSessionRequest, MxCommandReply, MxCommandRequest, MxEvent,
OpenSessionReply, OpenSessionRequest, StreamEventsRequest,
AcknowledgeAlarmReply, AcknowledgeAlarmRequest, ActiveAlarmSnapshot, CloseSessionReply,
CloseSessionRequest, MxCommandReply, MxCommandRequest, MxEvent, OpenSessionReply,
OpenSessionRequest, QueryActiveAlarmsRequest, StreamEventsRequest,
};
use crate::options::ClientOptions;
use crate::session::Session;
@@ -32,6 +33,13 @@ pub type RawGatewayClient = MxAccessGatewayClient<InterceptedService<Channel, Au
pub type EventStream =
std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<MxEvent, Error>> + Send + 'static>>;
/// Pinned, boxed [`ActiveAlarmSnapshot`] stream returned by
/// [`GatewayClient::query_active_alarms`]. Errors are pre-mapped from
/// `tonic::Status` to [`Error`]; dropping the stream cancels the call.
pub type ActiveAlarmStream = std::pin::Pin<
Box<dyn futures_core::Stream<Item = Result<ActiveAlarmSnapshot, Error>> + Send + 'static>,
>;
/// Thin async wrapper around the generated gateway client.
///
/// The wrapper is `Clone`: every clone shares the underlying tonic channel
@@ -194,6 +202,56 @@ impl GatewayClient {
Ok(Box::pin(stream))
}
/// Acknowledge an active MXAccess alarm condition through the gateway.
///
/// The gateway authenticates the request against the API key's
/// `invoke:alarm-ack` scope and forwards the acknowledge to the worker's
/// MXAccess session; the resulting native MxStatus is returned in the
/// reply. Acks are idempotent at the MxAccess layer.
///
/// # Errors
///
/// Returns [`Error::ProtocolStatus`] when the gateway accepts the call but
/// reports a non-OK protocol status, plus any of the [`Error`] variants
/// produced by transport failures.
pub async fn acknowledge_alarm(
&self,
request: AcknowledgeAlarmRequest,
) -> Result<AcknowledgeAlarmReply, Error> {
let mut client = self.inner.clone();
let response = client.acknowledge_alarm(self.unary_request(request)).await?;
let reply = response.into_inner();
ensure_protocol_success("acknowledge alarm", reply.protocol_status.as_ref())?;
Ok(reply)
}
/// Open the server-streaming `QueryActiveAlarms` RPC — the gateway's
/// ConditionRefresh equivalent.
///
/// The returned [`ActiveAlarmStream`] yields one [`ActiveAlarmSnapshot`]
/// per currently-active alarm. Dropping the stream cancels the gRPC call
/// cooperatively. Optional alarm-reference prefix scoping
/// (`request.alarm_filter_prefix`) limits the stream to a sub-tree.
///
/// # Errors
///
/// Returns the `tonic::Status` mapped through [`Error::from`] if the
/// server rejects the request.
pub async fn query_active_alarms(
&self,
request: QueryActiveAlarmsRequest,
) -> Result<ActiveAlarmStream, Error> {
let mut client = self.inner.clone();
let response = client
.query_active_alarms(self.stream_request(request))
.await?;
let stream = futures_util::StreamExt::map(response.into_inner(), |result| {
result.map_err(Error::from)
});
Ok(Box::pin(stream))
}
fn unary_request<T>(&self, message: T) -> Request<T> {
let mut request = Request::new(message);
request.set_timeout(self.call_timeout);
+1 -1
View File
@@ -7,7 +7,7 @@
pub const CLIENT_VERSION: &str = "0.1.0-dev";
/// Public gateway gRPC protocol version this client targets.
pub const GATEWAY_PROTOCOL_VERSION: u32 = 2;
pub const GATEWAY_PROTOCOL_VERSION: u32 = 3;
/// Internal worker IPC protocol version this client expects sessions to use.
pub const WORKER_PROTOCOL_VERSION: u32 = 1;