Fix review findings across compositor and shell

This commit is contained in:
Samuel Onoja committed 2026-10-13 04:02:17 +01:00
1 parent 5d9673aad3
commit f7e5efb245
30 files changed
+1222 -181

No files matched your search

+56 -10
View File
@@ -1,19 +1,22 @@
//! Bounded serialization and delivery outside the compositor event loop.
use ferese_ipc::{Response, activities::Snapshot};
use std::sync::{
Arc,
Arc, RwLock,
mpsc::{self, SyncSender},
};
type Recipient = (u64, SyncSender<Response>);
struct Job {
epoch: u64,
snapshot: Arc<Snapshot>,
recipients: Vec<Recipient>,
}
pub(super) struct Publisher(SyncSender<Job>);
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 || {
@@ -26,23 +29,47 @@ impl Publisher {
cached = Some((job.snapshot.clone(), serde_json::json!(job.snapshot.as_ref())));
}
let value = &cached.as_ref().unwrap().1;
for (id, sender) in job.recipients {
// Client queues are bounded too. A slow/disconnected client
// cannot block serialization or another subscriber.
let _ = sender.try_send(Response::success(id, value.clone()));
}
// 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))
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>) {
if let Err(error) = self.0.try_send(Job { snapshot, recipients }) {
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(
@@ -58,10 +85,29 @@ 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);
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();