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}