1pub mod ulid;
14
15use crate::obs::log::Logger;
16use crate::store::{Envelope, KeySeq, PutOutcome, SharedStore, StoreError};
17use serde::{Deserialize, Serialize};
18use serde_json::{Value, json};
19use std::collections::{BTreeMap, HashMap};
20use std::sync::Mutex;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::time::{Duration, Instant};
23
24pub(crate) use crate::store::now_ms;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub enum Kind {
29 Manifest,
30 Inbox,
31 Context,
32 Run,
33 Subagent,
34 Task,
35 Memory,
36 Artifact,
37 Timer,
38 Audit,
39 Cred,
43}
44
45impl Kind {
46 pub fn as_str(self) -> &'static str {
47 match self {
48 Kind::Manifest => "manifest",
49 Kind::Inbox => "inbox",
50 Kind::Context => "context",
51 Kind::Run => "run",
52 Kind::Subagent => "subagent",
53 Kind::Task => "task",
54 Kind::Memory => "memory",
55 Kind::Artifact => "artifact",
56 Kind::Timer => "timer",
57 Kind::Audit => "audit",
58 Kind::Cred => "cred",
59 }
60 }
61 pub fn parse(s: &str) -> Option<Kind> {
62 Some(match s {
63 "manifest" => Kind::Manifest,
64 "inbox" => Kind::Inbox,
65 "context" => Kind::Context,
66 "run" => Kind::Run,
67 "subagent" => Kind::Subagent,
68 "task" => Kind::Task,
69 "memory" => Kind::Memory,
70 "artifact" => Kind::Artifact,
71 "timer" => Kind::Timer,
72 "audit" => Kind::Audit,
73 "cred" => Kind::Cred,
74 _ => return None,
75 })
76 }
77 pub fn indexed(self) -> bool {
81 !matches!(
82 self,
83 Kind::Manifest | Kind::Memory | Kind::Audit | Kind::Cred
84 )
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct EntityRef {
91 pub kind: String,
92 pub id: String,
93 pub seq: u64,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
98pub struct Manifest {
99 #[serde(default)]
100 pub generation: u64,
101 #[serde(default)]
102 pub created: u64,
103 #[serde(default)]
104 pub updated: u64,
105 #[serde(default)]
106 pub entities: Vec<EntityRef>,
107 #[serde(default)]
109 pub starts: BTreeMap<String, Value>,
110 #[serde(default)]
112 pub budget: Value,
113 #[serde(default)]
114 pub lifecycle: Value,
115}
116
117impl Manifest {
118 fn upsert(&mut self, kind: &str, id: &str, seq: u64) {
119 match self
120 .entities
121 .iter_mut()
122 .find(|e| e.kind == kind && e.id == id)
123 {
124 Some(e) => e.seq = seq,
125 None => self.entities.push(EntityRef {
126 kind: kind.to_string(),
127 id: id.to_string(),
128 seq,
129 }),
130 }
131 }
132 fn remove(&mut self, kind: &str, id: &str) {
133 self.entities.retain(|e| !(e.kind == kind && e.id == id));
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub struct InboxEvent {
140 pub id: String,
141 pub kind: String,
142 pub ts: u64,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub principal: Option<String>,
145 pub payload: Value,
146 #[serde(default)]
147 pub status: InboxStatus,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
151#[serde(rename_all = "lowercase")]
152pub enum InboxStatus {
153 #[default]
154 Pending,
155 Done,
156}
157
158impl InboxEvent {
159 pub fn new(kind: &str, principal: Option<String>, payload: Value) -> InboxEvent {
160 InboxEvent {
161 id: ulid::new(),
162 kind: kind.to_string(),
163 ts: now_ms(),
164 principal,
165 payload,
166 status: InboxStatus::Pending,
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct TimerRecord {
174 pub id: String,
175 pub deadline_ms: u64,
176 pub owner: Value,
177 #[serde(default)]
178 pub payload: Value,
179}
180
181#[derive(Debug, Clone)]
184pub struct Policy {
185 pub debounce: Duration,
186 pub on_error: crate::config::v2::StoreOnError,
187 pub retries: u32,
188}
189
190impl Default for Policy {
191 fn default() -> Self {
192 Policy {
193 debounce: Duration::from_millis(250),
194 on_error: crate::config::v2::StoreOnError::Halt,
195 retries: 3,
196 }
197 }
198}
199
200impl Policy {
201 pub fn from_settings(s: &crate::config::v2::Store) -> Policy {
202 Policy {
203 debounce: Duration::from_millis(s.checkpoint.debounce_ms.unwrap_or(250)),
204 on_error: s.on_error,
205 retries: 3,
206 }
207 }
208}
209
210#[derive(Debug, Default)]
212pub struct Restored {
213 pub manifest: Option<Manifest>,
215 pub entities: BTreeMap<String, Vec<Envelope>>,
217 pub lost: Vec<EntityRef>,
219 pub unindexed: Vec<EntityRef>,
222}
223
224impl Restored {
225 pub fn inbox_pending(&self) -> Vec<InboxEvent> {
226 let mut out: Vec<InboxEvent> = self
227 .entities
228 .get("inbox")
229 .map(|v| {
230 v.iter()
231 .filter_map(|e| serde_json::from_value::<InboxEvent>(e.state.clone()).ok())
232 .filter(|e| e.status == InboxStatus::Pending)
233 .collect()
234 })
235 .unwrap_or_default();
236 out.sort_by(|a, b| a.ts.cmp(&b.ts).then(a.id.cmp(&b.id)));
237 out
238 }
239 pub fn timers(&self) -> Vec<TimerRecord> {
240 self.entities
241 .get("timer")
242 .map(|v| {
243 v.iter()
244 .filter_map(|e| serde_json::from_value(e.state.clone()).ok())
245 .collect()
246 })
247 .unwrap_or_default()
248 }
249 pub fn of(&self, kind: Kind) -> &[Envelope] {
250 self.entities
251 .get(kind.as_str())
252 .map(Vec::as_slice)
253 .unwrap_or(&[])
254 }
255 pub fn count(&self) -> usize {
256 self.entities.values().map(Vec::len).sum()
257 }
258}
259
260pub struct Durable {
262 store: SharedStore,
263 prefix: String,
264 instance: String,
265 policy: Policy,
266 seqs: Mutex<HashMap<String, u64>>,
269 manifest: Mutex<Manifest>,
270 manifest_dirty: AtomicBool,
271 last_flush: Mutex<Instant>,
272 degraded: AtomicBool,
273 log: Option<Logger>,
274}
275
276impl Durable {
277 pub fn new(
278 store: SharedStore,
279 prefix: &str,
280 instance: &str,
281 policy: Policy,
282 log: Option<Logger>,
283 ) -> Durable {
284 Durable {
285 store,
286 prefix: prefix.to_string(),
287 instance: instance.to_string(),
288 policy,
289 seqs: Mutex::new(HashMap::new()),
290 manifest: Mutex::new(Manifest::default()),
291 manifest_dirty: AtomicBool::new(false),
292 last_flush: Mutex::new(Instant::now()),
293 degraded: AtomicBool::new(false),
294 log,
295 }
296 }
297
298 pub fn store_kind(&self) -> &'static str {
299 self.store.kind()
300 }
301 pub fn instance(&self) -> &str {
302 &self.instance
303 }
304 pub fn prefix(&self) -> &str {
305 &self.prefix
306 }
307 pub fn key(&self, kind: Kind, id: &str) -> String {
308 crate::store::key(&self.prefix, &self.instance, kind.as_str(), id)
309 }
310 pub fn is_degraded(&self) -> bool {
312 self.degraded.load(Ordering::Relaxed)
313 }
314 pub fn policy(&self) -> &Policy {
315 &self.policy
316 }
317
318 pub fn put(
325 &self,
326 kind: Kind,
327 id: &str,
328 state: Value,
329 hash: Option<String>,
330 ) -> Result<u64, StoreError> {
331 let key = self.key(kind, id);
332 let mut adopted = false;
333 let started = std::time::Instant::now();
334 loop {
335 let (seq, warmed) = {
336 let seqs = self.seqs.lock().unwrap_or_else(|e| e.into_inner());
337 match seqs.get(&key).copied() {
338 Some(s) => (s + 1, true),
339 None => (1, false),
340 }
341 };
342 let env = Envelope::new(
343 kind.as_str(),
344 id,
345 seq,
346 &self.instance,
347 hash.clone(),
348 state.clone(),
349 );
350 kill_point("state.before_put");
351 let outcome = crate::store::with_retry(
352 || self.store.put(&key, seq, &env.to_value()),
353 self.policy.retries,
354 );
355 match outcome {
356 Ok(PutOutcome::Ok) => {
357 self.seqs
358 .lock()
359 .unwrap_or_else(|e| e.into_inner())
360 .insert(key.clone(), seq);
361 if kind.indexed() {
362 let mut m = self.manifest.lock().unwrap_or_else(|e| e.into_inner());
363 m.upsert(kind.as_str(), id, seq);
364 m.updated = now_ms();
365 self.manifest_dirty.store(true, Ordering::Relaxed);
366 }
367 self.degraded.store(false, Ordering::Relaxed);
368 kill_point("state.after_put");
369 crate::obs::metrics::record_store_op(
370 "ok",
371 started.elapsed().as_millis() as u64,
372 );
373 return Ok(seq);
374 }
375 Ok(PutOutcome::Conflict { latest_seq }) => {
376 if !warmed && !adopted {
377 if let Some(l) = latest_seq {
381 self.seqs
382 .lock()
383 .unwrap_or_else(|e| e.into_inner())
384 .insert(key.clone(), l);
385 adopted = true;
386 self.log_event("store.seq_adopted", json!({"key": key, "latest": l}));
387 continue;
388 }
389 }
390 self.log_event(
391 "store.conflict",
392 json!({"key": key, "seq": seq, "latest": latest_seq}),
393 );
394 crate::obs::metrics::record_store_op(
395 "conflict",
396 started.elapsed().as_millis() as u64,
397 );
398 return Err(StoreError::Conflict(format!(
399 "key {key}: another writer owns it (our seq {seq}, latest {latest_seq:?})"
400 )));
401 }
402 Err(e) => {
403 self.log_event("store.put.fail", json!({"key": key, "err": e.to_string()}));
404 crate::obs::metrics::record_store_op(
405 "error",
406 started.elapsed().as_millis() as u64,
407 );
408 if self.policy.on_error == crate::config::v2::StoreOnError::Degrade {
409 self.degraded.store(true, Ordering::Relaxed);
410 self.seqs
413 .lock()
414 .unwrap_or_else(|e| e.into_inner())
415 .insert(key.clone(), seq);
416 return Ok(seq);
417 }
418 return Err(e);
419 }
420 }
421 }
422 }
423
424 pub fn get(&self, kind: Kind, id: &str) -> Result<Option<Envelope>, StoreError> {
426 let key = self.key(kind, id);
427 let v = crate::store::with_retry(|| self.store.get(&key, None), self.policy.retries)?;
428 match v {
429 None => Ok(None),
430 Some(v) => {
431 let env = Envelope::from_value(v)?;
432 self.seqs
433 .lock()
434 .unwrap_or_else(|e| e.into_inner())
435 .insert(key, env.seq);
436 Ok(if env.is_tombstone() { None } else { Some(env) })
437 }
438 }
439 }
440
441 pub fn delete(&self, kind: Kind, id: &str) -> Result<(), StoreError> {
444 let key = self.key(kind, id);
445 match crate::store::with_retry(|| self.store.delete(&key), self.policy.retries) {
446 Ok(()) => {
447 self.seqs
448 .lock()
449 .unwrap_or_else(|e| e.into_inner())
450 .remove(&key);
451 }
452 Err(StoreError::Unsupported(_)) => {
453 self.put(kind, id, Value::Null, None)?;
454 }
455 Err(e) => return Err(e),
456 }
457 if kind.indexed() {
458 let mut m = self.manifest.lock().unwrap_or_else(|e| e.into_inner());
459 m.remove(kind.as_str(), id);
460 m.updated = now_ms();
461 self.manifest_dirty.store(true, Ordering::Relaxed);
462 }
463 Ok(())
464 }
465
466 pub fn list(&self, kind: Kind) -> Result<Vec<KeySeq>, StoreError> {
468 let prefix = format!("{}/{}/{}/", self.prefix, self.instance, kind.as_str());
469 self.store.list(&prefix)
470 }
471
472 pub fn inbox_put(&self, ev: &InboxEvent) -> Result<u64, StoreError> {
476 let seq = self.put(
477 Kind::Inbox,
478 &ev.id,
479 serde_json::to_value(ev).unwrap_or(Value::Null),
480 None,
481 )?;
482 kill_point("inbox.after_put");
483 Ok(seq)
484 }
485
486 pub fn inbox_done(&self, id: &str) -> Result<(), StoreError> {
488 self.delete(Kind::Inbox, id)
489 }
490
491 pub fn timer_arm(&self, t: &TimerRecord) -> Result<u64, StoreError> {
492 self.put(
493 Kind::Timer,
494 &t.id,
495 serde_json::to_value(t).unwrap_or(Value::Null),
496 None,
497 )
498 }
499
500 pub fn timer_disarm(&self, id: &str) -> Result<(), StoreError> {
501 self.delete(Kind::Timer, id)
502 }
503
504 pub fn manifest(&self) -> Manifest {
507 self.manifest
508 .lock()
509 .unwrap_or_else(|e| e.into_inner())
510 .clone()
511 }
512
513 pub fn manifest_update(&self, f: impl FnOnce(&mut Manifest)) {
516 let mut m = self.manifest.lock().unwrap_or_else(|e| e.into_inner());
517 f(&mut m);
518 m.updated = now_ms();
519 self.manifest_dirty.store(true, Ordering::Relaxed);
520 }
521
522 pub fn flush(&self, force: bool) -> Result<bool, StoreError> {
524 if !self.manifest_dirty.load(Ordering::Relaxed) {
525 return Ok(false);
526 }
527 {
528 let last = self.last_flush.lock().unwrap_or_else(|e| e.into_inner());
529 if !force && last.elapsed() < self.policy.debounce {
530 return Ok(false);
531 }
532 }
533 let snapshot = self.manifest();
534 self.put(
535 Kind::Manifest,
536 "agent",
537 serde_json::to_value(&snapshot).unwrap_or(Value::Null),
538 None,
539 )?;
540 self.manifest_dirty.store(false, Ordering::Relaxed);
541 *self.last_flush.lock().unwrap_or_else(|e| e.into_inner()) = Instant::now();
542 Ok(true)
543 }
544
545 pub fn restore(&self) -> Result<Restored, StoreError> {
552 let mut out = Restored::default();
553 let (manifest, fresh) = match self.get(Kind::Manifest, "agent")? {
554 None => (
555 Manifest {
556 generation: 0,
557 created: now_ms(),
558 updated: now_ms(),
559 ..Manifest::default()
560 },
561 true,
562 ),
563 Some(env) => (
564 serde_json::from_value::<Manifest>(env.state.clone())
565 .map_err(|e| StoreError::Corrupt(format!("manifest does not parse: {e}")))?,
566 false,
567 ),
568 };
569 for r in &manifest.entities {
571 let Some(kind) = Kind::parse(&r.kind) else {
572 out.lost.push(r.clone());
573 continue;
574 };
575 match self.get(kind, &r.id)? {
576 Some(env) => out.entities.entry(r.kind.clone()).or_default().push(env),
577 None => out.lost.push(r.clone()),
578 }
579 }
580 for kind in [
583 Kind::Inbox,
584 Kind::Context,
585 Kind::Run,
586 Kind::Subagent,
587 Kind::Task,
588 Kind::Timer,
589 Kind::Artifact,
590 ] {
591 match self.list(kind) {
592 Ok(keys) => {
593 for ks in keys {
594 let Some((_, id)) =
595 crate::store::parse_key(&self.prefix, &self.instance, &ks.key)
596 else {
597 continue;
598 };
599 let indexed = manifest
600 .entities
601 .iter()
602 .any(|e| e.kind == kind.as_str() && e.id == id);
603 if indexed {
604 continue;
605 }
606 if let Some(env) = self.get(kind, id)? {
607 out.unindexed.push(EntityRef {
608 kind: kind.as_str().to_string(),
609 id: id.to_string(),
610 seq: env.seq,
611 });
612 out.entities
613 .entry(kind.as_str().to_string())
614 .or_default()
615 .push(env);
616 }
617 }
618 }
619 Err(StoreError::Unsupported(_)) => {}
620 Err(e) => return Err(e),
621 }
622 }
623 let mut m = manifest.clone();
627 m.entities.retain(|e| !out.lost.iter().any(|l| l == e));
628 for u in &out.unindexed {
629 m.upsert(&u.kind, &u.id, u.seq);
630 }
631 m.generation += 1;
632 m.updated = now_ms();
633 *self.manifest.lock().unwrap_or_else(|e| e.into_inner()) = m.clone();
634 self.manifest_dirty.store(true, Ordering::Relaxed);
635 self.flush(true)?;
636 if fresh && out.count() == 0 {
637 self.log_event("restore.fresh", json!({"generation": 1}));
638 return Ok(out);
639 }
640 self.log_event(
641 "restore.done",
642 json!({
643 "generation": m.generation,
644 "fresh_manifest": fresh,
645 "entities": out.count(),
646 "lost": out.lost.len(),
647 "unindexed": out.unindexed.len(),
648 "inbox_pending": out.inbox_pending().len(),
649 }),
650 );
651 out.manifest = Some(m);
652 Ok(out)
653 }
654
655 fn log_event(&self, event: &str, fields: Value) {
656 if let Some(l) = &self.log {
657 match event {
658 e if e.ends_with(".fail") || e == "store.conflict" => l.warn(event, fields),
659 _ => l.info(event, fields),
660 }
661 }
662 }
663}
664
665pub fn kill_point(name: &str) {
669 #[cfg(any(feature = "internal-mocks", debug_assertions))]
670 {
671 if std::env::var("AGENTD_TEST_KILL_AT").as_deref() == Ok(name) {
672 #[cfg(unix)]
673 unsafe {
674 libc::raise(libc::SIGKILL);
675 }
676 std::process::abort();
677 }
678 }
679 #[cfg(not(any(feature = "internal-mocks", debug_assertions)))]
680 {
681 let _ = name;
682 }
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688 use crate::store::Store;
689 use crate::store::memory::MemoryStore;
690 use std::sync::Arc;
691
692 fn durable(store: Arc<MemoryStore>) -> Durable {
693 Durable::new(
694 store,
695 "agentd",
696 "inst",
697 Policy {
698 debounce: Duration::from_millis(0),
699 ..Policy::default()
700 },
701 None,
702 )
703 }
704
705 #[test]
706 fn put_allocates_seqs_indexes_and_flushes_manifest() {
707 let mem = Arc::new(MemoryStore::new());
708 let d = durable(mem.clone());
709 assert!(d.restore().unwrap().manifest.is_none(), "fresh");
710 assert_eq!(
711 d.put(
712 Kind::Run,
713 "r1",
714 json!({"status": "running"}),
715 Some("h".into())
716 )
717 .unwrap(),
718 1
719 );
720 assert_eq!(
721 d.put(Kind::Run, "r1", json!({"status": "done"}), Some("h".into()))
722 .unwrap(),
723 2
724 );
725 assert_eq!(
726 d.put(Kind::Context, "root", json!({"v": 1}), None).unwrap(),
727 1
728 );
729 let env = d.get(Kind::Run, "r1").unwrap().unwrap();
730 assert_eq!(env.seq, 2);
731 assert_eq!(env.state["status"], json!("done"));
732 assert_eq!(env.hash.as_deref(), Some("h"));
733 assert!(d.flush(true).unwrap());
735 let m = d.manifest();
736 assert_eq!(m.entities.len(), 2);
737 assert!(
738 m.entities
739 .iter()
740 .any(|e| e.kind == "run" && e.id == "r1" && e.seq == 2)
741 );
742 assert!(!d.flush(true).unwrap(), "clean after a flush");
743 d.delete(Kind::Context, "root").unwrap();
745 assert!(d.get(Kind::Context, "root").unwrap().is_none());
746 assert_eq!(d.manifest().entities.len(), 1);
747 }
748
749 #[test]
750 fn conflicts_are_fatal_on_owned_keys_but_adopted_on_first_touch() {
751 let mem = Arc::new(MemoryStore::new());
752 let stale = Envelope::new("run", "old", 5, "inst", None, json!({"x": 1}));
754 mem.put("agentd/inst/run/old", 5, &stale.to_value())
755 .unwrap();
756 let d = durable(mem.clone());
757 assert_eq!(d.put(Kind::Run, "old", json!({"x": 2}), None).unwrap(), 6);
759 let other = Envelope::new("run", "old", 7, "other", None, json!({"x": 3}));
761 mem.put("agentd/inst/run/old", 7, &other.to_value())
762 .unwrap();
763 assert!(matches!(
764 d.put(Kind::Run, "old", json!({"x": 4}), None),
765 Err(StoreError::Conflict(_))
766 ));
767 }
768
769 #[test]
770 fn inbox_write_ahead_timers_and_restore() {
771 let mem = Arc::new(MemoryStore::new());
772 {
773 let d = durable(mem.clone());
774 d.restore().unwrap();
775 let e1 = InboxEvent::new(
776 "a2a_message",
777 Some("user:andrii".into()),
778 json!({"text": "hi"}),
779 );
780 let e2 = InboxEvent::new("start_fired", None, json!({"workflow": "w"}));
781 d.inbox_put(&e1).unwrap();
782 d.inbox_put(&e2).unwrap();
783 d.inbox_done(&e1.id).unwrap();
784 d.timer_arm(&TimerRecord {
785 id: "t1".into(),
786 deadline_ms: 42,
787 owner: json!({"run": "r"}),
788 payload: Value::Null,
789 })
790 .unwrap();
791 d.put(
792 Kind::Run,
793 "r",
794 json!({"status": "running"}),
795 Some("hash".into()),
796 )
797 .unwrap();
798 d.put(Kind::Task, "task-1", json!({"state": "working"}), None)
799 .unwrap();
800 d.manifest_update(|m| {
801 m.starts.insert("w.s".into(), json!({"last_fired": 1}));
802 });
803 d.put(Kind::Subagent, "gone", json!({}), None).unwrap();
805 d.flush(true).unwrap();
806 mem.delete("agentd/inst/subagent/gone").unwrap();
807 d.put(Kind::Run, "r2", json!({"status": "running"}), None)
810 .unwrap();
811 }
812 let d2 = durable(mem.clone());
814 let r = d2.restore().unwrap();
815 let m = r.manifest.as_ref().unwrap();
816 assert_eq!(m.generation, 2, "generation bumped");
817 assert_eq!(m.starts["w.s"]["last_fired"], json!(1));
818 let pending = r.inbox_pending();
819 assert_eq!(pending.len(), 1, "the done event does not replay");
820 assert_eq!(pending[0].kind, "start_fired");
821 assert_eq!(r.timers().len(), 1);
822 assert_eq!(r.timers()[0].deadline_ms, 42);
823 assert_eq!(
824 r.of(Kind::Run).len(),
825 2,
826 "indexed + unindexed runs restored"
827 );
828 assert!(r.unindexed.iter().any(|u| u.id == "r2"));
829 assert!(
830 r.lost
831 .iter()
832 .any(|l| l.kind == "subagent" && l.id == "gone")
833 );
834 assert_eq!(r.of(Kind::Task).len(), 1);
835 assert_eq!(
837 d2.put(
838 Kind::Run,
839 "r",
840 json!({"status": "done"}),
841 Some("hash".into())
842 )
843 .unwrap(),
844 2
845 );
846 assert!(!d2.manifest().entities.iter().any(|e| e.id == "gone"));
848 }
849
850 #[test]
851 fn degrade_policy_keeps_going_and_flags_it() {
852 let mem = Arc::new(MemoryStore::new());
853 let d = Durable::new(
854 mem.clone(),
855 "agentd",
856 "inst",
857 Policy {
858 debounce: Duration::from_millis(0),
859 on_error: crate::config::v2::StoreOnError::Degrade,
860 retries: 1,
861 },
862 None,
863 );
864 mem.fail_next(1);
865 assert_eq!(
866 d.put(Kind::Run, "r", json!({}), None).unwrap(),
867 1,
868 "degraded write reports the intended seq"
869 );
870 assert!(d.is_degraded());
871 assert_eq!(
872 d.put(Kind::Run, "r", json!({}), None).unwrap(),
873 2,
874 "seq not reused"
875 );
876 assert!(!d.is_degraded(), "a successful write clears the flag");
877 let d2 = durable(mem.clone());
879 mem.fail_next(5);
880 assert!(matches!(
881 d2.put(Kind::Run, "x", json!({}), None),
882 Err(StoreError::Io(_))
883 ));
884 }
885}