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}