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