use crate::agent::consumer::{AgentMessage, decode_agent_record};
use crate::error::LaserError;
use crate::types::MessageId;
use dashmap::DashMap;
use iggy::prelude::*;
use std::sync::{Arc, Weak};
use std::time::Duration;
use tokio::sync::oneshot;
const REPLY_BATCH: u32 = 200;
const REPLY_POLL_INTERVAL: Duration = Duration::from_millis(20);
#[derive(Clone)]
pub(crate) struct ReplyHub {
inner: Arc<ReplyHubInner>,
}
struct ReplyHubInner {
waiters: DashMap<String, ReplyWaiter>,
task: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
#[cfg(feature = "sign")]
verifier: Option<Arc<crate::sign::KeyRegistry>>,
}
struct ReplyWaiter {
sender: oneshot::Sender<AgentMessage>,
#[cfg(feature = "sign")]
expected_signer: Option<String>,
}
impl Drop for ReplyHubInner {
fn drop(&mut self) {
if let Some(task) = self
.task
.lock()
.expect("reply-hub task lock is not poisoned")
.take()
{
task.abort();
}
}
}
impl ReplyHub {
pub(crate) async fn create(
client: Arc<IggyClient>,
stream: String,
topic: Identifier,
#[cfg(feature = "sign")] verifier: Option<Arc<crate::sign::KeyRegistry>>,
) -> Result<Self, LaserError> {
let stream_id = Identifier::named(&stream)?;
let consumer = Consumer::new(Identifier::named("laser-reply-hub")?);
let mut offsets: Vec<u64> = Vec::new();
if let Some(details) = client.get_topic(&stream_id, &topic).await? {
let partitions = crate::poll::bounded_partitions(details.partitions_count);
offsets = vec![0u64; partitions as usize];
for partition in 0..partitions {
let polled = client
.poll_messages(
&stream_id,
&topic,
Some(partition),
&consumer,
&PollingStrategy::last(),
1,
false,
)
.await?;
if let Some(last) = polled.messages.last() {
offsets[partition as usize] = last.header.offset.saturating_add(1);
}
}
}
let inner = Arc::new(ReplyHubInner {
waiters: DashMap::new(),
task: std::sync::Mutex::new(None),
#[cfg(feature = "sign")]
verifier,
});
let handle = tokio::spawn(dispatch_loop(
client,
stream_id,
topic,
consumer,
offsets,
Arc::downgrade(&inner),
));
*inner
.task
.lock()
.expect("reply-hub task lock is not poisoned") = Some(handle);
Ok(Self { inner })
}
pub(crate) fn subscribe(
&self,
correlation: String,
expected_signer: Option<String>,
) -> ReplyTicket {
let (tx, rx) = oneshot::channel();
#[cfg(not(feature = "sign"))]
let _ = expected_signer;
self.inner.waiters.insert(
correlation.clone(),
ReplyWaiter {
sender: tx,
#[cfg(feature = "sign")]
expected_signer,
},
);
ReplyTicket {
hub: Arc::downgrade(&self.inner),
correlation,
rx,
}
}
}
pub(crate) struct ReplyTicket {
hub: Weak<ReplyHubInner>,
correlation: String,
rx: oneshot::Receiver<AgentMessage>,
}
impl ReplyTicket {
pub(crate) async fn wait(mut self, timeout: Duration) -> Result<AgentMessage, LaserError> {
match tokio::time::timeout(timeout, &mut self.rx).await {
Ok(Ok(message)) => Ok(message),
Ok(Err(_)) | Err(_) => Err(LaserError::Timeout("reply")),
}
}
}
impl Drop for ReplyTicket {
fn drop(&mut self) {
if let Some(hub) = self.hub.upgrade() {
hub.waiters.remove(&self.correlation);
}
}
}
async fn dispatch_loop(
client: Arc<IggyClient>,
stream: Identifier,
topic: Identifier,
consumer: Consumer,
mut offsets: Vec<u64>,
hub: Weak<ReplyHubInner>,
) {
loop {
let Some(inner) = hub.upgrade() else {
return;
};
let partitions = match client.get_topic(&stream, &topic).await {
Ok(Some(details)) => crate::poll::bounded_partitions(details.partitions_count),
_ => {
drop(inner);
tokio::time::sleep(REPLY_POLL_INTERVAL).await;
continue;
}
};
if (offsets.len() as u32) < partitions {
offsets.resize(partitions as usize, 0);
}
let mut dispatched = false;
for partition in 0..partitions {
let from = offsets[partition as usize];
let batch = match crate::poll::drain_partition(
&client,
&stream,
&topic,
&consumer,
crate::poll::DrainRange::open(partition, from),
REPLY_BATCH,
)
.await
{
Ok(batch) => batch,
Err(_) => continue,
};
offsets[partition as usize] = batch.next_offset;
for message in batch.messages {
let Ok(decoded) = decode_agent_record(&message, laser_wire::agent::features::NONE)
else {
continue;
};
let provenance = decoded.provenance;
let Some(correlation) = provenance.correlation_id.clone() else {
continue;
};
let Some(waiter) = inner.waiters.get(&correlation) else {
continue;
};
#[cfg(feature = "sign")]
let verified_principal = match &inner.verifier {
Some(registry) => {
let Some(envelope) = decoded.envelope.as_ref() else {
continue;
};
let Some(context) = decoded.signature_context.as_ref() else {
continue;
};
let Ok(verified) = registry.verify_observed_at(
envelope,
context,
decoded.observed_at_micros,
) else {
continue;
};
if waiter
.expected_signer
.as_deref()
.is_some_and(|expected| expected != verified.principal.as_str())
{
continue;
}
Some(verified.principal)
}
None => None,
};
#[cfg(not(feature = "sign"))]
let verified_principal = None;
drop(waiter);
let Some((_, waiter)) = inner.waiters.remove(&correlation) else {
continue;
};
let reply = AgentMessage {
provenance,
id: MessageId::new(partition, message.header.offset),
payload: message.payload.to_vec(),
envelope: decoded.envelope,
content_type: decoded.content_type,
verified_principal,
};
let _ = waiter.sender.send(reply);
dispatched = true;
}
}
drop(inner);
if !dispatched {
tokio::time::sleep(REPLY_POLL_INTERVAL).await;
}
}
}