#![cfg(with_server)]
use std::{
collections::{hash_map::Entry, HashMap},
future::Future,
time::Duration,
};
use futures::{channel::mpsc, StreamExt as _};
use linera_base::identifiers::ChainId;
#[cfg(with_metrics)]
use linera_base::time::Instant;
use linera_core::data_types::CrossChainRequest;
use rand::Rng as _;
use tracing::{trace, warn};
use crate::config::ShardId;
#[cfg(with_metrics)]
mod metrics {
use std::sync::LazyLock;
use linera_base::prometheus_util::{
exponential_bucket_latencies, register_histogram, register_int_gauge,
};
use prometheus::{Histogram, IntGauge};
pub static CROSS_CHAIN_MESSAGE_TASKS: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge(
"cross_chain_message_tasks",
"Number of concurrent cross-chain message tasks",
)
});
pub static CROSS_CHAIN_QUEUE_WAIT_TIME: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram(
"cross_chain_queue_wait_time",
"Time (ms) a cross-chain message waits in queue before handle_request is called",
exponential_bucket_latencies(10_000.0),
)
});
}
#[expect(clippy::too_many_arguments)]
pub(crate) async fn forward_cross_chain_queries<F, G>(
nickname: String,
cross_chain_max_retries: u32,
cross_chain_retry_delay: Duration,
cross_chain_max_backoff: Duration,
cross_chain_sender_delay: Duration,
cross_chain_sender_failure_rate: f32,
this_shard: ShardId,
mut receiver: mpsc::Receiver<(CrossChainRequest, ShardId)>,
handle_request: F,
) where
F: Fn(ShardId, CrossChainRequest) -> G + Send + Clone + 'static,
G: Future<Output = anyhow::Result<()>>,
{
let mut steps = futures::stream::FuturesUnordered::new();
let mut job_states: HashMap<QueueId, JobState> = HashMap::new();
let run_task = |task: Task| async move {
#[cfg(with_metrics)]
{
let queue_wait_time_ms = task.queued_at.elapsed().as_secs_f64() * 1000.0;
metrics::CROSS_CHAIN_QUEUE_WAIT_TIME.observe(queue_wait_time_ms);
}
handle_request(task.shard_id, task.request).await
};
let run_action = |action, queue, state: JobState| async move {
linera_base::time::timer::sleep(cross_chain_sender_delay).await;
let to_shard = state.task.shard_id;
(
queue,
match action {
Action::Proceed { .. } => {
let target_chain_id = state.task.request.target_chain_id();
if let Err(error) = run_task(state.task).await {
warn!(
nickname = state.nickname,
?error,
retry = state.retries,
from_shard = this_shard,
to_shard,
chain_id = %target_chain_id,
"Failed to send cross-chain query",
);
Action::Retry
} else {
trace!(from_shard = this_shard, to_shard, "Sent cross-chain query",);
Action::Proceed {
id: state.id.wrapping_add(1),
}
}
}
Action::Retry => {
let delay = cross_chain_retry_delay
.saturating_mul(state.retries)
.min(cross_chain_max_backoff);
linera_base::time::timer::sleep(delay).await;
Action::Proceed { id: state.id }
}
},
)
};
loop {
#[cfg(with_metrics)]
metrics::CROSS_CHAIN_MESSAGE_TASKS.set(job_states.len() as i64);
tokio::select! {
Some((queue, action)) = steps.next() => {
let Entry::Occupied(mut state) = job_states.entry(queue) else {
panic!("running job without state");
};
if state.get().is_finished(&action, cross_chain_max_retries) {
state.remove();
continue;
}
if let Action::Retry = action {
state.get_mut().retries += 1
}
steps.push(run_action.clone()(action, queue, state.get().clone()));
}
request = receiver.next() => {
let Some((request, shard_id)) = request else { break };
if rand::thread_rng().gen::<f32>() < cross_chain_sender_failure_rate {
warn!("Dropped 1 cross-chain message intentionally.");
continue;
}
let queue = QueueId::new(&request);
let task = Task {
shard_id,
request,
#[cfg(with_metrics)]
queued_at: Instant::now(),
};
match job_states.entry(queue) {
Entry::Vacant(entry) => steps.push(run_action.clone()(
Action::Proceed { id: 0 },
queue,
entry.insert(JobState {
id: 0,
retries: 0,
nickname: nickname.clone(),
task,
}).clone(),
)),
Entry::Occupied(mut entry) => {
entry.insert(JobState {
id: entry.get().id + 1,
retries: 0,
nickname: nickname.clone(),
task,
});
}
}
}
else => (),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct QueueId {
sender: ChainId,
recipient: ChainId,
is_update: bool,
}
impl QueueId {
fn new(request: &CrossChainRequest) -> Self {
let (sender, recipient, is_update) = match request {
CrossChainRequest::UpdateRecipient {
sender, recipient, ..
} => (*sender, *recipient, true),
CrossChainRequest::ConfirmUpdatedRecipient {
sender, recipient, ..
}
| CrossChainRequest::RevertConfirm {
sender, recipient, ..
} => (*sender, *recipient, false),
};
QueueId {
sender,
recipient,
is_update,
}
}
}
enum Action {
Proceed { id: usize },
Retry,
}
#[derive(Clone)]
struct Task {
pub shard_id: ShardId,
pub request: linera_core::data_types::CrossChainRequest,
#[cfg(with_metrics)]
pub queued_at: Instant,
}
#[derive(Clone)]
struct JobState {
pub id: usize,
pub retries: u32,
pub nickname: String,
pub task: Task,
}
impl JobState {
fn is_finished(&self, action: &Action, max_retries: u32) -> bool {
match action {
Action::Proceed { id } => self.id < *id,
Action::Retry => self.retries >= max_retries,
}
}
}