Point the Rust client at the StreamAlarms alarm feed

Replace GatewayClient::query_active_alarms with stream_alarms, an
AlarmFeedStream over AlarmFeedMessage served by the gateway's central
alarm monitor (snapshot, snapshot_complete, then live transitions).
Drops session_id from the acknowledge surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-21 16:49:26 -04:00
parent 1ad0be8276
commit 1b6ca07bb5
2 changed files with 58 additions and 40 deletions
+19 -18
View File
@@ -16,9 +16,9 @@ use crate::auth::AuthInterceptor;
use crate::error::{ensure_command_success, ensure_protocol_success, Error}; 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::mx_access_gateway_client::MxAccessGatewayClient;
use crate::generated::mxaccess_gateway::v1::{ use crate::generated::mxaccess_gateway::v1::{
AcknowledgeAlarmReply, AcknowledgeAlarmRequest, ActiveAlarmSnapshot, CloseSessionReply, AcknowledgeAlarmReply, AcknowledgeAlarmRequest, AlarmFeedMessage, CloseSessionReply,
CloseSessionRequest, MxCommandReply, MxCommandRequest, MxEvent, OpenSessionReply, CloseSessionRequest, MxCommandReply, MxCommandRequest, MxEvent, OpenSessionReply,
OpenSessionRequest, QueryActiveAlarmsRequest, StreamEventsRequest, OpenSessionRequest, StreamAlarmsRequest, StreamEventsRequest,
}; };
use crate::options::ClientOptions; use crate::options::ClientOptions;
use crate::session::Session; use crate::session::Session;
@@ -33,11 +33,11 @@ pub type RawGatewayClient = MxAccessGatewayClient<InterceptedService<Channel, Au
pub type EventStream = pub type EventStream =
std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<MxEvent, Error>> + Send + 'static>>; std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<MxEvent, Error>> + Send + 'static>>;
/// Pinned, boxed [`ActiveAlarmSnapshot`] stream returned by /// Pinned, boxed [`AlarmFeedMessage`] stream returned by
/// [`GatewayClient::query_active_alarms`]. Errors are pre-mapped from /// [`GatewayClient::stream_alarms`]. Errors are pre-mapped from
/// `tonic::Status` to [`Error`]; dropping the stream cancels the call. /// `tonic::Status` to [`Error`]; dropping the stream cancels the call.
pub type ActiveAlarmStream = std::pin::Pin< pub type AlarmFeedStream = std::pin::Pin<
Box<dyn futures_core::Stream<Item = Result<ActiveAlarmSnapshot, Error>> + Send + 'static>, Box<dyn futures_core::Stream<Item = Result<AlarmFeedMessage, Error>> + Send + 'static>,
>; >;
/// Thin async wrapper around the generated gateway client. /// Thin async wrapper around the generated gateway client.
@@ -227,26 +227,27 @@ impl GatewayClient {
Ok(reply) Ok(reply)
} }
/// Open the server-streaming `QueryActiveAlarms` RPC — the gateway's /// Attach to the gateway's central `StreamAlarms` feed.
/// ConditionRefresh equivalent.
/// ///
/// The returned [`ActiveAlarmStream`] yields one [`ActiveAlarmSnapshot`] /// The returned [`AlarmFeedStream`] opens with one [`AlarmFeedMessage`]
/// per currently-active alarm. Dropping the stream cancels the gRPC call /// per currently-active alarm (the ConditionRefresh snapshot), then a
/// cooperatively. Optional alarm-reference prefix scoping /// single `snapshot_complete`, then a `transition` for every subsequent
/// (`request.alarm_filter_prefix`) limits the stream to a sub-tree. /// raise / acknowledge / clear. It 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.
/// Optional alarm-reference prefix scoping (`request.alarm_filter_prefix`)
/// limits the stream to a sub-tree.
/// ///
/// # Errors /// # Errors
/// ///
/// Returns the `tonic::Status` mapped through [`Error::from`] if the /// Returns the `tonic::Status` mapped through [`Error::from`] if the
/// server rejects the request. /// server rejects the request.
pub async fn query_active_alarms( pub async fn stream_alarms(
&self, &self,
request: QueryActiveAlarmsRequest, request: StreamAlarmsRequest,
) -> Result<ActiveAlarmStream, Error> { ) -> Result<AlarmFeedStream, Error> {
let mut client = self.inner.clone(); let mut client = self.inner.clone();
let response = client let response = client.stream_alarms(self.stream_request(request)).await?;
.query_active_alarms(self.stream_request(request))
.await?;
let stream = futures_util::StreamExt::map(response.into_inner(), |result| { let stream = futures_util::StreamExt::map(response.into_inner(), |result| {
result.map_err(Error::from) result.map_err(Error::from)
}); });
+39 -22
View File
@@ -8,6 +8,7 @@ use std::time::Duration;
use futures_core::Stream; use futures_core::Stream;
use futures_util::StreamExt; use futures_util::StreamExt;
use mxgateway_client::generated::mxaccess_gateway::v1::alarm_feed_message;
use mxgateway_client::generated::mxaccess_gateway::v1::mx_access_gateway_server::{ use mxgateway_client::generated::mxaccess_gateway::v1::mx_access_gateway_server::{
MxAccessGateway, MxAccessGatewayServer, MxAccessGateway, MxAccessGatewayServer,
}; };
@@ -16,12 +17,12 @@ use mxgateway_client::generated::mxaccess_gateway::v1::mx_command_reply;
use mxgateway_client::generated::mxaccess_gateway::v1::mx_value::Kind; use mxgateway_client::generated::mxaccess_gateway::v1::mx_value::Kind;
use mxgateway_client::generated::mxaccess_gateway::v1::{ use mxgateway_client::generated::mxaccess_gateway::v1::{
AcknowledgeAlarmReply, AcknowledgeAlarmRequest, ActiveAlarmSnapshot, AddItemReply, AcknowledgeAlarmReply, AcknowledgeAlarmRequest, ActiveAlarmSnapshot, AddItemReply,
BulkReadReply, BulkReadResult, BulkSubscribeReply, BulkWriteReply, BulkWriteResult, AlarmFeedMessage, BulkReadReply, BulkReadResult, BulkSubscribeReply, BulkWriteReply,
CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply, MxDataType, MxEvent, BulkWriteResult, CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply,
MxEventFamily, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue, OpenSessionReply, MxDataType, MxEvent, MxEventFamily, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue,
OpenSessionRequest, ProtocolStatus, ProtocolStatusCode, QueryActiveAlarmsRequest, SessionState, OpenSessionReply, OpenSessionRequest, ProtocolStatus, ProtocolStatusCode, SessionState,
StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry, StreamAlarmsRequest, StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry,
WriteSecuredBulkEntry, WriteSecured2BulkEntry, WriteSecuredBulkEntry,
}; };
use mxgateway_client::{ use mxgateway_client::{
ApiKey, ClientOptions, CommandError, Error, GatewayClient, MxStatus, MxValue as ClientMxValue, ApiKey, ClientOptions, CommandError, Error, GatewayClient, MxStatus, MxValue as ClientMxValue,
@@ -208,7 +209,6 @@ async fn acknowledge_alarm_returns_reply_with_native_status() {
let reply = client let reply = client
.acknowledge_alarm(AcknowledgeAlarmRequest { .acknowledge_alarm(AcknowledgeAlarmRequest {
session_id: "session-fixture".to_owned(),
client_correlation_id: "corr-1".to_owned(), client_correlation_id: "corr-1".to_owned(),
alarm_full_reference: "Tank01.Level.HiHi".to_owned(), alarm_full_reference: "Tank01.Level.HiHi".to_owned(),
comment: "investigating".to_owned(), comment: "investigating".to_owned(),
@@ -225,7 +225,7 @@ async fn acknowledge_alarm_returns_reply_with_native_status() {
} }
#[tokio::test] #[tokio::test]
async fn query_active_alarms_streams_snapshot_rows() { async fn stream_alarms_streams_snapshot_then_complete() {
let state = Arc::new(FakeState::default()); let state = Arc::new(FakeState::default());
let endpoint = spawn_fake_gateway(state.clone()).await; let endpoint = spawn_fake_gateway(state.clone()).await;
let client = GatewayClient::connect(ClientOptions::new(endpoint)) let client = GatewayClient::connect(ClientOptions::new(endpoint))
@@ -233,15 +233,23 @@ async fn query_active_alarms_streams_snapshot_rows() {
.unwrap(); .unwrap();
let mut stream = client let mut stream = client
.query_active_alarms(QueryActiveAlarmsRequest { .stream_alarms(StreamAlarmsRequest::default())
session_id: "session-fixture".to_owned(),
..QueryActiveAlarmsRequest::default()
})
.await .await
.unwrap(); .unwrap();
let first = stream.next().await.unwrap().unwrap(); let first = stream.next().await.unwrap().unwrap();
assert_eq!(first.alarm_full_reference, "Tank01.Level.HiHi"); match first.payload {
Some(alarm_feed_message::Payload::ActiveAlarm(snapshot)) => {
assert_eq!(snapshot.alarm_full_reference, "Tank01.Level.HiHi");
}
other => panic!("expected an active-alarm snapshot, got {other:?}"),
}
let second = stream.next().await.unwrap().unwrap();
assert_eq!(
second.payload,
Some(alarm_feed_message::Payload::SnapshotComplete(true)),
);
} }
#[test] #[test]
@@ -907,7 +915,6 @@ impl MxAccessGateway for FakeGateway {
_request: Request<AcknowledgeAlarmRequest>, _request: Request<AcknowledgeAlarmRequest>,
) -> Result<Response<AcknowledgeAlarmReply>, Status> { ) -> Result<Response<AcknowledgeAlarmReply>, Status> {
Ok(Response::new(AcknowledgeAlarmReply { Ok(Response::new(AcknowledgeAlarmReply {
session_id: "session-fixture".to_owned(),
correlation_id: "corr-1".to_owned(), correlation_id: "corr-1".to_owned(),
protocol_status: Some(ok_status("ack ok")), protocol_status: Some(ok_status("ack ok")),
status: Some(MxStatusProxy { status: Some(MxStatusProxy {
@@ -920,18 +927,28 @@ impl MxAccessGateway for FakeGateway {
})) }))
} }
type QueryActiveAlarmsStream = type StreamAlarmsStream =
Pin<Box<dyn Stream<Item = Result<ActiveAlarmSnapshot, Status>> + Send + 'static>>; Pin<Box<dyn Stream<Item = Result<AlarmFeedMessage, Status>> + Send + 'static>>;
async fn query_active_alarms( async fn stream_alarms(
&self, &self,
_request: Request<QueryActiveAlarmsRequest>, _request: Request<StreamAlarmsRequest>,
) -> Result<Response<Self::QueryActiveAlarmsStream>, Status> { ) -> Result<Response<Self::StreamAlarmsStream>, Status> {
let (sender, receiver) = mpsc::channel(4); let (sender, receiver) = mpsc::channel(4);
sender sender
.send(Ok(ActiveAlarmSnapshot { .send(Ok(AlarmFeedMessage {
alarm_full_reference: "Tank01.Level.HiHi".to_owned(), payload: Some(alarm_feed_message::Payload::ActiveAlarm(
..ActiveAlarmSnapshot::default() ActiveAlarmSnapshot {
alarm_full_reference: "Tank01.Level.HiHi".to_owned(),
..ActiveAlarmSnapshot::default()
},
)),
}))
.await
.unwrap();
sender
.send(Ok(AlarmFeedMessage {
payload: Some(alarm_feed_message::Payload::SnapshotComplete(true)),
})) }))
.await .await
.unwrap(); .unwrap();