1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::sync::{mpsc, Arc, Mutex};
5
6use serde_json::Value;
7
8use crate::client::Client;
9use crate::error::Error;
10use crate::evidence::Lease as EvidenceProviderLease;
11use crate::framing::encode_frame;
12use crate::limits::Limits;
13use crate::marker::encode_marker;
14use crate::messages::{RevisionCommit, SemanticFullMessage};
15use crate::tree::Snapshot;
16use crate::validate::validate_snapshot;
17
18struct Publication {
19 revision: i64,
20 tree_frame: Option<Vec<u8>>,
21 commit_frame: Vec<u8>,
22}
23
24pub struct PublicationQueue {
30 sender: Option<mpsc::SyncSender<Publication>>,
31 token: String,
32 session_id: String,
33 limits: Limits,
34 marker_enabled: bool,
35 revision: i64,
36 evidence_lease: Option<EvidenceProviderLease>,
37 failed: Arc<AtomicBool>,
38 dropped: AtomicU64,
39 fatal: Arc<Mutex<Option<(String, String)>>>,
40 worker: Option<std::thread::JoinHandle<()>>,
41 #[cfg(test)]
42 done: Mutex<mpsc::Receiver<()>>,
43}
44
45impl PublicationQueue {
46 pub fn new(client: Client, capacity: usize) -> Result<Self, Error> {
51 Self::new_inner(client, capacity, None)
52 }
53
54 fn new_inner(
55 mut client: Client,
56 capacity: usize,
57 #[cfg(test)] gate: Option<Arc<TestWorkerGate>>,
58 #[cfg(not(test))] _gate: Option<()>,
59 ) -> Result<Self, Error> {
60 if capacity == 0 || !client.connected() {
61 return Err(Error::PublicationWorkerFailed);
62 }
63 let (token, session_id, limits, marker_enabled, revision) = client
64 .publication_config()
65 .ok_or(Error::PublicationWorkerFailed)?;
66 let evidence_lease = client.take_evidence_lease();
67 let (sender, receiver) = mpsc::sync_channel::<Publication>(capacity);
68 #[cfg(test)]
69 let (done_sender, done_receiver) = mpsc::sync_channel(1);
70 let failed = Arc::new(AtomicBool::new(false));
71 let worker_failed = failed.clone();
72 let fatal: Arc<Mutex<Option<(String, String)>>> = Arc::new(Mutex::new(None));
73 let worker_fatal = fatal.clone();
74 let worker = std::thread::Builder::new()
75 .name("termwright-semantic-publication".into())
76 .spawn(move || {
77 while let Ok(publication) = receiver.recv() {
78 #[cfg(test)]
79 if let Some(gate) = gate.as_ref() {
80 gate.enter();
81 }
82 let result = (|| {
83 if let Some(frame) = publication.tree_frame.as_ref() {
84 client.write_frame(frame)?;
85 }
86 client.write_frame(&publication.commit_frame)?;
87 client.accept_queued_publication(
88 publication.revision,
89 publication.tree_frame.is_some(),
90 );
91 Ok::<(), Error>(())
92 })();
93 if result.is_err() {
94 worker_failed.store(true, Ordering::Release);
95 client.close();
96 #[cfg(test)]
97 let _ = done_sender.send(());
98 return;
99 }
100 }
101 let fatal = worker_fatal.lock().ok().and_then(|mut value| value.take());
102 if let Some((code, message)) = fatal {
103 let _ = client.fail(&code, message);
104 } else {
105 client.close();
106 }
107 #[cfg(test)]
108 let _ = done_sender.send(());
109 })
110 .map_err(Error::Io)?;
111 Ok(Self {
112 sender: Some(sender),
113 token,
114 session_id,
115 limits,
116 marker_enabled,
117 revision,
118 evidence_lease,
119 failed,
120 dropped: AtomicU64::new(0),
121 fatal,
122 worker: Some(worker),
123 #[cfg(test)]
124 done: Mutex::new(done_receiver),
125 })
126 }
127
128 pub fn limits(&self) -> &Limits {
130 &self.limits
131 }
132
133 pub fn publish(&mut self, snapshot: &mut Snapshot) -> Result<Option<String>, Error> {
135 if self.failed.load(Ordering::Acquire) {
136 return Err(Error::PublicationWorkerFailed);
137 }
138 let revision = self.revision + 1;
139 snapshot.v = 3;
140 snapshot.session_id = self.session_id.clone();
141 snapshot.revision = revision;
142 if let Some(lease) = self.evidence_lease.as_ref() {
143 snapshot.provider_evidence =
144 lease.collect(&self.session_id, revision, snapshot.columns, snapshot.rows);
145 }
146 let body = serde_json::to_string(&snapshot).map_err(|_| {
147 Error::Protocol(crate::error::Violation::new(
148 "frame-malformed",
149 "snapshot is not JSON-serialisable",
150 ))
151 })?;
152 let parsed: Value = serde_json::from_str(&body).expect("just serialised");
153 validate_snapshot(&parsed, &self.limits)?;
154 let marker = if self.marker_enabled {
155 Some(encode_marker(&self.token, &self.session_id, revision)?)
156 } else {
157 None
158 };
159 let tree_frame = Some(encode_frame(
160 &SemanticFullMessage::new(snapshot),
161 self.limits.max_frame_bytes,
162 )?);
163 let commit_frame =
164 encode_frame(&RevisionCommit::new(revision), self.limits.max_frame_bytes)?;
165 let publication = Publication {
166 revision,
167 tree_frame,
168 commit_frame,
169 };
170 let Some(sender) = self.sender.as_ref() else {
171 self.failed.store(true, Ordering::Release);
172 return Err(Error::PublicationWorkerFailed);
173 };
174 match sender.try_send(publication) {
175 Ok(()) => {
176 self.revision = revision;
177 Ok(marker)
178 }
179 Err(mpsc::TrySendError::Full(_)) => {
180 self.dropped.fetch_add(1, Ordering::Relaxed);
181 Err(Error::PublicationQueueFull)
182 }
183 Err(mpsc::TrySendError::Disconnected(_)) => {
184 self.failed.store(true, Ordering::Release);
185 Err(Error::PublicationWorkerFailed)
186 }
187 }
188 }
189
190 pub fn dropped(&self) -> u64 {
192 self.dropped.load(Ordering::Relaxed)
193 }
194
195 pub fn failed(&self) -> bool {
197 self.failed.load(Ordering::Acquire)
198 }
199
200 pub fn fail(&mut self, code: impl Into<String>, message: impl Into<String>) {
203 self.failed.store(true, Ordering::Release);
204 if let Ok(mut fatal) = self.fatal.lock() {
205 *fatal = Some((code.into(), message.into()));
206 }
207 self.sender.take();
210 }
211
212 pub fn shutdown(mut self) -> bool {
217 self.sender.take();
218 if let Some(worker) = self.worker.take() {
219 let _ = worker.join();
220 }
221 !self.failed.load(Ordering::Acquire)
222 }
223}
224
225#[cfg(test)]
226struct TestWorkerGate {
227 entered: mpsc::SyncSender<()>,
228 release: Mutex<mpsc::Receiver<()>>,
229}
230
231#[cfg(test)]
232impl TestWorkerGate {
233 fn enter(&self) {
234 let _ = self.entered.send(());
235 let _ = self.release.lock().expect("gate lock").recv();
236 }
237}
238
239#[cfg(all(test, unix))]
240mod tests {
241 use super::*;
242 use std::io::Read;
243 use std::os::unix::net::UnixStream;
244
245 use crate::FrameDecoder;
246 use crate::{Node, Role};
247
248 fn snapshot() -> Snapshot {
249 let mut snapshot = Snapshot::new(80, 24);
250 snapshot.push(Node::new("root", Role::Application, "fixture"));
251 snapshot
252 }
253
254 fn gated_queue(
255 capacity: usize,
256 ) -> (
257 PublicationQueue,
258 UnixStream,
259 mpsc::Receiver<()>,
260 mpsc::SyncSender<()>,
261 ) {
262 let (client_stream, server_stream) = UnixStream::pair().expect("socket pair");
263 let client = Client::test_connected(client_stream);
264 let (entered_tx, entered_rx) = mpsc::sync_channel(1);
265 let (release_tx, release_rx) = mpsc::sync_channel(1);
266 let gate = Arc::new(TestWorkerGate {
267 entered: entered_tx,
268 release: Mutex::new(release_rx),
269 });
270 let queue = PublicationQueue::new_inner(client, capacity, Some(gate)).expect("queue");
271 (queue, server_stream, entered_rx, release_tx)
272 }
273
274 #[test]
275 fn full_queue_drops_without_revision_gap_or_marker() {
276 let (mut queue, mut server, entered, release) = gated_queue(1);
277 assert!(queue.publish(&mut snapshot()).unwrap().is_some());
278 entered.recv().expect("worker entered first job");
279 assert!(queue.publish(&mut snapshot()).unwrap().is_some());
280 assert!(matches!(
281 queue.publish(&mut snapshot()),
282 Err(Error::PublicationQueueFull)
283 ));
284 assert_eq!(queue.revision, 2);
285 assert_eq!(queue.dropped(), 1);
286 release.send(()).unwrap();
287 entered.recv().expect("worker entered second job");
288 release.send(()).unwrap();
289 queue.fail("test-complete", "close");
290 queue.done.lock().unwrap().recv().expect("worker stopped");
291 let mut bytes = Vec::new();
292 let _ = server.read_to_end(&mut bytes);
293 let mut decoder = FrameDecoder::new(
294 crate::DEFAULT_LIMITS.max_frame_bytes,
295 crate::DEFAULT_LIMITS.max_depth,
296 );
297 let frames = decoder.push(&bytes).expect("ordered frames decode");
298 let names: Vec<_> = frames
299 .iter()
300 .take(4)
301 .map(|frame| frame.value["type"].as_str())
302 .collect();
303 assert_eq!(
304 names,
305 [
306 Some("semantic-full"),
307 Some("revision-commit"),
308 Some("semantic-full"),
309 Some("revision-commit")
310 ]
311 );
312 }
313
314 #[test]
315 fn worker_failure_permanently_refuses_later_markers() {
316 let (mut queue, server, entered, release) = gated_queue(1);
317 assert!(queue.publish(&mut snapshot()).unwrap().is_some());
318 entered.recv().expect("worker entered");
319 drop(server);
320 release.send(()).unwrap();
321 queue.done.lock().unwrap().recv().expect("worker stopped");
324 assert!(matches!(
325 queue.publish(&mut snapshot()),
326 Err(Error::PublicationWorkerFailed)
327 ));
328 }
329
330 #[test]
331 fn shutdown_drains_an_admitted_one_frame_process_before_returning() {
332 let (client_stream, mut server) = UnixStream::pair().expect("socket pair");
333 let client = Client::test_connected(client_stream);
334 let mut queue = PublicationQueue::new(client, 1).expect("queue");
335 assert!(queue.publish(&mut snapshot()).unwrap().is_some());
336
337 assert!(queue.shutdown(), "clean drain reported worker failure");
338
339 let mut bytes = Vec::new();
340 server
341 .read_to_end(&mut bytes)
342 .expect("worker closed socket");
343 let mut decoder = FrameDecoder::new(
344 crate::DEFAULT_LIMITS.max_frame_bytes,
345 crate::DEFAULT_LIMITS.max_depth,
346 );
347 let frames = decoder.push(&bytes).expect("ordered frames decode");
348 assert_eq!(frames.len(), 2);
349 assert_eq!(frames[0].value["type"], "semantic-full");
350 assert_eq!(frames[1].value["type"], "revision-commit");
351 }
352}