1use std::collections::BTreeMap;
20use std::sync::Arc;
21use std::time::Duration;
22
23use async_trait::async_trait;
24use thiserror::Error;
25use time::OffsetDateTime;
26
27use super::Clock;
28use super::clock::timeout;
29
30pub const MAX_ACTOR_VALUE_BYTES: usize = 1024 * 1024;
34
35pub const MAX_ACTOR_MESSAGE_BYTES: usize = 1024 * 1024;
37
38pub const MAX_ACTOR_REPLY_BYTES: usize = 1024 * 1024;
40
41pub const MAX_ACTOR_KEY_BYTES: usize = 512;
44
45pub const MAX_ACTOR_KIND_BYTES: usize = 64;
48
49pub const ACTOR_CALL_TIMEOUT: Duration = Duration::from_secs(30);
52
53#[derive(Debug, Clone, PartialEq, Eq, Error)]
55pub enum ActorError {
56 #[error("no actor host or handler is configured")]
58 NotConfigured,
59 #[error("actor rejected by size: {0}")]
61 TooLarge(String),
62 #[error("actor call timed out")]
64 Timeout,
65 #[error("actor handler failed: {0}")]
67 Handler(String),
68 #[error("actor operation failed: {0}")]
70 Operation(String),
71}
72
73pub fn validate_actor_kind(kind: &str) -> Result<(), ActorError> {
81 let valid = !kind.is_empty()
82 && kind.len() <= MAX_ACTOR_KIND_BYTES
83 && kind.bytes().all(|byte| {
84 byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-'
85 });
86 if valid {
87 return Ok(());
88 }
89 Err(ActorError::Operation(format!(
90 "actor kind `{kind}` must be 1..={MAX_ACTOR_KIND_BYTES} bytes of [a-z0-9_-]"
91 )))
92}
93
94pub fn validate_actor_key(key: &str) -> Result<(), ActorError> {
102 if key.is_empty() {
103 return Err(ActorError::Operation(
104 "an actor key cannot be empty".to_owned(),
105 ));
106 }
107 if key.len() > MAX_ACTOR_KEY_BYTES {
108 return Err(ActorError::TooLarge(format!(
109 "actor key of {} bytes exceeds the {MAX_ACTOR_KEY_BYTES}-byte bound",
110 key.len()
111 )));
112 }
113 Ok(())
114}
115
116pub fn check_actor_call(kind: &str, key: &str, message: &[u8]) -> Result<(), ActorError> {
126 validate_actor_kind(kind)?;
127 validate_actor_key(key)?;
128 if message.len() > MAX_ACTOR_MESSAGE_BYTES {
129 return Err(ActorError::TooLarge(format!(
130 "actor message of {} bytes exceeds the {MAX_ACTOR_MESSAGE_BYTES}-byte bound",
131 message.len()
132 )));
133 }
134 Ok(())
135}
136
137fn check_prefix(prefix: &str) -> Result<(), ActorError> {
140 if prefix.len() > MAX_ACTOR_KEY_BYTES {
141 return Err(ActorError::TooLarge(format!(
142 "actor key prefix of {} bytes exceeds the {MAX_ACTOR_KEY_BYTES}-byte bound",
143 prefix.len()
144 )));
145 }
146 Ok(())
147}
148
149#[async_trait]
154pub trait Actors: Send + Sync {
155 async fn call(&self, kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError>;
160}
161
162#[async_trait]
170pub trait ActorHandler: Send + Sync {
171 async fn on_message(
174 &self,
175 ctx: &mut dyn ActorContext,
176 message: &[u8],
177 ) -> Result<Vec<u8>, ActorError>;
178
179 async fn on_alarm(&self, ctx: &mut dyn ActorContext) -> Result<(), ActorError>;
182}
183
184#[async_trait]
188pub trait ActorContext: Send {
189 fn kind(&self) -> &str;
191 fn key(&self) -> &str;
193 fn now(&self) -> OffsetDateTime;
195 async fn get(&mut self, key: &str) -> Result<Option<Vec<u8>>, ActorError>;
197 async fn put(&mut self, key: &str, value: Vec<u8>) -> Result<(), ActorError>;
199 async fn delete(&mut self, key: &str) -> Result<(), ActorError>;
201 async fn delete_all(&mut self) -> Result<(), ActorError>;
204 async fn list_prefix(&mut self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError>;
207 async fn set_alarm(&mut self, at: OffsetDateTime) -> Result<(), ActorError>;
209 async fn alarm(&mut self) -> Result<Option<OffsetDateTime>, ActorError>;
212 async fn cancel_alarm(&mut self) -> Result<(), ActorError>;
214}
215
216#[derive(Debug, Clone, Default, PartialEq, Eq)]
222pub struct ActorWrites {
223 pub clear_all: bool,
225 pub entries: BTreeMap<String, Option<Vec<u8>>>,
227 pub alarm: Option<Option<OffsetDateTime>>,
230}
231
232impl ActorWrites {
233 #[must_use]
235 pub fn is_empty(&self) -> bool {
236 !self.clear_all && self.entries.is_empty() && self.alarm.is_none()
237 }
238}
239
240#[async_trait]
244pub trait ActorStore: Send + Sync {
245 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, ActorError>;
247 async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError>;
249 async fn alarm(&self) -> Result<Option<OffsetDateTime>, ActorError>;
251 async fn commit(&self, writes: ActorWrites) -> Result<(), ActorError>;
253}
254
255struct StagedContext<'a> {
259 store: &'a dyn ActorStore,
260 clock: &'a dyn Clock,
261 kind: &'a str,
262 key: &'a str,
263 writes: ActorWrites,
264}
265
266impl<'a> StagedContext<'a> {
267 fn new(store: &'a dyn ActorStore, clock: &'a dyn Clock, kind: &'a str, key: &'a str) -> Self {
268 Self {
269 store,
270 clock,
271 kind,
272 key,
273 writes: ActorWrites::default(),
274 }
275 }
276
277 fn into_writes(self) -> ActorWrites {
278 self.writes
279 }
280}
281
282#[async_trait]
283impl ActorContext for StagedContext<'_> {
284 fn kind(&self) -> &str {
285 self.kind
286 }
287
288 fn key(&self) -> &str {
289 self.key
290 }
291
292 fn now(&self) -> OffsetDateTime {
293 self.clock.now()
294 }
295
296 async fn get(&mut self, key: &str) -> Result<Option<Vec<u8>>, ActorError> {
297 validate_actor_key(key)?;
298 if let Some(value) = self.writes.entries.get(key) {
299 return Ok(value.clone());
300 }
301 if self.writes.clear_all {
302 return Ok(None);
303 }
304 self.store.get(key).await
305 }
306
307 async fn put(&mut self, key: &str, value: Vec<u8>) -> Result<(), ActorError> {
308 validate_actor_key(key)?;
309 if value.len() > MAX_ACTOR_VALUE_BYTES {
310 return Err(ActorError::TooLarge(format!(
311 "actor value of {} bytes exceeds the {MAX_ACTOR_VALUE_BYTES}-byte bound",
312 value.len()
313 )));
314 }
315 self.writes.entries.insert(key.to_owned(), Some(value));
316 Ok(())
317 }
318
319 async fn delete(&mut self, key: &str) -> Result<(), ActorError> {
320 validate_actor_key(key)?;
321 self.writes.entries.insert(key.to_owned(), None);
322 Ok(())
323 }
324
325 async fn delete_all(&mut self) -> Result<(), ActorError> {
326 self.writes.clear_all = true;
327 self.writes.entries.clear();
328 Ok(())
329 }
330
331 async fn list_prefix(&mut self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError> {
332 check_prefix(prefix)?;
333 let mut merged: BTreeMap<String, Vec<u8>> = if self.writes.clear_all {
334 BTreeMap::new()
335 } else {
336 self.store.list_prefix(prefix).await?.into_iter().collect()
337 };
338 for (key, value) in &self.writes.entries {
339 if key.starts_with(prefix) {
340 match value {
341 Some(value) => {
342 merged.insert(key.clone(), value.clone());
343 }
344 None => {
345 merged.remove(key);
346 }
347 }
348 }
349 }
350 Ok(merged.into_iter().collect())
351 }
352
353 async fn set_alarm(&mut self, at: OffsetDateTime) -> Result<(), ActorError> {
354 self.writes.alarm = Some(Some(at));
355 Ok(())
356 }
357
358 async fn alarm(&mut self) -> Result<Option<OffsetDateTime>, ActorError> {
359 match &self.writes.alarm {
360 Some(pending) => Ok(*pending),
361 None => self.store.alarm().await,
362 }
363 }
364
365 async fn cancel_alarm(&mut self) -> Result<(), ActorError> {
366 self.writes.alarm = Some(None);
367 Ok(())
368 }
369}
370
371pub async fn run_actor_message(
382 handler: &dyn ActorHandler,
383 store: &dyn ActorStore,
384 clock: &dyn Clock,
385 kind: &str,
386 key: &str,
387 message: &[u8],
388) -> Result<Vec<u8>, ActorError> {
389 check_actor_call(kind, key, message)?;
390 let mut ctx = StagedContext::new(store, clock, kind, key);
391 let reply = handler.on_message(&mut ctx, message).await?;
392 if reply.len() > MAX_ACTOR_REPLY_BYTES {
393 return Err(ActorError::TooLarge(format!(
394 "actor reply of {} bytes exceeds the {MAX_ACTOR_REPLY_BYTES}-byte bound",
395 reply.len()
396 )));
397 }
398 commit_if_any(store, ctx.into_writes()).await?;
399 Ok(reply)
400}
401
402async fn commit_if_any(store: &dyn ActorStore, writes: ActorWrites) -> Result<(), ActorError> {
405 if writes.is_empty() {
406 return Ok(());
407 }
408 store.commit(writes).await
409}
410
411pub async fn run_actor_alarm(
420 handler: &dyn ActorHandler,
421 store: &dyn ActorStore,
422 clock: &dyn Clock,
423 kind: &str,
424 key: &str,
425) -> Result<(), ActorError> {
426 validate_actor_kind(kind)?;
427 validate_actor_key(key)?;
428 let mut ctx = StagedContext::new(store, clock, kind, key);
429 ctx.writes.alarm = Some(None);
430 handler.on_alarm(&mut ctx).await?;
431 commit_if_any(store, ctx.into_writes()).await?;
432 Ok(())
433}
434
435#[derive(Clone, Default)]
439pub struct ActorHandlers {
440 handlers: BTreeMap<String, Arc<dyn ActorHandler>>,
441}
442
443impl ActorHandlers {
444 #[must_use]
446 pub fn new() -> Self {
447 Self::default()
448 }
449
450 #[must_use]
452 pub fn with(mut self, kind: &str, handler: Arc<dyn ActorHandler>) -> Self {
453 self.handlers.insert(kind.to_owned(), handler);
454 self
455 }
456
457 #[must_use]
459 pub fn get(&self, kind: &str) -> Option<Arc<dyn ActorHandler>> {
460 self.handlers.get(kind).map(Arc::clone)
461 }
462
463 pub fn kinds(&self) -> impl Iterator<Item = &str> {
465 self.handlers.keys().map(String::as_str)
466 }
467}
468
469impl std::fmt::Debug for ActorHandlers {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 f.debug_struct("ActorHandlers")
472 .field("kinds", &self.kinds().collect::<Vec<_>>())
473 .finish()
474 }
475}
476
477pub struct ScopedActors {
484 inner: Arc<dyn Actors>,
485 module: String,
486 kinds: &'static [&'static str],
487 clock: Option<Arc<dyn Clock>>,
488}
489
490impl ScopedActors {
491 #[must_use]
494 pub fn new(
495 inner: Arc<dyn Actors>,
496 module: &str,
497 kinds: &'static [&'static str],
498 clock: Option<Arc<dyn Clock>>,
499 ) -> Self {
500 Self {
501 inner,
502 module: module.to_owned(),
503 kinds,
504 clock,
505 }
506 }
507}
508
509#[async_trait]
510impl Actors for ScopedActors {
511 async fn call(&self, kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
512 if !self.kinds.contains(&kind) {
513 return Err(ActorError::Operation(format!(
514 "module `{}` does not declare actor kind `{kind}`",
515 self.module
516 )));
517 }
518 check_actor_call(kind, key, message)?;
519 let inner = Arc::clone(&self.inner);
520 let kind = kind.to_owned();
521 let key = key.to_owned();
522 let message = message.to_vec();
523 let reply = match &self.clock {
524 Some(clock) => timeout(
525 clock.as_ref(),
526 async move { inner.call(&kind, &key, &message).await },
527 ACTOR_CALL_TIMEOUT,
528 )
529 .await
530 .ok_or(ActorError::Timeout)??,
531 None => inner.call(&kind, &key, &message).await?,
535 };
536 if reply.len() > MAX_ACTOR_REPLY_BYTES {
537 return Err(ActorError::TooLarge(format!(
538 "actor reply of {} bytes exceeds the {MAX_ACTOR_REPLY_BYTES}-byte bound",
539 reply.len()
540 )));
541 }
542 Ok(reply)
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 #![allow(clippy::disallowed_types)]
552
553 use super::*;
554 use std::sync::Mutex;
555
556 #[derive(Default)]
558 struct RecordingActors {
559 seen: Mutex<Vec<(String, String)>>,
560 reply: Vec<u8>,
561 }
562
563 #[async_trait]
564 impl Actors for RecordingActors {
565 async fn call(
566 &self,
567 kind: &str,
568 key: &str,
569 _message: &[u8],
570 ) -> Result<Vec<u8>, ActorError> {
571 self.seen
572 .lock()
573 .unwrap()
574 .push((kind.to_owned(), key.to_owned()));
575 Ok(self.reply.clone())
576 }
577 }
578
579 #[pollster::test]
580 async fn a_scoped_actors_refuses_an_undeclared_kind_before_the_host() {
581 let host = Arc::new(RecordingActors::default());
582 let scoped = ScopedActors::new(
583 host.clone(),
584 "cms",
585 &["counter"],
586 Some(Arc::new(crate::ports::SystemClock)),
587 );
588 let err = scoped.call("other", "k", b"x").await.unwrap_err();
589 assert!(
590 matches!(err, ActorError::Operation(ref m) if m.contains("does not declare actor kind `other`")),
591 "got {err}"
592 );
593 assert!(host.seen.lock().unwrap().is_empty());
594 }
595
596 #[pollster::test]
597 async fn a_message_at_the_bound_passes_and_one_over_is_refused() {
598 let host = Arc::new(RecordingActors::default());
599 let scoped = ScopedActors::new(
600 host.clone(),
601 "cms",
602 &["counter"],
603 Some(Arc::new(crate::ports::SystemClock)),
604 );
605 let exact = vec![0_u8; MAX_ACTOR_MESSAGE_BYTES];
606 scoped.call("counter", "k", &exact).await.expect("at bound");
607 let over = vec![0_u8; MAX_ACTOR_MESSAGE_BYTES + 1];
608 let err = scoped.call("counter", "k", &over).await.unwrap_err();
609 assert!(matches!(err, ActorError::TooLarge(_)), "got {err}");
610 assert_eq!(
611 host.seen.lock().unwrap().len(),
612 1,
613 "the oversized message never reached the host"
614 );
615 }
616
617 #[pollster::test]
618 async fn an_oversized_reply_is_refused() {
619 let host = Arc::new(RecordingActors {
620 seen: Mutex::new(Vec::new()),
621 reply: vec![0_u8; MAX_ACTOR_REPLY_BYTES + 1],
622 });
623 let scoped = ScopedActors::new(
624 host,
625 "cms",
626 &["counter"],
627 Some(Arc::new(crate::ports::SystemClock)),
628 );
629 let err = scoped.call("counter", "k", b"x").await.unwrap_err();
630 assert!(matches!(err, ActorError::TooLarge(_)), "got {err}");
631 }
632
633 #[pollster::test]
634 async fn a_call_without_a_clock_reaches_the_host_with_no_timeout() {
635 let host = Arc::new(RecordingActors::default());
636 let scoped = ScopedActors::new(host.clone(), "cms", &["counter"], None);
637 scoped.call("counter", "k", b"x").await.expect("no clock");
640 assert_eq!(
641 host.seen.lock().unwrap().as_slice(),
642 &[("counter".to_owned(), "k".to_owned())]
643 );
644 }
645
646 #[derive(Default)]
649 struct MemStore {
650 values: Mutex<BTreeMap<String, Vec<u8>>>,
651 alarm: Mutex<Option<OffsetDateTime>>,
652 }
653
654 #[async_trait]
655 impl ActorStore for MemStore {
656 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, ActorError> {
657 Ok(self.values.lock().unwrap().get(key).cloned())
658 }
659 async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError> {
660 Ok(self
661 .values
662 .lock()
663 .unwrap()
664 .iter()
665 .filter(|(k, _)| k.starts_with(prefix))
666 .map(|(k, v)| (k.clone(), v.clone()))
667 .collect())
668 }
669 async fn alarm(&self) -> Result<Option<OffsetDateTime>, ActorError> {
670 Ok(*self.alarm.lock().unwrap())
671 }
672 async fn commit(&self, writes: ActorWrites) -> Result<(), ActorError> {
673 let mut values = self.values.lock().unwrap();
674 if writes.clear_all {
675 values.clear();
676 }
677 for (key, value) in writes.entries {
678 match value {
679 Some(value) => {
680 values.insert(key, value);
681 }
682 None => {
683 values.remove(&key);
684 }
685 }
686 }
687 if let Some(alarm) = writes.alarm {
688 *self.alarm.lock().unwrap() = alarm;
689 }
690 Ok(())
691 }
692 }
693
694 struct ScriptedHandler {
697 put: Option<(String, Vec<u8>)>,
698 fail: bool,
699 reply_len: usize,
700 rearm: bool,
701 }
702
703 #[async_trait]
704 impl ActorHandler for ScriptedHandler {
705 async fn on_message(
706 &self,
707 ctx: &mut dyn ActorContext,
708 _message: &[u8],
709 ) -> Result<Vec<u8>, ActorError> {
710 if let Some((key, value)) = &self.put {
711 ctx.put(key, value.clone()).await?;
712 }
713 if self.fail {
714 return Err(ActorError::Handler("boom".to_owned()));
715 }
716 Ok(vec![0_u8; self.reply_len])
717 }
718
719 async fn on_alarm(&self, ctx: &mut dyn ActorContext) -> Result<(), ActorError> {
720 if let Some((key, value)) = &self.put {
721 ctx.put(key, value.clone()).await?;
722 }
723 if self.fail {
724 return Err(ActorError::Handler("boom".to_owned()));
725 }
726 if self.rearm {
727 ctx.set_alarm(OffsetDateTime::UNIX_EPOCH).await?;
728 }
729 Ok(())
730 }
731 }
732
733 fn clock() -> crate::ports::SystemClock {
734 crate::ports::SystemClock
735 }
736
737 #[pollster::test]
738 async fn a_handler_error_commits_nothing() {
739 let store = MemStore::default();
740 let handler = ScriptedHandler {
741 put: Some(("k".to_owned(), b"v".to_vec())),
742 fail: true,
743 reply_len: 0,
744 rearm: false,
745 };
746 let err = run_actor_message(&handler, &store, &clock(), "counter", "a", b"m")
747 .await
748 .unwrap_err();
749 assert!(matches!(err, ActorError::Handler(_)), "got {err}");
750 assert!(store.get("k").await.unwrap().is_none());
751 }
752
753 #[pollster::test]
754 async fn an_oversized_reply_commits_nothing() {
755 let store = MemStore::default();
756 let handler = ScriptedHandler {
757 put: Some(("k".to_owned(), b"v".to_vec())),
758 fail: false,
759 reply_len: MAX_ACTOR_REPLY_BYTES + 1,
760 rearm: false,
761 };
762 assert!(matches!(
763 run_actor_message(&handler, &store, &clock(), "counter", "a", b"m")
764 .await
765 .unwrap_err(),
766 ActorError::TooLarge(_)
767 ));
768 assert!(store.get("k").await.unwrap().is_none());
769 }
770
771 #[pollster::test]
772 async fn a_successful_message_commits_its_writes() {
773 let store = MemStore::default();
774 let handler = ScriptedHandler {
775 put: Some(("k".to_owned(), b"v".to_vec())),
776 fail: false,
777 reply_len: 3,
778 rearm: false,
779 };
780 let reply = run_actor_message(&handler, &store, &clock(), "counter", "a", b"m")
781 .await
782 .unwrap();
783 assert_eq!(reply, vec![0_u8; 3]);
784 assert_eq!(store.get("k").await.unwrap().as_deref(), Some(&b"v"[..]));
785 }
786
787 #[pollster::test]
788 async fn an_oversized_put_is_refused() {
789 let store = MemStore::default();
790 let handler = ScriptedHandler {
791 put: Some(("k".to_owned(), vec![0_u8; MAX_ACTOR_VALUE_BYTES + 1])),
792 fail: false,
793 reply_len: 0,
794 rearm: false,
795 };
796 assert!(matches!(
797 run_actor_message(&handler, &store, &clock(), "counter", "a", b"m")
798 .await
799 .unwrap_err(),
800 ActorError::TooLarge(_)
801 ));
802 assert!(store.values.lock().unwrap().is_empty());
803 }
804
805 struct StageReader;
808
809 #[async_trait]
810 impl ActorHandler for StageReader {
811 async fn on_message(
812 &self,
813 ctx: &mut dyn ActorContext,
814 _message: &[u8],
815 ) -> Result<Vec<u8>, ActorError> {
816 ctx.put("a/1", b"one".to_vec()).await?;
817 ctx.put("b/1", b"two".to_vec()).await?;
818 assert_eq!(ctx.get("a/1").await?.as_deref(), Some(&b"one"[..]));
819 assert_eq!(ctx.list_prefix("a/").await?.len(), 1);
820 ctx.delete("a/1").await?;
821 assert!(ctx.get("a/1").await?.is_none());
822 assert!(ctx.list_prefix("a/").await?.is_empty());
823 Ok(Vec::new())
824 }
825
826 async fn on_alarm(&self, _ctx: &mut dyn ActorContext) -> Result<(), ActorError> {
827 Ok(())
828 }
829 }
830
831 #[pollster::test]
832 async fn a_handler_sees_its_own_staged_writes() {
833 let store = MemStore::default();
834 run_actor_message(&StageReader, &store, &clock(), "counter", "a", b"m")
835 .await
836 .unwrap();
837 assert_eq!(
839 store.get("b/1").await.unwrap().as_deref(),
840 Some(&b"two"[..])
841 );
842 assert!(store.get("a/1").await.unwrap().is_none());
843 }
844
845 struct ClearAfterPut;
847
848 #[async_trait]
849 impl ActorHandler for ClearAfterPut {
850 async fn on_message(
851 &self,
852 ctx: &mut dyn ActorContext,
853 _message: &[u8],
854 ) -> Result<Vec<u8>, ActorError> {
855 ctx.put("k", b"v".to_vec()).await?;
856 ctx.delete_all().await?;
857 assert!(ctx.get("k").await?.is_none());
858 Ok(Vec::new())
859 }
860
861 async fn on_alarm(&self, _ctx: &mut dyn ActorContext) -> Result<(), ActorError> {
862 Ok(())
863 }
864 }
865
866 #[pollster::test]
867 async fn delete_all_drops_staged_writes() {
868 let store = MemStore::default();
869 let mut seeded = BTreeMap::new();
870 seeded.insert("old".to_owned(), b"x".to_vec());
871 *store.values.lock().unwrap() = seeded;
872 run_actor_message(&ClearAfterPut, &store, &clock(), "counter", "a", b"m")
873 .await
874 .unwrap();
875 assert!(store.values.lock().unwrap().is_empty());
876 }
877
878 #[pollster::test]
879 async fn an_alarm_is_cancelled_unless_re_armed() {
880 let store = MemStore::default();
881 *store.alarm.lock().unwrap() = Some(OffsetDateTime::UNIX_EPOCH);
882 let quiet = ScriptedHandler {
883 put: None,
884 fail: false,
885 reply_len: 0,
886 rearm: false,
887 };
888 run_actor_alarm(&quiet, &store, &clock(), "counter", "a")
889 .await
890 .unwrap();
891 assert!(store.alarm().await.unwrap().is_none());
892
893 *store.alarm.lock().unwrap() = Some(OffsetDateTime::UNIX_EPOCH);
894 let rearming = ScriptedHandler {
895 put: None,
896 fail: false,
897 reply_len: 0,
898 rearm: true,
899 };
900 run_actor_alarm(&rearming, &store, &clock(), "counter", "a")
901 .await
902 .unwrap();
903 assert_eq!(
904 store.alarm().await.unwrap(),
905 Some(OffsetDateTime::UNIX_EPOCH)
906 );
907 }
908
909 #[pollster::test]
910 async fn a_failing_alarm_commits_nothing() {
911 let store = MemStore::default();
912 *store.alarm.lock().unwrap() = Some(OffsetDateTime::UNIX_EPOCH);
913 let handler = ScriptedHandler {
914 put: Some(("k".to_owned(), b"v".to_vec())),
915 fail: true,
916 reply_len: 0,
917 rearm: false,
918 };
919 assert!(
920 run_actor_alarm(&handler, &store, &clock(), "counter", "a")
921 .await
922 .is_err()
923 );
924 assert_eq!(
927 store.alarm().await.unwrap(),
928 Some(OffsetDateTime::UNIX_EPOCH)
929 );
930 assert!(store.get("k").await.unwrap().is_none());
931 }
932
933 #[test]
934 fn kind_and_key_validation() {
935 assert!(validate_actor_kind("counter").is_ok());
936 assert!(validate_actor_kind("a-b_9").is_ok());
937 for bad in [
938 "",
939 "Counter",
940 "a b",
941 "a:b",
942 &"a".repeat(MAX_ACTOR_KIND_BYTES + 1),
943 ] {
944 assert!(
945 matches!(validate_actor_kind(bad), Err(ActorError::Operation(_))),
946 "kind `{bad}` should be refused"
947 );
948 }
949 assert!(validate_actor_key("k").is_ok());
950 assert!(matches!(
951 validate_actor_key(""),
952 Err(ActorError::Operation(_))
953 ));
954 assert!(matches!(
955 validate_actor_key(&"k".repeat(MAX_ACTOR_KEY_BYTES + 1)),
956 Err(ActorError::TooLarge(_))
957 ));
958 }
959}