1use std::time::Duration;
4
5#[cfg(test)]
6use behavior::Recipient;
7use behavior::{
8 Actions, Address, Behavior, BehaviorActed, BehaviorBase, EventLayer, InterpreterRequests,
9 Never, NoBirths, SendEffects, User,
10};
11use thiserror::Error;
12
13use crate::DeliveryRoute;
14use crate::{ScheduleAfter, TimedEvent, TimerGeneration, TimerId};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct PresenceVersion(pub u64);
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum PresencePhase {
23 Present {
25 version: PresenceVersion,
27 generation: TimerGeneration,
29 lifetime: Duration,
31 },
32 Expired {
34 version: PresenceVersion,
36 generation: TimerGeneration,
38 },
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct PresenceEntry<K> {
44 pub participant: K,
46 pub timer_id: TimerId,
48 pub phase: PresencePhase,
50}
51
52#[derive(Debug, Error, Clone, PartialEq, Eq)]
54pub enum PresenceError<K> {
55 #[error("presence evidence is stale")]
57 Stale {
58 participant: K,
60 observed: PresenceVersion,
62 current: PresenceVersion,
64 lifetime: Duration,
66 },
67 #[error("presence evidence conflicts at the committed version")]
69 ConflictingVersion {
70 participant: K,
72 version: PresenceVersion,
74 lifetime: Duration,
76 },
77 #[error("presence timer key collides with a live participant")]
79 TimerCollision {
80 participant: K,
82 existing: K,
84 timer_id: TimerId,
86 version: PresenceVersion,
88 lifetime: Duration,
90 },
91 #[error("presence timer generation is exhausted")]
93 GenerationExhausted {
94 participant: K,
96 version: PresenceVersion,
98 lifetime: Duration,
100 },
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum PresenceOutcome<K> {
106 Announced {
108 participant: K,
110 version: PresenceVersion,
112 generation: TimerGeneration,
114 },
115 Refreshed {
117 participant: K,
119 version: PresenceVersion,
121 generation: TimerGeneration,
123 },
124 Unchanged {
126 participant: K,
128 version: PresenceVersion,
130 generation: TimerGeneration,
132 },
133 Expired {
135 participant: K,
137 version: PresenceVersion,
139 generation: TimerGeneration,
141 },
142 Rejected(PresenceError<K>),
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct PresenceReport<K> {
149 pub entries: Vec<PresenceEntry<K>>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum PresenceReply<K> {
156 Outcome(PresenceOutcome<K>),
158 Report(PresenceReport<K>),
160}
161
162pub enum PresenceMessage<K, Route> {
164 Announce {
166 participant: K,
168 version: PresenceVersion,
170 lifetime: Duration,
172 reply_to: Route,
174 },
175 Query {
177 reply_to: Route,
179 },
180}
181
182#[derive(behavior_macros::SendProduct)]
184pub struct PresenceSends<ReplySends, Schedules> {
185 pub replies: ReplySends,
187 pub schedules: Schedules,
189}
190
191struct Record<K, Route> {
192 entry: PresenceEntry<K>,
193 notify: Route,
194}
195
196pub struct Presence<
212 A: Address,
213 K,
214 Route: DeliveryRoute<Protocol: behavior::Protocol<Addr = A, Msg = PresenceReply<K>>>,
215> {
216 timer_id: fn(&K) -> TimerId,
217 records: Vec<Record<K, Route>>,
218 marker: core::marker::PhantomData<fn() -> A>,
219}
220type PresenceActions<A, ReplySends> =
221 Actions<A, Never, PresenceSends<ReplySends, InterpreterRequests<ScheduleAfter>>, NoBirths>;
222impl<A, K, Route> Presence<A, K, Route>
223where
224 A: Address,
225 K: Clone + Eq,
226 Route: DeliveryRoute<Protocol: behavior::Protocol<Addr = A, Msg = PresenceReply<K>>> + Clone,
227{
228 #[must_use]
230 pub const fn new(timer_id: fn(&K) -> TimerId) -> Self {
231 Self {
232 timer_id,
233 records: Vec::new(),
234 marker: core::marker::PhantomData,
235 }
236 }
237 #[must_use]
239 pub fn report(&self) -> PresenceReport<K> {
240 PresenceReport {
241 entries: self
242 .records
243 .iter()
244 .map(|record| record.entry.clone())
245 .collect(),
246 }
247 }
248 fn reply(reply_to: Route, reply: PresenceReply<K>) -> PresenceActions<A, Route::Sends> {
249 Actions::send(PresenceSends {
250 replies: reply_to.deliver(reply),
251 schedules: InterpreterRequests::empty(),
252 })
253 }
254
255 fn next_generation(&self, timer_id: TimerId) -> Option<TimerGeneration> {
256 self.records
257 .iter()
258 .filter(|record| record.entry.timer_id == timer_id)
259 .map(|record| match record.entry.phase {
260 PresencePhase::Present { generation, .. }
261 | PresencePhase::Expired { generation, .. } => generation,
262 })
263 .max_by_key(|generation| generation.0)
264 .map_or(Some(TimerGeneration(0)), |generation| {
265 generation.0.checked_add(1).map(TimerGeneration)
266 })
267 }
268 fn announce(
269 &mut self,
270 participant: K,
271 version: PresenceVersion,
272 lifetime: Duration,
273 reply_to: Route,
274 ) -> PresenceActions<A, Route::Sends> {
275 let timer_id = (self.timer_id)(&participant);
276 if let Some(existing) = self.records.iter().find(|record| {
277 record.entry.participant != participant
278 && record.entry.timer_id == timer_id
279 && matches!(record.entry.phase, PresencePhase::Present { .. })
280 }) {
281 return Self::reply(
282 reply_to,
283 PresenceReply::Outcome(PresenceOutcome::Rejected(PresenceError::TimerCollision {
284 participant,
285 existing: existing.entry.participant.clone(),
286 timer_id,
287 version,
288 lifetime,
289 })),
290 );
291 }
292 let existing = self
293 .records
294 .iter()
295 .position(|record| record.entry.participant == participant);
296 match existing {
297 Some(index) => self.refresh(index, participant, version, lifetime, reply_to, timer_id),
298 None => self.introduce(participant, version, lifetime, reply_to, timer_id),
299 }
300 }
301
302 fn introduce(
303 &mut self,
304 participant: K,
305 version: PresenceVersion,
306 lifetime: Duration,
307 reply_to: Route,
308 timer_id: TimerId,
309 ) -> PresenceActions<A, Route::Sends> {
310 let Some(generation) = self.next_generation(timer_id) else {
311 return Self::reply(
312 reply_to,
313 PresenceReply::Outcome(PresenceOutcome::Rejected(
314 PresenceError::GenerationExhausted {
315 participant,
316 version,
317 lifetime,
318 },
319 )),
320 );
321 };
322 self.records.push(Record {
323 entry: PresenceEntry {
324 participant: participant.clone(),
325 timer_id,
326 phase: PresencePhase::Present {
327 version,
328 generation,
329 lifetime,
330 },
331 },
332 notify: reply_to.clone(),
333 });
334 Actions::send(PresenceSends {
335 replies: reply_to.deliver(PresenceReply::Outcome(PresenceOutcome::Announced {
336 participant,
337 version,
338 generation,
339 })),
340 schedules: InterpreterRequests::one(ScheduleAfter::new(timer_id, generation, lifetime)),
341 })
342 }
343
344 fn refresh(
345 &mut self,
346 index: usize,
347 participant: K,
348 version: PresenceVersion,
349 lifetime: Duration,
350 reply_to: Route,
351 timer_id: TimerId,
352 ) -> PresenceActions<A, Route::Sends> {
353 let current_version = match self.records[index].entry.phase {
354 PresencePhase::Present { version, .. } | PresencePhase::Expired { version, .. } => {
355 version
356 }
357 };
358 if version < current_version {
359 return Self::reply(
360 reply_to,
361 PresenceReply::Outcome(PresenceOutcome::Rejected(PresenceError::Stale {
362 participant,
363 observed: version,
364 current: current_version,
365 lifetime,
366 })),
367 );
368 }
369 if version == current_version {
370 if let PresencePhase::Present {
371 generation,
372 lifetime: current,
373 ..
374 } = self.records[index].entry.phase
375 && current == lifetime
376 {
377 return Self::reply(
378 reply_to,
379 PresenceReply::Outcome(PresenceOutcome::Unchanged {
380 participant,
381 version,
382 generation,
383 }),
384 );
385 }
386 return Self::reply(
387 reply_to,
388 PresenceReply::Outcome(PresenceOutcome::Rejected(
389 PresenceError::ConflictingVersion {
390 participant,
391 version,
392 lifetime,
393 },
394 )),
395 );
396 }
397 let Some(generation) = self.next_generation(timer_id) else {
398 return Self::reply(
399 reply_to,
400 PresenceReply::Outcome(PresenceOutcome::Rejected(
401 PresenceError::GenerationExhausted {
402 participant,
403 version,
404 lifetime,
405 },
406 )),
407 );
408 };
409 self.records[index].entry.phase = PresencePhase::Present {
410 version,
411 generation,
412 lifetime,
413 };
414 self.records[index].notify = reply_to.clone();
415 Actions::send(PresenceSends {
416 replies: reply_to.deliver(PresenceReply::Outcome(PresenceOutcome::Refreshed {
417 participant,
418 version,
419 generation,
420 })),
421 schedules: InterpreterRequests::one(ScheduleAfter::new(timer_id, generation, lifetime)),
422 })
423 }
424}
425impl<A, K, Route> BehaviorBase for Presence<A, K, Route>
426where
427 A: Address,
428 Route: DeliveryRoute<Protocol: behavior::Protocol<Addr = A, Msg = PresenceReply<K>>>,
429{
430 type Base = Self;
431 fn base(&self) -> &Self {
432 self
433 }
434}
435impl<A, K, Route> behavior::Protocol for Presence<A, K, Route>
436where
437 A: Address,
438 Route: DeliveryRoute<Protocol: behavior::Protocol<Addr = A, Msg = PresenceReply<K>>>,
439{
440 type Addr = A;
441 type Msg = PresenceMessage<K, Route>;
442}
443
444impl<A, K, Route> Behavior for Presence<A, K, Route>
445where
446 A: Address,
447 K: Clone + Eq,
448 Route: DeliveryRoute<Protocol: behavior::Protocol<Addr = A, Msg = PresenceReply<K>>> + Clone,
449 Route::Sends: behavior::SendsFor<TimedEvent<User<A, PresenceMessage<K, Route>>>>,
450{
451 type Protocol = Self;
452 type Event = TimedEvent<User<A, behavior::BehaviorMessage<Self>>>;
453 type Sends = PresenceSends<Route::Sends, InterpreterRequests<ScheduleAfter>>;
454 type Ph = Never;
455 type Error = Never;
456 type Birth = NoBirths;
457 fn transition(&mut self, _: behavior::ActiveTurn, event: Self::Event) -> BehaviorActed<Self> {
458 Ok(match event {
459 EventLayer::Inner(event) => match event.message {
460 PresenceMessage::Announce {
461 participant,
462 version,
463 lifetime,
464 reply_to,
465 } => self.announce(participant, version, lifetime, reply_to),
466 PresenceMessage::Query { reply_to } => {
467 Self::reply(reply_to, PresenceReply::Report(self.report()))
468 }
469 },
470 EventLayer::Owned(elapsed) => {
471 let Some(index)=self.records.iter().position(|record|record.entry.timer_id==elapsed.id&&matches!(record.entry.phase,PresencePhase::Present{generation,..}if generation==elapsed.generation))else{return Ok(Actions::cont());};
472 let (version, generation) = match self.records[index].entry.phase {
473 PresencePhase::Present {
474 version,
475 generation,
476 ..
477 } => (version, generation),
478 PresencePhase::Expired { .. } => return Ok(Actions::cont()),
479 };
480 let participant = self.records[index].entry.participant.clone();
481 let notify = self.records[index].notify.clone();
482 self.records[index].entry.phase = PresencePhase::Expired {
483 version,
484 generation,
485 };
486 Self::reply(
487 notify,
488 PresenceReply::Outcome(PresenceOutcome::Expired {
489 participant,
490 version,
491 generation,
492 }),
493 )
494 }
495 })
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use crate::{Activate as _, TimerElapsed};
503 use behavior::MailAddr;
504 #[derive(Debug, Clone, PartialEq, Eq)]
505 struct Participant(u8);
506 struct Reply;
507 impl behavior::Protocol for Reply {
508 type Addr = MailAddr;
509 type Msg = PresenceReply<Participant>;
510 }
511
512 impl Behavior for Reply {
513 type Protocol = Self;
514 type Event = User<MailAddr, behavior::BehaviorMessage<Self>>;
515 type Sends = Vec<Never>;
516 type Ph = Never;
517 type Error = Never;
518 type Birth = NoBirths;
519 fn transition(&mut self, _: behavior::ActiveTurn, _: Self::Event) -> BehaviorActed<Self> {
520 Ok(Actions::cont())
521 }
522 }
523 type Subject = Presence<MailAddr, Participant, Recipient<Reply>>;
524 fn reply() -> Recipient<Reply> {
525 Recipient::global(MailAddr(9))
526 }
527 fn duration() -> Duration {
528 Duration::from_secs(1)
529 }
530 fn timer(key: &Participant) -> TimerId {
531 TimerId(u64::from(key.0))
532 }
533 #[test]
534 fn refresh_and_expiry_are_version_and_generation_safe() {
535 let mut s = (Subject::new(timer)).initialize().unwrap().behavior;
536 let announced = s
537 .receive(
538 MailAddr(0),
539 PresenceMessage::Announce {
540 participant: Participant(1),
541 version: PresenceVersion(1),
542 lifetime: duration(),
543 reply_to: reply(),
544 },
545 )
546 .unwrap();
547 assert_eq!(announced.sends.schedules.len(), 1);
548 assert_eq!(announced.sends.replies.len(), 1);
549 assert!(announced.creates.is_empty());
550 assert_eq!(announced.become_, behavior::Step::Continue);
551 let refreshed = s
552 .receive(
553 MailAddr(0),
554 PresenceMessage::Announce {
555 participant: Participant(1),
556 version: PresenceVersion(2),
557 lifetime: duration(),
558 reply_to: reply(),
559 },
560 )
561 .unwrap();
562 assert_eq!(
563 refreshed.sends.schedules.as_slice()[0].generation,
564 TimerGeneration(1)
565 );
566 assert_eq!(refreshed.sends.replies.len(), 1);
567 assert!(refreshed.creates.is_empty());
568 assert_eq!(refreshed.become_, behavior::Step::Continue);
569 assert!(
570 s.on_path(TimerElapsed::new(TimerId(1), TimerGeneration(0)))
571 .unwrap()
572 .sends
573 .replies
574 .is_empty()
575 );
576 let expired = s
577 .on_path(TimerElapsed::new(TimerId(1), TimerGeneration(1)))
578 .unwrap();
579 assert!(matches!(
580 expired.sends.replies[0].message,
581 PresenceReply::Outcome(PresenceOutcome::Expired {
582 participant: Participant(1),
583 ..
584 })
585 ));
586 assert!(expired.sends.schedules.is_empty());
587 assert!(expired.creates.is_empty());
588 assert_eq!(expired.become_, behavior::Step::Continue);
589 }
590 #[test]
591 fn collision_and_stale_evidence_are_atomic() {
592 fn collision(_: &Participant) -> TimerId {
593 TimerId(1)
594 }
595 let mut s = (Subject::new(collision)).initialize().unwrap().behavior;
596 let announced = s
597 .receive(
598 MailAddr(0),
599 PresenceMessage::Announce {
600 participant: Participant(1),
601 version: PresenceVersion(2),
602 lifetime: duration(),
603 reply_to: reply(),
604 },
605 )
606 .unwrap();
607 assert_eq!(announced.sends.schedules.len(), 1);
608 assert_eq!(announced.sends.replies.len(), 1);
609 assert!(announced.creates.is_empty());
610 assert_eq!(announced.become_, behavior::Step::Continue);
611 let collision = s
612 .receive(
613 MailAddr(0),
614 PresenceMessage::Announce {
615 participant: Participant(2),
616 version: PresenceVersion(1),
617 lifetime: duration(),
618 reply_to: reply(),
619 },
620 )
621 .unwrap();
622 assert!(matches!(
623 collision.sends.replies[0].message,
624 PresenceReply::Outcome(PresenceOutcome::Rejected(PresenceError::TimerCollision {
625 participant: Participant(2),
626 existing: Participant(1),
627 ..
628 }))
629 ));
630 let stale = s
631 .receive(
632 MailAddr(0),
633 PresenceMessage::Announce {
634 participant: Participant(1),
635 version: PresenceVersion(1),
636 lifetime: duration(),
637 reply_to: reply(),
638 },
639 )
640 .unwrap();
641 assert!(matches!(
642 stale.sends.replies[0].message,
643 PresenceReply::Outcome(PresenceOutcome::Rejected(PresenceError::Stale { .. }))
644 ));
645 assert_eq!(s.report().entries.len(), 1);
646 }
647
648 #[test]
649 fn timer_key_reuse_cannot_reinterpret_an_earlier_participants_expiry() {
650 fn shared(_: &Participant) -> TimerId {
651 TimerId(1)
652 }
653 let mut subject = (Subject::new(shared)).initialize().unwrap().behavior;
654 let announce = |participant| PresenceMessage::Announce {
655 participant,
656 version: PresenceVersion(1),
657 lifetime: duration(),
658 reply_to: reply(),
659 };
660
661 let first = subject
662 .receive(MailAddr(0), announce(Participant(1)))
663 .unwrap();
664 assert_eq!(
665 first.sends.schedules.as_slice()[0].generation,
666 TimerGeneration(0)
667 );
668 let expired = subject
669 .on_path(TimerElapsed::new(TimerId(1), TimerGeneration(0)))
670 .unwrap();
671 assert!(expired.sends.schedules.is_empty());
672 assert_eq!(expired.sends.replies.len(), 1);
673 assert!(expired.creates.is_empty());
674 assert_eq!(expired.become_, behavior::Step::Continue);
675
676 let second = subject
677 .receive(MailAddr(0), announce(Participant(2)))
678 .unwrap();
679 assert_eq!(
680 second.sends.schedules.as_slice()[0].generation,
681 TimerGeneration(1)
682 );
683 let delayed_duplicate = subject
684 .on_path(TimerElapsed::new(TimerId(1), TimerGeneration(0)))
685 .unwrap();
686 assert!(delayed_duplicate.sends.replies.is_empty());
687 assert!(matches!(
688 subject.report().entries[1].phase,
689 PresencePhase::Present {
690 generation: TimerGeneration(1),
691 ..
692 }
693 ));
694 }
695
696 #[test]
697 fn identical_evidence_is_idempotent_without_rescheduling() {
698 let mut s = (Subject::new(timer)).initialize().unwrap().behavior;
699 let message = || PresenceMessage::Announce {
700 participant: Participant(1),
701 version: PresenceVersion(1),
702 lifetime: duration(),
703 reply_to: reply(),
704 };
705 let announced = s.receive(MailAddr(0), message()).unwrap();
706 assert_eq!(announced.sends.schedules.len(), 1);
707 assert_eq!(announced.sends.replies.len(), 1);
708 assert!(announced.creates.is_empty());
709 assert_eq!(announced.become_, behavior::Step::Continue);
710 let unchanged = s.receive(MailAddr(0), message()).unwrap();
711 assert!(unchanged.sends.schedules.as_slice().is_empty());
712 assert!(matches!(
713 unchanged.sends.replies[0].message,
714 PresenceReply::Outcome(PresenceOutcome::Unchanged {
715 generation: TimerGeneration(0),
716 ..
717 })
718 ));
719 }
720
721 #[test]
722 fn exhausted_generation_rejects_without_mutation() {
723 let mut definition = Subject::new(timer);
724 definition.records.push(Record {
725 entry: PresenceEntry {
726 participant: Participant(1),
727 timer_id: TimerId(1),
728 phase: PresencePhase::Present {
729 version: PresenceVersion(1),
730 generation: TimerGeneration(u64::MAX),
731 lifetime: duration(),
732 },
733 },
734 notify: reply(),
735 });
736 let mut s = (definition).initialize().unwrap().behavior;
737 let rejected = s
738 .receive(
739 MailAddr(0),
740 PresenceMessage::Announce {
741 participant: Participant(1),
742 version: PresenceVersion(2),
743 lifetime: duration(),
744 reply_to: reply(),
745 },
746 )
747 .unwrap();
748 assert!(matches!(
749 rejected.sends.replies[0].message,
750 PresenceReply::Outcome(PresenceOutcome::Rejected(
751 PresenceError::GenerationExhausted { .. }
752 ))
753 ));
754 assert!(matches!(
755 s.report().entries[0].phase,
756 PresencePhase::Present {
757 version: PresenceVersion(1),
758 generation: TimerGeneration(u64::MAX),
759 ..
760 }
761 ));
762 }
763}