Files
feresaria/compositor/src/activities/publication.rs
T

142 lines
5.0 KiB
Rust

//! Bounded serialization and delivery outside the compositor event loop.
use ferese_ipc::Response;
use ferese_ipc::activities::Snapshot;
use std::sync::mpsc::{self, SyncSender};
use std::sync::{Arc, RwLock};
type Recipient = (u64, SyncSender<Response>);
struct Job {
epoch: u64,
snapshot: Arc<Snapshot>,
recipients: Vec<Recipient>,
}
pub(super) struct Publisher(SyncSender<Job>, Arc<RwLock<u64>>);
impl Publisher {
pub fn new() -> std::io::Result<Self> {
let (send, receive) = mpsc::sync_channel::<Job>(8);
let epoch = Arc::new(RwLock::new(0));
let worker_epoch = epoch.clone();
std::thread::Builder::new()
.name("ferese-activity-snapshots".into())
.spawn(move || {
let mut cached: Option<(Arc<Snapshot>, serde_json::Value)> = None;
while let Ok(job) = receive.recv() {
if !cached
.as_ref()
.is_some_and(|(snapshot, _)| Arc::ptr_eq(snapshot, &job.snapshot))
{
cached = Some((
job.snapshot.clone(),
serde_json::json!(job.snapshot.as_ref()),
));
}
let value = &cached
.as_ref()
.expect("activity serialization populated the cache for this revision")
.1;
// Lock acquisition revokes queued/in-flight serialization.
// Hold the guard through delivery to order sends before revocation.
deliver(job.epoch, &worker_epoch, job.recipients, value);
}
})?;
Ok(Self(send, epoch))
}
pub fn revoke(&self) {
let mut epoch = self.1.write().expect("activity publication epoch");
*epoch = epoch.wrapping_add(1);
}
pub fn send(&self, snapshot: Arc<Snapshot>, recipients: Vec<Recipient>) {
let epoch = *self.1.read().expect("activity publication epoch");
if let Err(error) = self.0.try_send(Job {
epoch,
snapshot,
recipients,
}) {
let (mpsc::TrySendError::Full(job) | mpsc::TrySendError::Disconnected(job)) = error;
unavailable(job.recipients);
}
}
}
fn deliver(
epoch: u64,
current: &RwLock<u64>,
recipients: Vec<Recipient>,
value: &serde_json::Value,
) {
let current = current.read().expect("activity publication epoch");
if epoch != *current {
locked(recipients);
return;
}
for (id, sender) in recipients {
// A slow/disconnected client cannot block another subscriber.
let _ = sender.try_send(Response::success(id, value.clone()));
}
}
pub(super) fn locked(recipients: Vec<Recipient>) {
for (id, sender) in recipients {
let _ = sender.try_send(Response::error(id, "session_locked", "Session is locked"));
}
}
pub(super) fn unavailable(recipients: Vec<Recipient>) {
for (id, sender) in recipients {
let _ = sender.try_send(Response::error(
id,
"activity_busy",
"Activity snapshot delivery unavailable; retry",
));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn lock_revokes_serialized_jobs_even_after_unlock() {
let (send, _receive) = mpsc::sync_channel(1);
let publisher = Publisher(send, Arc::new(RwLock::new(0)));
let job_epoch = *publisher.1.read().unwrap();
publisher.revoke();
let (reply, receive) = mpsc::sync_channel(1);
deliver(
job_epoch,
&publisher.1,
vec![(9, reply)],
&serde_json::json!({"title": "private"}),
);
let response = receive.recv().unwrap();
assert_eq!(response.id, 9);
assert!(response.result.is_none());
assert_eq!(response.error.unwrap().code, "session_locked");
}
#[test]
fn saturated_queue_rejects_without_blocking_and_slow_clients_do_not_block_others() {
let (send, _receive) = mpsc::sync_channel(0);
let publisher = Publisher(send, Arc::new(RwLock::new(0)));
let (reply, receive) = mpsc::sync_channel(1);
publisher.send(Arc::new(Snapshot::default()), vec![(7, reply)]);
let response = receive.recv_timeout(Duration::from_secs(1)).unwrap();
assert_eq!(response.id, 7);
assert_eq!(response.error.unwrap().code, "activity_busy");
let publisher = Publisher::new().unwrap();
let (slow, _slow_receive) = mpsc::sync_channel(1);
slow.send(Response::success(1, serde_json::json!({})))
.unwrap();
let (fast, fast_receive) = mpsc::sync_channel(1);
publisher.send(Arc::new(Snapshot::default()), vec![(2, slow), (3, fast)]);
assert_eq!(
fast_receive
.recv_timeout(Duration::from_secs(1))
.unwrap()
.id,
3
);
}
}