1mod codec;
7mod dynamic_subscriber;
8mod frame;
9mod nats_transport;
10mod traits;
11mod transport;
12pub mod zmq_transport;
13
14pub use codec::{Codec, MsgpackCodec};
15pub use dynamic_subscriber::DynamicSubscriber;
16pub use frame::{FRAME_HEADER_SIZE, FRAME_VERSION, Frame, FrameError, FrameHeader};
17pub use traits::{EventEnvelope, EventStream, TypedEventStream};
18pub use transport::{EventTransportRx, EventTransportTx, WireStream};
19pub use zmq_transport::{
20 DynamicZmqSubSocket, ValidatedEnvelope, ValidatedZmqSource, ValidatedZmqSourceError,
21 ZmqPubTransport, ZmqSubTransport, ZmqWireMessage,
22};
23
24pub use crate::discovery::{EventScope, EventTransportKind};
26
27use std::net::{IpAddr, SocketAddr};
28use std::num::NonZeroUsize;
29use std::sync::Arc;
30use std::sync::atomic::{AtomicU64, Ordering};
31use std::time::{SystemTime, UNIX_EPOCH};
32
33use anyhow::{Context as _, Result};
34use bytes::Bytes;
35use futures::{Stream, StreamExt};
36use lru::LruCache;
37use rand::TryRngCore;
38use serde::Serialize;
39use serde::de::DeserializeOwned;
40use std::pin::Pin;
41use std::task::{Context, Poll};
42
43use crate::DistributedRuntime;
44use crate::component::{Component, Endpoint, Namespace};
45use crate::config::environment_names::event_plane::DYN_EVENT_PLANE_HOST;
46use crate::discovery::{
47 Discovery, DiscoveryInstance, DiscoveryQuery, DiscoverySpec, EventChannelQuery, EventTransport,
48 MAX_JSON_SAFE_PUBLISHER_ID,
49};
50use crate::protocols::EndpointId;
51use crate::traits::DistributedRuntimeProvider;
52use crate::utils::ip_resolver::{
53 DefaultIpResolver, IpResolver, host_override_from_env, resolve_host_or_interface,
54 resolve_local_host,
55};
56
57fn event_plane_host_from_env() -> Result<IpAddr> {
58 event_plane_host_from_env_with_resolver(&DefaultIpResolver)
59}
60
61fn event_plane_host_from_env_with_resolver<R: IpResolver>(resolver: &R) -> Result<IpAddr> {
62 let Some(host) = host_override_from_env(DYN_EVENT_PLANE_HOST)? else {
63 let resolved = resolve_local_host(resolver)?;
64 if resolved.used_loopback_fallback() {
65 tracing::warn!(
66 address = %resolved.advertise_ip(),
67 "No usable non-loopback IP address found; set DYN_EVENT_PLANE_HOST to an address reachable by subscribers"
68 );
69 }
70 return Ok(resolved.advertise_ip());
71 };
72
73 resolve_host_or_interface(&host, resolver)
74 .map_err(|error| anyhow::anyhow!("Invalid {DYN_EVENT_PLANE_HOST} value '{host}': {error}"))
75 .and_then(|resolved| {
76 if resolved.bind_ip().to_canonical().is_unspecified() {
77 anyhow::bail!("Invalid {DYN_EVENT_PLANE_HOST} value '{host}': unspecified IP addresses cannot be advertised");
78 }
79 Ok(resolved.advertise_ip())
80 })
81}
82
83fn direct_zmq_public_endpoint(advertised_ip: IpAddr, actual_bind_endpoint: &str) -> Result<String> {
84 let bind_address = actual_bind_endpoint
85 .strip_prefix("tcp://")
86 .ok_or_else(|| anyhow::anyhow!("invalid ZMQ TCP bind endpoint: {actual_bind_endpoint}"))?
87 .parse::<SocketAddr>()
88 .map_err(|error| {
89 anyhow::anyhow!("invalid ZMQ TCP bind endpoint '{actual_bind_endpoint}': {error}")
90 })?;
91 Ok(format!(
92 "tcp://{}",
93 SocketAddr::new(advertised_ip, bind_address.port())
94 ))
95}
96
97#[derive(Debug, Clone)]
103struct BrokerEndpoints {
104 xsub_endpoints: Vec<String>,
105 xpub_endpoints: Vec<String>,
106}
107
108pub fn uses_direct_zmq(transport_kind: EventTransportKind) -> bool {
114 uses_direct_zmq_from_lookup(transport_kind, |key| std::env::var_os(key))
115}
116
117fn uses_direct_zmq_from_lookup(
118 transport_kind: EventTransportKind,
119 mut get_env: impl FnMut(&str) -> Option<std::ffi::OsString>,
120) -> bool {
121 if transport_kind != EventTransportKind::Zmq {
122 return false;
123 }
124
125 if get_env(crate::config::environment_names::zmq_broker::DYN_ZMQ_BROKER_URL).is_some() {
126 return false;
127 }
128
129 !get_env(crate::config::environment_names::zmq_broker::DYN_ZMQ_BROKER_ENABLED)
130 .is_some_and(|value| crate::config::is_truthy(&value.to_string_lossy()))
131}
132
133async fn resolve_zmq_broker(
136 drt: &DistributedRuntime,
137 scope: &EventScope,
138) -> Result<Option<BrokerEndpoints>> {
139 if let Ok(broker_url) =
141 std::env::var(crate::config::environment_names::zmq_broker::DYN_ZMQ_BROKER_URL)
142 {
143 let (xsub_endpoints, xpub_endpoints) = parse_broker_url(&broker_url)?;
144 tracing::info!(
145 num_xsub = xsub_endpoints.len(),
146 num_xpub = xpub_endpoints.len(),
147 "Using explicit ZMQ broker URL"
148 );
149 return Ok(Some(BrokerEndpoints {
150 xsub_endpoints,
151 xpub_endpoints,
152 }));
153 }
154
155 if crate::config::env_is_truthy(
157 crate::config::environment_names::zmq_broker::DYN_ZMQ_BROKER_ENABLED,
158 ) {
159 let query = DiscoveryQuery::EventChannels(EventChannelQuery::component(
160 scope.namespace().to_string(),
161 "zmq_broker".to_string(),
162 ));
163
164 let instances = drt.discovery().list(query).await?;
165
166 let mut xsub_endpoints = Vec::new();
168 let mut xpub_endpoints = Vec::new();
169
170 for instance in instances {
171 if let DiscoveryInstance::EventChannel { transport, .. } = instance
172 && let EventTransport::ZmqBroker {
173 xsub_endpoints: xsubs,
174 xpub_endpoints: xpubs,
175 } = transport
176 {
177 xsub_endpoints.extend(xsubs);
178 xpub_endpoints.extend(xpubs);
179 }
180 }
181
182 if xsub_endpoints.is_empty() {
183 anyhow::bail!(
184 "DYN_ZMQ_BROKER_ENABLED is set but no broker found in discovery for namespace '{}'",
185 scope.namespace()
186 );
187 }
188
189 tracing::info!(
190 num_brokers = xsub_endpoints.len(),
191 "Discovered ZMQ brokers from discovery plane"
192 );
193
194 return Ok(Some(BrokerEndpoints {
195 xsub_endpoints,
196 xpub_endpoints,
197 }));
198 }
199
200 Ok(None)
202}
203
204fn parse_broker_url(url: &str) -> Result<(Vec<String>, Vec<String>)> {
206 let parts: Vec<&str> = url.split(',').map(|s| s.trim()).collect();
207 if parts.len() != 2 {
208 anyhow::bail!(
209 "Invalid broker URL format. Expected 'xsub=<urls> , xpub=<urls>', got: {}",
210 url
211 );
212 }
213
214 let mut xsub_endpoints = Vec::new();
215 let mut xpub_endpoints = Vec::new();
216
217 for part in parts {
218 if let Some(urls_str) = part.strip_prefix("xsub=") {
219 xsub_endpoints = urls_str
220 .split(';')
221 .map(|s| s.trim().to_string())
222 .filter(|s| !s.is_empty())
223 .collect();
224 } else if let Some(urls_str) = part.strip_prefix("xpub=") {
225 xpub_endpoints = urls_str
226 .split(';')
227 .map(|s| s.trim().to_string())
228 .filter(|s| !s.is_empty())
229 .collect();
230 } else {
231 anyhow::bail!(
232 "Invalid broker URL part. Expected 'xsub=' or 'xpub=' prefix, got: {}",
233 part
234 );
235 }
236 }
237
238 if xsub_endpoints.is_empty() || xpub_endpoints.is_empty() {
239 anyhow::bail!(
240 "Broker URL must contain at least one xsub and one xpub endpoint. Got xsub={:?}, xpub={:?}",
241 xsub_endpoints,
242 xpub_endpoints
243 );
244 }
245
246 Ok((xsub_endpoints, xpub_endpoints))
247}
248
249struct DeduplicatingStream {
252 inner: WireStream,
253 codec: Arc<Codec>,
254 seen_events: LruCache<(u64, u64), ()>, }
256
257impl DeduplicatingStream {
258 fn new(inner: WireStream, codec: Arc<Codec>, cache_size: usize) -> Self {
259 Self {
260 inner,
261 codec,
262 seen_events: LruCache::new(
263 NonZeroUsize::new(cache_size).expect("cache_size must be non-zero"),
264 ),
265 }
266 }
267}
268
269impl Stream for DeduplicatingStream {
270 type Item = Result<Bytes>;
271
272 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
273 loop {
274 match Pin::new(&mut self.inner).poll_next(cx) {
275 Poll::Ready(Some(Ok(bytes))) => {
276 match self.codec.decode_envelope_identity(&bytes) {
278 Ok(key) => {
279 if self.seen_events.contains(&key) {
281 tracing::debug!(
283 publisher_id = key.0,
284 sequence = key.1,
285 "Filtered duplicate event from multi-broker setup"
286 );
287 continue;
288 }
289
290 self.seen_events.put(key, ());
292 return Poll::Ready(Some(Ok(bytes)));
293 }
294 Err(e) => {
295 tracing::warn!(error = %e, "Failed to decode envelope for deduplication");
296 return Poll::Ready(Some(Err(e)));
297 }
298 }
299 }
300 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
301 Poll::Ready(None) => return Poll::Ready(None),
302 Poll::Pending => return Poll::Pending,
303 }
304 }
305 }
306}
307
308fn discovery_safe_publisher_id(random_id: u64) -> u64 {
310 random_id & MAX_JSON_SAFE_PUBLISHER_ID
311}
312
313pub struct EventPublisher {
315 transport_kind: EventTransportKind,
316 topic: String,
317 subject: String,
318 publisher_id: u64,
319 sequence: AtomicU64,
320 tx: Arc<dyn EventTransportTx>,
321 codec: Arc<Codec>,
322 runtime_handle: tokio::runtime::Handle,
323 graceful_shutdown_tracker: Arc<crate::utils::GracefulShutdownTracker>,
325 discovery_client: Option<Arc<dyn Discovery>>,
327 discovery_instance: Option<crate::discovery::DiscoveryInstance>,
328}
329
330impl EventPublisher {
331 pub async fn for_endpoint(endpoint: &Endpoint, topic: impl Into<String>) -> Result<Self> {
333 let transport_kind = endpoint.drt().default_event_transport_kind();
334 Self::for_endpoint_with_transport(endpoint, topic, transport_kind).await
335 }
336
337 pub async fn for_endpoint_with_transport(
339 endpoint: &Endpoint,
340 topic: impl Into<String>,
341 transport_kind: EventTransportKind,
342 ) -> Result<Self> {
343 Self::new_internal(
344 endpoint.drt(),
345 EventScope::Endpoint {
346 endpoint: endpoint.id(),
347 },
348 topic.into(),
349 transport_kind,
350 )
351 .await
352 }
353
354 pub async fn for_endpoint_id(
356 drt: &DistributedRuntime,
357 endpoint: &EndpointId,
358 topic: impl Into<String>,
359 ) -> Result<Self> {
360 let transport_kind = drt.default_event_transport_kind();
361 Self::for_endpoint_id_with_transport(drt, endpoint, topic, transport_kind).await
362 }
363
364 pub async fn for_endpoint_id_with_transport(
366 drt: &DistributedRuntime,
367 endpoint: &EndpointId,
368 topic: impl Into<String>,
369 transport_kind: EventTransportKind,
370 ) -> Result<Self> {
371 Self::new_internal(
372 drt,
373 EventScope::Endpoint {
374 endpoint: endpoint.clone(),
375 },
376 topic.into(),
377 transport_kind,
378 )
379 .await
380 }
381
382 pub async fn for_component(comp: &Component, topic: impl Into<String>) -> Result<Self> {
390 let transport_kind = comp.drt().default_event_transport_kind();
391 Self::for_component_with_transport(comp, topic, transport_kind).await
392 }
393
394 pub async fn for_component_with_transport(
396 comp: &Component,
397 topic: impl Into<String>,
398 transport_kind: EventTransportKind,
399 ) -> Result<Self> {
400 let drt = comp.drt();
401 let scope = EventScope::Component {
402 namespace: comp.namespace().name(),
403 component: comp.name().to_string(),
404 };
405 Self::new_internal(drt, scope, topic.into(), transport_kind).await
406 }
407
408 pub async fn for_namespace(ns: &Namespace, topic: impl Into<String>) -> Result<Self> {
416 let transport_kind = ns.drt().default_event_transport_kind();
417 Self::for_namespace_with_transport(ns, topic, transport_kind).await
418 }
419
420 pub async fn for_namespace_with_transport(
422 ns: &Namespace,
423 topic: impl Into<String>,
424 transport_kind: EventTransportKind,
425 ) -> Result<Self> {
426 let drt = ns.drt();
427 let scope = EventScope::Namespace { name: ns.name() };
428 Self::new_internal(drt, scope, topic.into(), transport_kind).await
429 }
430
431 async fn new_internal(
432 drt: &DistributedRuntime,
433 scope: EventScope,
434 topic: String,
435 transport_kind: EventTransportKind,
436 ) -> Result<Self> {
437 let publisher_id = discovery_safe_publisher_id(
442 rand::rngs::OsRng
443 .try_next_u64()
444 .map_err(|error| anyhow::anyhow!("failed to generate publisher ID: {error}"))?,
445 );
446 let discovery = Some(drt.discovery());
447 let runtime_handle = drt.runtime().secondary();
448 let subject = scope.subject(&topic);
449 let graceful_shutdown_tracker = drt.graceful_shutdown_tracker();
450
451 enum TransportSetup {
453 Nats(Arc<dyn EventTransportTx>, Arc<Codec>),
454 ZmqDirect(Arc<dyn EventTransportTx>, Arc<Codec>, String), ZmqBroker(Arc<dyn EventTransportTx>, Arc<Codec>),
456 }
457
458 let transport_setup = match transport_kind {
459 EventTransportKind::Nats => {
460 let transport = Arc::new(nats_transport::NatsTransport::new_publisher(
461 drt.clone(),
462 subject.clone(),
463 ));
464 let codec = Arc::new(Codec::Msgpack(MsgpackCodec));
465 TransportSetup::Nats(transport as Arc<dyn EventTransportTx>, codec)
466 }
467 EventTransportKind::Zmq => {
468 if let Some(broker) = resolve_zmq_broker(drt, &scope).await? {
470 let pub_transport = if broker.xsub_endpoints.len() == 1 {
472 zmq_transport::ZmqPubTransport::connect(&broker.xsub_endpoints[0], &subject)
473 .await?
474 } else {
475 zmq_transport::ZmqPubTransport::connect_multiple(
476 &broker.xsub_endpoints,
477 &subject,
478 )
479 .await?
480 };
481
482 let codec = Arc::new(Codec::Msgpack(MsgpackCodec));
483 TransportSetup::ZmqBroker(
484 Arc::new(pub_transport) as Arc<dyn EventTransportTx>,
485 codec,
486 )
487 } else {
488 let advertised_host = event_plane_host_from_env()?;
490 let bind_endpoint = if advertised_host.is_ipv4() {
491 "tcp://0.0.0.0:0"
492 } else {
493 "tcp://[::]:0"
494 };
495 let (pub_transport, actual_bind_endpoint) = std::thread::spawn({
496 let topic = topic.clone();
497 move || -> Result<(ZmqPubTransport, String)> {
498 let rt = tokio::runtime::Builder::new_current_thread()
499 .enable_all()
500 .build()
501 .context("Failed to create Tokio runtime for ZMQ")?;
502
503 rt.block_on(ZmqPubTransport::bind(bind_endpoint, &topic))
504 .with_context(|| {
505 format!(
506 "Failed to bind direct ZMQ publisher at {bind_endpoint} for advertised address {advertised_host}; check DYN_EVENT_PLANE_HOST and host IP support"
507 )
508 })
509 }
510 })
511 .join()
512 .map_err(|_| anyhow::anyhow!("ZMQ initialization thread panicked"))??;
513
514 let public_endpoint =
515 direct_zmq_public_endpoint(advertised_host, &actual_bind_endpoint)?;
516 tracing::info!(%public_endpoint, "direct ZMQ publisher advertised endpoint");
517
518 let codec = Arc::new(Codec::Msgpack(MsgpackCodec));
519 TransportSetup::ZmqDirect(
520 Arc::new(pub_transport) as Arc<dyn EventTransportTx>,
521 codec,
522 public_endpoint,
523 )
524 }
525 }
526 };
527
528 let (tx, codec, discovery_instance) = match transport_setup {
530 TransportSetup::Nats(tx, codec) => {
531 let transport_config = EventTransport::nats(scope.subject_prefix());
532 let spec = DiscoverySpec::EventChannel {
533 scope: scope.clone(),
534 topic: topic.clone(),
535 publisher_id,
536 transport: transport_config,
537 };
538
539 let discovery_instance = drt.discovery().register(spec).await?;
540 tracing::info!(
541 topic = %topic,
542 transport = ?transport_kind,
543 publisher_id = %publisher_id,
544 "EventPublisher registered with discovery"
545 );
546 (tx, codec, Some(discovery_instance))
547 }
548 TransportSetup::ZmqDirect(tx, codec, public_endpoint) => {
549 let transport_config = EventTransport::zmq(public_endpoint);
550 let spec = DiscoverySpec::EventChannel {
551 scope: scope.clone(),
552 topic: topic.clone(),
553 publisher_id,
554 transport: transport_config,
555 };
556
557 let discovery_instance = drt.discovery().register(spec).await?;
558 tracing::info!(
559 topic = %topic,
560 transport = ?transport_kind,
561 publisher_id = %publisher_id,
562 "EventPublisher registered with discovery (direct mode)"
563 );
564 (tx, codec, Some(discovery_instance))
565 }
566 TransportSetup::ZmqBroker(tx, codec) => {
567 tracing::info!(
568 topic = %topic,
569 transport = ?transport_kind,
570 "EventPublisher in broker mode - skipping discovery registration"
571 );
572 (tx, codec, None)
573 }
574 };
575
576 Ok(Self {
577 transport_kind,
578 topic,
579 subject,
580 publisher_id,
581 sequence: AtomicU64::new(0),
582 tx,
583 codec,
584 runtime_handle,
585 graceful_shutdown_tracker,
586 discovery_client: discovery,
587 discovery_instance,
588 })
589 }
590
591 pub async fn publish<T: Serialize + Send + Sync>(&self, event: &T) -> Result<()> {
593 let payload = self.codec.encode_payload(event)?;
594 self.publish_bytes_ref(payload.as_ref()).await
595 }
596
597 pub async fn publish_bytes(&self, bytes: Vec<u8>) -> Result<()> {
599 self.publish_bytes_ref(&bytes).await
600 }
601
602 pub async fn publish_bytes_ref(&self, bytes: &[u8]) -> Result<()> {
604 let envelope_bytes = self.codec.encode_envelope_parts(
605 self.publisher_id,
606 self.sequence.fetch_add(1, Ordering::SeqCst),
607 current_timestamp_ms(),
608 &self.topic,
609 bytes,
610 )?;
611
612 self.tx.publish(&self.subject, envelope_bytes).await
613 }
614
615 pub fn publisher_id(&self) -> u64 {
617 self.publisher_id
618 }
619
620 pub fn topic(&self) -> &str {
622 &self.topic
623 }
624
625 pub fn transport_kind(&self) -> EventTransportKind {
627 self.transport_kind
628 }
629}
630
631impl Drop for EventPublisher {
632 fn drop(&mut self) {
633 if let (Some(discovery), Some(instance)) =
635 (self.discovery_client.take(), self.discovery_instance.take())
636 {
637 let topic = self.topic.clone();
638 let publisher_id = instance.instance_id();
639 let runtime_handle = self.runtime_handle.clone();
640 let shutdown_guard = self.graceful_shutdown_tracker.register_task();
641
642 let spawn_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
645 runtime_handle.spawn(async move {
646 let _shutdown_guard = shutdown_guard;
647 match discovery.unregister(instance).await {
648 Ok(()) => {
649 tracing::info!(
650 topic = %topic,
651 publisher_id = %publisher_id,
652 "EventPublisher unregistered from discovery"
653 );
654 }
655 Err(e) => {
656 tracing::warn!(
657 topic = %topic,
658 publisher_id = %publisher_id,
659 error = %e,
660 "Failed to unregister EventPublisher from discovery"
661 );
662 }
663 }
664 });
665 }));
666
667 if spawn_result.is_err() {
668 tracing::warn!(
669 topic = %self.topic,
670 publisher_id = %publisher_id,
671 "Skipping EventPublisher unregister during drop because the runtime is unavailable"
672 );
673 }
674 }
675 }
676}
677
678pub struct EventSubscriber {
680 stream: EventStream,
681 #[allow(dead_code)]
682 scope: EventScope,
683 #[allow(dead_code)]
684 topic: String,
685 codec: Arc<Codec>,
686}
687
688impl EventSubscriber {
689 pub async fn for_endpoint(endpoint: &Endpoint, topic: impl Into<String>) -> Result<Self> {
691 let transport_kind = endpoint.drt().default_event_transport_kind();
692 Self::for_endpoint_with_transport(endpoint, topic, transport_kind).await
693 }
694
695 pub async fn for_endpoint_with_transport(
697 endpoint: &Endpoint,
698 topic: impl Into<String>,
699 transport_kind: EventTransportKind,
700 ) -> Result<Self> {
701 Self::for_endpoint_id_with_transport(endpoint.drt(), &endpoint.id(), topic, transport_kind)
702 .await
703 }
704
705 pub async fn for_endpoint_id(
708 drt: &DistributedRuntime,
709 endpoint: &EndpointId,
710 topic: impl Into<String>,
711 ) -> Result<Self> {
712 let transport_kind = drt.default_event_transport_kind();
713 Self::for_endpoint_id_with_transport(drt, endpoint, topic, transport_kind).await
714 }
715
716 pub async fn for_endpoint_id_with_transport(
718 drt: &DistributedRuntime,
719 endpoint: &EndpointId,
720 topic: impl Into<String>,
721 transport_kind: EventTransportKind,
722 ) -> Result<Self> {
723 Self::new_internal(
724 drt,
725 EventScope::Endpoint {
726 endpoint: endpoint.clone(),
727 },
728 topic.into(),
729 transport_kind,
730 )
731 .await
732 }
733
734 pub async fn for_component(comp: &Component, topic: impl Into<String>) -> Result<Self> {
742 let transport_kind = comp.drt().default_event_transport_kind();
743 Self::for_component_with_transport(comp, topic, transport_kind).await
744 }
745
746 pub async fn for_component_with_transport(
748 comp: &Component,
749 topic: impl Into<String>,
750 transport_kind: EventTransportKind,
751 ) -> Result<Self> {
752 let drt = comp.drt();
753 let scope = EventScope::Component {
754 namespace: comp.namespace().name(),
755 component: comp.name().to_string(),
756 };
757 Self::new_internal(drt, scope, topic.into(), transport_kind).await
758 }
759
760 pub async fn for_namespace(ns: &Namespace, topic: impl Into<String>) -> Result<Self> {
768 let transport_kind = ns.drt().default_event_transport_kind();
769 Self::for_namespace_with_transport(ns, topic, transport_kind).await
770 }
771
772 pub async fn for_namespace_with_transport(
774 ns: &Namespace,
775 topic: impl Into<String>,
776 transport_kind: EventTransportKind,
777 ) -> Result<Self> {
778 let drt = ns.drt();
779 let scope = EventScope::Namespace { name: ns.name() };
780 Self::new_internal(drt, scope, topic.into(), transport_kind).await
781 }
782
783 async fn new_internal(
784 drt: &DistributedRuntime,
785 scope: EventScope,
786 topic: String,
787 transport_kind: EventTransportKind,
788 ) -> Result<Self> {
789 let discovery = drt.discovery();
790 let routing_key = scope.subject(&topic);
791
792 let (wire_stream, codec): (WireStream, Arc<Codec>) = match transport_kind {
794 EventTransportKind::Nats => {
795 let transport = nats_transport::NatsTransport::new(drt.clone());
796 let stream = transport.subscribe(&routing_key).await?;
797 let codec = Arc::new(Codec::Msgpack(MsgpackCodec));
798 (stream, codec)
799 }
800 EventTransportKind::Zmq => {
801 if let Some(broker) = resolve_zmq_broker(drt, &scope).await? {
803 let codec = Arc::new(Codec::Msgpack(MsgpackCodec));
805
806 let stream: WireStream = if broker.xpub_endpoints.len() == 1 {
807 let stream = zmq_transport::ZmqSubTransport::connect_single_consumer(
810 &broker.xpub_endpoints[0],
811 &routing_key,
812 )
813 .await?;
814 Box::pin(stream.map(|result| result.map(|message| message.payload)))
815 } else {
816 let inner_stream =
818 zmq_transport::ZmqSubTransport::connect_single_consumer_multiple(
819 &broker.xpub_endpoints,
820 &routing_key,
821 )
822 .await?;
823 let inner_stream: WireStream = Box::pin(
824 inner_stream.map(|result| result.map(|message| message.payload)),
825 );
826
827 Box::pin(DeduplicatingStream::new(
829 inner_stream,
830 codec.clone(),
831 100_000,
832 ))
833 };
834
835 (stream, codec)
836 } else {
837 let query = match &scope {
839 EventScope::Namespace { name } => {
840 crate::discovery::DiscoveryQuery::EventChannels(
841 crate::discovery::EventChannelQuery::namespace_topic(
842 name.clone(),
843 topic.clone(),
844 ),
845 )
846 }
847 EventScope::Component {
848 namespace,
849 component,
850 } => crate::discovery::DiscoveryQuery::EventChannels(
851 crate::discovery::EventChannelQuery::topic(
852 namespace.clone(),
853 component.clone(),
854 topic.clone(),
855 ),
856 ),
857 EventScope::Endpoint { endpoint } => {
858 crate::discovery::DiscoveryQuery::EventChannels(
859 crate::discovery::EventChannelQuery::endpoint_topic(
860 endpoint.clone(),
861 topic.clone(),
862 ),
863 )
864 }
865 };
866
867 let subscriber = Arc::new(DynamicSubscriber::with_cancel_token(
868 discovery,
869 query,
870 topic.clone(),
871 drt.primary_token().child_token(),
872 ));
873
874 let stream = subscriber.start_zmq().await?;
875 let codec = Arc::new(Codec::Msgpack(MsgpackCodec));
876 (stream, codec)
877 }
878 }
879 };
880
881 let topic_filter = topic.clone();
883 let codec_for_stream = codec.clone();
884 let stream = wire_stream.filter_map(move |result| {
885 let codec = codec_for_stream.clone();
886 let topic_filter = topic_filter.clone();
887 async move {
888 match result {
889 Ok(bytes) => match codec.decode_envelope(&bytes) {
890 Ok(envelope) => {
891 if envelope.topic == topic_filter {
893 Some(Ok(envelope))
894 } else {
895 None
896 }
897 }
898 Err(e) => Some(Err(e)),
899 },
900 Err(e) => Some(Err(e)),
901 }
902 }
903 });
904
905 tracing::info!(
906 topic = %topic,
907 transport = ?transport_kind,
908 "EventSubscriber created"
909 );
910
911 Ok(Self {
912 stream: Box::pin(stream),
913 scope,
914 topic,
915 codec,
916 })
917 }
918
919 pub async fn next(&mut self) -> Option<Result<EventEnvelope>> {
921 self.stream.next().await
922 }
923
924 pub fn typed<T: DeserializeOwned + Send + 'static>(self) -> TypedEventSubscriber<T> {
926 TypedEventSubscriber {
927 stream: self.stream,
928 codec: self.codec,
929 _marker: std::marker::PhantomData,
930 }
931 }
932}
933
934pub struct TypedEventSubscriber<T> {
936 stream: EventStream,
937 codec: Arc<Codec>,
938 _marker: std::marker::PhantomData<T>,
939}
940
941impl<T: DeserializeOwned + Send + 'static> TypedEventSubscriber<T> {
942 pub async fn next(&mut self) -> Option<Result<(EventEnvelope, T)>> {
944 std::future::poll_fn(|cx| self.poll_next(cx)).await
945 }
946
947 pub fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<(EventEnvelope, T)>>> {
949 match self.stream.as_mut().poll_next(cx) {
950 Poll::Ready(Some(envelope)) => Poll::Ready(Some(match envelope {
951 Ok(env) => match self.codec.decode_payload(&env.payload) {
952 Ok(typed) => Ok((env, typed)),
953 Err(e) => Err(e),
954 },
955 Err(e) => Err(e),
956 })),
957 Poll::Ready(None) => Poll::Ready(None),
958 Poll::Pending => Poll::Pending,
959 }
960 }
961}
962
963fn current_timestamp_ms() -> u64 {
965 SystemTime::now()
966 .duration_since(UNIX_EPOCH)
967 .map(|d| d.as_millis() as u64)
968 .unwrap_or(0)
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974 use crate::config::environment_names::zmq_broker as broker_env;
975 use crate::utils::ip_resolver::test_support::StubResolver;
976
977 #[test]
978 fn direct_zmq_automatic_host_selection() {
979 let mut resolver = StubResolver::not_found();
980 resolver.interfaces = vec![
981 ("lo", "127.0.0.1".parse().unwrap()),
982 ("lo", "::1".parse().unwrap()),
983 ("eth0", "2001:db8::20".parse().unwrap()),
984 ];
985
986 assert_eq!(
987 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, None::<&str>)], || {
988 event_plane_host_from_env_with_resolver(&resolver)
989 })
990 .unwrap(),
991 "2001:db8::20".parse::<IpAddr>().unwrap()
992 );
993 resolver
994 .interfaces
995 .push(("eth0", "192.0.2.20".parse().unwrap()));
996 assert_eq!(
997 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, None::<&str>)], || {
998 event_plane_host_from_env_with_resolver(&resolver)
999 })
1000 .unwrap(),
1001 "192.0.2.20".parse::<IpAddr>().unwrap()
1002 );
1003 }
1004
1005 #[test]
1006 fn direct_zmq_advertise_host_from_env_resolves_ips_and_interfaces() {
1007 let mut resolver = StubResolver::not_found();
1008 resolver.interfaces = vec![
1009 ("ib0", "192.0.2.20".parse().unwrap()),
1010 ("ib6", "2001:db8::20".parse().unwrap()),
1011 ];
1012
1013 assert_eq!(
1014 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, Some(" 192.0.2.10 "))], || {
1015 event_plane_host_from_env_with_resolver(&resolver)
1016 })
1017 .unwrap(),
1018 "192.0.2.10".parse::<IpAddr>().unwrap()
1019 );
1020 assert_eq!(
1021 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, Some("ib0"))], || {
1022 event_plane_host_from_env_with_resolver(&resolver)
1023 })
1024 .unwrap(),
1025 "192.0.2.20".parse::<IpAddr>().unwrap()
1026 );
1027 assert_eq!(
1028 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, Some("ib6"))], || {
1029 event_plane_host_from_env_with_resolver(&resolver)
1030 })
1031 .unwrap(),
1032 "2001:db8::20".parse::<IpAddr>().unwrap()
1033 );
1034 for host in ["2001:db8::10", "[2001:db8::10]"] {
1035 assert_eq!(
1036 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, Some(host))], || {
1037 event_plane_host_from_env_with_resolver(&resolver)
1038 })
1039 .unwrap(),
1040 "2001:db8::10".parse::<IpAddr>().unwrap()
1041 );
1042 }
1043 }
1044
1045 #[test]
1046 fn direct_zmq_advertise_host_falls_back_to_ipv6_and_rejects_wildcards() {
1047 let mut resolver = StubResolver::not_found();
1048 resolver.interfaces = vec![("eth0", "2001:db8::1".parse().unwrap())];
1049 assert_eq!(
1050 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, None::<&str>)], || {
1051 event_plane_host_from_env_with_resolver(&resolver)
1052 })
1053 .unwrap(),
1054 "2001:db8::1".parse::<IpAddr>().unwrap()
1055 );
1056 assert_eq!(
1057 temp_env::with_vars([(DYN_EVENT_PLANE_HOST, Some(" \t"))], || {
1058 event_plane_host_from_env_with_resolver(&resolver)
1059 })
1060 .unwrap(),
1061 "2001:db8::1".parse::<IpAddr>().unwrap()
1062 );
1063
1064 for host in [
1065 "0.0.0.0",
1066 "::",
1067 "[::]",
1068 "::ffff:0.0.0.0",
1069 "[::ffff:0.0.0.0]",
1070 ] {
1071 let error = temp_env::with_vars([(DYN_EVENT_PLANE_HOST, Some(host))], || {
1072 event_plane_host_from_env_with_resolver(&resolver)
1073 })
1074 .unwrap_err();
1075 assert_eq!(
1076 error.to_string(),
1077 format!(
1078 "Invalid DYN_EVENT_PLANE_HOST value '{host}': unspecified IP addresses cannot be advertised"
1079 )
1080 );
1081 }
1082 }
1083
1084 #[test]
1085 fn direct_zmq_public_endpoint_formats_ipv4_and_ipv6() {
1086 assert_eq!(
1087 direct_zmq_public_endpoint("192.0.2.10".parse().unwrap(), "tcp://0.0.0.0:4321")
1088 .unwrap(),
1089 "tcp://192.0.2.10:4321"
1090 );
1091 assert_eq!(
1092 direct_zmq_public_endpoint("2001:db8::10".parse().unwrap(), "tcp://[::]:4321").unwrap(),
1093 "tcp://[2001:db8::10]:4321"
1094 );
1095 }
1096
1097 #[rstest::rstest]
1098 #[case("127.0.0.1")]
1099 #[case("[::1]")]
1100 #[tokio::test]
1101 async fn direct_zmq_publisher_serves_advertised_endpoint(#[case] host: &str) {
1102 if host == "[::1]"
1103 && let Err(error) = std::net::TcpListener::bind("[::1]:0")
1104 {
1105 eprintln!("Skipping IPv6 publisher test: {error}");
1106 return;
1107 }
1108 temp_env::async_with_vars(
1109 [
1110 (DYN_EVENT_PLANE_HOST, Some(host)),
1111 (broker_env::DYN_ZMQ_BROKER_URL, None::<&str>),
1112 (broker_env::DYN_ZMQ_BROKER_ENABLED, None::<&str>),
1113 ],
1114 async {
1115 let runtime = crate::Runtime::from_current().expect("create runtime handle");
1116 let drt = DistributedRuntime::new(
1117 runtime,
1118 crate::distributed::DistributedConfig::process_local(),
1119 )
1120 .await
1121 .expect("create distributed runtime");
1122 let component = drt
1123 .namespace("event-publisher-host-test")
1124 .expect("create namespace")
1125 .component("worker")
1126 .expect("create component");
1127
1128 let publisher = EventPublisher::for_component_with_transport(
1129 &component,
1130 "events",
1131 EventTransportKind::Zmq,
1132 )
1133 .await
1134 .expect("create publisher");
1135
1136 let instances = drt
1137 .discovery()
1138 .list(DiscoveryQuery::EventChannels(EventChannelQuery::topic(
1139 "event-publisher-host-test",
1140 "worker",
1141 "events",
1142 )))
1143 .await
1144 .expect("list event publishers");
1145 assert_eq!(instances.len(), 1);
1146
1147 let endpoint = match &instances[0] {
1148 DiscoveryInstance::EventChannel {
1149 transport: EventTransport::Zmq { endpoint },
1150 ..
1151 } => endpoint,
1152 instance => panic!("expected direct ZMQ event channel, got {instance:?}"),
1153 };
1154 let address = endpoint
1155 .strip_prefix("tcp://")
1156 .unwrap()
1157 .parse::<SocketAddr>()
1158 .expect("advertised endpoint should include an IP address and port");
1159 assert_eq!(
1160 address.ip(),
1161 host.trim_matches(['[', ']']).parse::<IpAddr>().unwrap()
1162 );
1163 assert_ne!(address.port(), 0);
1164
1165 let mut subscriber = EventSubscriber::for_component_with_transport(
1166 &component,
1167 "events",
1168 EventTransportKind::Zmq,
1169 )
1170 .await
1171 .expect("subscribe through discovery");
1172
1173 let receive = async {
1174 loop {
1175 publisher
1176 .publish_bytes(vec![0x42])
1177 .await
1178 .expect("publish event");
1179 if let Ok(event) = tokio::time::timeout(
1181 std::time::Duration::from_millis(100),
1182 subscriber.next(),
1183 )
1184 .await
1185 {
1186 let event =
1187 event.expect("event stream is open").expect("receive event");
1188 assert_eq!(event.publisher_id, publisher.publisher_id());
1189 assert_eq!(event.payload.as_ref(), &[0x42]);
1190 break;
1191 }
1192 }
1193 };
1194 tokio::time::timeout(std::time::Duration::from_secs(5), receive)
1195 .await
1196 .expect("subscriber should receive through the advertised address and port");
1197
1198 drop(publisher);
1199 },
1200 )
1201 .await;
1202 }
1203
1204 #[tokio::test]
1205 async fn direct_zmq_publisher_rejects_invalid_configured_host() {
1206 temp_env::async_with_vars(
1207 [
1208 (DYN_EVENT_PLANE_HOST, Some("::")),
1209 (broker_env::DYN_ZMQ_BROKER_URL, None::<&str>),
1210 (broker_env::DYN_ZMQ_BROKER_ENABLED, None::<&str>),
1211 ],
1212 async {
1213 let runtime = crate::Runtime::from_current().expect("create runtime handle");
1214 let drt = DistributedRuntime::new(
1215 runtime,
1216 crate::distributed::DistributedConfig::process_local(),
1217 )
1218 .await
1219 .expect("create distributed runtime");
1220 let component = drt
1221 .namespace("event-publisher-invalid-host-test")
1222 .expect("create namespace")
1223 .component("worker")
1224 .expect("create component");
1225
1226 match EventPublisher::for_component_with_transport(
1227 &component,
1228 "events",
1229 EventTransportKind::Zmq,
1230 )
1231 .await
1232 {
1233 Ok(_) => panic!("invalid host should reject publisher creation"),
1234 Err(error) => assert_eq!(
1235 error.to_string(),
1236 "Invalid DYN_EVENT_PLANE_HOST value '::': unspecified IP addresses cannot be advertised"
1237 ),
1238 }
1239 },
1240 )
1241 .await;
1242 }
1243
1244 #[tokio::test]
1245 async fn multi_broker_stream_deduplicates_by_publisher_and_sequence() {
1246 let codec = Arc::new(Codec::default());
1247 let first = codec
1248 .encode_envelope_parts(7, 11, 1, "events", b"first")
1249 .unwrap();
1250 let second = codec
1251 .encode_envelope_parts(7, 12, 2, "events", b"second")
1252 .unwrap();
1253 let other_publisher = codec
1254 .encode_envelope_parts(8, 11, 3, "events", b"other")
1255 .unwrap();
1256 let expected_first = first.clone();
1257 let expected_second = second.clone();
1258 let expected_other = other_publisher.clone();
1259 let inner: WireStream = Box::pin(futures::stream::iter(vec![
1260 Ok(first.clone()),
1261 Ok(first),
1262 Ok(second),
1263 Ok(other_publisher),
1264 ]));
1265 let mut stream = DeduplicatingStream::new(inner, codec, 16);
1266
1267 assert_eq!(stream.next().await.unwrap().unwrap(), expected_first);
1268 assert_eq!(stream.next().await.unwrap().unwrap(), expected_second);
1269 assert_eq!(stream.next().await.unwrap().unwrap(), expected_other);
1270 assert!(stream.next().await.is_none());
1271 }
1272
1273 #[test]
1274 fn publisher_ids_survive_a_json_number_round_trip() {
1275 let unsafe_id: u64 = 13_584_172_880_116_487_724;
1278 assert!(unsafe_id > MAX_JSON_SAFE_PUBLISHER_ID);
1279 assert_ne!(unsafe_id as f64 as u64, unsafe_id);
1280
1281 for random_id in [0, 1, u64::MAX, unsafe_id, 6_633_287_539_119_378] {
1282 let publisher_id = discovery_safe_publisher_id(random_id);
1283 assert!(
1284 publisher_id <= MAX_JSON_SAFE_PUBLISHER_ID,
1285 "publisher ID {publisher_id} exceeds the JSON-safe integer range"
1286 );
1287 assert_eq!(
1288 publisher_id as f64 as u64, publisher_id,
1289 "publisher ID {publisher_id} must survive an f64 round trip"
1290 );
1291 }
1292 }
1293
1294 #[test]
1295 fn direct_zmq_topology_selection_is_narrow() {
1296 let lookup = |url: Option<&str>, enabled: Option<&str>| {
1297 let url = url.map(std::ffi::OsString::from);
1298 let enabled = enabled.map(std::ffi::OsString::from);
1299 move |key: &str| match key {
1300 broker_env::DYN_ZMQ_BROKER_URL => url.clone(),
1301 broker_env::DYN_ZMQ_BROKER_ENABLED => enabled.clone(),
1302 _ => None,
1303 }
1304 };
1305
1306 assert!(uses_direct_zmq_from_lookup(
1307 EventTransportKind::Zmq,
1308 lookup(None, None)
1309 ));
1310 assert!(!uses_direct_zmq_from_lookup(
1311 EventTransportKind::Zmq,
1312 lookup(Some("xsub=tcp://broker:5555,xpub=tcp://broker:5556"), None)
1313 ));
1314 assert!(!uses_direct_zmq_from_lookup(
1315 EventTransportKind::Zmq,
1316 lookup(None, Some("true"))
1317 ));
1318 assert!(uses_direct_zmq_from_lookup(
1319 EventTransportKind::Zmq,
1320 lookup(None, Some("false"))
1321 ));
1322 assert!(!uses_direct_zmq_from_lookup(
1323 EventTransportKind::Nats,
1324 lookup(None, None)
1325 ));
1326 }
1327
1328 #[tokio::test]
1329 async fn direct_zmq_endpoint_scopes_are_isolated() {
1330 temp_env::async_with_vars(
1331 [
1332 (DYN_EVENT_PLANE_HOST, None::<&str>),
1333 (broker_env::DYN_ZMQ_BROKER_URL, None::<&str>),
1334 (broker_env::DYN_ZMQ_BROKER_ENABLED, None::<&str>),
1335 ],
1336 async {
1337 let runtime = crate::Runtime::from_current().expect("create runtime handle");
1338 let drt = DistributedRuntime::new(
1339 runtime,
1340 crate::distributed::DistributedConfig::process_local(),
1341 )
1342 .await
1343 .expect("create distributed runtime");
1344 let component = drt
1345 .namespace("endpoint-event-isolation-test")
1346 .expect("create namespace")
1347 .component("worker")
1348 .expect("create component");
1349 let endpoint_a = component.endpoint("a");
1350 let endpoint_b = component.endpoint("b");
1351
1352 let publisher_a = EventPublisher::for_endpoint_with_transport(
1353 &endpoint_a,
1354 "events",
1355 EventTransportKind::Zmq,
1356 )
1357 .await
1358 .expect("create endpoint A publisher");
1359 let publisher_b = EventPublisher::for_endpoint_with_transport(
1360 &endpoint_b,
1361 "events",
1362 EventTransportKind::Zmq,
1363 )
1364 .await
1365 .expect("create endpoint B publisher");
1366 let mut subscriber_a = EventSubscriber::for_endpoint_with_transport(
1367 &endpoint_a,
1368 "events",
1369 EventTransportKind::Zmq,
1370 )
1371 .await
1372 .expect("create endpoint A subscriber");
1373 let mut subscriber_b = EventSubscriber::for_endpoint_with_transport(
1374 &endpoint_b,
1375 "events",
1376 EventTransportKind::Zmq,
1377 )
1378 .await
1379 .expect("create endpoint B subscriber");
1380
1381 let receive = async {
1382 loop {
1383 publisher_a
1384 .publish_bytes(vec![0xa1])
1385 .await
1386 .expect("publish endpoint A event");
1387 publisher_b
1388 .publish_bytes(vec![0xb2])
1389 .await
1390 .expect("publish endpoint B event");
1391
1392 let a = tokio::time::timeout(
1393 std::time::Duration::from_millis(100),
1394 subscriber_a.next(),
1395 )
1396 .await;
1397 let b = tokio::time::timeout(
1398 std::time::Duration::from_millis(100),
1399 subscriber_b.next(),
1400 )
1401 .await;
1402 if let (Ok(Some(Ok(a))), Ok(Some(Ok(b)))) = (a, b) {
1403 assert_eq!(a.publisher_id, publisher_a.publisher_id());
1404 assert_eq!(a.payload.as_ref(), &[0xa1]);
1405 assert_eq!(b.publisher_id, publisher_b.publisher_id());
1406 assert_eq!(b.payload.as_ref(), &[0xb2]);
1407 break;
1408 }
1409
1410 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1411 }
1412 };
1413
1414 tokio::time::timeout(std::time::Duration::from_secs(5), receive)
1415 .await
1416 .expect("each endpoint subscriber should receive only its own publisher");
1417 },
1418 )
1419 .await;
1420 }
1421
1422 #[tokio::test]
1423 async fn direct_zmq_publishers_in_one_endpoint_fan_into_one_subscriber() {
1424 temp_env::async_with_vars(
1425 [
1426 (DYN_EVENT_PLANE_HOST, None::<&str>),
1427 (broker_env::DYN_ZMQ_BROKER_URL, None::<&str>),
1428 (broker_env::DYN_ZMQ_BROKER_ENABLED, None::<&str>),
1429 ],
1430 async {
1431 let runtime = crate::Runtime::from_current().expect("create runtime handle");
1432 let drt = DistributedRuntime::new(
1433 runtime,
1434 crate::distributed::DistributedConfig::process_local(),
1435 )
1436 .await
1437 .expect("create distributed runtime");
1438 let endpoint = drt
1439 .namespace("endpoint-event-fan-in-test")
1440 .expect("create namespace")
1441 .component("worker")
1442 .expect("create component")
1443 .endpoint("generate");
1444 let publisher_a = EventPublisher::for_endpoint_with_transport(
1445 &endpoint,
1446 "events",
1447 EventTransportKind::Zmq,
1448 )
1449 .await
1450 .expect("create first publisher");
1451 let publisher_b = EventPublisher::for_endpoint_with_transport(
1452 &endpoint,
1453 "events",
1454 EventTransportKind::Zmq,
1455 )
1456 .await
1457 .expect("create second publisher");
1458 let mut subscriber = EventSubscriber::for_endpoint_with_transport(
1459 &endpoint,
1460 "events",
1461 EventTransportKind::Zmq,
1462 )
1463 .await
1464 .expect("create subscriber");
1465
1466 let receive = async {
1467 let mut publisher_ids = std::collections::HashSet::new();
1468 while publisher_ids.len() < 2 {
1469 publisher_a.publish_bytes(vec![0xa1]).await.unwrap();
1470 publisher_b.publish_bytes(vec![0xb2]).await.unwrap();
1471 if let Ok(Some(Ok(envelope))) = tokio::time::timeout(
1472 std::time::Duration::from_millis(100),
1473 subscriber.next(),
1474 )
1475 .await
1476 {
1477 publisher_ids.insert(envelope.publisher_id);
1478 }
1479 }
1480 assert_eq!(
1481 publisher_ids,
1482 std::collections::HashSet::from([
1483 publisher_a.publisher_id(),
1484 publisher_b.publisher_id(),
1485 ])
1486 );
1487 for publisher_id in [publisher_a.publisher_id(), publisher_b.publisher_id()] {
1488 assert!(
1489 publisher_id <= MAX_JSON_SAFE_PUBLISHER_ID,
1490 "publisher ID {publisher_id} exceeds the JSON-safe integer range"
1491 );
1492 }
1493 };
1494 tokio::time::timeout(std::time::Duration::from_secs(5), receive)
1495 .await
1496 .expect("subscriber should receive both endpoint publishers");
1497 },
1498 )
1499 .await;
1500 }
1501
1502 #[tokio::test]
1503 async fn same_topic_publishers_are_independent_across_recreation() {
1504 temp_env::async_with_vars(
1505 [
1506 (DYN_EVENT_PLANE_HOST, None::<&str>),
1507 (broker_env::DYN_ZMQ_BROKER_URL, None::<&str>),
1508 (broker_env::DYN_ZMQ_BROKER_ENABLED, None::<&str>),
1509 ],
1510 async {
1511 let runtime = crate::Runtime::from_current().expect("create runtime handle");
1512 let drt = DistributedRuntime::new(
1513 runtime,
1514 crate::distributed::DistributedConfig::process_local(),
1515 )
1516 .await
1517 .expect("create distributed runtime");
1518 let component = drt
1519 .namespace("event-publisher-test")
1520 .expect("create namespace")
1521 .component("worker")
1522 .expect("create component");
1523
1524 let publisher_a = EventPublisher::for_component_with_transport(
1525 &component,
1526 "events",
1527 EventTransportKind::Zmq,
1528 )
1529 .await
1530 .expect("create first publisher");
1531 let publisher_b = EventPublisher::for_component_with_transport(
1532 &component,
1533 "events",
1534 EventTransportKind::Zmq,
1535 )
1536 .await
1537 .expect("create second publisher");
1538 let publisher_a_id = publisher_a.publisher_id();
1539 let publisher_b_id = publisher_b.publisher_id();
1540
1541 assert_ne!(publisher_a_id, publisher_b_id);
1542
1543 let query = DiscoveryQuery::EventChannels(EventChannelQuery::topic(
1544 "event-publisher-test",
1545 "worker",
1546 "events",
1547 ));
1548 let instances = drt
1549 .discovery()
1550 .list(query.clone())
1551 .await
1552 .expect("list event publishers");
1553 assert_eq!(instances.len(), 2);
1554 assert!(
1555 instances
1556 .iter()
1557 .any(|instance| instance.instance_id() == publisher_a_id)
1558 );
1559 assert!(
1560 instances
1561 .iter()
1562 .any(|instance| instance.instance_id() == publisher_b_id)
1563 );
1564
1565 let mut subscriber = EventSubscriber::for_component_with_transport(
1566 &component,
1567 "events",
1568 EventTransportKind::Zmq,
1569 )
1570 .await
1571 .expect("create subscriber");
1572 let mut received_a = false;
1573 let mut received_b = false;
1574
1575 tokio::time::timeout(std::time::Duration::from_secs(5), async {
1576 while !received_a || !received_b {
1577 publisher_a
1578 .publish_bytes(vec![0xa1])
1579 .await
1580 .expect("publish from first publisher");
1581 publisher_b
1582 .publish_bytes(vec![0xb2])
1583 .await
1584 .expect("publish from second publisher");
1585
1586 if let Ok(Some(envelope)) = tokio::time::timeout(
1587 std::time::Duration::from_millis(100),
1588 subscriber.next(),
1589 )
1590 .await
1591 {
1592 let envelope = envelope.expect("receive event envelope");
1593 if envelope.publisher_id == publisher_a_id {
1594 assert_eq!(envelope.payload.as_ref(), &[0xa1]);
1595 received_a = true;
1596 } else if envelope.publisher_id == publisher_b_id {
1597 assert_eq!(envelope.payload.as_ref(), &[0xb2]);
1598 received_b = true;
1599 } else {
1600 panic!("event from unexpected publisher");
1601 }
1602 }
1603
1604 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1605 }
1606 })
1607 .await
1608 .expect("subscriber should receive events from both publishers");
1609
1610 drop(publisher_a);
1611 let publisher_a_recreated = EventPublisher::for_component_with_transport(
1612 &component,
1613 "events",
1614 EventTransportKind::Zmq,
1615 )
1616 .await
1617 .expect("recreate first publisher");
1618 let publisher_a_recreated_id = publisher_a_recreated.publisher_id();
1619
1620 assert_ne!(publisher_a_recreated_id, publisher_a_id);
1621 assert_ne!(publisher_a_recreated_id, publisher_b_id);
1622 assert_eq!(
1623 publisher_a_recreated.sequence.load(Ordering::SeqCst),
1624 0,
1625 "a recreated publisher starts a new sequence space"
1626 );
1627
1628 tokio::time::timeout(std::time::Duration::from_secs(1), async {
1629 loop {
1630 let instances = drt
1631 .discovery()
1632 .list(query.clone())
1633 .await
1634 .expect("list event publishers after recreation");
1635 if instances.len() == 2
1636 && instances
1637 .iter()
1638 .any(|instance| instance.instance_id() == publisher_b_id)
1639 && instances
1640 .iter()
1641 .any(|instance| instance.instance_id() == publisher_a_recreated_id)
1642 {
1643 break;
1644 }
1645 tokio::task::yield_now().await;
1646 }
1647 })
1648 .await
1649 .expect("old publisher should unregister without removing current publishers");
1650
1651 let mut received_b_after_recreation = false;
1652 let mut received_recreated_a = false;
1653
1654 tokio::time::timeout(std::time::Duration::from_secs(5), async {
1655 while !received_b_after_recreation || !received_recreated_a {
1656 publisher_b
1657 .publish_bytes(vec![0xb3])
1658 .await
1659 .expect("publish from second publisher after recreation");
1660 publisher_a_recreated
1661 .publish_bytes(vec![0xa2])
1662 .await
1663 .expect("publish from recreated publisher");
1664
1665 if let Ok(Some(envelope)) = tokio::time::timeout(
1666 std::time::Duration::from_millis(100),
1667 subscriber.next(),
1668 )
1669 .await
1670 {
1671 let envelope = envelope.expect("receive event envelope after drop");
1672 if envelope.publisher_id == publisher_b_id
1673 && envelope.payload.as_ref() == [0xb3]
1674 {
1675 received_b_after_recreation = true;
1676 } else if envelope.publisher_id == publisher_a_recreated_id
1677 && envelope.payload.as_ref() == [0xa2]
1678 {
1679 received_recreated_a = true;
1680 }
1681 }
1682
1683 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1684 }
1685 })
1686 .await
1687 .expect("subscriber should receive from surviving and recreated publishers");
1688 },
1689 )
1690 .await;
1691 }
1692
1693 #[tokio::test]
1694 async fn dropped_publisher_unregister_completes_within_graceful_shutdown() {
1695 temp_env::async_with_vars(
1696 [
1697 (DYN_EVENT_PLANE_HOST, None::<&str>),
1698 (broker_env::DYN_ZMQ_BROKER_URL, None::<&str>),
1699 (broker_env::DYN_ZMQ_BROKER_ENABLED, None::<&str>),
1700 ],
1701 async {
1702 let runtime = crate::Runtime::from_current().expect("create runtime handle");
1703 let drt = DistributedRuntime::new(
1704 runtime,
1705 crate::distributed::DistributedConfig::process_local(),
1706 )
1707 .await
1708 .expect("create distributed runtime");
1709 let component = drt
1710 .namespace("event-publisher-shutdown-test")
1711 .expect("create namespace")
1712 .component("worker")
1713 .expect("create component");
1714
1715 let publisher = EventPublisher::for_component_with_transport(
1716 &component,
1717 "events",
1718 EventTransportKind::Zmq,
1719 )
1720 .await
1721 .expect("create publisher");
1722 let publisher_id = publisher.publisher_id();
1723
1724 let query = DiscoveryQuery::EventChannels(EventChannelQuery::topic(
1725 "event-publisher-shutdown-test",
1726 "worker",
1727 "events",
1728 ));
1729 let instances = drt
1730 .discovery()
1731 .list(query.clone())
1732 .await
1733 .expect("list event publishers");
1734 assert_eq!(instances.len(), 1);
1735 assert_eq!(instances[0].instance_id(), publisher_id);
1736
1737 let tracker = drt.graceful_shutdown_tracker();
1738 assert_eq!(tracker.get_count(), 0);
1739
1740 let main_token = drt.runtime().primary_token();
1741 let endpoint_token = drt.runtime().child_token();
1742
1743 drop(publisher);
1747 assert_eq!(
1748 tracker.get_count(),
1749 1,
1750 "dropping a publisher must register its unregister work with the graceful-shutdown tracker"
1751 );
1752
1753 drt.runtime().shutdown();
1754
1755 tokio::time::timeout(std::time::Duration::from_secs(5), main_token.cancelled())
1756 .await
1757 .expect("graceful shutdown should complete once the unregister task finishes");
1758
1759 assert!(endpoint_token.is_cancelled());
1760 assert_eq!(
1761 tracker.get_count(),
1762 0,
1763 "the unregister task must release its graceful-shutdown guard"
1764 );
1765
1766 let instances = drt
1769 .discovery()
1770 .list(query)
1771 .await
1772 .expect("list event publishers after shutdown");
1773 assert!(
1774 instances.is_empty(),
1775 "unregister must complete within the graceful-shutdown window"
1776 );
1777 },
1778 )
1779 .await;
1780 }
1781
1782 #[tokio::test]
1783 async fn runtime_cancellation_stops_retained_direct_zmq_subscriber() {
1784 temp_env::async_with_vars(
1785 [
1786 (broker_env::DYN_ZMQ_BROKER_URL, None::<&str>),
1787 (broker_env::DYN_ZMQ_BROKER_ENABLED, None::<&str>),
1788 ],
1789 async {
1790 let runtime = crate::Runtime::from_current().expect("create runtime handle");
1791 let drt = DistributedRuntime::new(
1792 runtime,
1793 crate::distributed::DistributedConfig::process_local(),
1794 )
1795 .await
1796 .expect("create distributed runtime");
1797 let component = drt
1798 .namespace("event-subscriber-shutdown-test")
1799 .expect("create namespace")
1800 .component("worker")
1801 .expect("create component");
1802
1803 let mut subscriber = EventSubscriber::for_component_with_transport(
1804 &component,
1805 "events",
1806 EventTransportKind::Zmq,
1807 )
1808 .await
1809 .expect("create subscriber");
1810
1811 drt.primary_token().cancel();
1812
1813 let next =
1814 tokio::time::timeout(std::time::Duration::from_secs(1), subscriber.next())
1815 .await
1816 .expect("runtime cancellation should stop the subscriber stream");
1817 assert!(next.is_none());
1818 },
1819 )
1820 .await;
1821 }
1822
1823 #[test]
1824 fn test_event_scope_subject_prefix() {
1825 let scopes = [
1826 (
1827 EventScope::Namespace {
1828 name: "ns.one".to_string(),
1829 },
1830 "namespace.ns%2Eone",
1831 ),
1832 (
1833 EventScope::Component {
1834 namespace: "ns.one".to_string(),
1835 component: "worker/*".to_string(),
1836 },
1837 "namespace.ns%2Eone.component.worker%2F%2A",
1838 ),
1839 (
1840 EventScope::Endpoint {
1841 endpoint: EndpointId {
1842 namespace: "ns.one".to_string(),
1843 component: "worker/*".to_string(),
1844 name: "generate.>".to_string(),
1845 },
1846 },
1847 "namespace.ns%2Eone.component.worker%2F%2A.endpoint.generate%2E%3E",
1848 ),
1849 ];
1850
1851 for (scope, expected_prefix) in scopes {
1852 assert_eq!(scope.subject_prefix(), expected_prefix);
1853 assert_eq!(
1854 scope.subject("kv.events/*"),
1855 format!("{expected_prefix}.kv%2Eevents%2F%2A")
1856 );
1857 }
1858 }
1859
1860 #[test]
1861 fn test_event_scope_accessors() {
1862 let ns_scope = EventScope::Namespace {
1863 name: "my-ns".to_string(),
1864 };
1865 assert_eq!(ns_scope.namespace(), "my-ns");
1866 assert_eq!(ns_scope.component(), None);
1867
1868 let comp_scope = EventScope::Component {
1869 namespace: "my-ns".to_string(),
1870 component: "my-comp".to_string(),
1871 };
1872 assert_eq!(comp_scope.namespace(), "my-ns");
1873 assert_eq!(comp_scope.component(), Some("my-comp"));
1874 }
1875
1876 #[test]
1877 fn test_event_envelope_serde() {
1878 let envelope = EventEnvelope {
1879 publisher_id: 42,
1880 sequence: 10,
1881 published_at: 1700000000000,
1882 topic: "test-topic".to_string(),
1883 payload: Bytes::from("test data"),
1884 };
1885
1886 let json = serde_json::to_string(&envelope).expect("serialize");
1887 let deserialized: EventEnvelope = serde_json::from_str(&json).expect("deserialize");
1888
1889 assert_eq!(deserialized.publisher_id, 42);
1890 assert_eq!(deserialized.sequence, 10);
1891 assert_eq!(deserialized.published_at, 1700000000000);
1892 assert_eq!(deserialized.topic, "test-topic");
1893 assert_eq!(deserialized.payload, Bytes::from("test data"));
1894 }
1895}