use bytes::Bytes;
use flume::{Receiver, Sender};
use monocoque_core::rt::{OwnedReadHalf, OwnedWriteHalf, TcpListener, TcpStream};
use monocoque_core::subscription::SubscriptionEvent;
use crate::handshake::perform_handshake_with_options;
use crate::session::SocketType;
use monocoque_core::options::SocketOptions;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::thread;
use tracing::{debug, error, trace, warn};
const MAX_COALESCE_MSGS: usize = 4096;
const COALESCE_BYTE_LIMIT: usize = 4 * 1024 * 1024;
type SubscriberId = u64;
#[derive(Default, Clone)]
struct SubscriptionUnion {
match_all: usize,
prefixes: Vec<(Vec<u8>, usize)>,
}
impl SubscriptionUnion {
fn add_subscriber(&mut self) {
self.match_all += 1;
}
fn subscribe(&mut self, prefix: &[u8], sub_was_empty: bool) {
if sub_was_empty {
self.match_all = self.match_all.saturating_sub(1);
}
if let Some(entry) = self.prefixes.iter_mut().find(|(p, _)| p == prefix) {
entry.1 += 1;
} else {
self.prefixes.push((prefix.to_vec(), 1));
}
}
fn unsubscribe(&mut self, prefix: &[u8], sub_now_empty: bool) {
if let Some(pos) = self.prefixes.iter().position(|(p, _)| p == prefix) {
self.prefixes[pos].1 -= 1;
if self.prefixes[pos].1 == 0 {
self.prefixes.swap_remove(pos);
}
}
if sub_now_empty {
self.match_all += 1;
}
}
#[inline]
fn matches(&self, topic: &[u8]) -> bool {
self.match_all > 0
|| self
.prefixes
.iter()
.any(|(p, _)| topic.starts_with(p.as_slice()))
}
}
struct SharedSubscriptions {
union: RwLock<SubscriptionUnion>,
generation: AtomicU64,
}
impl SharedSubscriptions {
fn new() -> Arc<Self> {
Arc::new(Self {
union: RwLock::new(SubscriptionUnion::default()),
generation: AtomicU64::new(0),
})
}
fn update(&self, f: impl FnOnce(&mut SubscriptionUnion)) {
f(&mut self.union.write());
self.generation.fetch_add(1, Ordering::Release);
}
}
type SubscriptionState = Arc<RwLock<Vec<Bytes>>>;
type SubCipher = Arc<parking_lot::Mutex<crate::security::curve::CurveMessageCipher>>;
enum WorkerCommand {
AddSubscriber {
id: SubscriberId,
stream: TcpStream,
subscriptions: SubscriptionState,
cipher: Option<SubCipher>,
max_frame_size: Option<usize>,
union: Arc<SharedSubscriptions>,
},
Broadcast { message: Arc<Vec<Bytes>> },
Shutdown,
}
struct WorkerSubscriber {
id: SubscriberId,
stream: OwnedWriteHalf,
subscriptions: SubscriptionState,
cipher: Option<SubCipher>,
}
impl WorkerSubscriber {
fn matches(&self, msg: &[Bytes]) -> bool {
let subs = self.subscriptions.read();
if subs.is_empty() {
return true;
}
if let Some(first_frame) = msg.first() {
for sub in subs.iter() {
if sub.is_empty()
|| (first_frame.len() >= sub.len() && first_frame[..sub.len()] == sub[..])
{
return true;
}
}
}
false
}
}
#[allow(clippy::significant_drop_tightening)]
async fn subscription_reader(
id: SubscriberId,
mut reader: OwnedReadHalf,
subscriptions: SubscriptionState,
cipher: Option<SubCipher>,
max_frame_size: Option<usize>,
union: Arc<SharedSubscriptions>,
) {
use compio_buf::BufResult;
use compio_io::AsyncRead;
use monocoque_core::buffer::SegmentedBuffer;
trace!("[PUB] Subscription reader started for subscriber {}", id);
let mut recv_buf = SegmentedBuffer::new();
let mut decoder = max_frame_size.map_or_else(
crate::codec::ZmtpDecoder::new,
crate::codec::ZmtpDecoder::with_max_frame_size,
);
let mut buf = vec![0u8; 256];
loop {
let BufResult(result, returned_buf) = reader.read(buf).await;
buf = returned_buf;
match result {
Ok(0) => {
debug!("[PUB] Subscriber {} disconnected (subscription reader)", id);
break;
}
Ok(n) => {
recv_buf.push(Bytes::copy_from_slice(&buf[..n]));
loop {
match decoder.decode(&mut recv_buf) {
Ok(Some(frame)) => {
let payload = if frame.is_command() {
if let Some(ref arc_cipher) = cipher {
if crate::security::curve::CurveMessageCipher::is_curve_message(
&frame.payload,
) {
let mut cipher_guard = arc_cipher.lock();
match cipher_guard.decrypt_frame(&frame.payload) {
Ok((_more, data)) => data,
Err(_) => continue,
}
} else {
continue;
}
} else {
continue;
}
} else if cipher.is_some() {
break;
} else {
frame.payload
};
if let Some(event) = SubscriptionEvent::from_bytes(payload) {
let mut subs = subscriptions.write();
match event {
SubscriptionEvent::Subscribe(prefix) => {
if !subs.contains(&prefix) {
trace!(
"[PUB] Subscriber {} subscribed to {:?}",
id, prefix
);
let was_empty = subs.is_empty();
union.update(|u| u.subscribe(&prefix, was_empty));
subs.push(prefix);
}
}
SubscriptionEvent::Unsubscribe(prefix) => {
let before = subs.len();
subs.retain(|s| s != &prefix);
if subs.len() < before {
trace!(
"[PUB] Subscriber {} unsubscribed from {:?}",
id, prefix
);
let now_empty = subs.is_empty();
union.update(|u| u.unsubscribe(&prefix, now_empty));
}
}
}
}
}
Ok(None) => break, Err(e) => {
debug!(
"[PUB] Subscription reader for subscriber {} decode error: {}",
id, e
);
break;
}
}
}
}
Err(e) => {
debug!(
"[PUB] Subscription reader for subscriber {} error: {}",
id, e
);
break;
}
}
}
trace!("[PUB] Subscription reader exiting for subscriber {}", id);
}
#[allow(clippy::too_many_lines)]
fn worker_thread(worker_id: usize, rx: Receiver<WorkerCommand>, sub_count: Arc<AtomicUsize>) {
debug!("[Worker {}] Starting", worker_id);
let rt = match monocoque_core::rt::LocalRuntime::new() {
Ok(rt) => rt,
Err(e) => {
error!("[Worker {}] Failed to create runtime: {}", worker_id, e);
return;
}
};
rt.block_on(async move {
let mut subscribers: HashMap<SubscriberId, WorkerSubscriber> = HashMap::new();
loop {
match rx.recv_async().await {
Ok(WorkerCommand::AddSubscriber {
id,
stream,
subscriptions,
cipher,
max_frame_size,
union,
}) => {
add_subscriber(
&mut subscribers,
worker_id,
id,
stream,
subscriptions,
cipher,
max_frame_size,
union,
);
}
Ok(WorkerCommand::Broadcast { message }) => {
let mut batch: Vec<Arc<Vec<Bytes>>> = Vec::with_capacity(4);
let mut batch_bytes = approx_wire_len(&message);
batch.push(message);
let mut deferred: Option<WorkerCommand> = None;
while batch.len() < MAX_COALESCE_MSGS && batch_bytes < COALESCE_BYTE_LIMIT {
match rx.try_recv() {
Ok(WorkerCommand::Broadcast { message }) => {
batch_bytes += approx_wire_len(&message);
batch.push(message);
}
Ok(other) => {
deferred = Some(other);
break;
}
Err(_) => break,
}
}
trace!(
"[Worker {}] Broadcasting batch of {} to {} subscribers",
worker_id,
batch.len(),
subscribers.len()
);
let mut full_batch: Option<Bytes> = None;
let mut plain_wire: Vec<Option<Bytes>> = vec![None; batch.len()];
let mut dead_subs = Vec::new();
for sub in subscribers.values_mut() {
let plaintext_all =
sub.cipher.is_none() && sub.subscriptions.read().is_empty();
let out: Bytes = if plaintext_all {
full_batch
.get_or_insert_with(|| {
let mut buf = bytes::BytesMut::new();
for msg in &batch {
crate::codec::encode_multipart(msg, &mut buf);
}
buf.freeze()
})
.clone()
} else {
let mut buf = bytes::BytesMut::new();
let mut failed = false;
for (idx, msg) in batch.iter().enumerate() {
if !sub.matches(msg) {
continue;
}
if let Some(ref arc_cipher) = sub.cipher {
if let Some(wire) = encode_curve_wire(msg, arc_cipher) {
buf.extend_from_slice(&wire);
} else {
failed = true;
break;
}
} else {
let wire = plain_wire[idx]
.get_or_insert_with(|| encode_plain_wire(msg));
buf.extend_from_slice(wire);
}
}
if failed {
dead_subs.push(sub.id);
continue;
}
buf.freeze()
};
if out.is_empty() {
continue; }
let send_result = monocoque_core::rt::timeout(
std::time::Duration::from_secs(5),
send_all_to_stream(&mut sub.stream, out),
)
.await;
match send_result {
Ok(Ok(())) => {
trace!("[Worker {}] Sent to subscriber {}", worker_id, sub.id);
}
Ok(Err(e)) => {
debug!(
"[Worker {}] Subscriber {} send error: {}",
worker_id, sub.id, e
);
dead_subs.push(sub.id);
}
Err(_) => {
warn!("[Worker {}] Subscriber {} timed out", worker_id, sub.id);
dead_subs.push(sub.id);
}
}
}
if !dead_subs.is_empty() {
sub_count.fetch_sub(dead_subs.len(), Ordering::Relaxed);
}
for id in dead_subs {
subscribers.remove(&id);
debug!("[Worker {}] Removed dead subscriber {}", worker_id, id);
}
match deferred {
Some(WorkerCommand::AddSubscriber {
id,
stream,
subscriptions,
cipher,
max_frame_size,
union,
}) => {
add_subscriber(
&mut subscribers,
worker_id,
id,
stream,
subscriptions,
cipher,
max_frame_size,
union,
);
}
Some(WorkerCommand::Shutdown) => {
debug!("[Worker {}] Shutting down", worker_id);
break;
}
Some(WorkerCommand::Broadcast { .. }) | None => {}
}
}
Ok(WorkerCommand::Shutdown) => {
debug!("[Worker {}] Shutting down", worker_id);
break;
}
Err(_) => {
debug!("[Worker {}] Channel closed, exiting", worker_id);
break;
}
}
}
});
debug!("[Worker {}] Stopped", worker_id);
}
async fn send_all_to_stream(stream: &mut OwnedWriteHalf, data: Bytes) -> io::Result<()> {
use compio_buf::BufResult;
use compio_io::AsyncWriteExt;
let BufResult(res, _) = stream.write_all(data).await;
res
}
fn encode_plain_wire(msg: &[Bytes]) -> Bytes {
let mut buf = bytes::BytesMut::new();
crate::codec::encode_multipart(msg, &mut buf);
buf.freeze()
}
fn encode_curve_wire(msg: &[Bytes], cipher: &SubCipher) -> Option<Bytes> {
let last = msg.len().saturating_sub(1);
let mut buf = bytes::BytesMut::new();
let mut cipher = cipher.lock();
for (i, frame) in msg.iter().enumerate() {
let body = cipher.encrypt_frame(frame, i < last).ok()?;
crate::base::append_zmtp_cmd_frame(&mut buf, &body);
}
drop(cipher);
Some(buf.freeze())
}
fn approx_wire_len(msg: &[Bytes]) -> usize {
msg.iter().map(|f| f.len() + 9).sum()
}
#[allow(clippy::too_many_arguments)]
fn add_subscriber(
subscribers: &mut HashMap<SubscriberId, WorkerSubscriber>,
worker_id: usize,
id: SubscriberId,
stream: TcpStream,
subscriptions: SubscriptionState,
cipher: Option<SubCipher>,
max_frame_size: Option<usize>,
union: Arc<SharedSubscriptions>,
) {
debug!("[Worker {}] Adding subscriber {}", worker_id, id);
let (read_half, write_half) = stream.into_split();
let sub_state = Arc::clone(&subscriptions);
let reader_cipher = cipher.clone();
monocoque_core::rt::spawn_detached(subscription_reader(
id,
read_half,
sub_state,
reader_cipher,
max_frame_size,
union,
));
subscribers.insert(
id,
WorkerSubscriber {
id,
stream: write_half,
subscriptions,
cipher,
},
);
}
pub struct PubSocket {
workers: Vec<Sender<WorkerCommand>>,
worker_sub_counts: Vec<Arc<AtomicUsize>>,
subscription_union: Arc<SharedSubscriptions>,
local_union: SubscriptionUnion,
local_gen: u64,
next_id: SubscriberId,
next_worker: usize,
options: SocketOptions,
subscriber_count: usize,
is_poisoned: bool,
drop_count: Arc<AtomicU64>,
}
impl PubSocket {
pub fn new() -> Self {
Self::with_workers(num_cpus::get().max(2))
}
pub fn with_workers(worker_count: usize) -> Self {
Self::with_workers_opts(worker_count, SocketOptions::default())
}
pub fn with_workers_opts(worker_count: usize, options: SocketOptions) -> Self {
let hwm = options.send_hwm;
debug!(
"[PUB] Starting {} worker threads (channel HWM={})",
worker_count, hwm
);
let mut workers = Vec::with_capacity(worker_count);
let mut worker_sub_counts = Vec::with_capacity(worker_count);
for i in 0..worker_count {
let (tx, rx) = flume::bounded(hwm);
let sub_count = Arc::new(AtomicUsize::new(0));
let worker_count = Arc::clone(&sub_count);
thread::Builder::new()
.name(format!("pub-worker-{}", i))
.spawn(move || worker_thread(i, rx, worker_count))
.expect("Failed to spawn worker thread");
workers.push(tx);
worker_sub_counts.push(sub_count);
}
Self {
workers,
worker_sub_counts,
subscription_union: SharedSubscriptions::new(),
local_union: SubscriptionUnion::default(),
local_gen: 0,
next_id: 1,
next_worker: 0,
options,
subscriber_count: 0,
is_poisoned: false,
drop_count: Arc::new(AtomicU64::new(0)),
}
}
pub async fn accept_subscriber(&mut self, listener: &TcpListener) -> io::Result<SubscriberId> {
let (stream, addr) = listener.accept().await?;
crate::utils::configure_tcp_stream(&stream, &self.options, "PUB")?;
debug!("[PUB] Accepted connection from {}", addr);
let mut stream = stream;
let handshake_result = perform_handshake_with_options(
&mut stream,
SocketType::Pub,
None,
Some(self.options.handshake_timeout),
&self.options,
)
.await
.map_err(|e| io::Error::other(format!("Handshake failed: {}", e)))?;
debug!(peer_socket_type = ?handshake_result.peer_socket_type, "[PUB] Handshake complete");
let id = self.next_id;
self.next_id += 1;
let subscriptions = Arc::new(RwLock::new(Vec::new()));
let worker_idx = self.next_worker;
self.next_worker = (self.next_worker + 1) % self.workers.len();
self.worker_sub_counts[worker_idx].fetch_add(1, Ordering::Relaxed);
self.subscription_union
.update(SubscriptionUnion::add_subscriber);
debug!("[PUB] Assigning subscriber {} to worker {}", id, worker_idx);
let cipher = handshake_result
.curve_cipher
.map(|c| Arc::new(parking_lot::Mutex::new(c)));
self.workers[worker_idx]
.send_async(WorkerCommand::AddSubscriber {
id,
stream,
subscriptions,
cipher,
max_frame_size: self.options.max_msg_size,
union: Arc::clone(&self.subscription_union),
})
.await
.map_err(|e| io::Error::other(format!("Failed to send to worker: {}", e)))?;
self.subscriber_count += 1;
debug!(
"[PUB] Subscriber {} accepted and subscription reader started (total: {})",
id, self.subscriber_count
);
Ok(id)
}
pub fn remove_subscriber(&mut self, _id: SubscriberId) {
self.subscriber_count = self.subscriber_count.saturating_sub(1);
}
#[inline]
fn prefilter_allows(&mut self, topic: &[u8]) -> bool {
if self.options.invert_matching {
return true;
}
let g = self.subscription_union.generation.load(Ordering::Acquire);
if g != self.local_gen {
self.local_union = self.subscription_union.union.read().clone();
self.local_gen = g;
}
self.local_union.matches(topic)
}
fn dispatch(&self, message: &Arc<Vec<Bytes>>) -> io::Result<()> {
for (idx, worker) in self.workers.iter().enumerate() {
if self.worker_sub_counts[idx].load(Ordering::Relaxed) == 0 {
continue;
}
match worker.try_send(WorkerCommand::Broadcast {
message: Arc::clone(message),
}) {
Ok(()) => {}
Err(flume::TrySendError::Full(_)) => {
self.drop_count.fetch_add(1, Ordering::Relaxed);
debug!("[PUB] Worker {} channel full (HWM), message dropped", idx);
}
Err(flume::TrySendError::Disconnected(_)) => {
return Err(io::Error::other(format!(
"Worker {} channel disconnected",
idx
)));
}
}
}
Ok(())
}
pub async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
if self.is_poisoned {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Socket is poisoned from previous incomplete operation",
));
}
if msg.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Empty message"));
}
if !self.prefilter_allows(msg.first().map_or(&[][..], |f| f.as_ref())) {
return Ok(());
}
self.dispatch(&Arc::new(msg))
}
pub async fn send_frames(&mut self, frames: &[Bytes]) -> io::Result<()> {
if self.is_poisoned {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Socket is poisoned from previous incomplete operation",
));
}
if frames.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Empty message"));
}
if !self.prefilter_allows(frames.first().map_or(&[][..], |f| f.as_ref())) {
return Ok(());
}
self.dispatch(&Arc::new(frames.to_vec()))
}
#[inline]
pub const fn subscriber_count(&self) -> usize {
self.subscriber_count
}
#[inline]
pub fn drop_count(&self) -> u64 {
self.drop_count.load(Ordering::Relaxed)
}
#[inline]
pub const fn options(&self) -> &SocketOptions {
&self.options
}
#[inline]
pub fn options_mut(&mut self) -> &mut SocketOptions {
&mut self.options
}
#[inline]
pub const fn socket_type(&self) -> SocketType {
SocketType::Pub
}
}
impl Default for PubSocket {
fn default() -> Self {
Self::new()
}
}
impl Drop for PubSocket {
fn drop(&mut self) {
debug!("[PUB] Shutting down {} workers", self.workers.len());
for worker in &self.workers {
let _ = worker.send(WorkerCommand::Shutdown);
}
}
}
#[async_trait::async_trait(?Send)]
impl crate::Socket for PubSocket {
async fn send(&mut self, msg: Vec<Bytes>) -> io::Result<()> {
self.send(msg).await
}
async fn recv(&mut self) -> io::Result<Option<Vec<Bytes>>> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"PUB sockets do not support receive operations",
))
}
fn socket_type(&self) -> SocketType {
SocketType::Pub
}
}