nodedb_lite/sync/array/inbound/
reject.rs1use nodedb_array::sync::hlc::Hlc;
8use nodedb_types::sync::wire::array::ArrayRejectMsg;
9
10use crate::error::LiteError;
11use crate::storage::engine::StorageEngine;
12use crate::sync::array::reject_policy::{RejectDisposition, reject_disposition};
13
14use super::dispatcher::ArrayInbound;
15use super::outcome::InboundOutcome;
16
17impl<S: StorageEngine> ArrayInbound<S> {
18 pub async fn handle_reject(&self, msg: &ArrayRejectMsg) -> Result<InboundOutcome, LiteError> {
29 let op_hlc = Hlc::from_bytes(&msg.op_hlc_bytes);
30 match reject_disposition(msg.reason) {
31 RejectDisposition::Drop => {
32 let was_present = self.pending.remove(op_hlc).await?;
33 tracing::error!(
34 array = %msg.array,
35 reason = ?msg.reason,
36 detail = %msg.detail,
37 op_hlc_physical_ms = op_hlc.physical_ms,
38 was_in_pending = was_present,
39 "array op permanently refused by Origin; op dropped, the write never reaches Origin"
40 );
41 }
42 RejectDisposition::CatchUp => {
43 tracing::warn!(
44 array = %msg.array,
45 detail = %msg.detail,
46 "array catch-up cursor is below Origin's retention floor; requesting a snapshot"
47 );
48 if let Err(e) = self.catchup.record_reject_retention_floor(&msg.array).await {
49 tracing::warn!(
50 array = %msg.array,
51 error = %e,
52 "array_inbound: failed to persist catchup_needed flag"
53 );
54 }
55 }
56 disposition @ (RejectDisposition::ResendSchema | RejectDisposition::Park) => {
57 tracing::warn!(
58 array = %msg.array,
59 reason = ?msg.reason,
60 detail = %msg.detail,
61 op_hlc_physical_ms = op_hlc.physical_ms,
62 ?disposition,
63 "array op refused by Origin; op kept for retry"
64 );
65 }
66 }
67
68 Ok(InboundOutcome::RejectAcknowledged)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use nodedb_types::sync::wire::array::{ArrayRejectMsg, ArrayRejectReason};
75
76 use super::super::fixtures::{hlc, make_inbound, put_op};
77 use super::super::outcome::InboundOutcome;
78
79 fn reject(reason: ArrayRejectReason) -> ArrayRejectMsg {
80 ArrayRejectMsg {
81 array: "any".into(),
82 op_hlc_bytes: hlc(10).to_bytes(),
83 reason,
84 detail: "refused".into(),
85 }
86 }
87
88 #[tokio::test(flavor = "multi_thread")]
89 async fn shape_invalid_reject_drops_op_from_pending() {
90 let (inbound, _schemas, pending, _storage) = make_inbound().await;
91 pending.enqueue(&put_op("any", 10, 1)).await.unwrap();
92
93 let outcome = inbound
94 .handle_reject(&reject(ArrayRejectReason::ShapeInvalid))
95 .await
96 .unwrap();
97 assert_eq!(outcome, InboundOutcome::RejectAcknowledged);
98 assert_eq!(pending.len().await.unwrap(), 0, "op must be removed");
99 }
100
101 #[tokio::test(flavor = "multi_thread")]
102 async fn retryable_rejects_keep_op_in_pending() {
103 let (inbound, _schemas, pending, _storage) = make_inbound().await;
104 pending.enqueue(&put_op("any", 10, 1)).await.unwrap();
105
106 for reason in [
107 ArrayRejectReason::ArrayUnknown,
108 ArrayRejectReason::SchemaTooNew,
109 ArrayRejectReason::EngineUnavailable,
110 ArrayRejectReason::SnapshotCapacity,
111 ArrayRejectReason::SnapshotBufferLost,
112 ArrayRejectReason::Unauthenticated,
113 ArrayRejectReason::RetentionFloor,
114 ] {
115 let outcome = inbound.handle_reject(&reject(reason)).await.unwrap();
116 assert_eq!(outcome, InboundOutcome::RejectAcknowledged);
117 assert_eq!(
118 pending.len().await.unwrap(),
119 1,
120 "{reason:?} must keep the op queued"
121 );
122 }
123 }
124}