1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::{Duration, Instant};
4
5use chrono::{DateTime, Utc};
6
7use async_trait::async_trait;
8use tokio::io::AsyncWriteExt;
9use tokio::sync::Mutex;
10
11use crate::event::Event;
12
13#[derive(Debug, thiserror::Error)]
14pub enum JournalError {
15 #[error("sequence conflict for run {run_id}: expected {expected}, got {got}")]
16 Conflict { run_id: String, expected: u64, got: u64 },
17 #[error("io error: {0}")]
18 Io(#[from] std::io::Error),
19 #[error("corrupt journal entry: {0}")]
20 Corrupt(String),
21 #[error("journal backend error: {0}")]
22 Backend(String),
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct LeaseHolder {
28 pub owner: String,
29 pub expires_at: DateTime<Utc>,
30}
31
32#[async_trait]
42pub trait Journal: Send + Sync {
43 async fn append(&self, event: &Event) -> Result<(), JournalError>;
44 async fn load(&self, run_id: &str) -> Result<Vec<Event>, JournalError>;
45
46 async fn acquire_lease(
49 &self,
50 _run_id: &str,
51 _owner: &str,
52 _ttl: Duration,
53 ) -> Result<Result<(), LeaseHolder>, JournalError> {
54 Ok(Ok(()))
55 }
56
57 async fn renew_lease(&self, _run_id: &str, _owner: &str, _ttl: Duration) -> Result<bool, JournalError> {
59 Ok(true)
60 }
61
62 async fn release_lease(&self, _run_id: &str, _owner: &str) -> Result<(), JournalError> {
63 Ok(())
64 }
65}
66
67#[derive(Default)]
69struct Leases(std::sync::Mutex<HashMap<String, (String, Instant)>>);
70
71impl Leases {
72 fn acquire(&self, run_id: &str, owner: &str, ttl: Duration) -> Result<(), LeaseHolder> {
73 let mut map = self.0.lock().unwrap();
74 let now = Instant::now();
75 if let Some((holder, until)) = map.get(run_id) {
76 if holder != owner && *until > now {
77 let left = chrono::Duration::from_std(*until - now).unwrap_or_default();
78 return Err(LeaseHolder { owner: holder.clone(), expires_at: Utc::now() + left });
79 }
80 }
81 map.insert(run_id.to_owned(), (owner.to_owned(), now + ttl));
82 Ok(())
83 }
84
85 fn renew(&self, run_id: &str, owner: &str, ttl: Duration) -> bool {
86 let mut map = self.0.lock().unwrap();
87 match map.get_mut(run_id) {
88 Some((holder, until)) if holder == owner => {
89 *until = Instant::now() + ttl;
90 true
91 }
92 _ => false,
93 }
94 }
95
96 fn release(&self, run_id: &str, owner: &str) {
97 let mut map = self.0.lock().unwrap();
98 if map.get(run_id).is_some_and(|(holder, _)| holder == owner) {
99 map.remove(run_id);
100 }
101 }
102}
103
104#[derive(Default)]
105pub struct InMemoryJournal {
106 runs: Mutex<HashMap<String, Vec<Event>>>,
107 leases: Leases,
108}
109
110impl InMemoryJournal {
111 pub fn new() -> Self {
112 Self::default()
113 }
114}
115
116#[async_trait]
117impl Journal for InMemoryJournal {
118 async fn append(&self, event: &Event) -> Result<(), JournalError> {
119 let mut runs = self.runs.lock().await;
120 let events = runs.entry(event.run_id.clone()).or_default();
121 let expected = events.len() as u64;
122 if event.seq != expected {
123 return Err(JournalError::Conflict { run_id: event.run_id.clone(), expected, got: event.seq });
124 }
125 events.push(event.clone());
126 Ok(())
127 }
128
129 async fn load(&self, run_id: &str) -> Result<Vec<Event>, JournalError> {
130 Ok(self.runs.lock().await.get(run_id).cloned().unwrap_or_default())
131 }
132
133 async fn acquire_lease(
134 &self,
135 run_id: &str,
136 owner: &str,
137 ttl: Duration,
138 ) -> Result<Result<(), LeaseHolder>, JournalError> {
139 Ok(self.leases.acquire(run_id, owner, ttl))
140 }
141
142 async fn renew_lease(&self, run_id: &str, owner: &str, ttl: Duration) -> Result<bool, JournalError> {
143 Ok(self.leases.renew(run_id, owner, ttl))
144 }
145
146 async fn release_lease(&self, run_id: &str, owner: &str) -> Result<(), JournalError> {
147 self.leases.release(run_id, owner);
148 Ok(())
149 }
150}
151
152pub struct JsonlJournal {
157 dir: PathBuf,
158 next_seq: Mutex<HashMap<String, u64>>,
159 leases: Leases,
161}
162
163impl JsonlJournal {
164 pub fn new(dir: impl Into<PathBuf>) -> Result<Self, JournalError> {
165 let dir = dir.into();
166 std::fs::create_dir_all(&dir)?;
167 Ok(Self { dir, next_seq: Mutex::new(HashMap::new()), leases: Leases::default() })
168 }
169
170 pub fn path_for(&self, run_id: &str) -> PathBuf {
171 self.dir.join(format!("{run_id}.jsonl"))
172 }
173
174 pub fn read_file(path: &std::path::Path) -> Result<Vec<Event>, JournalError> {
176 let text = std::fs::read_to_string(path)?;
177 parse_lines(&text)
178 }
179}
180
181fn parse_lines(text: &str) -> Result<Vec<Event>, JournalError> {
182 text.lines()
183 .filter(|l| !l.trim().is_empty())
184 .map(|l| serde_json::from_str(l).map_err(|e| JournalError::Corrupt(e.to_string())))
185 .collect()
186}
187
188#[async_trait]
189impl Journal for JsonlJournal {
190 async fn append(&self, event: &Event) -> Result<(), JournalError> {
191 let mut seqs = self.next_seq.lock().await;
192 let expected = match seqs.get(&event.run_id) {
193 Some(n) => *n,
194 None => self.load(&event.run_id).await?.len() as u64,
195 };
196 if event.seq != expected {
197 return Err(JournalError::Conflict { run_id: event.run_id.clone(), expected, got: event.seq });
198 }
199
200 let mut line = serde_json::to_vec(event).map_err(|e| JournalError::Corrupt(e.to_string()))?;
201 line.push(b'\n');
202 let mut file =
203 tokio::fs::OpenOptions::new().create(true).append(true).open(self.path_for(&event.run_id)).await?;
204 file.write_all(&line).await?;
205 file.sync_data().await?;
206
207 seqs.insert(event.run_id.clone(), expected + 1);
208 Ok(())
209 }
210
211 async fn load(&self, run_id: &str) -> Result<Vec<Event>, JournalError> {
212 match tokio::fs::read_to_string(self.path_for(run_id)).await {
213 Ok(text) => parse_lines(&text),
214 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
215 Err(e) => Err(e.into()),
216 }
217 }
218
219 async fn acquire_lease(
220 &self,
221 run_id: &str,
222 owner: &str,
223 ttl: Duration,
224 ) -> Result<Result<(), LeaseHolder>, JournalError> {
225 Ok(self.leases.acquire(run_id, owner, ttl))
226 }
227
228 async fn renew_lease(&self, run_id: &str, owner: &str, ttl: Duration) -> Result<bool, JournalError> {
229 Ok(self.leases.renew(run_id, owner, ttl))
230 }
231
232 async fn release_lease(&self, run_id: &str, owner: &str) -> Result<(), JournalError> {
233 self.leases.release(run_id, owner);
234 Ok(())
235 }
236}