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