use crate::error::{ZmqError, ZmqResult};
use crate::runtime::mailbox::DEFAULT_MAILBOX_CAPACITY;
use crate::runtime::{ActorType, EventBus, MailboxSender, SystemEvent, WaitGroup};
use crate::socket::{Socket, SocketType};
use std::collections::HashMap;
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::Arc;
use std::time::Duration;
use tracing::warn;
#[cfg(feature = "io-uring")]
use crate::uring::global_state;
#[derive(Debug, Clone)]
#[cfg(feature = "inproc")]
pub(crate) struct InprocBinding {
pub(crate) binder_core_id: usize,
}
#[derive(Debug)]
pub(crate) struct ContextInner {
pub(crate) next_handle: Arc<std::sync::atomic::AtomicUsize>,
pub(crate) sockets: parking_lot::RwLock<HashMap<usize, MailboxSender>>,
#[cfg(feature = "inproc")]
pub(crate) inproc_registry: parking_lot::RwLock<HashMap<String, InprocBinding>>,
event_bus: Arc<EventBus>,
actor_wait_group: WaitGroup,
pub(crate) shutdown_initiated: AtomicBool,
actor_mailbox_capacity: usize,
}
impl ContextInner {
fn new(actor_mailbox_capacity: usize) -> ZmqResult<Self> {
let event_bus = Arc::new(EventBus::new());
let actor_wait_group = WaitGroup::new();
#[cfg(feature = "io-uring")]
{
match global_state::ensure_global_uring_systems_started() {
Ok(_) | Err(ZmqError::InvalidState(_)) => {}
Err(err) => {
return Err(err);
}
}
global_state::get_global_uring_worker_op_tx()?;
}
Ok(Self {
next_handle: Arc::new(std::sync::atomic::AtomicUsize::new(1)), sockets: parking_lot::RwLock::new(HashMap::new()),
#[cfg(feature = "inproc")]
inproc_registry: parking_lot::RwLock::new(HashMap::new()),
event_bus,
actor_wait_group,
shutdown_initiated: AtomicBool::new(false),
actor_mailbox_capacity,
})
}
pub(crate) fn get_actor_mailbox_capacity(&self) -> usize {
self.actor_mailbox_capacity
}
pub(crate) fn next_handle(&self) -> usize {
self.next_handle.fetch_add(1, AtomicOrdering::Relaxed)
}
pub(crate) fn register_socket(&self, handle: usize, command_sender: MailboxSender) {
let mut sockets_w = self.sockets.write();
sockets_w.insert(handle, command_sender);
tracing::debug!(socket_handle = handle, "Socket command mailbox registered");
}
pub(crate) fn unregister_socket(&self, handle: usize) {
let mut sockets_w = self.sockets.write();
if sockets_w.remove(&handle).is_some() {
tracing::debug!(
socket_handle = handle,
"Socket command mailbox unregistered"
);
} else {
tracing::warn!(
socket_handle = handle,
"Attempted to unregister non-existent socket handle"
);
}
}
pub(crate) async fn shutdown(&self) {
if self
.shutdown_initiated
.compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
.is_ok()
{
tracing::info!("Context shutdown initiated.");
if let Err(e) = self.event_bus.publish(SystemEvent::ContextTerminating) {
tracing::warn!(
"Publishing ContextTerminating event failed (receivers={}): {}",
self.event_bus.subscriber_count(),
e
);
} else {
tracing::debug!("Published ContextTerminating event via bus.");
}
} else {
tracing::debug!("Context shutdown already initiated.");
}
}
pub(crate) async fn wait_for_termination(&self) {
if !self.shutdown_initiated.load(AtomicOrdering::Acquire) {
tracing::warn!("Context::term waiting but shutdown not initiated? Proceeding anyway.");
}
let initial_count = self.actor_wait_group.get_count();
tracing::debug!(
count = initial_count,
"Context wait_for_termination starting wait on WG (includes listener task)..."
);
let wait_timeout = Duration::from_secs(10); match tokio::time::timeout(wait_timeout, self.actor_wait_group.wait()).await {
Ok(()) => {
tracing::info!(
initial_count,
final_count = self.actor_wait_group.get_count(),
"Context termination complete (WaitGroup reached zero)."
);
}
Err(_) => {
let final_count = self.actor_wait_group.get_count();
tracing::error!(
initial_count,
final_count,
timeout=?wait_timeout,
"Context wait_for_termination timed out! {} actors may not have stopped correctly.",
final_count );
}
}
}
#[cfg(feature = "inproc")]
pub(crate) fn register_inproc(
&self,
name: String,
binder_core_id: usize,
) -> Result<(), ZmqError> {
let mut registry = self.inproc_registry.write();
if registry.contains_key(&name) {
Err(ZmqError::AddrInUse(format!("inproc://{}", name)))
} else {
tracing::debug!(inproc_name = %name, binder_core_id = binder_core_id, "Registering inproc binding");
registry.insert(name, InprocBinding { binder_core_id });
Ok(())
}
}
#[cfg(feature = "inproc")]
pub(crate) fn unregister_inproc(&self, name: &str) {
let mut registry = self.inproc_registry.write();
if registry.remove(name).is_some() {
tracing::debug!(inproc_name = %name, "Unregistered inproc binding");
}
}
#[cfg(feature = "inproc")]
pub(crate) fn lookup_inproc(&self, name: &str) -> Option<InprocBinding> {
self.inproc_registry.read().get(name).cloned()
}
pub(crate) fn get_socket_command_sender(&self, handle: usize) -> Option<MailboxSender> {
self.sockets.read().get(&handle).cloned()
}
pub(crate) fn event_bus(&self) -> Arc<EventBus> {
self.event_bus.clone()
}
}
#[derive(Clone)]
pub struct Context {
inner: Arc<ContextInner>,
}
impl Context {
pub fn new() -> Result<Self, ZmqError> {
Self::with_capacity(None)
}
pub fn with_capacity(actor_mailbox_capacity: Option<usize>) -> Result<Self, ZmqError> {
let capacity = actor_mailbox_capacity
.map(|c| c.max(1)) .unwrap_or(DEFAULT_MAILBOX_CAPACITY);
tracing::debug!(target_capacity = capacity, "Creating new rzmq Context");
Ok(Self {
inner: Arc::new(ContextInner::new(capacity)?),
})
}
pub fn socket(&self, socket_type: SocketType) -> Result<Socket, ZmqError> {
let handle = self.inner.next_handle();
tracing::debug!(socket_type = ?socket_type, handle = handle, "Creating socket");
let (socket_logic, command_sender) =
crate::socket::create_socket_actor(handle, self.clone(), socket_type)?;
self.inner.register_socket(handle, command_sender.clone());
Ok(Socket::new(socket_logic, command_sender))
}
pub async fn shutdown(&self) -> Result<(), ZmqError> {
self.inner.shutdown().await;
Ok(())
}
pub async fn term(&self) -> Result<(), ZmqError> {
self.inner.shutdown().await; self.inner.wait_for_termination().await;
Ok(())
}
pub(crate) fn inner(&self) -> &Arc<ContextInner> {
&self.inner
}
pub(crate) fn event_bus(&self) -> Arc<EventBus> {
self.inner.event_bus() }
pub(crate) fn publish_actor_started(
&self,
handle_id: usize,
actor_type: ActorType,
parent_id: Option<usize>,
) {
let event = SystemEvent::ActorStarted {
handle_id,
actor_type,
parent_id,
};
if let Err(e) = self.inner.event_bus().publish(event) {
tracing::warn!(
actor_handle = handle_id,
?actor_type,
"Failed to publish ActorStarted event: {}",
e
);
}
let wg = &self.inner.actor_wait_group; wg.add(1); }
pub(crate) fn publish_actor_stopping(
&self,
handle_id: usize,
actor_type: ActorType,
parent_id: Option<usize>,
endpoint_uri: Option<String>,
error: Option<ZmqError>,
) {
let event = SystemEvent::ActorStopping {
handle_id,
actor_type,
parent_id,
endpoint_uri,
error,
};
if let Err(e) = self.inner.event_bus().publish(event) {
if !std::thread::panicking() {
warn!(
"WARN: Failed to publish ActorStopping event for handle {}: {} (receivers={})",
handle_id,
e,
self.inner.event_bus().subscriber_count()
);
}
} else {
}
let wg = &self.inner.actor_wait_group;
if wg.get_count() > 0 {
tracing::trace!(
actor_handle = handle_id,
?actor_type,
wg_prev = wg.get_count(),
"Decrementing WaitGroup for stopping actor."
);
wg.done(); } else {
if !std::thread::panicking() {
warn!(
"WARN: Attempted WaitGroup done() for handle {} ({:?}) but count was already zero!",
handle_id, actor_type
);
}
}
}
}
impl fmt::Debug for Context {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context").finish_non_exhaustive()
}
}
pub fn context() -> Result<Context, ZmqError> {
Context::new()
}
#[cfg(test)]
mod additional_context_tests {
use super::*;
#[tokio::test]
async fn test_context_next_handle_uniqueness() {
let ctx = Context::new().expect("Failed to create context");
let inner = ctx.inner().clone();
let mut tasks = vec![];
for _ in 0..10 {
let inner_clone = inner.clone();
tasks.push(tokio::spawn(async move {
let mut allocated = vec![];
for _ in 0..1000 {
allocated.push(inner_clone.next_handle());
}
allocated
}));
}
let mut all_ids = std::collections::HashSet::new();
for h in tasks {
let ids = h.await.unwrap();
for id in ids {
assert!(all_ids.insert(id), "Duplicate ID allocated: {}", id);
}
}
}
#[tokio::test]
#[cfg(feature = "inproc")]
async fn test_context_inproc_registry() {
let ctx = Context::new().unwrap();
let inner = ctx.inner();
let name = "shared-inproc-channel".to_string();
assert!(inner.register_inproc(name.clone(), 100).is_ok());
let dup = inner.register_inproc(name.clone(), 200);
assert!(matches!(dup, Err(ZmqError::AddrInUse(_))));
let binding = inner.lookup_inproc(&name).unwrap();
assert_eq!(binding.binder_core_id, 100);
inner.unregister_inproc(&name);
assert!(inner.lookup_inproc(&name).is_none());
}
}