Skip to main content

bark/movement/
manager.rs

1use std::cmp::PartialEq;
2use std::collections::{HashMap, HashSet};
3use std::sync::Arc;
4
5use tokio::sync::{Mutex, OwnedMutexGuard, RwLock};
6
7use crate::movement::{Movement, MovementId, MovementStatus, MovementSubsystem};
8use crate::movement::error::MovementError;
9use crate::movement::update::MovementUpdate;
10use crate::notification::NotificationDispatch;
11use crate::persist::BarkPersister;
12use crate::subsystem::Subsystem;
13
14/// A minimalist helper class to handle movement registration and updating based on unique
15/// [Subsystem] values.
16pub struct MovementManager {
17	db: Arc<dyn BarkPersister>,
18	subsystem_ids: RwLock<HashSet<Subsystem>>,
19	active_movements: RwLock<HashMap<MovementId, Arc<Mutex<Movement>>>>,
20	notifications: NotificationDispatch,
21}
22
23impl MovementManager {
24	/// Creates an instances of the [MovementManager].
25	pub(crate) fn new(
26		db: Arc<dyn BarkPersister>,
27		notifications: NotificationDispatch,
28	) -> Self {
29		Self {
30			db, notifications,
31			subsystem_ids: RwLock::new(HashSet::new()),
32			active_movements: RwLock::new(HashMap::new()),
33		}
34	}
35
36	/// Registers a subsystem with the movement manager. Subsystems are identified using unique
37	/// names, to maintain this guarantee a unique [Subsystem] will be generated and returned by
38	/// this function. Future calls to register or modify movements must provide this ID.
39	pub async fn register_subsystem(&self, id: Subsystem) -> anyhow::Result<(), MovementError> {
40		let mut guard = self.subsystem_ids.write().await;
41		if guard.contains(&id) {
42			Err(MovementError::SubsystemError {
43				id, error: "Subsystem already registered".into(),
44			})
45		} else {
46			guard.insert(id);
47			Ok(())
48		}
49	}
50
51	/// Persists the new movement to the db
52	///
53	/// This is a helper for the constructors but doesn't emit a notification.
54	async fn persist_new_movement(
55		&self,
56		subsystem_id: Subsystem,
57		movement_kind: impl Into<String>,
58		action_id: Option<&str>,
59	) -> anyhow::Result<MovementId, MovementError> {
60		self.db.create_new_movement(
61			MovementStatus::Pending,
62			&MovementSubsystem {
63				name: subsystem_id.as_name().to_string(),
64				kind: movement_kind.into(),
65			},
66			chrono::Local::now(),
67			action_id,
68		).await.map_err(|e| MovementError::CreationError { e })
69	}
70
71	/// Begins the process of creating a new movement. This newly created movement will be defaulted
72	/// to a [MovementStatus::Pending] state. It can then be updated by using [MovementUpdate] in
73	/// combination with [MovementManager::update_movement].
74	///
75	/// [MovementManager::finish_movement] can be used once a movement has finished (whether
76	/// successful or not).
77	///
78	/// This method also dispatches the movement as a notification.
79	///
80	/// Parameters:
81	/// - subsystem_id: The ID of the subsystem that wishes to start a new movement.
82	/// - movement_kind: A descriptor for the type of movement being performed, e.g. "send",
83	///   "receive", "round".
84	///
85	/// Errors:
86	/// - If the subsystem ID is not recognized.
87	/// - If a database error occurs.
88	pub async fn new_movement(
89		&self,
90		subsystem_id: Subsystem,
91		movement_kind: impl Into<String>,
92	) -> anyhow::Result<MovementId, MovementError> {
93		let id = self.persist_new_movement(subsystem_id, movement_kind, None).await?;
94		let movement = self.db.get_movement_by_id(id).await
95			.map_err(|e| MovementError::LoadError { id, e })?;
96		self.notifications.dispatch_movement_created(movement);
97		Ok(id)
98	}
99
100	/// Creates a new [Movement] and returns a [MovementGuard] to manage it. The guard will call
101	/// [MovementManager::finish_movement] on drop unless [MovementGuard::success] has already been
102	/// called.
103	///
104	/// See [MovementManager::new_movement] and [MovementGuard::new] for more information.
105	///
106	/// This method also dispatches the movement as a notification.
107	///
108	/// Parameters:
109	/// - subsystem_id: The ID of the subsystem that wishes to start a new movement.
110	/// - movement_kind: A descriptor for the type of movement being performed, e.g. "send",
111	///   "receive", "round".
112	/// - on_drop: Determines what status the movement will be set to when the guard is dropped.
113	pub async fn new_guarded_movement(
114		self: &Arc<Self>,
115		subsystem_id: Subsystem,
116		movement_kind: impl Into<String>,
117		on_drop: OnDropStatus,
118	) -> anyhow::Result<MovementGuard, MovementError> {
119		Ok(MovementGuard::new(
120			self.new_movement(subsystem_id, movement_kind).await?, self.clone(), on_drop,
121		))
122	}
123
124	/// Similar to [MovementManager::new_movement] but it immediately calls
125	/// [MovementManager::update_movement] afterward.
126	///
127	/// This method also dispatches the movement as a notification.
128	///
129	/// Parameters:
130	/// - subsystem_id: The ID of the subsystem that wishes to start a new movement.
131	/// - movement_kind: A descriptor for the type of movement being performed, e.g. "send",
132	///   "receive", "round".
133	/// - update: Describes the initial state of the movement.
134	///
135	/// Errors:
136	/// - If the subsystem ID is not recognized.
137	/// - If a database error occurs.
138	pub async fn new_movement_with_update(
139		&self,
140		subsystem_id: Subsystem,
141		movement_kind: impl Into<String>,
142		update: MovementUpdate,
143	) -> anyhow::Result<MovementId, MovementError> {
144		let id = self.persist_new_movement(subsystem_id, movement_kind, None).await?;
145		self.update_movement(id, update).await?;
146		let movement = self.db.get_movement_by_id(id).await
147			.map_err(|e| MovementError::LoadError { id, e })?;
148		self.notifications.dispatch_movement_created(movement);
149		Ok(id)
150	}
151
152	/// Find-or-create the movement owned by `action_id`, applying `update` on
153	/// first creation.
154	///
155	/// This is the idempotent counterpart to [MovementManager::new_movement_with_update]
156	/// for movements created mid-way through a wallet action: a re-driven step
157	/// (crash recovery, an early wake, the reentrancy double-drive) reuses the
158	/// existing movement instead of inserting a duplicate. The `created`
159	/// notification is dispatched only on the first, real insert.
160	pub async fn get_or_create_movement_with_action(
161		&self,
162		subsystem_id: Subsystem,
163		movement_kind: impl Into<String>,
164		action_id: &str,
165		update: MovementUpdate,
166	) -> anyhow::Result<MovementId, MovementError> {
167		let subsystem = MovementSubsystem {
168			name: subsystem_id.as_name().to_string(),
169			kind: movement_kind.into(),
170		};
171		let (id, created) = self.db.get_or_create_movement_for_action(
172			&subsystem, chrono::Local::now(), action_id, update,
173		).await.map_err(|e| MovementError::CreationError { e })?;
174
175		// Only the first, real insert should announce the movement.
176		if created {
177			let movement = self.db.get_movement_by_id(id).await
178				.map_err(|e| MovementError::LoadError { id, e })?;
179			self.notifications.dispatch_movement_created(movement);
180		}
181		Ok(id)
182	}
183
184	/// Similar to [MovementManager::new_guarded_movement] but it immediately calls
185	/// [MovementManager::update_movement] after creating the [Movement].
186	///
187	/// This method also dispatches the movement as a notification.
188	///
189	/// Parameters:
190	/// - subsystem_id: The ID of the subsystem that wishes to start a new movement.
191	/// - movement_kind: A descriptor for the type of movement being performed, e.g. "send",
192	///   "receive", "round".
193	/// - on_drop: Determines what status the movement will be set to when the guard is dropped.
194	/// - update: Describes the initial state of the movement.
195	///
196	/// Errors:
197	/// - If the subsystem ID is not recognized.
198	/// - If a database error occurs.
199	pub async fn new_guarded_movement_with_update(
200		self: &Arc<Self>,
201		subsystem_id: Subsystem,
202		movement_kind: impl Into<String>,
203		on_drop: OnDropStatus,
204		update: MovementUpdate,
205	) -> anyhow::Result<MovementGuard, MovementError> {
206		Ok(MovementGuard::new(
207			self.new_movement_with_update(subsystem_id, movement_kind, update).await?,
208			self.clone(),
209			on_drop,
210		))
211	}
212
213	/// Creates and marks a [Movement] as finished based on the given parameters. This is useful for
214	/// one-shot movements where the details are known at the time of creation, an example would be
215	/// when receiving funds asynchronously from a third party.
216	///
217	/// This method also dispatches the movement as a notification.
218	///
219	/// Parameters:
220	/// - subsystem_id: The ID of the subsystem that wishes to start a new movement.
221	/// - movement_kind: A descriptor for the type of movement being performed, e.g. "send",
222	///   "receive", "round".
223	/// - status: The [MovementStatus] to set. This can't be [MovementStatus::Pending].
224	/// - details: Contains information about the movement, e.g. what VTXOs were consumed or
225	///   produced.
226	///
227	/// Errors:
228	/// - If the subsystem ID is not recognized.
229	/// - If [MovementStatus::Pending] is given.
230	/// - If a database error occurs.
231	pub async fn new_finished_movement(
232		&self,
233		subsystem_id: Subsystem,
234		movement_kind: impl Into<String>,
235		status: MovementStatus,
236		details: MovementUpdate,
237	) -> anyhow::Result<MovementId, MovementError> {
238		if status == MovementStatus::Pending {
239			return Err(MovementError::IncorrectPendingStatus);
240		}
241		let id = self.persist_new_movement(subsystem_id, movement_kind, None).await?;
242		let mut movement = self.db.get_movement_by_id(id).await
243			.map_err(|e| MovementError::LoadError { id, e })?;
244		let at = chrono::Local::now();
245		details.apply_to(&mut movement, at);
246		movement.status = status;
247		movement.time.completed_at = Some(at);
248		self.db.update_movement(&movement).await
249			.map_err(|e| MovementError::PersisterError { id, e })?;
250		self.notifications.dispatch_movement_created(movement);
251		Ok(id)
252	}
253
254	/// Updates a movement with the given parameters.
255	///
256	/// See also: [MovementManager::new_movement] and [MovementManager::finish_movement]
257	///
258	/// This method also dispatches the movement as a notification.
259	///
260	/// Parameters:
261	/// - id: The ID of the movement previously created by [MovementManager::new_movement].
262	/// - update: Specifies properties to set on the movement. `Option` fields will be ignored if
263	///   they are `None`. `Some` will result in that particular field being overwritten.
264	///
265	/// Errors:
266	/// - If the [MovementId] is not recognized.
267	/// - If a movement is not [MovementStatus::Pending].
268	/// - If a database error occurs.
269	pub async fn update_movement(
270		&self,
271		id: MovementId,
272		update: MovementUpdate,
273	) -> anyhow::Result<(), MovementError> {
274		// Ensure the movement is loaded.
275		let mut guard = self.get_cached_movement(id).await?;
276
277		// Apply the update to the movement.
278		update.apply_to(&mut *guard, chrono::Local::now());
279
280		// Persist the changes using a read lock.
281		self.db.update_movement(&guard).await
282			.map_err(|e| MovementError::PersisterError { id, e })?;
283
284		self.notifications.dispatch_movement_updated(guard.clone());
285
286		// Drop the movement if it's in a finished state as this was likely a one-time update.
287		if guard.status != MovementStatus::Pending {
288			drop(guard);
289			self.unload_movement_from_cache(id).await?;
290		}
291		Ok(())
292	}
293
294	/// Applies an [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) JSON Merge Patch to a
295	/// movement's metadata. A non-object patch resets metadata to an empty object.
296	pub async fn patch_metadata(
297		&self,
298		id: MovementId,
299		patch: &serde_json::Value,
300	) -> anyhow::Result<(), MovementError> {
301		let mut guard = self.get_cached_movement(id).await?;
302
303		let mut value = serde_json::Value::Object(std::mem::take(&mut guard.metadata));
304		crate::utils::json_patch::merge(&mut value, patch);
305		guard.metadata = match value {
306			serde_json::Value::Object(map) => map,
307			_ => serde_json::Map::new(),
308		};
309		guard.time.updated_at = chrono::Local::now();
310
311		self.db.update_movement(&guard).await
312			.map_err(|e| MovementError::PersisterError { id, e })?;
313		self.notifications.dispatch_movement_updated(guard.clone());
314
315		if guard.status != MovementStatus::Pending {
316			drop(guard);
317			self.unload_movement_from_cache(id).await?;
318		}
319		Ok(())
320	}
321
322	/// Finalizes a movement, setting it to the given [MovementStatus].
323	///
324	/// See also: [MovementManager::new_movement] and [MovementManager::update_movement]
325	///
326	/// This method also dispatches the movement as a notification.
327	///
328	/// Parameters:
329	/// - id: The ID of the movement previously created by [MovementManager::new_movement].
330	/// - new_status: The final [MovementStatus] to set. This can't be [MovementStatus::Pending].
331	///
332	/// Errors:
333	/// - If the movement ID is not recognized.
334	/// - If [MovementStatus::Pending] is given.
335	/// - If a database error occurs.
336	pub async fn finish_movement(
337		&self,
338		id: MovementId,
339		new_status: MovementStatus,
340	) -> anyhow::Result<(), MovementError> {
341		if new_status == MovementStatus::Pending {
342			return Err(MovementError::IncorrectPendingStatus);
343		}
344
345		// Ensure the movement is loaded.
346		let mut guard = self.get_cached_movement(id).await?;
347
348		// Update the status and persist it.
349		guard.status = new_status;
350		guard.time.completed_at = Some(chrono::Local::now());
351		self.db.update_movement(&*guard).await
352			.map_err(|e| MovementError::PersisterError { id, e })?;
353
354		self.notifications.dispatch_movement_updated(guard.clone());
355
356		drop(guard);
357		self.unload_movement_from_cache(id).await
358	}
359
360	/// Applies a [MovementUpdate] before finalizing the movement with
361	/// [MovementManager::finish_movement].
362	///
363	/// This method also dispatches the movement as a notification.
364	///
365	/// Parameters:
366	/// - id: The ID of the movement previously created by [MovementManager::new_movement].
367	/// - new_status: The final [MovementStatus] to set. This can't be [MovementStatus::Pending].
368	/// - update: Contains information to apply to the movement before finalizing it.
369	///
370	/// Errors:
371	/// - If the movement ID is not recognized.
372	/// - If [MovementStatus::Pending] is given.
373	/// - If a database error occurs.
374	pub async fn finish_movement_with_update(
375		&self,
376		id: MovementId,
377		new_status: MovementStatus,
378		update: MovementUpdate,
379	) -> anyhow::Result<(), MovementError> {
380		if new_status == MovementStatus::Pending {
381			return Err(MovementError::IncorrectPendingStatus);
382		}
383
384		let mut guard = self.get_cached_movement(id).await?;
385
386		update.apply_to(&mut *guard, chrono::Local::now());
387		guard.status = new_status;
388		guard.time.completed_at = Some(chrono::Local::now());
389		self.db.update_movement(&*guard).await
390			.map_err(|e| MovementError::PersisterError { id, e })?;
391
392		self.notifications.dispatch_movement_updated(guard.clone());
393
394		drop(guard);
395		self.unload_movement_from_cache(id).await
396	}
397
398	async fn get_cached_movement(
399		&self,
400		id: MovementId,
401	) -> anyhow::Result<OwnedMutexGuard<Movement>, MovementError> {
402		if let Some(lock) = self.active_movements.read().await.get(&id).cloned() {
403			return Ok(lock.lock_owned().await);
404		}
405
406		let movement_lock = {
407			// Acquire a write lock and check if another thread already loaded the movement.
408			let active_guard = self.active_movements.write().await;
409			if let Some(lock) = active_guard.get(&id).cloned() {
410				lock
411			} else {
412				Arc::new(Mutex::new(
413					self.db.get_movement_by_id(id).await
414						.map_err(|e| MovementError::LoadError { id, e })?
415				))
416			}
417		};
418		Ok(movement_lock.lock_owned().await)
419	}
420
421	async fn unload_movement_from_cache(&self, id: MovementId) -> anyhow::Result<(), MovementError> {
422		let mut lock = self.active_movements.write().await;
423		lock.remove(&id);
424		Ok(())
425	}
426}
427
428/// Determines the state to set a [Movement] to when a [MovementGuard] is dropped.
429///
430/// See [MovementGuard::new] for more information.
431#[derive(Debug, Copy, Clone, PartialEq, Eq)]
432pub enum OnDropStatus {
433	/// Marks the [Movement] as [MovementStatus::Canceled].
434	Canceled,
435	/// Marks the [Movement] as [MovementStatus::Failed].
436	Failed,
437}
438
439impl From<OnDropStatus> for MovementStatus {
440	fn from(status: OnDropStatus) -> Self {
441		match status {
442			OnDropStatus::Canceled => MovementStatus::Canceled,
443			OnDropStatus::Failed => MovementStatus::Failed,
444		}
445	}
446}
447
448/// A RAII helper class to ensure that pending movements get marked as finished in case an error
449/// occurs. You can construct a guard for an existing [Movement] with [MovementGuard::new].
450/// Alternatively, a [MovementGuard] can be coupled to a movement using
451/// [MovementGuard::new].
452///
453/// When the [MovementGuard] is dropped from the stack, it will finalize the movement according to
454/// the configured [OnDropStatus] unless [MovementGuard::success] has already been called.
455pub struct MovementGuard {
456	id: MovementId,
457	manager: Arc<MovementManager>,
458	on_drop: OnDropStatus,
459	has_finished: bool,
460}
461
462impl<'a> MovementGuard {
463	/// Constructs a [MovementGuard] to manage a pre-existing [Movement].
464	///
465	/// Parameters:
466	/// - id: The ID of the [Movement] to update.
467	/// - manager: A reference to the [MovementManager] so the guard can update the [Movement].
468	/// - on_drop: Determines what status the movement will be set to when the guard is dropped.
469	pub fn new(
470		id: MovementId,
471		manager: Arc<MovementManager>,
472		on_drop: OnDropStatus,
473	) -> Self {
474		Self {
475			id,
476			manager,
477			on_drop,
478			has_finished: false,
479		}
480	}
481
482	/// Gets the [MovementId] stored by this guard.
483	pub fn id(&self) -> MovementId {
484		self.id
485	}
486
487	/// Sets a different [OnDropStatus] to apply to the movement upon dropping the [MovementGuard].
488	///
489	/// Parameters:
490	/// - on_drop: Determines what status the movement will be set to when the guard is dropped.
491	pub fn set_on_drop_status(&mut self, status: OnDropStatus) {
492		self.on_drop = status;
493	}
494
495	/// Applies an update to the managed [Movement].
496	///
497	/// See [MovementManager::update_movement] for more information.
498	///
499	/// Parameters:
500	/// - update: Specifies properties to set on the movement. `Option` fields will be ignored if
501	///   they are `None`. `Some` will result in that particular field being overwritten.
502	pub async fn apply_update(
503		&self,
504		update: MovementUpdate,
505	) -> anyhow::Result<(), MovementError> {
506		self.manager.update_movement(self.id, update).await
507	}
508
509	/// Same as [MovementGuard::success] but sets [Movement::status] to [MovementStatus::Canceled].
510	pub async fn cancel(&mut self) -> anyhow::Result<(), MovementError> {
511		self.stop();
512		self.manager.finish_movement(self.id, MovementStatus::Canceled).await
513	}
514
515	/// Same as [MovementGuard::success] but sets [Movement::status] to [MovementStatus::Failed].
516	pub async fn fail(&mut self) -> anyhow::Result<(), MovementError> {
517		self.stop();
518		self.manager.finish_movement(self.id, MovementStatus::Failed).await
519	}
520
521	/// Finalizes a movement, setting it to [MovementStatus::Successful]. If the [MovementGuard] is
522	/// dropped after calling this function, no further changes will be made to the [Movement].
523	///
524	/// See [MovementManager::finish_movement] for more information.
525	pub async fn success(
526		&mut self,
527	) -> anyhow::Result<(), MovementError> {
528		self.stop();
529		self.manager.finish_movement(self.id, MovementStatus::Successful).await
530	}
531
532	/// Prevents the guard from making further changes to the movement after being dropped. Manual
533	/// actions such as [MovementGuard::apply_update] will continue to work.
534	pub fn stop(&mut self) {
535		self.has_finished = true;
536	}
537}
538
539impl Drop for MovementGuard {
540	fn drop(&mut self) {
541		if !self.has_finished {
542			// Asynchronously mark the movement as finished since we are being dropped.
543			let manager = self.manager.clone();
544			let id = self.id;
545			let on_drop = self.on_drop;
546
547			crate::utils::spawn(async move {
548				if let Err(e) = manager.finish_movement(id, on_drop.into()).await {
549					log::error!("An error occurred in MovementGuard::drop(): {:#}", e);
550				}
551			});
552		}
553	}
554}