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, Debug)]
77pub struct ObserverPublicationTarget {
78 inbox: Weak<Mutex<ReadyPublications>>,
79 waker: ReadyWaker,
80}
81
82impl ObserverPublicationTarget {
83 pub(crate) fn publish(
88 &self,
89 publication: ObserverPublication,
90 ) -> Result<bool, ParticipantPublicationError> {
91 let Some(inbox) = self.inbox.upgrade() else {
92 return Ok(false);
93 };
94 let should_wake = {
95 let mut inbox = inbox
96 .lock()
97 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
98 let replacing = inbox
99 .observer_progressed
100 .contains_key(&publication.conversation_id);
101 if !replacing {
102 let occupied = u64::try_from(inbox.observer_progressed.len()).unwrap_or(u64::MAX);
103 if occupied >= inbox.limit {
104 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
105 }
106 }
107 let was_empty = inbox.is_empty();
108 inbox
109 .observer_progressed
110 .insert(publication.conversation_id, publication);
111 was_empty
112 };
113 if should_wake {
114 self.waker.fire();
115 }
116 Ok(true)
117 }
118}
119
120#[derive(Clone, Copy, Debug, thiserror::Error)]
121pub enum ParticipantPublicationError {
122 #[error("participant publication incarnation {incarnation:?} is already registered")]
124 DuplicateRegistration {
125 incarnation: ConnectionIncarnation,
127 },
128 #[error("participant publication inbox is poisoned")]
130 InboxPoisoned,
131 #[error("participant publication inbox exceeds its signed conversation bound {limit}")]
133 InboxCapacity {
134 limit: u64,
136 },
137}
138
139#[derive(Debug)]
140struct ReadyPublications {
141 limit: u64,
142 conversations: BTreeSet<ConversationId>,
143 observer_progressed: BTreeMap<ConversationId, ObserverPublication>,
144}
145
146impl ReadyPublications {
147 fn is_empty(&self) -> bool {
148 self.conversations.is_empty() && self.observer_progressed.is_empty()
149 }
150}
151
152#[derive(Debug)]
156pub struct ReadyPublicationBatch {
157 pub(crate) conversations: Vec<ConversationId>,
158 pub(crate) observer_progressed: Vec<ObserverPublication>,
159}
160
161#[derive(Debug)]
168pub struct ParticipantPublicationInbox {
169 inner: Arc<Mutex<ReadyPublications>>,
170}
171
172impl ParticipantPublicationInbox {
173 #[must_use]
176 pub(crate) fn new(limit: u64) -> Self {
177 Self {
178 inner: Arc::new(Mutex::new(ReadyPublications {
179 limit,
180 conversations: BTreeSet::new(),
181 observer_progressed: BTreeMap::new(),
182 })),
183 }
184 }
185
186 fn weak(&self) -> Weak<Mutex<ReadyPublications>> {
187 Arc::downgrade(&self.inner)
188 }
189
190 pub(crate) fn take_ready(&self) -> Result<ReadyPublicationBatch, ParticipantPublicationError> {
192 let mut inbox = self
193 .inner
194 .lock()
195 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
196 Ok(ReadyPublicationBatch {
197 conversations: std::mem::take(&mut inbox.conversations)
198 .into_iter()
199 .collect(),
200 observer_progressed: std::mem::take(&mut inbox.observer_progressed)
201 .into_values()
202 .collect(),
203 })
204 }
205
206 pub(crate) fn requeue(
209 &self,
210 conversations: impl IntoIterator<Item = ConversationId>,
211 ) -> Result<(), ParticipantPublicationError> {
212 let mut inbox = self
213 .inner
214 .lock()
215 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
216 for conversation_id in conversations {
217 if inbox.conversations.contains(&conversation_id) {
218 continue;
219 }
220 let occupied = u64::try_from(inbox.conversations.len()).unwrap_or(u64::MAX);
221 if occupied >= inbox.limit {
222 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
223 }
224 inbox.conversations.insert(conversation_id);
225 }
226 drop(inbox);
227 Ok(())
228 }
229
230 pub(crate) fn requeue_observers(
234 &self,
235 publications: impl IntoIterator<Item = ObserverPublication>,
236 ) -> Result<(), ParticipantPublicationError> {
237 let mut inbox = self
238 .inner
239 .lock()
240 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
241 for publication in publications {
242 if inbox
243 .observer_progressed
244 .contains_key(&publication.conversation_id)
245 {
246 continue;
247 }
248 let occupied = u64::try_from(inbox.observer_progressed.len()).unwrap_or(u64::MAX);
249 if occupied >= inbox.limit {
250 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
251 }
252 inbox
253 .observer_progressed
254 .insert(publication.conversation_id, publication);
255 }
256 drop(inbox);
257 Ok(())
258 }
259
260 pub(crate) fn has_pending(&self) -> Result<bool, ParticipantPublicationError> {
262 self.inner
263 .lock()
264 .map(|inbox| !inbox.is_empty())
265 .map_err(|_| ParticipantPublicationError::InboxPoisoned)
266 }
267}
268
269#[derive(Debug)]
270struct ParticipantPublicationHandle {
271 inbox: Weak<Mutex<ReadyPublications>>,
272 waker: ReadyWaker,
273}
274
275#[derive(Debug, Default)]
277pub struct ParticipantPublicationRegistry {
278 registrations: Mutex<BTreeMap<ConnectionIncarnation, ParticipantPublicationHandle>>,
279 #[cfg(test)]
280 ready_fires: AtomicU64,
281}
282
283impl ParticipantPublicationRegistry {
284 pub(crate) fn register(
287 &self,
288 incarnation: ConnectionIncarnation,
289 inbox: &ParticipantPublicationInbox,
290 waker: ReadyWaker,
291 ) -> Result<(), ParticipantPublicationError> {
292 let mut registrations = self
293 .registrations
294 .lock()
295 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
296 if registrations
297 .get(&incarnation)
298 .is_some_and(|existing| existing.inbox.strong_count() > 0)
299 {
300 return Err(ParticipantPublicationError::DuplicateRegistration { incarnation });
301 }
302 registrations.insert(
303 incarnation,
304 ParticipantPublicationHandle {
305 inbox: inbox.weak(),
306 waker,
307 },
308 );
309 drop(registrations);
310 Ok(())
311 }
312
313 pub(crate) fn deregister(&self, incarnation: ConnectionIncarnation) {
316 if let Ok(mut registrations) = self.registrations.lock() {
317 registrations.remove(&incarnation);
318 }
319 }
320
321 pub(crate) fn observer_target(
325 &self,
326 incarnation: ConnectionIncarnation,
327 ) -> Result<Option<ObserverPublicationTarget>, ParticipantPublicationError> {
328 let registrations = self
329 .registrations
330 .lock()
331 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
332 let target = registrations.get(&incarnation).and_then(|handle| {
333 (handle.inbox.strong_count() > 0).then(|| ObserverPublicationTarget {
334 inbox: Weak::clone(&handle.inbox),
335 waker: handle.waker.clone(),
336 })
337 });
338 drop(registrations);
339 Ok(target)
340 }
341
342 pub(crate) fn notify(
347 &self,
348 incarnation: ConnectionIncarnation,
349 conversation_id: ConversationId,
350 ) -> Result<bool, ParticipantPublicationError> {
351 let (weak_inbox, waker) = {
352 let registrations = self
353 .registrations
354 .lock()
355 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
356 let Some(handle) = registrations.get(&incarnation) else {
357 return Ok(false);
358 };
359 let weak_inbox = Weak::clone(&handle.inbox);
360 let waker = handle.waker.clone();
361 drop(registrations);
362 (weak_inbox, waker)
363 };
364 let Some(inbox) = weak_inbox.upgrade() else {
365 self.deregister(incarnation);
366 return Ok(false);
367 };
368 let should_wake = {
369 let mut inbox = inbox
370 .lock()
371 .map_err(|_| ParticipantPublicationError::InboxPoisoned)?;
372 if inbox.conversations.contains(&conversation_id) {
373 return Ok(true);
374 }
375 let occupied = u64::try_from(inbox.conversations.len()).unwrap_or(u64::MAX);
376 if occupied >= inbox.limit {
377 return Err(ParticipantPublicationError::InboxCapacity { limit: inbox.limit });
378 }
379 let was_empty = inbox.is_empty();
380 inbox.conversations.insert(conversation_id);
381 was_empty
382 };
383 if should_wake {
384 #[cfg(test)]
385 self.ready_fires.fetch_add(1, Ordering::SeqCst);
386 waker.fire();
387 }
388 Ok(true)
389 }
390
391 #[cfg(test)]
392 pub(crate) fn ready_fire_count(&self) -> u64 {
393 self.ready_fires.load(Ordering::SeqCst)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use std::sync::Arc;
400 use std::sync::atomic::{AtomicU64, Ordering};
401
402 use liminal_protocol::wire::ConnectionIncarnation;
403
404 use super::{ParticipantPublicationInbox, ParticipantPublicationRegistry};
405 use crate::server::connection::ReadyWaker;
406
407 #[test]
408 fn parked_connection_wakes_on_outbox_and_no_polling_occurs()
409 -> Result<(), Box<dyn std::error::Error>> {
410 let incarnation = ConnectionIncarnation::new(12, 34);
411 let wake_count = Arc::new(AtomicU64::new(0));
412 let registry = ParticipantPublicationRegistry::default();
413 let inbox = ParticipantPublicationInbox::new(3);
414 registry.register(
415 incarnation,
416 &inbox,
417 ReadyWaker::for_test(Arc::clone(&wake_count)),
418 )?;
419
420 assert!(!inbox.has_pending()?);
421 assert!(registry.notify(incarnation, 7)?);
422 assert_eq!(wake_count.load(Ordering::SeqCst), 1);
423 assert!(inbox.has_pending()?);
424
425 assert!(registry.notify(incarnation, 7)?);
428 assert!(registry.notify(incarnation, 8)?);
429 assert_eq!(wake_count.load(Ordering::SeqCst), 1);
430 let ready = inbox.take_ready()?;
431 assert_eq!(ready.conversations, vec![7, 8]);
432 assert!(ready.observer_progressed.is_empty());
433 assert!(!inbox.has_pending()?);
434
435 assert!(registry.notify(incarnation, 9)?);
439 assert!(inbox.has_pending()?);
440 assert_eq!(wake_count.load(Ordering::SeqCst), 2);
441 let ready = inbox.take_ready()?;
442 assert_eq!(ready.conversations, vec![9]);
443 assert!(ready.observer_progressed.is_empty());
444 assert!(!inbox.has_pending()?);
445
446 let idle_count = wake_count.load(Ordering::SeqCst);
447 assert_eq!(wake_count.load(Ordering::SeqCst), idle_count);
448 registry.deregister(incarnation);
449 assert!(!registry.notify(incarnation, 10)?);
450 assert_eq!(wake_count.load(Ordering::SeqCst), idle_count);
451 Ok(())
452 }
453}