Skip to main content

fedimint_client/client/
handle.rs

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