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 acdp_types::trust_list::ListRejected;
70use serde_json::Value;
71
72use crate::recall::{verify_change, ChangeVerdict, HeldContext, Since};
73use crate::trust::TrustLists;
74use crate::{CrossRegistryResolver, RegistryClient, VerificationPolicy, VerifiedContext};
75
76/// Default number of runtime worker threads (see
77/// [`BlockingClient::with_worker_threads`]).
78pub const DEFAULT_WORKER_THREADS: usize = 2;
79
80/// The facade's runtime. The runtime itself lives on `acdp-blocking-rt`,
81/// which blocks on `shutdown` and then drops the runtime there: dropping a
82/// runtime from async context panics, and the last [`BlockingClient`]
83/// clone may well be dropped inside one.
84struct Executor {
85    handle: tokio::runtime::Handle,
86    _shutdown: tokio::sync::oneshot::Sender<()>,
87}
88
89impl Executor {
90    fn start(worker_threads: usize) -> Result<Self, AcdpError> {
91        let rt = tokio::runtime::Builder::new_multi_thread()
92            .worker_threads(worker_threads.max(1))
93            .thread_name("acdp-blocking-worker")
94            .enable_all()
95            .build()
96            .map_err(|e| AcdpError::Http(format!("blocking facade: runtime start failed: {e}")))?;
97        let handle = rt.handle().clone();
98        let (shutdown, stopped) = tokio::sync::oneshot::channel::<()>();
99        std::thread::Builder::new()
100            .name("acdp-blocking-rt".into())
101            .spawn(move || {
102                // Err means the sender dropped: the last clone is gone.
103                let _ = rt.block_on(stopped);
104                rt.shutdown_background();
105            })
106            .map_err(|e| AcdpError::Http(format!("blocking facade: thread start failed: {e}")))?;
107        Ok(Self {
108            handle,
109            _shutdown: shutdown,
110        })
111    }
112
113    /// Run `fut` on the facade runtime and wait for it until `deadline`.
114    fn run<T, F>(&self, deadline: Instant, fut: F) -> Result<T, AcdpError>
115    where
116        T: Send + 'static,
117        F: Future<Output = Result<T, AcdpError>> + Send + 'static,
118    {
119        let expired = || AcdpError::DeadlineExceeded("blocking call did not finish in time".into());
120        if Instant::now() >= deadline {
121            return Err(expired());
122        }
123        let (tx, rx) = mpsc::sync_channel(1);
124        let task = self.handle.spawn(async move {
125            let out = tokio::time::timeout_at(tokio::time::Instant::from_std(deadline), fut)
126                .await
127                .unwrap_or_else(|_| Err(expired()));
128            let _ = tx.send(out);
129        });
130        let wait = || {
131            let left = deadline.saturating_duration_since(Instant::now());
132            rx.recv_timeout(left)
133        };
134        let received = match tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()) {
135            Ok(tokio::runtime::RuntimeFlavor::MultiThread) => tokio::task::block_in_place(wait),
136            _ => wait(),
137        };
138        match received {
139            Ok(out) => out,
140            Err(mpsc::RecvTimeoutError::Timeout) => {
141                task.abort();
142                Err(expired())
143            }
144            Err(mpsc::RecvTimeoutError::Disconnected) => Err(AcdpError::Http(
145                "blocking facade: task ended without a result".into(),
146            )),
147        }
148    }
149}
150
151struct Inner {
152    exec: Executor,
153    client: RegistryClient,
154    resolver: WebResolver,
155}
156
157/// A synchronous client with a hard per-call deadline. See the
158/// [module docs](self).
159#[derive(Clone)]
160pub struct BlockingClient {
161    inner: Arc<Inner>,
162    cross: Option<Arc<CrossRegistryResolver>>,
163}
164
165impl std::fmt::Debug for BlockingClient {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        f.debug_struct("BlockingClient")
168            .field("cross_registry", &self.cross.is_some())
169            .finish_non_exhaustive()
170    }
171}
172
173impl BlockingClient {
174    /// Wrap `client` (the registry every call goes to) and `resolver`
175    /// (DID resolution for verification), with
176    /// [`DEFAULT_WORKER_THREADS`] runtime workers.
177    pub fn new(client: RegistryClient, resolver: WebResolver) -> Result<Self, AcdpError> {
178        Self::with_worker_threads(client, resolver, DEFAULT_WORKER_THREADS)
179    }
180
181    /// As [`Self::new`], with `worker_threads` runtime workers (at least
182    /// one). More workers let more concurrent calls verify signatures in
183    /// parallel; network waits do not need them.
184    pub fn with_worker_threads(
185        client: RegistryClient,
186        resolver: WebResolver,
187        worker_threads: usize,
188    ) -> Result<Self, AcdpError> {
189        Ok(Self {
190            inner: Arc::new(Inner {
191                exec: Executor::start(worker_threads)?,
192                client,
193                resolver,
194            }),
195            cross: None,
196        })
197    }
198
199    /// Enable [`Self::resolve`] with `resolver`. The result shares this
200    /// client's runtime; clones made before the call keep their previous
201    /// configuration.
202    pub fn with_cross_registry(mut self, resolver: CrossRegistryResolver) -> Self {
203        self.cross = Some(Arc::new(resolver));
204        self
205    }
206
207    /// The wrapped registry client.
208    pub fn client(&self) -> &RegistryClient {
209        &self.inner.client
210    }
211
212    /// The wrapped DID resolver.
213    pub fn resolver(&self) -> &WebResolver {
214        &self.inner.resolver
215    }
216
217    /// Run any future against the wrapped clients under `deadline`. The
218    /// escape hatch for async APIs without a dedicated method: `f`
219    /// receives a handle to this client and returns the future to run.
220    ///
221    /// ```no_run
222    /// # use std::time::{Duration, Instant};
223    /// # use acdp_client::blocking::BlockingClient;
224    /// # fn f(c: &BlockingClient) -> Result<(), acdp_primitives::error::AcdpError> {
225    /// let caps = c.run(Instant::now() + Duration::from_secs(1), |c| async move {
226    ///     c.client().capabilities().await
227    /// })?;
228    /// # Ok(()) }
229    /// ```
230    pub fn run<T, F, Fut>(&self, deadline: Instant, f: F) -> Result<T, AcdpError>
231    where
232        T: Send + 'static,
233        F: FnOnce(BlockingClient) -> Fut,
234        Fut: Future<Output = Result<T, AcdpError>> + Send + 'static,
235    {
236        self.inner.exec.run(deadline, f(self.clone()))
237    }
238
239    /// [`RegistryClient::retrieve`]: the context as served, unverified.
240    pub fn retrieve(&self, ctx_id: &CtxId, deadline: Instant) -> Result<FullContext, AcdpError> {
241        let ctx_id = ctx_id.clone();
242        self.run(
243            deadline,
244            |c| async move { c.client().retrieve(&ctx_id).await },
245        )
246    }
247
248    /// [`VerifiedContext::fetch_with_policy`]: retrieve and verify.
249    pub fn fetch_verified(
250        &self,
251        ctx_id: &CtxId,
252        policy: &VerificationPolicy,
253        deadline: Instant,
254    ) -> Result<VerifiedContext, AcdpError> {
255        let (ctx_id, policy) = (ctx_id.clone(), policy.clone());
256        self.run(deadline, |c| async move {
257            VerifiedContext::fetch_with_policy(c.client(), c.resolver(), &ctx_id, &policy).await
258        })
259    }
260
261    /// [`VerifiedContext::fetch_current_with_policy`]: retrieve and verify
262    /// a lineage's current head.
263    pub fn fetch_current_verified(
264        &self,
265        lineage_id: &LineageId,
266        policy: &VerificationPolicy,
267        deadline: Instant,
268    ) -> Result<VerifiedContext, AcdpError> {
269        let (lineage_id, policy) = (lineage_id.clone(), policy.clone());
270        self.run(deadline, |c| async move {
271            VerifiedContext::fetch_current_with_policy(
272                c.client(),
273                c.resolver(),
274                &lineage_id,
275                &policy,
276            )
277            .await
278        })
279    }
280
281    /// [`VerifiedContext::fetch_with_policy`] then
282    /// [`TrustLists::add_verified`]: fetch a trust-list version, verify
283    /// it, and hold it (RFC-ACDP-0020 §5.2). The outer error is a fetch
284    /// or verification failure; the inner one is the §5.2 rejection of
285    /// a body that verified but is not a valid version of any pin.
286    /// `lists` is updated only when the call completes in time.
287    pub fn add_trust_list(
288        &self,
289        lists: &mut TrustLists,
290        ctx_id: &CtxId,
291        policy: &VerificationPolicy,
292        deadline: Instant,
293    ) -> Result<Result<usize, ListRejected>, AcdpError> {
294        let (ctx_id, policy, mut owned) = (ctx_id.clone(), policy.clone(), lists.clone());
295        let (owned, held) = self.run(deadline, |c| async move {
296            let list =
297                VerifiedContext::fetch_with_policy(c.client(), c.resolver(), &ctx_id, &policy)
298                    .await?;
299            let held = owned.add_verified(&list, c.resolver()).await;
300            Ok((owned, held))
301        })?;
302        *lists = owned;
303        Ok(held)
304    }
305
306    /// [`CrossRegistryResolver::resolve`]: resolve and verify a context at
307    /// whichever registry `ctx_id` names. Requires
308    /// [`Self::with_cross_registry`]; otherwise fails with
309    /// [`AcdpError::SchemaViolation`] without issuing a request.
310    pub fn resolve(&self, ctx_id: &CtxId, deadline: Instant) -> Result<VerifiedContext, AcdpError> {
311        let Some(cross) = self.cross.clone() else {
312            return Err(AcdpError::SchemaViolation(
313                "BlockingClient::resolve needs with_cross_registry".into(),
314            ));
315        };
316        let ctx_id = ctx_id.clone();
317        self.run(deadline, |_| async move { cross.resolve(&ctx_id).await })
318    }
319
320    /// [`RegistryClient::changes`]: one page of the changes feed. Entries
321    /// are hints until [`Self::verify_change`] passes.
322    pub fn changes(
323        &self,
324        since: &Since,
325        filter: &WatchFilter,
326        limit: Option<u32>,
327        deadline: Instant,
328    ) -> Result<ChangesResponse, AcdpError> {
329        let (since, filter) = (since.clone(), filter.clone());
330        self.run(deadline, move |c| async move {
331            c.client().changes(&since, &filter, limit).await
332        })
333    }
334
335    /// [`verify_change`] (RFC-ACDP-0018 §7). `held` is copied, so the
336    /// borrow ends when this returns. `Ok(Rejected)` is a final verdict;
337    /// `Err` (including [`AcdpError::DeadlineExceeded`]) means the entry
338    /// was not decided and should be retried.
339    pub fn verify_change(
340        &self,
341        change: &Change,
342        held: Option<HeldContext<'_>>,
343        policy: &VerificationPolicy,
344        deadline: Instant,
345    ) -> Result<ChangeVerdict, AcdpError> {
346        let change = change.clone();
347        let held: Option<(Body, Vec<Value>)> = held.map(|h| (h.body.clone(), h.events.to_vec()));
348        let policy = policy.clone();
349        self.run(deadline, |c| async move {
350            let held = held
351                .as_ref()
352                .map(|(body, events)| HeldContext { body, events });
353            verify_change(c.client(), c.resolver(), &change, held, &policy).await
354        })
355    }
356}