1use std::ops;
2use std::sync::Arc;
3use std::time::Duration;
45use anyhow::format_err;
6use fedimint_core::runtime;
7use fedimint_core::util::FmtCompactAnyhow as _;
8use fedimint_logging::LOG_CLIENT;
9#[cfg(not(target_family = "wasm"))]
10use tokio::runtime::{Handle as RuntimeHandle, RuntimeFlavor};
11use tracing::{debug, error, trace, warn};
1213use super::Client;
14use crate::ClientBuilder;
1516/// User handle to the [`Client`] instance
17///
18/// On the drop of [`ClientHandle`] the client will be shut-down, and resources
19/// it used freed.
20///
21/// Notably it [`ops::Deref`]s to the [`Client`] where most
22/// methods live.
23///
24/// Put this in an Arc to clone it (see [`ClientHandleArc`]).
25#[derive(Debug)]
26pub struct ClientHandle {
27 inner: Option<Arc<Client>>,
28}
2930/// An alias for a reference counted [`ClientHandle`]
31pub type ClientHandleArc = Arc<ClientHandle>;
3233impl ClientHandle {
34/// Create
35pub(crate) fn new(inner: Arc<Client>) -> Self {
36 ClientHandle {
37 inner: inner.into(),
38 }
39 }
4041pub(crate) fn as_inner(&self) -> &Arc<Client> {
42self.inner.as_ref().expect("Inner always set")
43 }
4445pub fn start_executor(&self) {
46self.as_inner().start_executor();
47 }
4849/// Shutdown the client.
50pub async fn shutdown(mut self) {
51self.shutdown_inner().await;
52 }
5354async fn shutdown_inner(&mut self) {
55let Some(inner) = self.inner.take() else {
56error!(
57 target: LOG_CLIENT,
58"ClientHandleShared::shutdown called twice"
59);
60return;
61 };
62 inner.executor.stop_executor();
63let db = inner.db.clone();
64debug!(target: LOG_CLIENT, "Waiting for client task group to shut down");
65if let Err(err) = inner
66 .task_group
67 .clone()
68 .shutdown_join_all(Some(Duration::from_secs(30)))
69 .await
70{
71warn!(target: LOG_CLIENT, err = %err.fmt_compact_anyhow(), "Error waiting for client task group to shut down");
72 }
7374let client_strong_count = Arc::strong_count(&inner);
75debug!(target: LOG_CLIENT, "Dropping last handle to Client");
76// We are sure that no background tasks are running in the client anymore, so we
77 // can drop the (usually) last inner reference.
78drop(inner);
7980if client_strong_count != 1 {
81debug!(target: LOG_CLIENT, count = client_strong_count - 1, LOG_CLIENT, "External Client references remaining after last handle dropped");
82 }
8384let db_strong_count = db.strong_count();
85if db_strong_count != 1 {
86debug!(target: LOG_CLIENT, count = db_strong_count - 1, "External DB references remaining after last handle dropped");
87 }
88trace!(target: LOG_CLIENT, "Dropped last handle to Client");
89 }
9091/// Restart the client
92 ///
93 /// Returns false if there are other clones of [`ClientHandle`], or starting
94 /// the client again failed for some reason.
95 ///
96 /// Notably it will re-use the original [`fedimint_core::db::Database`]
97 /// handle, and not attempt to open it again.
98pub async fn restart(self) -> anyhow::Result<ClientHandle> {
99let (builder, config, api_secret, root_secret) = {
100let client = self
101.inner
102 .as_ref()
103 .ok_or_else(|| format_err!("Already stopped"))?;
104let builder = ClientBuilder::from_existing(client);
105let config = client.config().await;
106let api_secret = client.api_secret.clone();
107let root_secret = client.root_secret.clone();
108109 (builder, config, api_secret, root_secret)
110 };
111self.shutdown().await;
112113 builder.build(root_secret, config, api_secret, false).await
114}
115}
116117impl ops::Deref for ClientHandle {
118type Target = Client;
119120fn deref(&self) -> &Self::Target {
121self.inner.as_ref().expect("Must have inner client set")
122 }
123}
124125/// We need a separate drop implementation for `Client` that triggers
126/// `Executor::stop_executor` even though the `Drop` implementation of
127/// `ExecutorInner` should already take care of that. The reason is that as long
128/// as the executor task is active there may be a cycle in the
129/// `Arc<Client>`s such that at least one `Executor` never gets dropped.
130impl Drop for ClientHandle {
131fn drop(&mut self) {
132if self.inner.is_none() {
133return;
134 }
135136// We can't use block_on in single-threaded mode or wasm
137#[cfg(target_family = "wasm")]
138let can_block = false;
139#[cfg(not(target_family = "wasm"))]
140// nosemgrep: ban-raw-block-on
141let can_block = RuntimeHandle::current().runtime_flavor() != RuntimeFlavor::CurrentThread;
142if !can_block {
143let inner = self.inner.take().expect("Must have inner client set");
144 inner.executor.stop_executor();
145if cfg!(target_family = "wasm") {
146error!(target: LOG_CLIENT, "Automatic client shutdown is not possible on wasm, call ClientHandle::shutdown manually.");
147 } else {
148error!(target: LOG_CLIENT, "Automatic client shutdown is not possible on current thread runtime, call ClientHandle::shutdown manually.");
149 }
150return;
151 }
152153debug!(target: LOG_CLIENT, "Shutting down the Client on last handle drop");
154#[cfg(not(target_family = "wasm"))]
155runtime::block_in_place(|| {
156 runtime::block_on(self.shutdown_inner());
157 });
158 }
159}