Skip to main content

bark/daemon/
mod.rs

1pub mod tip_watcher;
2
3use std::sync::{Arc, Weak};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6
7use ark::rounds::RoundEvent;
8use futures::{FutureExt, StreamExt};
9use log::{info, trace, warn};
10#[cfg(not(feature = "wasm-web"))]
11use tokio::task::JoinHandle;
12use tokio_util::sync::CancellationToken;
13
14use crate::{Wallet, WalletInner};
15use crate::utils::ReconnectBackoff;
16
17
18
19/// A handle to a running background daemon
20#[cfg(not(feature = "wasm-web"))]
21pub struct DaemonHandle {
22	shutdown: CancellationToken,
23	jh: JoinHandle<()>,
24}
25
26/// A handle to a running background daemon for WASM
27#[cfg(feature = "wasm-web")]
28pub struct DaemonHandle {
29	shutdown: CancellationToken,
30}
31
32impl DaemonHandle {
33	/// Trigger the daemon process to stop
34	pub fn stop(&self) {
35		self.shutdown.cancel();
36	}
37
38	/// Stop the daemon process and wait for it to finish
39	pub async fn stop_wait(self) -> anyhow::Result<()> {
40		self.stop();
41		#[cfg(not(feature = "wasm-web"))]
42		self.jh.await?;
43		Ok(())
44	}
45}
46
47pub(crate) fn start_daemon(
48	wallet: &Wallet,
49) -> DaemonHandle {
50	let shutdown = CancellationToken::new();
51	let proc = DaemonProcess::new(shutdown.clone(), wallet);
52
53	#[cfg(not(feature = "wasm-web"))]
54	{
55		let jh = crate::utils::spawn(proc.run());
56		DaemonHandle { shutdown, jh }
57	}
58	#[cfg(feature = "wasm-web")]
59	{
60		crate::utils::spawn(proc.run());
61		DaemonHandle { shutdown }
62	}
63}
64
65/// The daemon is responsible for running the wallet and performing the
66/// necessary actions to keep the wallet in a healthy state
67struct DaemonProcess {
68	shutdown: CancellationToken,
69
70	connected: AtomicBool,
71
72	/// Deliberately a [Weak] reference: the daemon task must not keep the
73	/// wallet alive on its own. When the last user-held [Wallet] clone is
74	/// dropped, [WalletInner]'s drop glue cancels `shutdown` and the daemon
75	/// halts. See [DaemonProcess::wallet] for the upgrade rules.
76	wallet: Weak<WalletInner>,
77
78	/// Cached from the wallet config so the timer loops don't need to
79	/// upgrade `wallet` just to know how long to sleep.
80	sync_interval: Duration,
81
82	/// Cached [crate::Config::daemon_manual_sync].
83	manual_sync: bool,
84}
85
86impl DaemonProcess {
87	fn new(
88		shutdown: CancellationToken,
89		wallet: &Wallet,
90	) -> DaemonProcess {
91		DaemonProcess {
92			connected: AtomicBool::new(false),
93			shutdown,
94			sync_interval: Duration::from_secs(wallet.config().daemon_sync_interval_secs),
95			manual_sync: wallet.config().daemon_manual_sync,
96			wallet: Arc::downgrade(&wallet.inner),
97		}
98	}
99
100	/// Upgrade the weak wallet reference for one unit of work.
101	///
102	/// The returned [Wallet] keeps the wallet alive for as long as it is
103	/// held, so holds must stay bounded: a strong reference held across an
104	/// always-on await point (like a message stream) would prevent the
105	/// wallet's drop glue from ever cancelling the daemon, reintroducing
106	/// the leak the [Weak] reference exists to fix.
107	///
108	/// Returns [None] once the last user-held [Wallet] clone is dropped;
109	/// callers must treat that as a shutdown signal. The wallet's drop glue
110	/// cancels our token in that case, but we cancel here too so a failed
111	/// upgrade is a sufficient signal on its own.
112	fn wallet(&self) -> Option<Wallet> {
113		match self.wallet.upgrade() {
114			Some(inner) => Some(Wallet { inner }),
115			None => {
116				self.shutdown.cancel();
117				None
118			},
119		}
120	}
121
122	/// Recursively resubscribe to mailbox message stream by waiting and
123	/// calling [Wallet::subscribe_store_mailbox_messages] again until
124	/// the daemon is shutdown.
125	///
126	/// The mailbox stream is always-on and sets `connected` to `false`
127	/// when it breaks, so other processes can back off.
128	async fn run_mailbox_messages_process(&self) {
129		loop {
130			let shutdown = self.shutdown.clone();
131			if self.connected.load(Ordering::Relaxed) {
132				trace!("Daemon subscribing to mailbox message stream");
133				let r = Wallet::subscribe_process_mailbox_messages_weak(
134					self.wallet.clone(), None, shutdown,
135				).await;
136				if let Err(e) = r {
137					warn!("An error occurred while processing mailbox messages: {e:#}");
138					self.connected.store(false, Ordering::Relaxed);
139				}
140			}
141
142			futures::select! {
143				_ = bark_runtime::sleep(self.sync_interval).fuse() => {},
144				_ = self.shutdown.cancelled().fuse() => {
145					info!("Shutdown signal received! Shutting mailbox messages process...");
146					break;
147				},
148			}
149		}
150	}
151
152	async fn handle_round_event(&self, wallet: &Wallet, event: &RoundEvent) -> anyhow::Result<()> {
153		// Do a refresh if you need to
154		match &event {
155			&RoundEvent::Attempt(attempt) => {
156				if attempt.attempt_seq == 0 {
157					if let Err(err) = wallet.join_round_for_maintenance_refresh(attempt).await {
158						warn!("Failed to join round for maintenance refresh: {:#}", err);
159					}
160				};
161			},
162			_ => {},
163		};
164
165		wallet.progress_pending_rounds(Some(event)).await
166	}
167
168	/// Subscribe to the round event stream and process events
169	/// until it closes or the daemon shuts down.
170	///
171	/// `backoff` is reset whenever an event arrives, so a stream that stays
172	/// healthy for a while reconnects promptly after it eventually drops.
173	async fn process_round_event_stream(
174		&self,
175		backoff: &mut ReconnectBackoff,
176	) -> anyhow::Result<()> {
177		// Upgrade only to open the subscription: the stream owns just the
178		// gRPC connection, so waiting on it doesn't keep the wallet alive.
179		let mut events = {
180			let Some(wallet) = self.wallet() else { return Ok(()) };
181			trace!("Daemon subscribing to round event stream");
182			wallet.subscribe_round_events().await?
183		};
184		trace!("Daemon connected to round event stream");
185
186		loop {
187			futures::select! {
188				res = events.next().fuse() => {
189					match res {
190						Some(Ok(event)) => {
191							backoff.reset();
192							let Some(wallet) = self.wallet() else { return Ok(()) };
193							if let Err(e) = self.handle_round_event(&wallet, &event).await {
194								warn!("Error processing round event: {e:#}");
195							}
196						},
197						Some(Err(e)) => {
198							return Err(e.context("error on event stream"));
199						},
200						None => {
201							return Ok(());
202						},
203					}
204				},
205				_ = self.shutdown.cancelled().fuse() => {
206					info!("Shutdown signal received! Shutting round events stream...");
207					return Ok(());
208				},
209			}
210		}
211	}
212
213	/// Keep the round events subscription alive for the
214	/// lifetime of the daemon, reconnecting as needed.
215	async fn run_round_events_process(&self) {
216		let mut backoff = ReconnectBackoff::new();
217		loop {
218			if self.shutdown.is_cancelled() {
219				info!("Shutdown signal received! Shutting round events process...");
220				break;
221			}
222
223			match self.process_round_event_stream(&mut backoff).await {
224				Ok(()) => {},
225				// A tonic h2 stream reset is almost always a
226				// proxy- or server-side idle timeout rather than
227				// a real failure; resubscribe quietly.
228				Err(e) if crate::utils::is_h2_stream_error(&e) => {
229					trace!("Round events stream reset by server, reconnecting: {e:#}");
230				},
231				Err(e) => {
232					warn!("An error occured while processing pending rounds: {e:#}");
233				},
234			}
235
236			// Always back off before resubscribing. Otherwise a stream the
237			// server keeps closing quickly — including when it is rate-limiting
238			// us by resetting our streams — becomes a tight reconnect loop that
239			// floods the server with opened-then-reset streams. The backoff
240			// resets itself once a stream delivers an event, so healthy
241			// reconnects stay prompt.
242			futures::select! {
243				_ = backoff.wait().fuse() => {},
244				_ = self.shutdown.cancelled().fuse() => {
245					info!("Shutdown signal received! Shutting round events process...");
246					break;
247				},
248			}
249		}
250	}
251
252	/// Periodically try to reconnect when the server is not reachable.
253	///
254	/// Sets `connected` to `true` on success so the round-events
255	/// and mailbox streams start subscribing again.
256	async fn run_server_connection_check_process(&self) {
257		loop {
258			futures::select! {
259				_ = bark_runtime::sleep(self.sync_interval).fuse() => {},
260				_ = self.shutdown.cancelled().fuse() => {
261					info!("Shutdown signal received! Shutting server connection check process...");
262					break;
263				},
264			}
265
266			if self.connected.load(Ordering::Relaxed) {
267				continue;
268			}
269
270			let Some(wallet) = self.wallet() else { break };
271			let result = wallet.refresh_server().await;
272			if let Err(ref e) = result {
273				warn!("Ark server reconnect failed: {:#}", e);
274			} else {
275				info!("Ark server reconnected");
276				self.connected.store(true, Ordering::Relaxed);
277			}
278		}
279	}
280
281	async fn run_sync_processes(&self) {
282		// Watch the chain tip so new blocks trigger a sync pass immediately
283		// instead of waiting for the full sync interval, which stays as a
284		// backstop. On bitcoind the watcher listens on ZMQ when configured;
285		// otherwise (including Esplora and wasm) it polls the backend.
286		let mut tip_rx = match self.wallet() {
287			Some(wallet) => match wallet.chain().tip_watcher(self.sync_interval).await {
288				Ok(watcher) => Some(watcher.subscribe()),
289				Err(e) => {
290					warn!("Daemon failed to start tip watcher, syncing on interval only: {e:#}");
291					None
292				},
293			},
294			None => None,
295		};
296
297		// NB: a ticking interval needs Instant::now(), which panics on wasm,
298		// so the loop sleeps between iterations instead.
299		loop {
300			// using if let to scope the wallet variable
301			if let Some(wallet) = self.wallet() {
302				if self.connected.load(Ordering::Relaxed) {
303					if let Err(e) = wallet.chain().update_fee_rates(wallet.config().fallback_fee_rate).await {
304						warn!("An error occured while updating fee rates: {e:#}");
305					}
306
307					if let Err(e) = wallet.sync_pending_boards().await {
308						warn!("An error occured while syncing pending board: {e:#}");
309					}
310
311					if let Err(e) = wallet.sync_pending_offboards().await {
312						warn!("An error occured while syncing pending offboards: {e:#}");
313					}
314				}
315
316				if let Err(e) = wallet.sync_onchain().await {
317					warn!("An error occured while syncing onchain: {e:#}");
318				}
319
320				if let Err(e) = wallet.sync_pending_rounds().await {
321					warn!("An error occured while syncing pending rounds: {e:#}");
322				}
323			} else {
324				info!("Wallet has been dropped. Shutting down sync processes...");
325				break;
326			}
327
328			futures::select! {
329				_ = bark_runtime::sleep(self.sync_interval).fuse() => {},
330				triggered = async {
331					match tip_rx.as_mut() {
332						Some(rx) => rx.changed().await.is_ok(),
333						None => std::future::pending::<bool>().await,
334					}
335				}.fuse() => {
336					if triggered {
337						trace!("Daemon sync triggered by new chain tip");
338					} else {
339						warn!("Tip watcher stopped; daemon falls back to interval-only syncing");
340						tip_rx = None;
341					}
342				},
343				_ = self.shutdown.cancelled().fuse() => {
344					info!("Shutdown signal received! Shutting down sync processes...");
345					break;
346				},
347			}
348		}
349	}
350
351	/// Periodically progress unilateral exits.
352	///
353	/// This deliberately runs in its own loop rather than as part of
354	/// [Self::run_sync_processes]: a repeatedly panicking sync step would
355	/// restart that whole task from the top, and exit progression must not
356	/// stay blocked behind it.
357	async fn run_exit_progress_process(&self) {
358		loop {
359			if let Some(wallet) = self.wallet() {
360				if wallet.inner.onchain.is_some() {
361					if let Err(e) = wallet.exit_mgr().progress_exits_with_cpfp(&wallet, None).await {
362						warn!("An error occurred while progressing exits: {:#}", e);
363					}
364				} else {
365					if let Err(e) = wallet.exit_mgr().progress_exits(&wallet).await {
366						warn!("An error occurred while progressing exits: {:#}", e);
367					}
368				}
369			} else {
370				info!("Wallet has been dropped. Shutting down exit progress process...");
371				break;
372			}
373
374			futures::select! {
375				_ = bark_runtime::sleep(self.sync_interval).fuse() => {},
376				_ = self.shutdown.cancelled().fuse() => {
377					info!("Shutdown signal received! Shutting down exit progress process...");
378					break;
379				},
380			}
381		}
382	}
383
384	/// Run processes that only need to be run once on startup
385	async fn run_startup_tasks(&self) {
386		let Some(wallet) = self.wallet() else { return };
387
388		// Eagerly refresh the server connection before starting the other
389		// daemon tasks so they don't race the first connection check and
390		// skip their initial iteration with `connected = false` (which
391		// would delay mailbox subscription by `slow_interval`).
392		let result = wallet.refresh_server().await;
393		if let Err(ref e) = result {
394			warn!("Ark server refresh failed: {:#}", e);
395		}
396		let connected = wallet.inner.server.initialized();
397		self.connected.store(connected, Ordering::Relaxed);
398
399		if !self.manual_sync {
400			wallet.sync().await;
401		}
402	}
403
404	pub async fn run(self) {
405		{
406			let Some(wallet) = self.wallet() else { return };
407			info!("Starting daemon for wallet {}", wallet.fingerprint());
408		}
409
410		self.run_startup_tasks().await;
411		trace!("Daemon startup tasks complete, starting background processes");
412
413		if self.manual_sync {
414			// In manual-sync mode only the server connection heartbeat keeps
415			// running; everything else must be triggered via the REST API.
416			info!("Daemon running in manual-sync mode; background sync disabled");
417			let _ = self.run_server_connection_check_process().await;
418		} else {
419			#[cfg(not(feature = "wasm-web"))]
420			{
421				// Each loop runs in its own tokio task so that a panic in one
422				// (e.g. from a crafted round proposal) cannot silently kill the
423				// others — in particular exit monitoring / CPFP fee-bumping.
424				let proc = Arc::new(self);
425				let p1 = Arc::clone(&proc);
426				let p2 = Arc::clone(&proc);
427				let p3 = Arc::clone(&proc);
428				let p4 = Arc::clone(&proc);
429				let p5 = Arc::clone(&proc);
430				let _ = futures::join!(
431					supervised("server-connection", move || {
432						let p = Arc::clone(&p1);
433						async move { p.run_server_connection_check_process().await }
434					}),
435					supervised("round-events", move || {
436						let p = Arc::clone(&p2);
437						async move { p.run_round_events_process().await }
438					}),
439					supervised("sync", move || {
440						let p = Arc::clone(&p3);
441						async move { p.run_sync_processes().await }
442					}),
443					supervised("exit-progress", move || {
444						let p = Arc::clone(&p4);
445						async move { p.run_exit_progress_process().await }
446					}),
447					supervised("mailbox", move || {
448						let p = Arc::clone(&p5);
449						async move { p.run_mailbox_messages_process().await }
450					}),
451				);
452			}
453			#[cfg(feature = "wasm-web")]
454			{
455				let _ = futures::join!(
456					self.run_server_connection_check_process(),
457					self.run_round_events_process(),
458					self.run_sync_processes(),
459					self.run_exit_progress_process(),
460					self.run_mailbox_messages_process(),
461				);
462			}
463		}
464
465		info!("Daemon gracefully stopped");
466	}
467}
468
469/// Run `f` in its own [`tokio::spawn`] task, restarting it if it panics.
470///
471/// A clean return (shutdown signal) breaks the loop immediately.
472#[cfg(not(feature = "wasm-web"))]
473async fn supervised<F, Fut>(name: &'static str, f: F)
474where
475	F: Fn() -> Fut,
476	Fut: std::future::Future<Output = ()> + Send + 'static,
477{
478	loop {
479		match tokio::spawn(f()).await {
480			Ok(()) => break,
481			Err(e) => {
482				warn!("Daemon task '{}' terminated unexpectedly, restarting: {e}", name);
483				bark_runtime::sleep(Duration::from_secs(1)).await;
484			},
485		}
486	}
487}