Skip to main content

acdp_client/
blocking.rs

1//! Synchronous, runtime-agnostic facade over the async client (feature
2//! `blocking`).
3//!
4//! [`BlockingClient`] lets synchronous code (a sync trait impl, a plugin
5//! host, a thread pool) call [`RegistryClient`], [`VerifiedContext`],
6//! [`CrossRegistryResolver`] and [`verify_change`] without owning a tokio
7//! runtime:
8//!
9//! - **Its own runtime.** Every call runs on a small multi-threaded tokio
10//!   runtime that the facade starts on a dedicated thread and shuts down
11//!   on that same thread when the last clone is dropped. The caller's
12//!   thread only waits, so the facade works from plain threads and from
13//!   inside an existing tokio runtime alike, without the "cannot start a
14//!   runtime from within a runtime" panic.
15//! - **A hard per-call deadline.** Each method takes a
16//!   [`std::time::Instant`]. When it passes, the call returns
17//!   [`AcdpError::DeadlineExceeded`] and the in-flight work is cancelled.
18//!   No partial result is ever returned: a deadline fails closed. A
19//!   deadline already in the past fails without issuing any request.
20//! - **Thread-safe.** [`BlockingClient`] is `Clone + Send + Sync`; clones
21//!   share the runtime and the underlying clients, so one instance can
22//!   serve many threads concurrently.
23//!
24//! Give the facade its own [`RegistryClient`], [`WebResolver`] and
25//! [`CrossRegistryResolver`] rather than sharing instances that are also
26//! used on another runtime: an HTTP connection is driven by the runtime
27//! that opened it, and a pooled connection whose runtime has gone fails
28//! on reuse.
29//!
30//! Called from inside a tokio runtime, the facade blocks the calling
31//! worker thread while it waits (on a multi-threaded runtime it first
32//! hands the worker's other tasks off with
33//! `tokio::task::block_in_place`). Async callers should prefer the async
34//! API; the facade is for code that cannot be async.
35//!
36//! ```no_run
37//! use std::time::{Duration, Instant};
38//! use acdp_client::blocking::BlockingClient;
39//! use acdp_client::{RegistryClient, VerificationPolicy};
40//! use acdp_did::WebResolver;
41//! use acdp_types::CtxId;
42//!
43//! # fn main() -> Result<(), acdp_primitives::error::AcdpError> {
44//! let client = BlockingClient::new(
45//!     RegistryClient::new("https://registry.example.com")?,
46//!     WebResolver::new(),
47//! )?;
48//! let ctx_id = CtxId(
49//!     "acdp://registry.example.com/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa".into(),
50//! );
51//! let deadline = Instant::now() + Duration::from_secs(2);
52//! let verified =
53//!     client.fetch_verified(&ctx_id, &VerificationPolicy::default(), deadline)?;
54//! println!("{}", verified.body().ctx_id);
55//! # Ok(())
56//! # }
57//! ```
58
59use std::future::Future;
60use std::sync::mpsc;
61use std::sync::Arc;
62use std::time::Instant;
63
64use acdp_did::WebResolver;
65use acdp_primitives::error::AcdpError;
66use acdp_types::body::{Body, FullContext};
67use acdp_types::change::{Change, ChangesResponse, WatchFilter};
68use acdp_types::primitives::{CtxId, LineageId};
69use serde_json::Value;
70
71use crate::recall::{verify_change, ChangeVerdict, HeldContext, Since};
72use crate::{CrossRegistryResolver, RegistryClient, VerificationPolicy, VerifiedContext};
73
74/// Default number of runtime worker threads (see
75/// [`BlockingClient::with_worker_threads`]).
76pub const DEFAULT_WORKER_THREADS: usize = 2;
77
78/// The facade's runtime. The runtime itself lives on `acdp-blocking-rt`,
79/// which blocks on `shutdown` and then drops the runtime there: dropping a
80/// runtime from async context panics, and the last [`BlockingClient`]
81/// clone may well be dropped inside one.
82struct Executor {
83    handle: tokio::runtime::Handle,
84    _shutdown: tokio::sync::oneshot::Sender<()>,
85}
86
87impl Executor {
88    fn start(worker_threads: usize) -> Result<Self, AcdpError> {
89        let rt = tokio::runtime::Builder::new_multi_thread()
90            .worker_threads(worker_threads.max(1))
91            .thread_name("acdp-blocking-worker")
92            .enable_all()
93            .build()
94            .map_err(|e| AcdpError::Http(format!("blocking facade: runtime start failed: {e}")))?;
95        let handle = rt.handle().clone();
96        let (shutdown, stopped) = tokio::sync::oneshot::channel::<()>();
97        std::thread::Builder::new()
98            .name("acdp-blocking-rt".into())
99            .spawn(move || {
100                // Err means the sender dropped: the last clone is gone.
101                let _ = rt.block_on(stopped);
102                rt.shutdown_background();
103            })
104            .map_err(|e| AcdpError::Http(format!("blocking facade: thread start failed: {e}")))?;
105        Ok(Self {
106            handle,
107            _shutdown: shutdown,
108        })
109    }
110
111    /// Run `fut` on the facade runtime and wait for it until `deadline`.
112    fn run<T, F>(&self, deadline: Instant, fut: F) -> Result<T, AcdpError>
113    where
114        T: Send + 'static,
115        F: Future<Output = Result<T, AcdpError>> + Send + 'static,
116    {
117        let expired = || AcdpError::DeadlineExceeded("blocking call did not finish in time".into());
118        if Instant::now() >= deadline {
119            return Err(expired());
120        }
121        let (tx, rx) = mpsc::sync_channel(1);
122        let task = self.handle.spawn(async move {
123            let out = tokio::time::timeout_at(tokio::time::Instant::from_std(deadline), fut)
124                .await
125                .unwrap_or_else(|_| Err(expired()));
126            let _ = tx.send(out);
127        });
128        let wait = || {
129            let left = deadline.saturating_duration_since(Instant::now());
130            rx.recv_timeout(left)
131        };
132        let received = match tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()) {
133            Ok(tokio::runtime::RuntimeFlavor::MultiThread) => tokio::task::block_in_place(wait),
134            _ => wait(),
135        };
136        match received {
137            Ok(out) => out,
138            Err(mpsc::RecvTimeoutError::Timeout) => {
139                task.abort();
140                Err(expired())
141            }
142            Err(mpsc::RecvTimeoutError::Disconnected) => Err(AcdpError::Http(
143                "blocking facade: task ended without a result".into(),
144            )),
145        }
146    }
147}
148
149struct Inner {
150    exec: Executor,
151    client: RegistryClient,
152    resolver: WebResolver,
153}
154
155/// A synchronous client with a hard per-call deadline. See the
156/// [module docs](self).
157#[derive(Clone)]
158pub struct BlockingClient {
159    inner: Arc<Inner>,
160    cross: Option<Arc<CrossRegistryResolver>>,
161}
162
163impl std::fmt::Debug for BlockingClient {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.debug_struct("BlockingClient")
166            .field("cross_registry", &self.cross.is_some())
167            .finish_non_exhaustive()
168    }
169}
170
171impl BlockingClient {
172    /// Wrap `client` (the registry every call goes to) and `resolver`
173    /// (DID resolution for verification), with
174    /// [`DEFAULT_WORKER_THREADS`] runtime workers.
175    pub fn new(client: RegistryClient, resolver: WebResolver) -> Result<Self, AcdpError> {
176        Self::with_worker_threads(client, resolver, DEFAULT_WORKER_THREADS)
177    }
178
179    /// As [`Self::new`], with `worker_threads` runtime workers (at least
180    /// one). More workers let more concurrent calls verify signatures in
181    /// parallel; network waits do not need them.
182    pub fn with_worker_threads(
183        client: RegistryClient,
184        resolver: WebResolver,
185        worker_threads: usize,
186    ) -> Result<Self, AcdpError> {
187        Ok(Self {
188            inner: Arc::new(Inner {
189                exec: Executor::start(worker_threads)?,
190                client,
191                resolver,
192            }),
193            cross: None,
194        })
195    }
196
197    /// Enable [`Self::resolve`] with `resolver`. The result shares this
198    /// client's runtime; clones made before the call keep their previous
199    /// configuration.
200    pub fn with_cross_registry(mut self, resolver: CrossRegistryResolver) -> Self {
201        self.cross = Some(Arc::new(resolver));
202        self
203    }
204
205    /// The wrapped registry client.
206    pub fn client(&self) -> &RegistryClient {
207        &self.inner.client
208    }
209
210    /// The wrapped DID resolver.
211    pub fn resolver(&self) -> &WebResolver {
212        &self.inner.resolver
213    }
214
215    /// Run any future against the wrapped clients under `deadline`. The
216    /// escape hatch for async APIs without a dedicated method: `f`
217    /// receives a handle to this client and returns the future to run.
218    ///
219    /// ```no_run
220    /// # use std::time::{Duration, Instant};
221    /// # use acdp_client::blocking::BlockingClient;
222    /// # fn f(c: &BlockingClient) -> Result<(), acdp_primitives::error::AcdpError> {
223    /// let caps = c.run(Instant::now() + Duration::from_secs(1), |c| async move {
224    ///     c.client().capabilities().await
225    /// })?;
226    /// # Ok(()) }
227    /// ```
228    pub fn run<T, F, Fut>(&self, deadline: Instant, f: F) -> Result<T, AcdpError>
229    where
230        T: Send + 'static,
231        F: FnOnce(BlockingClient) -> Fut,
232        Fut: Future<Output = Result<T, AcdpError>> + Send + 'static,
233    {
234        self.inner.exec.run(deadline, f(self.clone()))
235    }
236
237    /// [`RegistryClient::retrieve`]: the context as served, unverified.
238    pub fn retrieve(&self, ctx_id: &CtxId, deadline: Instant) -> Result<FullContext, AcdpError> {
239        let ctx_id = ctx_id.clone();
240        self.run(
241            deadline,
242            |c| async move { c.client().retrieve(&ctx_id).await },
243        )
244    }
245
246    /// [`VerifiedContext::fetch_with_policy`]: retrieve and verify.
247    pub fn fetch_verified(
248        &self,
249        ctx_id: &CtxId,
250        policy: &VerificationPolicy,
251        deadline: Instant,
252    ) -> Result<VerifiedContext, AcdpError> {
253        let (ctx_id, policy) = (ctx_id.clone(), policy.clone());
254        self.run(deadline, |c| async move {
255            VerifiedContext::fetch_with_policy(c.client(), c.resolver(), &ctx_id, &policy).await
256        })
257    }
258
259    /// [`VerifiedContext::fetch_current_with_policy`]: retrieve and verify
260    /// a lineage's current head.
261    pub fn fetch_current_verified(
262        &self,
263        lineage_id: &LineageId,
264        policy: &VerificationPolicy,
265        deadline: Instant,
266    ) -> Result<VerifiedContext, AcdpError> {
267        let (lineage_id, policy) = (lineage_id.clone(), policy.clone());
268        self.run(deadline, |c| async move {
269            VerifiedContext::fetch_current_with_policy(
270                c.client(),
271                c.resolver(),
272                &lineage_id,
273                &policy,
274            )
275            .await
276        })
277    }
278
279    /// [`CrossRegistryResolver::resolve`]: resolve and verify a context at
280    /// whichever registry `ctx_id` names. Requires
281    /// [`Self::with_cross_registry`]; otherwise fails with
282    /// [`AcdpError::SchemaViolation`] without issuing a request.
283    pub fn resolve(&self, ctx_id: &CtxId, deadline: Instant) -> Result<VerifiedContext, AcdpError> {
284        let Some(cross) = self.cross.clone() else {
285            return Err(AcdpError::SchemaViolation(
286                "BlockingClient::resolve needs with_cross_registry".into(),
287            ));
288        };
289        let ctx_id = ctx_id.clone();
290        self.run(deadline, |_| async move { cross.resolve(&ctx_id).await })
291    }
292
293    /// [`RegistryClient::changes`]: one page of the changes feed. Entries
294    /// are hints until [`Self::verify_change`] passes.
295    pub fn changes(
296        &self,
297        since: &Since,
298        filter: &WatchFilter,
299        limit: Option<u32>,
300        deadline: Instant,
301    ) -> Result<ChangesResponse, AcdpError> {
302        let (since, filter) = (since.clone(), filter.clone());
303        self.run(deadline, move |c| async move {
304            c.client().changes(&since, &filter, limit).await
305        })
306    }
307
308    /// [`verify_change`] (RFC-ACDP-0018 ยง7). `held` is copied, so the
309    /// borrow ends when this returns. `Ok(Rejected)` is a final verdict;
310    /// `Err` (including [`AcdpError::DeadlineExceeded`]) means the entry
311    /// was not decided and should be retried.
312    pub fn verify_change(
313        &self,
314        change: &Change,
315        held: Option<HeldContext<'_>>,
316        policy: &VerificationPolicy,
317        deadline: Instant,
318    ) -> Result<ChangeVerdict, AcdpError> {
319        let change = change.clone();
320        let held: Option<(Body, Vec<Value>)> = held.map(|h| (h.body.clone(), h.events.to_vec()));
321        let policy = policy.clone();
322        self.run(deadline, |c| async move {
323            let held = held
324                .as_ref()
325                .map(|(body, events)| HeldContext { body, events });
326            verify_change(c.client(), c.resolver(), &change, held, &policy).await
327        })
328    }
329}