1use std::collections::HashSet;
2use std::fmt::Debug;
3use std::ops::Range;
4use std::sync::Arc;
5use std::time::{Duration, SystemTime};
6
7use fedimint_client_module::oplog::{
8 IOperationLog, JsonStringed, OperationLogEntry, OperationOutcome, UpdateStreamOrOutcome,
9};
10use fedimint_core::core::OperationId;
11use fedimint_core::db::{
12 Database, DatabaseError, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _,
13};
14use fedimint_core::task::{MaybeSend, MaybeSync};
15use fedimint_core::time::now;
16use fedimint_core::util::{BoxStream, FmtCompact as _};
17use fedimint_core::{apply, async_trait_maybe_send, maybe_add_send, maybe_add_send_sync};
18use fedimint_logging::LOG_CLIENT;
19use futures::StreamExt as _;
20use serde::Serialize;
21use serde::de::DeserializeOwned;
22use tokio::sync::RwLock;
23use tracing::{error, instrument, warn};
24
25use crate::db::{ChronologicalOperationLogKey, OperationLogKey};
26
27#[cfg(test)]
28mod tests;
29
30#[derive(Debug, Clone)]
31pub struct OperationLog {
32 db: Database,
33 oldest_entry: Arc<RwLock<Option<ChronologicalOperationLogKey>>>,
34}
35
36impl OperationLog {
37 pub fn new(db: Database) -> Self {
38 Self {
39 db,
40 oldest_entry: Arc::new(RwLock::new(None)),
41 }
42 }
43
44 async fn get_oldest_operation_log_key(&self) -> Option<ChronologicalOperationLogKey> {
48 if let Some(oldest_entry) = *self.oldest_entry.read().await {
49 return Some(oldest_entry);
50 }
51
52 let mut dbtx = self.db.begin_transaction_nc().await;
53 let oldest_entry = dbtx
54 .find_by_prefix(&crate::db::ChronologicalOperationLogKeyPrefix)
55 .await
56 .map(|(key, ())| key)
57 .next()
58 .await;
59
60 if let Some(oldest_entry) = oldest_entry {
61 *self.oldest_entry.write().await = Some(oldest_entry);
62 }
63
64 oldest_entry
65 }
66
67 pub async fn add_operation_log_entry_dbtx(
68 &self,
69 dbtx: &mut DatabaseTransaction<'_>,
70 operation_id: OperationId,
71 operation_type: &str,
72 operation_meta: impl serde::Serialize,
73 ) {
74 self.add_operation_log_entry_dbtx_with_creation_time(
75 dbtx,
76 operation_id,
77 operation_type,
78 operation_meta,
79 now(),
80 )
81 .await;
82 }
83
84 pub async fn add_operation_log_entry_dbtx_with_creation_time(
85 &self,
86 dbtx: &mut DatabaseTransaction<'_>,
87 operation_id: OperationId,
88 operation_type: &str,
89 operation_meta: impl serde::Serialize,
90 creation_time: SystemTime,
91 ) {
92 dbtx.insert_new_entry(
93 &OperationLogKey { operation_id },
94 &OperationLogEntry::new(
95 operation_type.to_string(),
96 JsonStringed(
97 serde_json::to_value(operation_meta)
98 .expect("Can only fail if meta is not serializable"),
99 ),
100 None,
101 ),
102 )
103 .await;
104 let chronological_operation_log_key = ChronologicalOperationLogKey {
105 creation_time,
106 operation_id,
107 };
108 dbtx.insert_new_entry(&chronological_operation_log_key, &())
109 .await;
110
111 let mut oldest_entry = self.oldest_entry.write().await;
112 if let Some(oldest_entry_key) = &mut *oldest_entry
113 && chronological_operation_log_key_is_older(
114 &chronological_operation_log_key,
115 oldest_entry_key,
116 )
117 {
118 *oldest_entry_key = chronological_operation_log_key;
119 }
120 }
121
122 #[deprecated(since = "0.6.0", note = "Use `paginate_operations_rev` instead")]
123 pub async fn list_operations(
124 &self,
125 limit: usize,
126 last_seen: Option<ChronologicalOperationLogKey>,
127 ) -> Vec<(ChronologicalOperationLogKey, OperationLogEntry)> {
128 self.paginate_operations_rev(limit, last_seen).await
129 }
130
131 pub async fn paginate_operations_rev(
134 &self,
135 limit: usize,
136 last_seen: Option<ChronologicalOperationLogKey>,
137 ) -> Vec<(ChronologicalOperationLogKey, OperationLogEntry)> {
138 const EPOCH_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 7);
139
140 let start_after_key = last_seen.unwrap_or_else(|| ChronologicalOperationLogKey {
141 creation_time: now() + Duration::from_secs(30),
145 operation_id: OperationId([0; 32]),
146 });
147
148 let Some(oldest_entry_key) = self.get_oldest_operation_log_key().await else {
149 return vec![];
150 };
151
152 let mut dbtx = self.db.begin_transaction_nc().await;
153 let mut operation_log_keys = Vec::with_capacity(32);
154
155 'outer: for key_range_rev in
160 rev_epoch_ranges(start_after_key, oldest_entry_key, EPOCH_DURATION)
161 {
162 let epoch_operation_log_keys_rev = dbtx
163 .find_by_range(key_range_rev)
164 .await
165 .map(|(key, ())| key)
166 .collect::<Vec<_>>()
167 .await;
168
169 for operation_log_key in epoch_operation_log_keys_rev.into_iter().rev() {
170 operation_log_keys.push(operation_log_key);
171 if operation_log_keys.len() >= limit {
172 break 'outer;
173 }
174 }
175 }
176
177 debug_assert!(
178 operation_log_keys.iter().collect::<HashSet<_>>().len() == operation_log_keys.len(),
179 "Operation log keys returned are not unique"
180 );
181
182 let mut operation_log_entries = Vec::with_capacity(operation_log_keys.len());
183 for operation_log_key in operation_log_keys {
184 let operation_log_entry = dbtx
185 .get_value(&OperationLogKey {
186 operation_id: operation_log_key.operation_id,
187 })
188 .await
189 .expect("Inconsistent DB");
190 operation_log_entries.push((operation_log_key, operation_log_entry));
191 }
192
193 operation_log_entries
194 }
195
196 pub async fn get_operation(&self, operation_id: OperationId) -> Option<OperationLogEntry> {
197 Self::get_operation_dbtx(
198 &mut self.db.begin_transaction_nc().await.into_nc(),
199 operation_id,
200 )
201 .await
202 }
203
204 pub async fn get_operation_dbtx(
205 dbtx: &mut DatabaseTransaction<'_>,
206 operation_id: OperationId,
207 ) -> Option<OperationLogEntry> {
208 dbtx.get_value(&OperationLogKey { operation_id }).await
209 }
210
211 #[instrument(target = LOG_CLIENT, skip(db), level = "debug")]
213 pub async fn set_operation_outcome(
214 db: &Database,
215 operation_id: OperationId,
216 outcome: &(impl Serialize + Debug),
217 ) -> Result<(), DatabaseError> {
218 let outcome_json =
219 JsonStringed(serde_json::to_value(outcome).expect("Outcome is not serializable"));
220
221 let mut dbtx = db.begin_transaction().await;
222 let mut operation = Self::get_operation_dbtx(&mut dbtx.to_ref_nc(), operation_id)
223 .await
224 .expect("Operation exists");
225 operation.set_outcome(OperationOutcome {
226 time: fedimint_core::time::now(),
227 outcome: outcome_json,
228 });
229 dbtx.insert_entry(&OperationLogKey { operation_id }, &operation)
230 .await;
231 dbtx.commit_tx_result().await?;
232
233 Ok(())
234 }
235
236 pub fn outcome_or_updates<U, S>(
255 db: &Database,
256 operation_id: OperationId,
257 operation_log_entry: &OperationLogEntry,
258 is_terminal: impl Fn(&U) -> bool + MaybeSend + MaybeSync + 'static,
259 stream_gen: impl FnOnce() -> S,
260 ) -> UpdateStreamOrOutcome<U>
261 where
262 U: Clone + Serialize + DeserializeOwned + Debug + MaybeSend + MaybeSync + 'static,
263 S: futures::Stream<Item = U> + MaybeSend + 'static,
264 {
265 match operation_log_entry.try_outcome::<U>() {
266 Ok(Some(outcome)) if is_terminal(&outcome) => {
267 return UpdateStreamOrOutcome::Outcome(outcome);
268 }
269 Ok(Some(_non_terminal)) => {
270 warn!(
271 target: LOG_CLIENT,
272 "Cached operation outcome is not a terminal update (cached by a previous \
273 version); rebuilding it from the update stream"
274 );
275 }
276 Ok(None) => {}
277 Err(err) => {
278 warn!(
279 target: LOG_CLIENT,
280 err = %err.fmt_compact(),
281 "Cached operation outcome failed to deserialize; rebuilding it from the update stream"
282 );
283 }
284 }
285 UpdateStreamOrOutcome::UpdateStream(caching_operation_update_stream(
286 db.clone(),
287 operation_id,
288 stream_gen(),
289 is_terminal,
290 ))
291 }
292
293 pub async fn optimistically_set_operation_outcome(
298 db: &Database,
299 operation_id: OperationId,
300 outcome: &(impl Serialize + Debug),
301 ) {
302 if let Err(e) = Self::set_operation_outcome(db, operation_id, outcome).await {
303 warn!(
304 target: LOG_CLIENT,
305 err = %e.fmt_compact(),
306 "Error setting operation outcome"
307 );
308 }
309 }
310}
311
312#[apply(async_trait_maybe_send!)]
313impl IOperationLog for OperationLog {
314 async fn get_operation(&self, operation_id: OperationId) -> Option<OperationLogEntry> {
315 OperationLog::get_operation(self, operation_id).await
316 }
317
318 async fn get_operation_dbtx(
319 &self,
320 dbtx: &mut DatabaseTransaction<'_>,
321 operation_id: OperationId,
322 ) -> Option<OperationLogEntry> {
323 OperationLog::get_operation_dbtx(dbtx, operation_id).await
324 }
325
326 async fn add_operation_log_entry_dbtx(
327 &self,
328 dbtx: &mut DatabaseTransaction<'_>,
329 operation_id: OperationId,
330 operation_type: &str,
331 operation_meta: serde_json::Value,
332 ) {
333 OperationLog::add_operation_log_entry_dbtx(
334 self,
335 dbtx,
336 operation_id,
337 operation_type,
338 operation_meta,
339 )
340 .await
341 }
342
343 fn caching_operation_update_stream(
344 &self,
345 operation_id: OperationId,
346 stream_gen: Box<maybe_add_send!(dyn FnOnce() -> BoxStream<'static, serde_json::Value>)>,
347 is_terminal: Box<maybe_add_send_sync!(dyn Fn(&serde_json::Value) -> bool)>,
348 ) -> BoxStream<'static, serde_json::Value> {
349 caching_operation_update_stream(self.db.clone(), operation_id, stream_gen(), is_terminal)
350 }
351}
352
353fn chronological_operation_log_key_is_older(
354 lhs: &ChronologicalOperationLogKey,
355 rhs: &ChronologicalOperationLogKey,
356) -> bool {
357 (lhs.creation_time, lhs.operation_id) < (rhs.creation_time, rhs.operation_id)
358}
359
360fn rev_epoch_ranges(
370 start_after: ChronologicalOperationLogKey,
371 last_entry: ChronologicalOperationLogKey,
372 epoch_duration: Duration,
373) -> impl Iterator<Item = Range<ChronologicalOperationLogKey>> {
374 (0..)
379 .map(move |epoch| start_after.creation_time - epoch * epoch_duration)
380 .take_while(move |&start_time| start_time >= last_entry.creation_time)
384 .map(move |start_time| {
385 let end_time = start_time - epoch_duration;
386
387 let start_key = if start_time == start_after.creation_time {
391 start_after
392 } else {
393 ChronologicalOperationLogKey {
394 creation_time: start_time,
395 operation_id: OperationId([0; 32]),
396 }
397 };
398
399 let end_key = ChronologicalOperationLogKey {
404 creation_time: end_time,
405 operation_id: OperationId([0; 32]),
406 };
407
408 Range {
412 start: end_key,
413 end: start_key,
414 }
415 })
416}
417
418pub fn caching_operation_update_stream<'a, U, S>(
427 db: Database,
428 operation_id: OperationId,
429 stream: S,
430 is_terminal: impl Fn(&U) -> bool + MaybeSend + 'a,
431) -> BoxStream<'a, U>
432where
433 U: Clone + Serialize + Debug + MaybeSend + MaybeSync + 'static,
434 S: futures::Stream<Item = U> + MaybeSend + 'a,
435{
436 let mut stream = Box::pin(stream);
437 Box::pin(async_stream::stream! {
438 let mut last_update = None;
439 while let Some(update) = stream.next().await {
440 yield update.clone();
441 last_update = Some(update);
442 }
443
444 let Some(last_update) = last_update else {
445 error!(
446 target: LOG_CLIENT,
447 "Stream ended without any updates, this should not happen!"
448 );
449 return;
450 };
451
452 if !is_terminal(&last_update) {
453 warn!(
454 target: LOG_CLIENT,
455 ?last_update,
456 "Operation update stream ended on a non-terminal update; not caching an outcome"
457 );
458 return;
459 }
460
461 OperationLog::optimistically_set_operation_outcome(&db, operation_id, &last_update).await;
462 })
463}