use std::future::Future;
use std::sync::mpsc;
use std::sync::Arc;
use std::time::Instant;
use acdp_did::WebResolver;
use acdp_primitives::error::AcdpError;
use acdp_types::body::{Body, FullContext};
use acdp_types::change::{Change, ChangesResponse, WatchFilter};
use acdp_types::primitives::{CtxId, LineageId};
use serde_json::Value;
use crate::recall::{verify_change, ChangeVerdict, HeldContext, Since};
use crate::{CrossRegistryResolver, RegistryClient, VerificationPolicy, VerifiedContext};
pub const DEFAULT_WORKER_THREADS: usize = 2;
struct Executor {
handle: tokio::runtime::Handle,
_shutdown: tokio::sync::oneshot::Sender<()>,
}
impl Executor {
fn start(worker_threads: usize) -> Result<Self, AcdpError> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(worker_threads.max(1))
.thread_name("acdp-blocking-worker")
.enable_all()
.build()
.map_err(|e| AcdpError::Http(format!("blocking facade: runtime start failed: {e}")))?;
let handle = rt.handle().clone();
let (shutdown, stopped) = tokio::sync::oneshot::channel::<()>();
std::thread::Builder::new()
.name("acdp-blocking-rt".into())
.spawn(move || {
let _ = rt.block_on(stopped);
rt.shutdown_background();
})
.map_err(|e| AcdpError::Http(format!("blocking facade: thread start failed: {e}")))?;
Ok(Self {
handle,
_shutdown: shutdown,
})
}
fn run<T, F>(&self, deadline: Instant, fut: F) -> Result<T, AcdpError>
where
T: Send + 'static,
F: Future<Output = Result<T, AcdpError>> + Send + 'static,
{
let expired = || AcdpError::DeadlineExceeded("blocking call did not finish in time".into());
if Instant::now() >= deadline {
return Err(expired());
}
let (tx, rx) = mpsc::sync_channel(1);
let task = self.handle.spawn(async move {
let out = tokio::time::timeout_at(tokio::time::Instant::from_std(deadline), fut)
.await
.unwrap_or_else(|_| Err(expired()));
let _ = tx.send(out);
});
let wait = || {
let left = deadline.saturating_duration_since(Instant::now());
rx.recv_timeout(left)
};
let received = match tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()) {
Ok(tokio::runtime::RuntimeFlavor::MultiThread) => tokio::task::block_in_place(wait),
_ => wait(),
};
match received {
Ok(out) => out,
Err(mpsc::RecvTimeoutError::Timeout) => {
task.abort();
Err(expired())
}
Err(mpsc::RecvTimeoutError::Disconnected) => Err(AcdpError::Http(
"blocking facade: task ended without a result".into(),
)),
}
}
}
struct Inner {
exec: Executor,
client: RegistryClient,
resolver: WebResolver,
}
#[derive(Clone)]
pub struct BlockingClient {
inner: Arc<Inner>,
cross: Option<Arc<CrossRegistryResolver>>,
}
impl std::fmt::Debug for BlockingClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlockingClient")
.field("cross_registry", &self.cross.is_some())
.finish_non_exhaustive()
}
}
impl BlockingClient {
pub fn new(client: RegistryClient, resolver: WebResolver) -> Result<Self, AcdpError> {
Self::with_worker_threads(client, resolver, DEFAULT_WORKER_THREADS)
}
pub fn with_worker_threads(
client: RegistryClient,
resolver: WebResolver,
worker_threads: usize,
) -> Result<Self, AcdpError> {
Ok(Self {
inner: Arc::new(Inner {
exec: Executor::start(worker_threads)?,
client,
resolver,
}),
cross: None,
})
}
pub fn with_cross_registry(mut self, resolver: CrossRegistryResolver) -> Self {
self.cross = Some(Arc::new(resolver));
self
}
pub fn client(&self) -> &RegistryClient {
&self.inner.client
}
pub fn resolver(&self) -> &WebResolver {
&self.inner.resolver
}
pub fn run<T, F, Fut>(&self, deadline: Instant, f: F) -> Result<T, AcdpError>
where
T: Send + 'static,
F: FnOnce(BlockingClient) -> Fut,
Fut: Future<Output = Result<T, AcdpError>> + Send + 'static,
{
self.inner.exec.run(deadline, f(self.clone()))
}
pub fn retrieve(&self, ctx_id: &CtxId, deadline: Instant) -> Result<FullContext, AcdpError> {
let ctx_id = ctx_id.clone();
self.run(
deadline,
|c| async move { c.client().retrieve(&ctx_id).await },
)
}
pub fn fetch_verified(
&self,
ctx_id: &CtxId,
policy: &VerificationPolicy,
deadline: Instant,
) -> Result<VerifiedContext, AcdpError> {
let (ctx_id, policy) = (ctx_id.clone(), policy.clone());
self.run(deadline, |c| async move {
VerifiedContext::fetch_with_policy(c.client(), c.resolver(), &ctx_id, &policy).await
})
}
pub fn fetch_current_verified(
&self,
lineage_id: &LineageId,
policy: &VerificationPolicy,
deadline: Instant,
) -> Result<VerifiedContext, AcdpError> {
let (lineage_id, policy) = (lineage_id.clone(), policy.clone());
self.run(deadline, |c| async move {
VerifiedContext::fetch_current_with_policy(
c.client(),
c.resolver(),
&lineage_id,
&policy,
)
.await
})
}
pub fn resolve(&self, ctx_id: &CtxId, deadline: Instant) -> Result<VerifiedContext, AcdpError> {
let Some(cross) = self.cross.clone() else {
return Err(AcdpError::SchemaViolation(
"BlockingClient::resolve needs with_cross_registry".into(),
));
};
let ctx_id = ctx_id.clone();
self.run(deadline, |_| async move { cross.resolve(&ctx_id).await })
}
pub fn changes(
&self,
since: &Since,
filter: &WatchFilter,
limit: Option<u32>,
deadline: Instant,
) -> Result<ChangesResponse, AcdpError> {
let (since, filter) = (since.clone(), filter.clone());
self.run(deadline, move |c| async move {
c.client().changes(&since, &filter, limit).await
})
}
pub fn verify_change(
&self,
change: &Change,
held: Option<HeldContext<'_>>,
policy: &VerificationPolicy,
deadline: Instant,
) -> Result<ChangeVerdict, AcdpError> {
let change = change.clone();
let held: Option<(Body, Vec<Value>)> = held.map(|h| (h.body.clone(), h.events.to_vec()));
let policy = policy.clone();
self.run(deadline, |c| async move {
let held = held
.as_ref()
.map(|(body, events)| HeldContext { body, events });
verify_change(c.client(), c.resolver(), &change, held, &policy).await
})
}
}