fix(indexer): stop ingest loop cooperatively on shutdown

This commit is contained in:
erhant 2026-07-09 14:50:12 +03:00
parent ff6d40f4df
commit 587836e47c
6 changed files with 49 additions and 19 deletions

1
Cargo.lock generated
View File

@ -9759,6 +9759,7 @@ dependencies = [
"tempfile", "tempfile",
"testcontainers", "testcontainers",
"tokio", "tokio",
"tokio-util",
"url", "url",
"wallet", "wallet",
] ]

View File

@ -5,6 +5,7 @@ pub use indexer_core::config::*;
use indexer_service_rpc::RpcServer as _; use indexer_service_rpc::RpcServer as _;
use jsonrpsee::server::{Server, ServerHandle}; use jsonrpsee::server::{Server, ServerHandle};
use log::{error, info}; use log::{error, info};
use tokio_util::sync::CancellationToken;
pub mod service; pub mod service;
@ -69,9 +70,10 @@ pub async fn run_server(
config: IndexerConfig, config: IndexerConfig,
storage_dir: &Path, storage_dir: &Path,
port: u16, port: u16,
shutdown: CancellationToken,
) -> Result<IndexerHandle> { ) -> Result<IndexerHandle> {
#[cfg(feature = "mock-responses")] #[cfg(feature = "mock-responses")]
let _ = (config, storage_dir); let _ = (config, storage_dir, shutdown);
let server = Server::builder() let server = Server::builder()
.build(SocketAddr::from(([0, 0, 0, 0], port))) .build(SocketAddr::from(([0, 0, 0, 0], port)))
@ -86,7 +88,7 @@ pub async fn run_server(
#[cfg(not(feature = "mock-responses"))] #[cfg(not(feature = "mock-responses"))]
let handle = { let handle = {
let service = service::IndexerService::new(config, storage_dir) let service = service::IndexerService::new(config, storage_dir, shutdown)
.await .await
.context("Failed to initialize indexer service")?; .context("Failed to initialize indexer service")?;
server.start(service.into_rpc()) server.start(service.into_rpc())

View File

@ -34,7 +34,9 @@ async fn main() -> Result<()> {
let cancellation_token = listen_for_shutdown_signal(); let cancellation_token = listen_for_shutdown_signal();
let config = indexer_service::IndexerConfig::from_path(&config_path)?; let config = indexer_service::IndexerConfig::from_path(&config_path)?;
let indexer_handle = indexer_service::run_server(config, data_dir.as_path(), port).await?; let indexer_handle =
indexer_service::run_server(config, data_dir.as_path(), port, cancellation_token.clone())
.await?;
tokio::select! { tokio::select! {
() = cancellation_token.cancelled() => { () = cancellation_token.cancelled() => {

View File

@ -2,7 +2,7 @@ use std::{path::Path, pin::pin, sync::Arc};
use anyhow::{Context as _, Result, bail}; use anyhow::{Context as _, Result, bail};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use futures::{StreamExt as _, never::Never}; use futures::StreamExt as _;
use indexer_core::{IndexerCore, config::IndexerConfig}; use indexer_core::{IndexerCore, config::IndexerConfig};
use indexer_service_protocol::{ use indexer_service_protocol::{
Account, AccountId, Block, BlockId, HashType, IndexerStatus, Transaction, Account, AccountId, Block, BlockId, HashType, IndexerStatus, Transaction,
@ -14,6 +14,7 @@ use jsonrpsee::{
}; };
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tokio_util::sync::CancellationToken;
pub struct IndexerService { pub struct IndexerService {
subscription_service: SubscriptionService, subscription_service: SubscriptionService,
@ -21,9 +22,13 @@ pub struct IndexerService {
} }
impl IndexerService { impl IndexerService {
pub async fn new(config: IndexerConfig, storage_dir: &Path) -> Result<Self> { pub async fn new(
config: IndexerConfig,
storage_dir: &Path,
shutdown: CancellationToken,
) -> Result<Self> {
let indexer = IndexerCore::new(config, storage_dir).await?; let indexer = IndexerCore::new(config, storage_dir).await?;
let subscription_service = SubscriptionService::spawn_new(indexer.clone()); let subscription_service = SubscriptionService::spawn_new(indexer.clone(), shutdown);
Ok(Self { Ok(Self {
subscription_service, subscription_service,
@ -170,15 +175,17 @@ impl indexer_service_rpc::RpcServer for IndexerService {
struct SubscriptionService { struct SubscriptionService {
parts: ArcSwap<SubscriptionLoopParts>, parts: ArcSwap<SubscriptionLoopParts>,
indexer: IndexerCore, indexer: IndexerCore,
shutdown: CancellationToken,
} }
impl SubscriptionService { impl SubscriptionService {
pub fn spawn_new(indexer: IndexerCore) -> Self { pub fn spawn_new(indexer: IndexerCore, shutdown: CancellationToken) -> Self {
let parts = Self::spawn_respond_subscribers_loop(indexer.clone()); let parts = Self::spawn_respond_subscribers_loop(indexer.clone(), shutdown.clone());
Self { Self {
parts: ArcSwap::new(Arc::new(parts)), parts: ArcSwap::new(Arc::new(parts)),
indexer, indexer,
shutdown,
} }
} }
@ -192,12 +199,16 @@ impl SubscriptionService {
// Respawn the subscription service loop if it has finished (either with error or panic) // Respawn the subscription service loop if it has finished (either with error or panic)
if guard.handle.is_finished() { if guard.handle.is_finished() {
drop(guard); drop(guard);
let new_parts = Self::spawn_respond_subscribers_loop(self.indexer.clone()); let new_parts = Self::spawn_respond_subscribers_loop(
self.indexer.clone(),
self.shutdown.clone(),
);
let old_handle_and_sender = self.parts.swap(Arc::new(new_parts)); let old_handle_and_sender = self.parts.swap(Arc::new(new_parts));
let old_parts = Arc::into_inner(old_handle_and_sender) let old_parts = Arc::into_inner(old_handle_and_sender)
.expect("There should be no other references to the old handle and sender"); .expect("There should be no other references to the old handle and sender");
match old_parts.handle.await { match old_parts.handle.await {
Ok(Ok(())) => {}
Ok(Err(err)) => { Ok(Err(err)) => {
error!( error!(
"Subscription service loop has unexpectedly finished with error: {err:#}" "Subscription service loop has unexpectedly finished with error: {err:#}"
@ -215,7 +226,10 @@ impl SubscriptionService {
Ok(()) Ok(())
} }
fn spawn_respond_subscribers_loop(indexer: IndexerCore) -> SubscriptionLoopParts { fn spawn_respond_subscribers_loop(
indexer: IndexerCore,
shutdown: CancellationToken,
) -> SubscriptionLoopParts {
let (new_subscription_sender, mut sub_receiver) = let (new_subscription_sender, mut sub_receiver) =
tokio::sync::mpsc::unbounded_channel::<Subscription<BlockId>>(); tokio::sync::mpsc::unbounded_channel::<Subscription<BlockId>>();
@ -231,6 +245,10 @@ impl SubscriptionService {
)] )]
loop { loop {
tokio::select! { tokio::select! {
() = shutdown.cancelled() => {
info!("Shutdown requested; stopping block ingestion");
return Ok(());
}
sub = sub_receiver.recv() => { sub = sub_receiver.recv() => {
let Some(subscription) = sub else { let Some(subscription) = sub else {
bail!("Subscription receiver closed unexpectedly"); bail!("Subscription receiver closed unexpectedly");
@ -259,10 +277,11 @@ impl SubscriptionService {
} }
} }
}; };
let res: anyhow::Result<futures::never::Never> = run_loop.await; let res: anyhow::Result<()> = run_loop.await;
let Err(err) = res; if let Err(err) = &res {
error!("Subscription service loop has unexpectedly finished with error: {err:#?}"); error!("Subscription service loop has unexpectedly finished with error: {err:#?}");
Err(err) }
res
}); });
SubscriptionLoopParts { SubscriptionLoopParts {
handle, handle,
@ -278,7 +297,7 @@ impl Drop for SubscriptionService {
} }
struct SubscriptionLoopParts { struct SubscriptionLoopParts {
handle: tokio::task::JoinHandle<Result<Never>>, handle: tokio::task::JoinHandle<Result<()>>,
new_subscription_sender: UnboundedSender<Subscription<BlockId>>, new_subscription_sender: UnboundedSender<Subscription<BlockId>>,
} }

View File

@ -32,4 +32,5 @@ serde_json.workspace = true
tempfile.workspace = true tempfile.workspace = true
testcontainers = { version = "0.27.3", features = ["docker-compose"] } testcontainers = { version = "0.27.3", features = ["docker-compose"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tokio-util.workspace = true
url.workspace = true url.workspace = true

View File

@ -124,10 +124,15 @@ pub async fn setup_indexer(bedrock_addr: SocketAddr) -> Result<(IndexerHandle, T
let indexer_config = let indexer_config =
config::indexer_config(bedrock_addr).context("Failed to create Indexer config")?; config::indexer_config(bedrock_addr).context("Failed to create Indexer config")?;
indexer_service::run_server(indexer_config, temp_indexer_dir.path(), 0) indexer_service::run_server(
.await indexer_config,
.context("Failed to run Indexer Service") temp_indexer_dir.path(),
.map(|handle| (handle, temp_indexer_dir)) 0,
tokio_util::sync::CancellationToken::new(),
)
.await
.context("Failed to run Indexer Service")
.map(|handle| (handle, temp_indexer_dir))
} }
pub async fn setup_sequencer( pub async fn setup_sequencer(