1use anyhow::{Context, Result};
2use chrono::{DateTime, Utc};
3use onlyne_proto::{ClientOp, Envelope, ErrorCode, MsgKind, Receipt, Report, ResBody, new_op_id};
4use onlyne_store::{ClientStore, IntentRow};
5use rusqlite::Connection;
6use serde_json::Value;
7use std::time::Duration;
8
9pub const PERMANENT_ERRORS: &[ErrorCode] = &[
15 ErrorCode::AclDenied,
16 ErrorCode::Conflict,
17 ErrorCode::Forbidden,
18 ErrorCode::UnknownRole,
19 ErrorCode::NotAdmin,
20 ErrorCode::BadFrame,
21 ErrorCode::FrameTooLarge,
22 ErrorCode::ProtocolVersion,
23];
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum IntentState {
27 Pending,
28 Retrying,
29 Accepted,
30 Exhausted,
31}
32
33impl IntentState {
34 pub fn as_str(self) -> &'static str {
35 match self {
36 Self::Pending => "pending",
37 Self::Retrying => "retrying",
38 Self::Accepted => "accepted",
39 Self::Exhausted => "exhausted",
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub enum IntentResult {
46 Accepted(Option<Receipt>),
47 Retryable(ErrorCode, String),
48 Dropped(ErrorCode, String),
49 Exhausted,
50}
51
52pub fn stamp_op_id(envelope: &mut Envelope) -> String {
61 if let Some(op_id) = &envelope.op_id {
62 return op_id.clone();
63 }
64 let op_id = new_op_id();
65 envelope.op_id = Some(op_id.clone());
66 op_id
67}
68
69#[derive(Clone)]
70pub struct IntentMachine {
71 pub store: ClientStore,
72 pub attempts: u32,
73 pub backoff_ms: Vec<u64>,
74}
75
76impl IntentMachine {
77 pub fn new(store: ClientStore, attempts: u32, backoff_ms: Vec<u64>) -> Self {
78 Self {
79 store,
80 attempts,
81 backoff_ms,
82 }
83 }
84
85 pub fn enqueue(&self, envelope: &Envelope) -> Result<bool> {
91 let mut stamped = envelope.clone();
92 let op_id = stamp_op_id(&mut stamped);
93 Ok(self
94 .store
95 .enqueue_intent(&op_id, &serde_json::to_value(&stamped)?)?)
96 }
97
98 pub fn enqueue_value(&self, op_id: &str, envelope: &Value) -> Result<bool> {
99 Ok(self.store.enqueue_intent(op_id, envelope)?)
100 }
101
102 pub fn pending(&self) -> Result<Vec<IntentRow>> {
103 Ok(self.store.flush_order()?)
104 }
105
106 pub fn next_delay(&self, attempt: u32) -> Duration {
107 let idx = attempt.saturating_sub(1) as usize;
108 Duration::from_millis(
109 self.backoff_ms
110 .get(idx)
111 .copied()
112 .or_else(|| self.backoff_ms.last().copied())
113 .unwrap_or(1_000),
114 )
115 }
116
117 pub fn attempt(&self, row: &IntentRow, response: Option<&ResBody>) -> Result<IntentResult> {
118 let op_id = row.op_id.as_str();
119 let Some(body) = response else {
120 let next = row.attempt.saturating_add(1) as u32;
121 if next >= self.attempts {
122 self.store
123 .exhaust_intent(op_id, "intent attempts exhausted")?;
124 self.record_exhausted(&row.env_json, row.attempt, "intent attempts exhausted")?;
125 return Ok(IntentResult::Exhausted);
126 }
127 let due = Utc::now() + self.next_delay(next);
128 self.store
129 .bump_intent(op_id, due, "connection unavailable")?;
130 return Ok(IntentResult::Retryable(
131 ErrorCode::Internal,
132 "connection unavailable".into(),
133 ));
134 };
135 self.apply_response(row, body)
136 }
137
138 pub fn defer(&self, row: &IntentRow, reason: &str) -> Result<IntentResult> {
143 let due = Utc::now() + self.next_delay(row.attempt.max(0) as u32 + 1);
144 self.store.defer_intent(&row.op_id, due, reason)?;
145 Ok(IntentResult::Retryable(
146 ErrorCode::Internal,
147 reason.to_string(),
148 ))
149 }
150
151 fn apply_response(
152 &self,
153 row: &IntentRow,
154 body: &onlyne_proto::ResBody,
155 ) -> Result<IntentResult> {
156 if body.ok {
157 let receipt = body
158 .data
159 .as_ref()
160 .and_then(|v| serde_json::from_value::<Receipt>(v.clone()).ok());
161 self.store
162 .accept_intent(&row.op_id, &body.data.clone().unwrap_or(Value::Null))?;
163 return Ok(IntentResult::Accepted(receipt));
164 }
165 let error = body
166 .error
167 .as_ref()
168 .context("error response missing payload")?;
169 if error.code == ErrorCode::Duplicate {
170 let receipt = body
171 .data
172 .as_ref()
173 .and_then(|v| serde_json::from_value::<Receipt>(v.clone()).ok());
174 self.store
175 .accept_intent(&row.op_id, &body.data.clone().unwrap_or(Value::Null))?;
176 return Ok(IntentResult::Accepted(receipt));
177 }
178 if error.message == onlyne_proto::HELLO_REQUIRED_MESSAGE {
182 return self.defer(row, "connection not authenticated");
183 }
184 if PERMANENT_ERRORS.contains(&error.code) {
185 self.delete_intent(&row.op_id)?;
186 return Ok(IntentResult::Dropped(error.code, error.message.clone()));
187 }
188 let next = row.attempt.saturating_add(1) as u32;
189 if next >= self.attempts {
190 self.store.exhaust_intent(&row.op_id, &error.message)?;
191 self.record_exhausted(&row.env_json, row.attempt, &error.message)?;
192 Ok(IntentResult::Exhausted)
193 } else {
194 let due = Utc::now() + self.next_delay(next);
195 self.store.bump_intent(&row.op_id, due, &error.message)?;
196 Ok(IntentResult::Retryable(error.code, error.message.clone()))
197 }
198 }
199
200 fn delete_intent(&self, op_id: &str) -> Result<()> {
201 let conn = Connection::open(self.store.path())?;
202 conn.execute("DELETE FROM intents WHERE op_id = ?", [op_id])?;
203 Ok(())
204 }
205
206 fn record_exhausted(&self, env: &Value, attempt: i64, reason: &str) -> Result<()> {
207 let task = env
208 .get("causality")
209 .and_then(|v| v.get("task"))
210 .and_then(Value::as_str)
211 .unwrap_or("");
212 let _ =
213 onlyne_session::record_fault(&self.store, task, "intent_exhausted", "intent", reason)?;
214 let _ = attempt;
215 let report = Report::Fault {
216 task_id: Some(task.to_string()),
217 session_id: None,
218 generation: None,
219 seq: None,
220 kind: "intent_exhausted".into(),
221 reason: reason.into(),
222 desired: None,
223 observed: None,
224 };
225 let _ = self.store.append_event(
226 "report_fault",
227 &serde_json::to_value(&report).unwrap_or(Value::Null),
228 );
229 Ok(())
230 }
231}
232
233pub fn op_for_intent(row: &IntentRow) -> Result<ClientOp> {
234 if let Ok(op) = serde_json::from_value::<ClientOp>(row.env_json.clone()) {
235 return Ok(op);
236 }
237 let envelope: Envelope = serde_json::from_value(row.env_json.clone())?;
238 Ok(ClientOp::Send(Box::new(envelope)))
239}
240
241pub fn due(row: &IntentRow) -> Result<DateTime<Utc>> {
242 Ok(DateTime::parse_from_rfc3339(&row.next_attempt_at)?.with_timezone(&Utc))
243}
244
245pub fn completion_task_id(row: &IntentRow) -> Option<String> {
263 if let Ok(envelope) = serde_json::from_value::<Envelope>(row.env_json.clone()) {
264 if envelope.kind != MsgKind::Completion {
265 return None;
266 }
267 return envelope.task_id().map(str::to_string);
268 }
269 match serde_json::from_value::<ClientOp>(row.env_json.clone()) {
270 Ok(ClientOp::Report(Report::Complete { task_id, .. })) => Some(task_id),
271 _ => None,
272 }
273}
274
275pub fn permanent_error(code: ErrorCode) -> bool {
276 PERMANENT_ERRORS.contains(&code)
277}