dynamo_runtime/transports/event_plane/
dynamic_subscriber.rs1use anyhow::Result;
10use bytes::Bytes;
11use futures::stream::StreamExt;
12use std::collections::HashMap;
13use std::sync::Arc;
14use tokio::sync::{RwLock, mpsc};
15use tokio_util::sync::CancellationToken;
16
17use super::transport::{EventTransportRx, WireStream};
18use super::zmq_transport::ZmqSubTransport;
19use crate::config::environment_names::event_plane::DYN_ZMQ_EVENT_SUBSCRIBER_CHANNEL_CAPACITY;
20use crate::discovery::{
21 Discovery, DiscoveryEvent, DiscoveryInstance, DiscoveryInstanceId, DiscoveryQuery,
22 EventTransport,
23};
24
25pub struct DynamicSubscriber {
27 discovery: Arc<dyn Discovery>,
28 query: DiscoveryQuery,
29 topic: String,
30 cancel_token: CancellationToken,
31}
32
33impl DynamicSubscriber {
34 pub fn new(discovery: Arc<dyn Discovery>, query: DiscoveryQuery, topic: String) -> Self {
35 Self {
36 discovery,
37 query,
38 topic,
39 cancel_token: CancellationToken::new(),
40 }
41 }
42
43 pub async fn start_zmq(self: Arc<Self>) -> Result<WireStream> {
45 let channel_cap = std::env::var(DYN_ZMQ_EVENT_SUBSCRIBER_CHANNEL_CAPACITY)
54 .ok()
55 .and_then(|v| v.parse::<usize>().ok())
56 .filter(|&n| n > 0)
57 .unwrap_or(100_000);
58 let (event_tx, event_rx) = mpsc::channel::<Bytes>(channel_cap);
59
60 let active_endpoints: Arc<
62 RwLock<HashMap<DiscoveryInstanceId, (String, CancellationToken)>>,
63 > = Arc::new(RwLock::new(HashMap::new()));
64
65 let subscriber_clone = Arc::clone(&self);
67
68 let discovery = Arc::clone(&self.discovery);
70 let query = self.query.clone();
71 let zmq_topic = self.topic.clone();
73 let cancel_token = self.cancel_token.clone();
74 let endpoints = Arc::clone(&active_endpoints);
75
76 tokio::spawn(async move {
77 tracing::debug!(
78 ?query,
79 cancel_token_cancelled = cancel_token.is_cancelled(),
80 "Attempting to start discovery watch"
81 );
82
83 let mut watch_stream = match discovery.list_and_watch(query.clone(), None).await {
85 Ok(stream) => {
86 tracing::debug!("Successfully obtained discovery watch stream");
87 stream
88 }
89 Err(e) => {
90 tracing::error!(error = %e, "Failed to start discovery watch");
91 return;
92 }
93 };
94
95 tracing::info!(?query, "Started dynamic discovery watch for ZMQ publishers");
96
97 while let Some(event_result) = watch_stream.next().await {
98 tracing::debug!("Received discovery event: {:?}", event_result);
99 if cancel_token.is_cancelled() {
100 tracing::info!("Dynamic subscriber cancelled, stopping watch");
101 break;
102 }
103
104 match event_result {
105 Ok(DiscoveryEvent::Added(instance)) => {
106 tracing::info!(instance = ?instance, "Discovery Added event received");
107 let instance_id = instance.id();
108
109 if let Some(endpoint) = Self::extract_zmq_endpoint(&instance, &zmq_topic) {
111 let mut endpoints_guard = endpoints.write().await;
112
113 if endpoints_guard.contains_key(&instance_id) {
115 tracing::debug!(endpoint = %endpoint, ?instance_id, "Already connected to ZMQ publisher");
116 continue;
117 }
118
119 tracing::info!(endpoint = %endpoint, ?instance_id, "Connecting to new ZMQ publisher");
120
121 let endpoint_cancel = CancellationToken::new();
123 endpoints_guard.insert(
124 instance_id.clone(),
125 (endpoint.clone(), endpoint_cancel.clone()),
126 );
127 drop(endpoints_guard);
128
129 let event_tx_clone = event_tx.clone();
131 let zmq_topic_clone = zmq_topic.clone();
132 let endpoint_clone = endpoint.clone();
133 let endpoints_clone = Arc::clone(&endpoints);
134 let instance_id_clone = instance_id.clone();
135
136 tokio::spawn(async move {
137 if let Err(e) = Self::consume_endpoint_stream(
138 &endpoint_clone,
139 &zmq_topic_clone,
140 event_tx_clone,
141 endpoint_cancel,
142 )
143 .await
144 {
145 tracing::warn!(
146 endpoint = %endpoint_clone,
147 error = %e,
148 "Error consuming ZMQ endpoint stream"
149 );
150 }
151 endpoints_clone.write().await.remove(&instance_id_clone);
153 });
154 } else {
155 tracing::debug!(
156 instance = ?instance,
157 expected_topic = %zmq_topic,
158 "Discovery event is not a matching ZMQ publisher"
159 );
160 }
161 }
162 Ok(DiscoveryEvent::Removed(instance_id)) => {
163 let is_expected_topic = matches!(
164 &instance_id,
165 DiscoveryInstanceId::EventChannel(channel_id)
166 if channel_id.topic == zmq_topic
167 );
168 if !is_expected_topic {
169 tracing::debug!(
170 ?instance_id,
171 expected_topic = %zmq_topic,
172 "Ignoring removal for unrelated event channel"
173 );
174 continue;
175 }
176
177 tracing::info!(
178 ?instance_id,
179 "ZMQ publisher removed from discovery, cancelling endpoint stream"
180 );
181
182 if let Some((_endpoint, cancel)) =
184 endpoints.write().await.remove(&instance_id)
185 {
186 cancel.cancel();
187 tracing::info!(?instance_id, "Cancelled endpoint stream");
188 } else {
189 tracing::debug!(
190 ?instance_id,
191 "No active endpoint found for removed stream instance"
192 );
193 }
194 }
195 Err(e) => {
196 tracing::error!(error = %e, "Discovery watch error");
197 break;
198 }
199 }
200 }
201
202 let endpoints_guard = endpoints.write().await;
204 for (_id, (_endpoint, cancel)) in endpoints_guard.iter() {
205 cancel.cancel();
206 }
207 tracing::info!("Discovery watch stream ended");
208 });
209
210 let stream = async_stream::stream! {
212 let _subscriber = subscriber_clone;
214 let mut rx = event_rx;
215 while let Some(bytes) = rx.recv().await {
216 yield Ok(bytes);
217 }
218 };
219
220 Ok(Box::pin(stream))
221 }
222
223 fn extract_zmq_endpoint(instance: &DiscoveryInstance, expected_topic: &str) -> Option<String> {
225 if let DiscoveryInstance::EventChannel {
226 topic, transport, ..
227 } = instance
228 && topic == expected_topic
229 && let EventTransport::Zmq { endpoint } = transport
230 {
231 return Some(endpoint.clone());
232 }
233 None
234 }
235
236 async fn consume_endpoint_stream(
238 endpoint: &str,
239 zmq_topic: &str,
240 event_tx: mpsc::Sender<Bytes>,
241 cancel_token: CancellationToken,
242 ) -> Result<()> {
243 let sub_transport = ZmqSubTransport::connect(endpoint, zmq_topic).await?;
245 let mut stream = sub_transport.subscribe(zmq_topic).await?;
246
247 tracing::info!(endpoint = %endpoint, topic = %zmq_topic, "Started consuming ZMQ endpoint stream");
248
249 loop {
250 tokio::select! {
251 _ = cancel_token.cancelled() => {
252 tracing::info!(endpoint = %endpoint, "Endpoint stream cancelled");
253 break;
254 }
255
256 event = stream.next() => {
257 match event {
258 Some(Ok(bytes)) => {
259 match event_tx.try_send(bytes) {
260 Ok(()) => {}
261 Err(mpsc::error::TrySendError::Full(_)) => {
262 tracing::trace!(endpoint = %endpoint, "Event subscriber channel full; dropping event");
266 }
267 Err(mpsc::error::TrySendError::Closed(_)) => {
268 tracing::warn!(endpoint = %endpoint, "Event channel closed, stopping endpoint stream");
269 break;
270 }
271 }
272 }
273 Some(Err(e)) => {
274 tracing::error!(
275 endpoint = %endpoint,
276 error = %e,
277 "Error receiving from ZMQ endpoint"
278 );
279 break;
280 }
281 None => {
282 tracing::info!(endpoint = %endpoint, "ZMQ endpoint stream ended");
283 break;
284 }
285 }
286 }
287 }
288 }
289
290 Ok(())
291 }
292
293 pub fn cancel(&self) {
295 self.cancel_token.cancel();
296 }
297}
298
299impl Drop for DynamicSubscriber {
300 fn drop(&mut self) {
301 self.cancel_token.cancel();
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn event_channel(topic: &str, transport: EventTransport) -> DiscoveryInstance {
310 DiscoveryInstance::EventChannel {
311 namespace: "test-ns".to_string(),
312 component: "test-component".to_string(),
313 topic: topic.to_string(),
314 instance_id: 1,
315 transport,
316 }
317 }
318
319 #[test]
320 fn extracts_only_matching_zmq_topic() {
321 let matching = event_channel("kv-events", EventTransport::zmq("tcp://127.0.0.1:1"));
322 let wrong_topic = event_channel("kv-metrics", EventTransport::zmq("tcp://127.0.0.1:2"));
323 let wrong_transport = event_channel(
324 "kv-events",
325 EventTransport::nats("namespace.test-ns.component.test-component"),
326 );
327
328 assert_eq!(
329 DynamicSubscriber::extract_zmq_endpoint(&matching, "kv-events").as_deref(),
330 Some("tcp://127.0.0.1:1")
331 );
332 assert_eq!(
333 DynamicSubscriber::extract_zmq_endpoint(&wrong_topic, "kv-events"),
334 None
335 );
336 assert_eq!(
337 DynamicSubscriber::extract_zmq_endpoint(&wrong_transport, "kv-events"),
338 None
339 );
340 }
341}