Skip to main content

fedimint_client/client/
handle.rs

1use std::ops;
2use std::sync::Arc;
3use std::time::Duration;
4
5use 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::{Instrument as _, debug, error, trace, warn};
12
13use super::Client;
14use crate::ClientBuilder;
15
16/// 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}
29
30/// An alias for a reference counted [`ClientHandle`]
31pub type ClientHandleArc = Arc<ClientHandle>;
32
33impl ClientHandle {
34    /// Create
35    pub(crate) fn new(inner: Arc<Client>) -> Self {
36        ClientHandle {
37            inner: inner.into(),
38        }
39    }
40
41    pub(crate) fn as_inner(&self) -> &Arc<Client> {
42        self.inner.as_ref().expect("Inner always set")
43    }
44
45    pub fn start_executor(&self) {
46        self.as_inner().start_executor();
47    }
48
49    /// Shutdown the client.
50    pub async fn shutdown(mut self) {
51        self.shutdown_inner().await;
52    }
53
54    async fn shutdown_inner(&mut self) {
55        let Some(inner) = self.inner.take() else {
56            error!(
57                target: LOG_CLIENT,
58                "ClientHandleShared::shutdown called twice"
59            );
60            return;
61        };
62        let client_span = inner.client_span.clone();
63        inner.executor.stop_executor();
64        let db = inner.db.clone();
65        client_span.in_scope(|| {
66            debug!(target: LOG_CLIENT, "Waiting for client task group to shut down");
67        });
68        if let Err(err) = inner
69            .task_group
70            .clone()
71            .shutdown_join_all(Some(Duration::from_secs(30)))
72            .instrument(client_span.clone())
73            .await
74        {
75            client_span.in_scope(|| {
76                warn!(target: LOG_CLIENT, err = %err.fmt_compact_anyhow(), "Error waiting for client task group to shut down");
77            });
78        }
79
80        let client_strong_count = Arc::strong_count(&inner);
81        client_span.in_scope(|| {
82            debug!(target: LOG_CLIENT, "Dropping last handle to Client");
83        });
84        // We are sure that no background tasks are running in the client anymore, so we
85        // can drop the (usually) last inner reference.
86        drop(inner);
87
88        client_span.in_scope(|| {
89            if client_strong_count != 1 {
90                debug!(target: LOG_CLIENT, count = client_strong_count - 1, LOG_CLIENT, "External Client references remaining after last handle dropped");
91            }
92
93            let db_strong_count = db.strong_count();
94            if db_strong_count != 1 {
95                debug!(target: LOG_CLIENT, count = db_strong_count - 1, "External DB references remaining after last handle dropped");
96            }
97            trace!(target: LOG_CLIENT, "Dropped last handle to Client");
98        });
99    }
100
101    /// Restart the client
102    ///
103    /// Returns false if there are other clones of [`ClientHandle`], or starting
104    /// the client again failed for some reason.
105    ///
106    /// Notably it will re-use the original [`fedimint_core::db::Database`]
107    /// handle, and not attempt to open it again.
108    pub async fn restart(self) -> anyhow::Result<ClientHandle> {
109        let (builder, config, api_secret, root_secret, db, endpoints) = {
110            let client = self
111                .inner
112                .as_ref()
113                .ok_or_else(|| format_err!("Already stopped"))?;
114            let builder = ClientBuilder::from_existing(client);
115            let config = client.config().await;
116            let api_secret = client.api_secret.clone();
117            let root_secret = client.root_secret.clone();
118            let db = client.db().clone();
119            let endpoints = client.endpoints().clone();
120
121            (builder, config, api_secret, root_secret, db, endpoints)
122        };
123        self.shutdown().await;
124
125        builder
126            .build(
127                endpoints,
128                db,
129                root_secret,
130                config,
131                api_secret,
132                false,
133                None,
134                None,
135                None, // chain_id should already be cached
136            )
137            .await
138    }
139}
140
141impl ops::Deref for ClientHandle {
142    type Target = Client;
143
144    fn deref(&self) -> &Self::Target {
145        self.inner.as_ref().expect("Must have inner client set")
146    }
147}
148
149/// We need a separate drop implementation for `Client` that triggers
150/// `Executor::stop_executor` even though the `Drop` implementation of
151/// `ExecutorInner` should already take care of that. The reason is that as long
152/// as the executor task is active there may be a cycle in the
153/// `Arc<Client>`s such that at least one `Executor` never gets dropped.
154impl Drop for ClientHandle {
155    fn drop(&mut self) {
156        if self.inner.is_none() {
157            return;
158        }
159
160        // We can't use block_on in single-threaded mode or wasm
161        #[cfg(target_family = "wasm")]
162        let can_block = false;
163        #[cfg(not(target_family = "wasm"))]
164        // nosemgrep: ban-raw-block-on
165        let can_block = RuntimeHandle::current().runtime_flavor() != RuntimeFlavor::CurrentThread;
166        if !can_block {
167            let inner = self.inner.take().expect("Must have inner client set");
168            inner.executor.stop_executor();
169            if cfg!(target_family = "wasm") {
170                error!(target: LOG_CLIENT, "Automatic client shutdown is not possible on wasm, call ClientHandle::shutdown manually.");
171            } else {
172                error!(target: LOG_CLIENT, "Automatic client shutdown is not possible on current thread runtime, call ClientHandle::shutdown manually.");
173            }
174            return;
175        }
176
177        self.inner
178            .as_ref()
179            .expect("Must have inner client set")
180            .client_span
181            .in_scope(|| {
182                debug!(target: LOG_CLIENT, "Shutting down the Client on last handle drop");
183            });
184        #[cfg(not(target_family = "wasm"))]
185        runtime::block_in_place(|| {
186            runtime::block_on(self.shutdown_inner());
187        });
188    }
189}