use crate::{
types::{
execution_tree, queries::BlockInfo, AccountTransactionEffects, BlockItemSummary,
ExecutionTree, SpecialTransactionOutcome,
},
v2::{self, upward::UnknownDataError, FinalizedBlockInfo, QueryError, QueryResult, Upward},
};
use concordium_base::{
base::{AbsoluteBlockHeight, Energy},
contracts_common::{AccountAddress, Amount, ContractAddress, OwnedEntrypointName},
hashes::TransactionHash,
smart_contracts::OwnedReceiveName,
};
use futures::{stream::FuturesOrdered, StreamExt, TryStreamExt as _};
use std::{
collections::{BTreeMap, BTreeSet},
time::Duration,
};
use tokio::time::error::Elapsed;
pub use tonic::async_trait;
pub struct TraverseConfig {
endpoints: Vec<v2::Endpoint>,
max_parallel: usize,
max_behind: std::time::Duration,
wait_after_fail: std::time::Duration,
start_height: AbsoluteBlockHeight,
}
#[derive(Debug, thiserror::Error)]
pub enum TraverseError {
#[error("Failed to connect: {0}")]
Connect(#[from] tonic::transport::Error),
#[error("Failed to query: {0}")]
Query(#[from] QueryError),
#[error("Timed out waiting for finalized blocks.")]
Elapsed(#[from] Elapsed),
#[error("UnknownDataError occurred: {0}")]
UnknownDataError(#[from] UnknownDataError),
#[error("Other error occurred: ${0}")]
OtherError(#[from] anyhow::Error),
}
impl From<OnFinalizationError> for TraverseError {
fn from(e: OnFinalizationError) -> Self {
match e {
OnFinalizationError::Query(query_error) => query_error.into(),
OnFinalizationError::UnknownDataError(err) => TraverseError::UnknownDataError(err),
OnFinalizationError::OtherError(err) => TraverseError::OtherError(err),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum OnFinalizationError {
#[error("Failed to query: {0}")]
Query(#[from] QueryError),
#[error("UnknownDataError occurred: ${0}")]
UnknownDataError(#[from] UnknownDataError),
#[error("Other error occurred: ${0}")]
OtherError(#[from] anyhow::Error),
}
impl From<tonic::Status> for OnFinalizationError {
fn from(s: tonic::Status) -> Self {
Self::Query(s.into())
}
}
pub type OnFinalizationResult<A> = Result<A, OnFinalizationError>;
pub type TraverseResult<A> = OnFinalizationResult<A>;
#[async_trait]
pub trait Indexer {
type Context: Send + Sync;
type Data: Send + Sync;
async fn on_connect<'a>(
&mut self,
endpoint: v2::Endpoint,
client: &'a mut v2::Client,
) -> QueryResult<Self::Context>;
async fn on_finalized<'a>(
&self,
client: v2::Client,
ctx: &'a Self::Context,
fbi: FinalizedBlockInfo,
) -> OnFinalizationResult<Self::Data>;
async fn on_failure(
&mut self,
endpoint: v2::Endpoint,
successive_failures: u64,
err: TraverseError,
) -> bool;
}
impl TraverseConfig {
pub fn new_single(endpoint: v2::Endpoint, start_height: AbsoluteBlockHeight) -> Self {
Self {
endpoints: vec![endpoint],
max_parallel: 4,
max_behind: Duration::from_secs(60),
wait_after_fail: Duration::from_secs(1),
start_height,
}
}
pub fn new(endpoints: Vec<v2::Endpoint>, start_height: AbsoluteBlockHeight) -> Option<Self> {
if endpoints.is_empty() {
return None;
}
Some(Self {
endpoints,
max_parallel: 4,
max_behind: Duration::from_secs(60),
wait_after_fail: Duration::from_secs(1),
start_height,
})
}
pub fn set_max_behind(self, max_behind: Duration) -> Self {
Self { max_behind, ..self }
}
pub fn set_wait_after_failure(self, wait_after_fail: Duration) -> Self {
Self {
wait_after_fail,
..self
}
}
pub fn push_endpoint(mut self, endpoint: v2::Endpoint) -> Self {
self.endpoints.push(endpoint);
self
}
pub fn set_max_parallel(self, max_parallel: usize) -> Self {
Self {
max_parallel,
..self
}
}
pub async fn traverse<I: Indexer>(
self,
mut indexer: I,
sender: tokio::sync::mpsc::Sender<I::Data>,
) -> TraverseResult<()> {
let TraverseConfig {
endpoints,
max_parallel,
max_behind,
wait_after_fail,
start_height: mut height,
} = self;
let mut successive_failures: u64 = 0;
for node_ep in endpoints.into_iter().cycle() {
if sender.is_closed() {
return Ok(());
}
if successive_failures > 0 {
tokio::time::sleep(wait_after_fail).await
}
let mut node = match v2::Client::new(node_ep.clone()).await {
Ok(v) => v,
Err(e) => {
successive_failures += 1;
let should_stop = indexer
.on_failure(node_ep, successive_failures, e.into())
.await;
if should_stop {
return Ok(());
} else {
continue;
}
}
};
let context = match indexer.on_connect(node_ep.clone(), &mut node).await {
Ok(a) => a,
Err(e) => {
successive_failures += 1;
let should_stop = indexer
.on_failure(node_ep, successive_failures, e.into())
.await;
if should_stop {
return Ok(());
} else {
continue;
}
}
};
let mut finalized_blocks = match node.get_finalized_blocks_from(height).await {
Ok(v) => v,
Err(e) => {
successive_failures += 1;
let should_stop = indexer
.on_failure(node_ep, successive_failures, e.into())
.await;
if should_stop {
return Ok(());
} else {
continue;
}
}
};
let mut preprocessors = FuturesOrdered::new();
let mut finalized_blocks_error = false;
'node_loop: loop {
tokio::select! {
biased;
Some(data) = preprocessors.next() => {
let data = match data {
Ok(v) => v,
Err(e) => {
drop(preprocessors);
successive_failures += 1;
let should_stop = indexer.on_failure(
node_ep,
successive_failures,
OnFinalizationError::into(e)
).await;
if should_stop {
return Ok(());
} else {
break 'node_loop;
}
}
};
if sender.send(data).await.is_err() {
return Ok(());
}
height = height.next();
successive_failures = 0;
if finalized_blocks_error {
break 'node_loop;
}
},
_ = async {}, if preprocessors.len() < max_parallel => {
let space = max_parallel - preprocessors.len();
match finalized_blocks
.next_chunk_timeout(space, max_behind)
.await
{
Ok((has_error, chunks)) => {
finalized_blocks_error = has_error;
for fb in chunks {
preprocessors.push_back(indexer.on_finalized(node.clone(), &context, fb));
}
},
Err(e) => {
drop(preprocessors);
successive_failures += 1;
let should_stop = indexer
.on_failure(node_ep, successive_failures, TraverseError::Elapsed(e))
.await;
if should_stop {
return Ok(());
} else {
break 'node_loop;
}
}
};
}
};
}
}
Ok(()) }
}
pub struct TransactionIndexer;
#[async_trait]
impl Indexer for TransactionIndexer {
type Context = ();
type Data = (BlockInfo, Vec<BlockItemSummary>);
async fn on_connect<'a>(
&mut self,
endpoint: v2::Endpoint,
_client: &'a mut v2::Client,
) -> QueryResult<()> {
tracing::info!(
target: "ccd_indexer",
"Connected to endpoint {}.",
endpoint.uri()
);
Ok(())
}
async fn on_finalized<'a>(
&self,
mut client: v2::Client,
_ctx: &'a (),
fbi: FinalizedBlockInfo,
) -> OnFinalizationResult<Self::Data> {
let bi = client.get_block_info(fbi.height).await?.response;
if bi.transaction_count != 0 {
let summary = client
.get_block_transaction_events(fbi.height)
.await?
.response
.try_collect::<Vec<_>>()
.await?;
Ok((bi, summary))
} else {
Ok((bi, Vec::new()))
}
}
async fn on_failure(
&mut self,
endpoint: v2::Endpoint,
successive_failures: u64,
err: TraverseError,
) -> bool {
tracing::warn!(
target: "ccd_indexer",
successive_failures,
"Failed when querying endpoint {}: {err}",
endpoint.uri()
);
false
}
}
pub struct ContractUpdateIndexer {
pub target_address: ContractAddress,
pub entrypoint: OwnedEntrypointName,
}
pub struct ContractUpdateInfo {
pub execution_tree: Upward<ExecutionTree>,
pub energy_cost: Energy,
pub cost: Amount,
pub transaction_hash: TransactionHash,
pub sender: AccountAddress,
}
fn update_info(summary: BlockItemSummary) -> Option<ContractUpdateInfo> {
let at = summary.details.known()?.account_transaction()?;
let AccountTransactionEffects::ContractUpdateIssued { effects } = at.effects.known()? else {
return None;
};
Some(ContractUpdateInfo {
execution_tree: execution_tree(effects)?,
energy_cost: summary.energy_cost,
cost: at.cost,
transaction_hash: summary.hash,
sender: at.sender,
})
}
#[async_trait]
impl Indexer for ContractUpdateIndexer {
type Context = ();
type Data = (BlockInfo, Vec<ContractUpdateInfo>);
async fn on_connect<'a>(
&mut self,
endpoint: v2::Endpoint,
_client: &'a mut v2::Client,
) -> QueryResult<()> {
tracing::info!(
target: "ccd_indexer",
"Connected to endpoint {}.",
endpoint.uri()
);
Ok(())
}
async fn on_finalized<'a>(
&self,
mut client: v2::Client,
_ctx: &'a (),
fbi: FinalizedBlockInfo,
) -> OnFinalizationResult<Self::Data> {
let bi = client
.get_block_info(fbi.height)
.await
.map_err(OnFinalizationError::from)?
.response;
if bi.transaction_count != 0 {
let summary = client
.get_block_transaction_events(fbi.height)
.await?
.response
.map_err(OnFinalizationError::from)
.try_filter_map(|summary| async move {
let Some(info) = update_info(summary) else {
return Ok(None);
};
let execution_tree = info.execution_tree.as_ref().known_or_err()?;
if execution_tree.address() == self.target_address
&& execution_tree.entrypoint() == self.entrypoint.as_entrypoint_name()
{
Ok(Some(info))
} else {
Ok(None)
}
})
.try_collect::<Vec<_>>()
.await?;
Ok((bi, summary))
} else {
Ok((bi, Vec::new()))
}
}
async fn on_failure(
&mut self,
endpoint: v2::Endpoint,
successive_failures: u64,
err: TraverseError,
) -> bool {
tracing::warn!(
target: "ccd_indexer",
successive_failures,
"Failed when querying endpoint {}: {err}",
endpoint.uri()
);
false
}
}
pub struct AffectedContractIndexer {
pub addresses: BTreeSet<ContractAddress>,
pub all: bool,
}
#[async_trait]
impl Indexer for AffectedContractIndexer {
type Context = ();
type Data = (
BlockInfo,
Vec<
v2::Upward<(
ContractUpdateInfo,
BTreeMap<ContractAddress, BTreeSet<OwnedReceiveName>>,
)>,
>,
);
async fn on_connect<'a>(
&mut self,
endpoint: v2::Endpoint,
_client: &'a mut v2::Client,
) -> QueryResult<()> {
tracing::info!(
target: "ccd_indexer",
"Connected to endpoint {}.",
endpoint.uri()
);
Ok(())
}
async fn on_finalized<'a>(
&self,
mut client: v2::Client,
_ctx: &'a (),
fbi: FinalizedBlockInfo,
) -> OnFinalizationResult<Self::Data> {
let bi = client.get_block_info(fbi.height).await?.response;
if bi.transaction_count != 0 {
let summary = client
.get_block_transaction_events(fbi.height)
.await?
.response
.map_err(OnFinalizationError::from)
.try_filter_map(|summary| async move {
let Some(info) = update_info(summary) else {
return Ok(None);
};
let execution_tree = info.execution_tree.as_ref().known_or_err()?;
let affected_addresses = execution_tree.affected_addresses();
let v2::Upward::Known(affected_addresses) = affected_addresses else {
return Ok(Some(v2::Upward::Unknown(())));
};
if (self.all
&& self
.addresses
.iter()
.all(|addr| affected_addresses.contains_key(addr)))
|| self
.addresses
.iter()
.any(|addr| affected_addresses.contains_key(addr))
{
Ok(Some(v2::Upward::Known((info, affected_addresses))))
} else {
Ok(None)
}
})
.try_collect::<Vec<_>>()
.await?;
Ok((bi, summary))
} else {
Ok((bi, Vec::new()))
}
}
async fn on_failure(
&mut self,
endpoint: v2::Endpoint,
successive_failures: u64,
err: TraverseError,
) -> bool {
tracing::warn!(
target: "ccd_indexer",
successive_failures,
"Failed when querying endpoint {}: {err}",
endpoint.uri()
);
false
}
}
pub struct BlockEventsIndexer;
#[async_trait]
impl Indexer for BlockEventsIndexer {
type Context = ();
type Data = (
BlockInfo,
Vec<BlockItemSummary>,
Vec<v2::Upward<SpecialTransactionOutcome>>,
);
async fn on_connect<'a>(
&mut self,
endpoint: v2::Endpoint,
client: &'a mut v2::Client,
) -> QueryResult<()> {
TransactionIndexer.on_connect(endpoint, client).await
}
async fn on_finalized<'a>(
&self,
client: v2::Client,
ctx: &'a (),
fbi: FinalizedBlockInfo,
) -> OnFinalizationResult<Self::Data> {
let mut special_client = client.clone();
let special = async move {
let events = special_client
.get_block_special_events(fbi.height)
.await?
.response
.try_collect()
.await?;
Ok(events)
};
let ((bi, summary), special) =
futures::try_join!(TransactionIndexer.on_finalized(client, ctx, fbi), special)?;
Ok((bi, summary, special))
}
async fn on_failure(
&mut self,
endpoint: v2::Endpoint,
successive_failures: u64,
err: TraverseError,
) -> bool {
TransactionIndexer
.on_failure(endpoint, successive_failures, err)
.await
}
}
#[async_trait]
pub trait ProcessEvent {
type Data;
type Error: std::fmt::Display + std::fmt::Debug;
type Description: std::fmt::Display;
async fn process(&mut self, data: &Self::Data) -> Result<Self::Description, Self::Error>;
async fn on_failure(
&mut self,
error: Self::Error,
failed_attempts: u32,
) -> Result<bool, Self::Error>;
}
pub struct ProcessorConfig {
wait_after_fail: std::time::Duration,
stop: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
}
impl Default for ProcessorConfig {
fn default() -> Self {
Self::new()
}
}
impl ProcessorConfig {
pub fn set_wait_after_failure(self, wait_after_fail: Duration) -> Self {
Self {
wait_after_fail,
..self
}
}
pub fn set_stop_signal(
self,
stop: impl std::future::Future<Output = ()> + Send + 'static,
) -> Self {
Self {
stop: Box::pin(stop),
..self
}
}
pub fn new() -> Self {
Self {
wait_after_fail: std::time::Duration::from_secs(5),
stop: Box::pin(std::future::pending()),
}
}
pub async fn process_events<P: ProcessEvent>(
self,
process: P,
events: tokio::sync::mpsc::Receiver<P::Data>,
) {
let stream = tokio_stream::wrappers::ReceiverStream::new(events);
self.process_event_stream(process, stream).await
}
pub async fn process_event_stream<P, E>(mut self, mut process: P, mut events: E)
where
P: ProcessEvent,
E: futures::Stream<Item = P::Data> + Unpin,
{
while let Some(event) = tokio::select! {
biased;
_ = &mut self.stop => None,
r = events.next() => r,
} {
let mut try_number: u32 = 0;
'outer: loop {
let start = tokio::time::Instant::now();
let response = process.process(&event).await;
let end = tokio::time::Instant::now();
let duration = end.duration_since(start).as_millis();
match response {
Ok(descr) => {
tracing::info!(
target: "ccd_event_processor",
"{descr} in {duration}ms."
);
break 'outer;
}
Err(e) => {
tracing::error!(
target: "ccd_event_processor",
"Failed to process event: {e}. Took {duration}ms to fail."
);
tracing::info!(
target: "ccd_event_processor",
"Retrying in {}ms.",
self.wait_after_fail.as_millis()
);
tokio::select! {
biased;
_ = &mut self.stop => {break 'outer},
_ = tokio::time::sleep(self.wait_after_fail) => {}
}
match process.on_failure(e, try_number + 1).await {
Ok(true) => {
}
Ok(false) => return,
Err(e) => {
tracing::warn!("Failed to restart: {e}.");
}
}
try_number += 1;
}
}
}
}
tracing::info!(
target: "ccd_event_processor",
"Terminating process_events due to channel closing."
);
}
}
pub async fn traverse_and_process<I: Indexer, P: ProcessEvent<Data = I::Data>>(
config: TraverseConfig,
i: I,
processor: ProcessorConfig,
p: P,
) -> TraverseResult<()> {
let (sender, receiver) = tokio::sync::mpsc::channel(10);
let fut1 = config.traverse(i, sender);
let fut2 = processor.process_events(p, receiver);
let (r1, ()) = futures::join!(fut1, fut2);
r1
}