liminal_server/server/participant/
publication.rs1use std::collections::{BTreeMap, BTreeSet};
10#[cfg(test)]
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, Mutex, Weak};
13
14use liminal_protocol::wire::{
15 BindingEpoch, ConnectionIncarnation, ConversationId, ParticipantDelivery, ParticipantId,
16 ServerPush,
17};
18
19use crate::server::connection::ReadyWaker;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct ParticipantOfferedProgress {
26 pub(crate) binding_epoch: BindingEpoch,
27 pub(crate) through_seq: u64,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct ParticipantPublication {
33 pub(crate) participant_id: ParticipantId,
34 pub(crate) binding_epoch: BindingEpoch,
35 pub(crate) delivery: ParticipantDelivery,
36}
37
38impl ParticipantPublication {
39 #[must_use]
40 pub(crate) const fn conversation_id(&self) -> ConversationId {
41 self.delivery.conversation_id
42 }
43
44 #[must_use]
45 pub(crate) const fn delivery_seq(&self) -> u64 {
46 self.delivery.delivery_seq
47 }
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub struct ObserverPublication {
56 pub(crate) conversation_id: ConversationId,
57 pub(crate) refused_epoch: u64,
58 pub(crate) observer_progress: u64,
59}
60
61impl ObserverPublication {
62 #[must_use]
63 pub(crate) const fn into_server_push(self) -> ServerPush {
64 ServerPush::ObserverProgressed {
65 conversation_id: self.conversation_id,
66 refused_epoch: self.refused_epoch,
67 observer_progress: self.observer_progress,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct MarkerSettledPublication {
86 pub(crate) conversation_id: ConversationId,
87 pub(crate) refused_epoch: u64,
88}
89
90impl MarkerSettledPublication {
91 #[must_use]
92 pub(crate) const fn into_server_push(self) -> ServerPush {
93 ServerPush::MarkerSettled {
94 conversation_id: self.conversation_id,
95 refused_epoch: self.refused_epoch,
96 }
97 }
98}
99
100#[derive(Clone, Debug)]
105pub struct ObserverPublicationTarget {
106 inbox: Weak<Mutex<ReadyPublications>>,
107 waker: ReadyWaker,
108}
109
110impl ObserverPublicationTarget {
111 pub(crate) fn publish(
116 &self,
117 publication: ObserverPublication,
118 ) -> Result<bool, ParticipantPublicationError> {
119 let Some(inbox) = self.inbox.upgrade() else {
120 return Ok(false);
121 };
122 let should_wake = {
123 let mut inbox = inbox
124 .lock()
125 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
126 let replacing = inbox
127 .observer_progressed
128 .contains_key(&publication.conversation_id);
129 if !replacing {
130 let occupied = u64::try_from(inbox.observer_progressed.len()).unwrap_or(u64::MAX);
131 if occupied >= inbox.limit {
132 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
133 }
134 }
135 let was_empty = inbox.is_empty();
136 inbox
137 .observer_progressed
138 .insert(publication.conversation_id, publication);
139 was_empty
140 };
141 if should_wake {
142 self.waker.fire();
143 }
144 Ok(true)
145 }
146}
147
148impl ObserverPublicationTarget {
149 pub(crate) fn publish_marker_settled(
159 &self,
160 publication: MarkerSettledPublication,
161 ) -> Result<bool, ParticipantPublicationError> {
162 let Some(inbox) = self.inbox.upgrade() else {
163 return Ok(false);
164 };
165 let should_wake = {
166 let mut inbox = inbox
167 .lock()
168 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
169 let replacing = inbox
170 .marker_settled
171 .contains_key(&publication.conversation_id);
172 if !replacing {
173 let occupied = u64::try_from(inbox.marker_settled.len()).unwrap_or(u64::MAX);
174 if occupied >= inbox.limit {
175 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
176 }
177 }
178 let was_empty = inbox.is_empty();
179 inbox
180 .marker_settled
181 .insert(publication.conversation_id, publication);
182 was_empty
183 };
184 if should_wake {
185 self.waker.fire();
186 }
187 Ok(true)
188 }
189}
190
191#[derive(Clone, Copy, Debug, thiserror::Error)]
192pub enum ParticipantPublicationError {
193 #[error("participant publication incarnation {incarnation:?} is already registered")]
195 DuplicateRegistration {
196 incarnation: ConnectionIncarnation,
198 },
199 #[error("participant publication inbox is poisoned")]
201 InboxPoisoned,
202 #[error("participant publication inbox exceeds its signed conversation bound {limit}")]
204 InboxCapacity {
205 limit: u64,
207 },
208}
209
210#[derive(Debug)]
211struct ReadyPublications {
212 limit: u64,
213 conversations: BTreeSet<ConversationId>,
214 observer_progressed: BTreeMap<ConversationId, ObserverPublication>,
215 marker_settled: BTreeMap<ConversationId, MarkerSettledPublication>,
216}
217
218impl ReadyPublications {
219 fn is_empty(&self) -> bool {
220 self.conversations.is_empty()
221 && self.observer_progressed.is_empty()
222 && self.marker_settled.is_empty()
223 }
224}
225
226#[derive(Debug)]
230pub struct ReadyPublicationBatch {
231 pub(crate) conversations: Vec<ConversationId>,
232 pub(crate) observer_progressed: Vec<ObserverPublication>,
233 pub(crate) marker_settled: Vec<MarkerSettledPublication>,
234}
235
236#[derive(Debug)]
243pub struct ParticipantPublicationInbox {
244 inner: Arc<Mutex<ReadyPublications>>,
245}
246
247impl ParticipantPublicationInbox {
248 #[must_use]
251 pub(crate) fn new(limit: u64) -> Self {
252 Self {
253 inner: Arc::new(Mutex::new(ReadyPublications {
254 limit,
255 conversations: BTreeSet::new(),
256 observer_progressed: BTreeMap::new(),
257 marker_settled: BTreeMap::new(),
258 })),
259 }
260 }
261
262 fn weak(&self) -> Weak<Mutex<ReadyPublications>> {
263 Arc::downgrade(&self.inner)
264 }
265
266 pub(crate) fn take_ready(&self) -> Result<ReadyPublicationBatch, ParticipantPublicationError> {
268 let mut inbox = self
269 .inner
270 .lock()
271 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
272 Ok(ReadyPublicationBatch {
273 conversations: std::mem::take(&mut inbox.conversations)
274 .into_iter()
275 .collect(),
276 observer_progressed: std::mem::take(&mut inbox.observer_progressed)
277 .into_values()
278 .collect(),
279 marker_settled: std::mem::take(&mut inbox.marker_settled)
280 .into_values()
281 .collect(),
282 })
283 }
284
285 pub(crate) fn requeue(
288 &self,
289 conversations: impl IntoIterator<Item = ConversationId>,
290 ) -> Result<(), ParticipantPublicationError> {
291 let mut inbox = self
292 .inner
293 .lock()
294 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
295 for conversation_id in conversations {
296 if inbox.conversations.contains(&conversation_id) {
297 continue;
298 }
299 let occupied = u64::try_from(inbox.conversations.len()).unwrap_or(u64::MAX);
300 if occupied >= inbox.limit {
301 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
302 }
303 inbox.conversations.insert(conversation_id);
304 }
305 drop(inbox);
306 Ok(())
307 }
308
309 pub(crate) fn requeue_observers(
313 &self,
314 publications: impl IntoIterator<Item = ObserverPublication>,
315 ) -> Result<(), ParticipantPublicationError> {
316 let mut inbox = self
317 .inner
318 .lock()
319 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
320 for publication in publications {
321 if inbox
322 .observer_progressed
323 .contains_key(&publication.conversation_id)
324 {
325 continue;
326 }
327 let occupied = u64::try_from(inbox.observer_progressed.len()).unwrap_or(u64::MAX);
328 if occupied >= inbox.limit {
329 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
330 }
331 inbox
332 .observer_progressed
333 .insert(publication.conversation_id, publication);
334 }
335 drop(inbox);
336 Ok(())
337 }
338
339 pub(crate) fn requeue_marker_settled(
343 &self,
344 publications: impl IntoIterator<Item = MarkerSettledPublication>,
345 ) -> Result<(), ParticipantPublicationError> {
346 let mut inbox = self
347 .inner
348 .lock()
349 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
350 for publication in publications {
351 if inbox
352 .marker_settled
353 .contains_key(&publication.conversation_id)
354 {
355 continue;
356 }
357 let occupied = u64::try_from(inbox.marker_settled.len()).unwrap_or(u64::MAX);
358 if occupied >= inbox.limit {
359 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
360 }
361 inbox
362 .marker_settled
363 .insert(publication.conversation_id, publication);
364 }
365 drop(inbox);
366 Ok(())
367 }
368
369 pub(crate) fn has_pending(&self) -> Result<bool, ParticipantPublicationError> {
371 self.inner
372 .lock()
373 .map(|inbox| !inbox.is_empty())
374 .map_err(|_| ParticipantPublicationError::InboxPoisoned)
375 }
376}
377
378#[derive(Debug)]
379struct ParticipantPublicationHandle {
380 inbox: Weak<Mutex<ReadyPublications>>,
381 waker: ReadyWaker,
382}
383
384#[derive(Debug, Default)]
386pub struct ParticipantPublicationRegistry {
387 registrations: Mutex<BTreeMap<ConnectionIncarnation, ParticipantPublicationHandle>>,
388 #[cfg(test)]
389 ready_fires: AtomicU64,
390}
391
392impl ParticipantPublicationRegistry {
393 pub(crate) fn register(
396 &self,
397 incarnation: ConnectionIncarnation,
398 inbox: &ParticipantPublicationInbox,
399 waker: ReadyWaker,
400 ) -> Result<(), ParticipantPublicationError> {
401 let mut registrations = self
402 .registrations
403 .lock()
404 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
405 if registrations
406 .get(&incarnation)
407 .is_some_and(|existing| existing.inbox.strong_count() > 0)
408 {
409 return Err(ParticipantPublicationError::DuplicateRegistration { incarnation });
410 }
411 registrations.insert(
412 incarnation,
413 ParticipantPublicationHandle {
414 inbox: inbox.weak(),
415 waker,
416 },
417 );
418 drop(registrations);
419 Ok(())
420 }
421
422 pub(crate) fn deregister(&self, incarnation: ConnectionIncarnation) {
425 if let Ok(mut registrations) = self.registrations.lock() {
426 registrations.remove(&incarnation);
427 }
428 }
429
430 pub(crate) fn observer_target(
434 &self,
435 incarnation: ConnectionIncarnation,
436 ) -> Result<Option<ObserverPublicationTarget>, ParticipantPublicationError> {
437 let registrations = self
438 .registrations
439 .lock()
440 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
441 let target = registrations.get(&incarnation).and_then(|handle| {
442 (handle.inbox.strong_count() > 0).then(|| ObserverPublicationTarget {
443 inbox: Weak::clone(&handle.inbox),
444 waker: handle.waker.clone(),
445 })
446 });
447 drop(registrations);
448 Ok(target)
449 }
450
451 pub(crate) fn notify(
456 &self,
457 incarnation: ConnectionIncarnation,
458 conversation_id: ConversationId,
459 ) -> Result<bool, ParticipantPublicationError> {
460 let (weak_inbox, waker) = {
461 let registrations = self
462 .registrations
463 .lock()
464 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
465 let Some(handle) = registrations.get(&incarnation) else {
466 return Ok(false);
467 };
468 let weak_inbox = Weak::clone(&handle.inbox);
469 let waker = handle.waker.clone();
470 drop(registrations);
471 (weak_inbox, waker)
472 };
473 let Some(inbox) = weak_inbox.upgrade() else {
474 self.deregister(incarnation);
475 return Ok(false);
476 };
477 let should_wake = {
478 let mut inbox = inbox
479 .lock()
480 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
481 if inbox.conversations.contains(&conversation_id) {
482 return Ok(true);
483 }
484 let occupied = u64::try_from(inbox.conversations.len()).unwrap_or(u64::MAX);
485 if occupied >= inbox.limit {
486 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
487 }
488 let was_empty = inbox.is_empty();
489 inbox.conversations.insert(conversation_id);
490 was_empty
491 };
492 if should_wake {
493 #[cfg(test)]
494 self.ready_fires.fetch_add(1, Ordering::SeqCst);
495 waker.fire();
496 }
497 Ok(true)
498 }
499
500 #[cfg(test)]
501 pub(crate) fn ready_fire_count(&self) -> u64 {
502 self.ready_fires.load(Ordering::SeqCst)
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use std::sync::Arc;
509 use std::sync::atomic::{AtomicU64, Ordering};
510
511 use liminal_protocol::wire::ConnectionIncarnation;
512
513 use super::{ParticipantPublicationInbox, ParticipantPublicationRegistry};
514 use crate::server::connection::ReadyWaker;
515
516 #[test]
517 fn parked_connection_wakes_on_outbox_and_no_polling_occurs()
518 -> Result<(), Box<dyn std::error::Error>> {
519 let incarnation = ConnectionIncarnation::new(12, 34);
520 let wake_count = Arc::new(AtomicU64::new(0));
521 let registry = ParticipantPublicationRegistry::default();
522 let inbox = ParticipantPublicationInbox::new(3);
523 registry.register(
524 incarnation,
525 &inbox,
526 ReadyWaker::for_test(Arc::clone(&wake_count)),
527 )?;
528
529 assert!(!inbox.has_pending()?);
530 assert!(registry.notify(incarnation, 7)?);
531 assert_eq!(wake_count.load(Ordering::SeqCst), 1);
532 assert!(inbox.has_pending()?);
533
534 assert!(registry.notify(incarnation, 7)?);
537 assert!(registry.notify(incarnation, 8)?);
538 assert_eq!(wake_count.load(Ordering::SeqCst), 1);
539 let ready = inbox.take_ready()?;
540 assert_eq!(ready.conversations, vec![7, 8]);
541 assert!(ready.observer_progressed.is_empty());
542 assert!(ready.marker_settled.is_empty());
543 assert!(!inbox.has_pending()?);
544
545 assert!(registry.notify(incarnation, 9)?);
549 assert!(inbox.has_pending()?);
550 assert_eq!(wake_count.load(Ordering::SeqCst), 2);
551 let ready = inbox.take_ready()?;
552 assert_eq!(ready.conversations, vec![9]);
553 assert!(ready.observer_progressed.is_empty());
554 assert!(!inbox.has_pending()?);
555
556 let idle_count = wake_count.load(Ordering::SeqCst);
557 assert_eq!(wake_count.load(Ordering::SeqCst), idle_count);
558 registry.deregister(incarnation);
559 assert!(!registry.notify(incarnation, 10)?);
560 assert_eq!(wake_count.load(Ordering::SeqCst), idle_count);
561 Ok(())
562 }
563}