1use std::sync::Arc;
9use std::time::Duration;
10
11use bytes::Bytes;
12use ruststream::{
13 Broker, ConnectedBroker, DefaultPublish, DescribeServer, OutgoingMessage, PairError,
14 PublishPolicy, Publisher, RequestReply, ServerSpec, Subscribe,
15};
16use tokio::sync::{Mutex, OnceCell, mpsc};
17use zeromq::prelude::*;
18use zeromq::util::PeerIdentity;
19use zeromq::{DealerSocket, RouterSendHalf, RouterSocket, SocketOptions};
20
21use crate::common::{DriverHandle, Lifecycle, SharedLifecycle, send_with_retry};
22use crate::endpoint::ZmqEndpoint;
23use crate::error::ZmqError;
24use crate::message::ZmqMessage;
25use crate::queue::ZmqSubscriber;
26use crate::wire;
27
28const REPLY_PREFIX: &str = "zmq-reply:";
30
31fn hex_encode(bytes: &[u8]) -> String {
32 use std::fmt::Write as _;
33 let mut out = String::with_capacity(bytes.len() * 2);
34 for b in bytes {
35 let _ = write!(out, "{b:02x}");
36 }
37 out
38}
39
40fn hex_decode(text: &str) -> Option<Vec<u8>> {
41 if text.len() % 2 != 0 {
42 return None;
43 }
44 (0..text.len())
45 .step_by(2)
46 .map(|i| u8::from_str_radix(&text[i..i + 2], 16).ok())
47 .collect()
48}
49
50#[derive(Debug, Clone)]
62#[must_use]
63pub struct ZmqRpc {
64 endpoint: ZmqEndpoint,
65 cell: Arc<OnceCell<RpcShared>>,
66}
67
68#[derive(Clone)]
69pub(crate) struct RpcShared {
70 lifecycle: SharedLifecycle,
71 router_tx: Arc<OnceCell<Arc<Mutex<RouterSendHalf>>>>,
73}
74
75impl std::fmt::Debug for RpcShared {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("RpcShared").finish_non_exhaustive()
78 }
79}
80
81impl ZmqRpc {
82 pub fn new(endpoint: ZmqEndpoint) -> Self {
84 Self {
85 endpoint,
86 cell: Arc::new(OnceCell::new()),
87 }
88 }
89
90 #[must_use]
92 pub fn publisher(&self) -> ZmqRpcPublisher {
93 ZmqRpcPublisher {
94 cell: Arc::clone(&self.cell),
95 }
96 }
97}
98
99impl Broker for ZmqRpc {
100 type Error = ZmqError;
101 type Connected = ConnectedZmqRpc;
102
103 async fn connect(self) -> Result<Self::Connected, Self::Error> {
104 let shared = self
105 .cell
106 .get_or_try_init(async || {
107 self.endpoint.validate()?;
108 Ok::<_, ZmqError>(RpcShared {
109 lifecycle: Arc::new(Lifecycle::new(self.endpoint.clone())),
110 router_tx: Arc::new(OnceCell::new()),
111 })
112 })
113 .await?
114 .clone();
115 Ok(ConnectedZmqRpc {
116 shared,
117 cell: self.cell,
118 })
119 }
120}
121
122impl DescribeServer for ZmqRpc {
123 fn describe_server(&self) -> ServerSpec {
124 ServerSpec::new(self.endpoint.address(), "zeromq")
125 }
126}
127
128#[derive(Debug)]
130pub struct ConnectedZmqRpc {
131 shared: RpcShared,
132 cell: Arc<OnceCell<RpcShared>>,
133}
134
135impl ConnectedZmqRpc {
136 #[must_use]
139 pub fn bound_address(&self) -> Option<String> {
140 self.shared.lifecycle.resolved.get().cloned()
141 }
142
143 #[must_use]
145 pub fn publisher(&self) -> ZmqRpcPublisher {
146 ZmqRpcPublisher {
147 cell: Arc::clone(&self.cell),
148 }
149 }
150}
151
152impl ConnectedBroker for ConnectedZmqRpc {
153 type Error = ZmqError;
154 type Closed = ();
155
156 async fn shutdown(self) -> Result<(), Self::Error> {
157 self.shared
158 .lifecycle
159 .closed
160 .store(true, std::sync::atomic::Ordering::Release);
161 Ok(())
162 }
163}
164
165impl Subscribe for ConnectedZmqRpc {
166 type Subscriber = ZmqSubscriber;
167
168 async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
169 self.shared.lifecycle.ensure_open()?;
170 let mut socket = RouterSocket::new();
171 self.shared.lifecycle.attach_receiver(&mut socket).await?;
172 let (send_half, mut recv_half) = socket.split();
173 let _ = self.shared.router_tx.set(Arc::new(Mutex::new(send_half)));
175
176 let (tx, rx) = mpsc::unbounded_channel();
177 let task = tokio::spawn(async move {
178 loop {
179 match recv_half.recv().await {
180 Ok(message) => {
181 let mut frames = message.into_vecdeque();
182 let Some(identity) = frames.pop_front() else {
183 continue;
184 };
185 let rest: Result<zeromq::ZmqMessage, _> = frames.try_into();
186 let Ok(rest) = rest else {
187 let _ = tx.send(Err(ZmqError::Wire(
188 "a request needs name and payload frames".into(),
189 )));
190 continue;
191 };
192 let item = wire::decode(rest).map(|(name, mut headers, payload)| {
193 headers.insert(
194 "reply-to",
195 format!("{REPLY_PREFIX}{}", hex_encode(&identity)),
196 );
197 ZmqMessage {
198 name,
199 headers,
200 payload,
201 }
202 });
203 if tx.send(item).is_err() {
204 break;
205 }
206 }
207 Err(err) => {
208 if tx.send(Err(ZmqError::Receive(err.to_string()))).is_err() {
209 break;
210 }
211 }
212 }
213 }
214 });
215 Ok(ZmqSubscriber::from_parts(
216 name.to_owned(),
217 rx,
218 DriverHandle { task },
219 ))
220 }
221}
222
223#[derive(Clone)]
226pub struct ZmqRpcPublisher {
227 cell: Arc<OnceCell<RpcShared>>,
228}
229
230impl std::fmt::Debug for ZmqRpcPublisher {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 f.debug_struct("ZmqRpcPublisher").finish_non_exhaustive()
233 }
234}
235
236impl ZmqRpcPublisher {
237 fn shared(&self) -> Result<&RpcShared, ZmqError> {
238 let shared = self.cell.get().ok_or(ZmqError::NotConnected)?;
239 shared.lifecycle.ensure_open()?;
240 Ok(shared)
241 }
242}
243
244impl Publisher for ZmqRpcPublisher {
245 type Error = ZmqError;
246
247 async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
248 let shared = self.shared()?;
249 let Some(identity_hex) = msg.name().strip_prefix(REPLY_PREFIX) else {
250 return Err(ZmqError::Send {
251 name: msg.name().to_owned(),
252 reason: format!(
253 "the rpc publisher routes '{REPLY_PREFIX}...' replies; use request() for outbound requests"
254 ),
255 });
256 };
257 let identity = hex_decode(identity_hex).ok_or_else(|| ZmqError::Send {
258 name: msg.name().to_owned(),
259 reason: "malformed reply address".to_owned(),
260 })?;
261 let router = shared.router_tx.get().ok_or_else(|| ZmqError::Send {
262 name: msg.name().to_owned(),
263 reason: "no responder subscription is attached".to_owned(),
264 })?;
265
266 let mut message = wire::encode("reply", msg.headers(), msg.payload());
267 message.push_front(Bytes::from(identity));
268 let mut router = router.lock().await;
269 router.send(message).await.map_err(|e| ZmqError::Send {
270 name: msg.name().to_owned(),
271 reason: e.to_string(),
272 })
273 }
274}
275
276impl RequestReply for ZmqRpcPublisher {
277 type Reply = ZmqMessage;
278
279 async fn request(
280 &self,
281 msg: OutgoingMessage<'_>,
282 timeout: Duration,
283 ) -> Result<Self::Reply, Self::Error> {
284 let shared = self.shared()?;
285 let (address, _) = shared.lifecycle.sender_address()?;
286
287 let mut options = SocketOptions::default();
290 options.peer_identity(PeerIdentity::new());
291 options.connect_timeout(timeout.min(Duration::from_secs(10)));
292 let mut dealer = DealerSocket::with_options(options);
293 dealer
294 .connect(&address)
295 .await
296 .map_err(|e| ZmqError::Endpoint {
297 endpoint: address.clone(),
298 source: crate::error::box_err(e),
299 })?;
300
301 let correlation = msg.headers().correlation_id().map_or_else(
303 || format!("req-{}-{}", std::process::id(), hex_encode(&rand_suffix())),
304 str::to_owned,
305 );
306 let mut headers = msg.headers().clone();
307 headers.insert("correlation-id", correlation.clone());
308 let request = wire::encode(msg.name(), &headers, msg.payload());
309 send_with_retry(&mut dealer, msg.name(), request).await?;
310
311 let exchange = async {
312 loop {
313 let reply = dealer
314 .recv()
315 .await
316 .map_err(|e| ZmqError::Receive(e.to_string()))?;
317 let (name, reply_headers, payload) = wire::decode(reply)?;
318 if reply_headers.correlation_id() == Some(correlation.as_str()) {
319 return Ok(ZmqMessage {
320 name,
321 headers: reply_headers,
322 payload,
323 });
324 }
325 }
326 };
327 tokio::time::timeout(timeout, exchange)
328 .await
329 .unwrap_or(Err(ZmqError::RequestTimeout))
330 }
331}
332
333fn rand_suffix() -> [u8; 8] {
336 use std::sync::atomic::{AtomicU64, Ordering};
337 static SEQ: AtomicU64 = AtomicU64::new(0);
338 let seq = SEQ.fetch_add(1, Ordering::Relaxed);
339 seq.to_be_bytes()
340}
341
342#[derive(Debug, Clone, Copy, Default)]
353#[must_use]
354pub struct ZmqRpcPublish;
355
356impl PublishPolicy<ConnectedZmqRpc> for ZmqRpcPublish {
357 type Live = ZmqRpcPublisher;
358
359 async fn pair(self, connected: &ConnectedZmqRpc) -> Result<Self::Live, PairError> {
360 Ok(connected.publisher())
361 }
362}
363
364impl DefaultPublish for ConnectedZmqRpc {
365 type Policy = ZmqRpcPublish;
366}