nodedb_lite/sync/array/reject_policy.rs
1//! What Lite does with a locally-originated op Origin refuses.
2//!
3//! Origin's inbound array path returns one `ArrayRejectMsg` per refused
4//! frame. Each reason has one fixed `ArrayRejectClass`, and the class decides
5//! the op's fate:
6//!
7//! | Class | Reasons | Disposition |
8//! |---|---|---|
9//! | `SchemaRequired` | `ArrayUnknown`, `SchemaTooNew` | [`RejectDisposition::ResendSchema`] |
10//! | `CatchUp` | `RetentionFloor` | [`RejectDisposition::CatchUp`] |
11//! | `Permanent` | `ShapeInvalid`, `EngineRefused`, `PermissionDenied` | [`RejectDisposition::Drop`] |
12//! | `Transient` | `EngineUnavailable`, `SnapshotCapacity`, `SnapshotBufferLost` | [`RejectDisposition::Park`] |
13//! | `Reauthenticate` | `Unauthenticated` | [`RejectDisposition::Park`] |
14//!
15//! A transient reject applied nothing, so the op stays durable and the next
16//! session re-sends it. Origin deduplicates a re-sent op by its HLC. An
17//! unauthenticated session ends with a fresh handshake, and the next session
18//! re-sends the op.
19
20use nodedb_types::sync::wire::array::{ArrayRejectClass, ArrayRejectReason};
21
22/// The fate of a locally-originated op Origin refused.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RejectDisposition {
25 /// Keep the op. Announce the array's schema again, then re-send the op.
26 ResendSchema,
27 /// The reject names this replica's catch-up cursor, not a local op. Ask
28 /// Origin for a snapshot. The pending queue is untouched.
29 CatchUp,
30 /// Origin can never accept the op. Remove it from the pending queue.
31 Drop,
32 /// Keep the op durable but stop sending it until the next session.
33 Park,
34}
35
36/// Map a reject reason to its disposition.
37pub fn reject_disposition(reason: ArrayRejectReason) -> RejectDisposition {
38 match reason.class() {
39 ArrayRejectClass::SchemaRequired => RejectDisposition::ResendSchema,
40 ArrayRejectClass::CatchUp => RejectDisposition::CatchUp,
41 ArrayRejectClass::Permanent => RejectDisposition::Drop,
42 ArrayRejectClass::Transient | ArrayRejectClass::Reauthenticate => RejectDisposition::Park,
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn schema_rejects_resend_the_schema() {
52 assert_eq!(
53 reject_disposition(ArrayRejectReason::ArrayUnknown),
54 RejectDisposition::ResendSchema
55 );
56 assert_eq!(
57 reject_disposition(ArrayRejectReason::SchemaTooNew),
58 RejectDisposition::ResendSchema
59 );
60 }
61
62 #[test]
63 fn retention_floor_requests_a_catch_up() {
64 assert_eq!(
65 reject_disposition(ArrayRejectReason::RetentionFloor),
66 RejectDisposition::CatchUp
67 );
68 }
69
70 #[test]
71 fn permanent_rejects_drop_the_op() {
72 for reason in [
73 ArrayRejectReason::ShapeInvalid,
74 ArrayRejectReason::EngineRefused,
75 ArrayRejectReason::PermissionDenied,
76 ] {
77 assert_eq!(reject_disposition(reason), RejectDisposition::Drop);
78 }
79 }
80
81 #[test]
82 fn transient_and_auth_rejects_park_the_op() {
83 for reason in [
84 ArrayRejectReason::EngineUnavailable,
85 ArrayRejectReason::SnapshotCapacity,
86 ArrayRejectReason::SnapshotBufferLost,
87 ArrayRejectReason::Unauthenticated,
88 ] {
89 assert_eq!(reject_disposition(reason), RejectDisposition::Park);
90 }
91 }
92}