From 02b66c6296860e7d7a2a6a8c233dbb5d98d62e73 Mon Sep 17 00:00:00 2001 From: Dzmitry Kalabuk Date: Wed, 1 Jul 2026 12:06:03 +0300 Subject: [PATCH] Report min finalized head for traceless datasets The traced and traceless hotblocks variants finalize independently, so advertising only the traced head let a traceless stream head-wait and 204 when it hadn't finalized that far yet. Report the minimum of both heads. Part of NET-888 --- src/hotblocks.rs | 16 ++++++++++++++++ src/http_server.rs | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/hotblocks.rs b/src/hotblocks.rs index b4f96fd..eb5bc90 100644 --- a/src/hotblocks.rs +++ b/src/hotblocks.rs @@ -149,6 +149,22 @@ impl HotblocksHandle { Ok(result) } + /// Like [`Self::get_finalized_head`], but treats a `null` body (no finalized + /// block yet) as `Ok(None)` instead of a deserialization error. + pub async fn get_finalized_head_opt( + &self, + dataset: &str, + ) -> Result, HotblocksErr> { + let result = self + .request_finalized_head(dataset) + .await? + .error_for_status()? + .json() + .await?; + + Ok(result) + } + pub async fn request_status(&self, dataset: &str) -> Result { let Some(url) = self.urls.get(dataset) else { return Err(HotblocksErr::UnknownDataset); diff --git a/src/http_server.rs b/src/http_server.rs index ce5e91a..14c218f 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -44,7 +44,7 @@ use crate::utils::logging::MethodRouterExt; use crate::{ config::Config, controller::task_manager::TaskManager, - hotblocks::HotblocksHandle, + hotblocks::{traceless_key, HotblocksHandle}, network::{NetworkClient, NoWorker, NotReady}, types::{ChunkId, DatasetId, ParsedQuery, RequestError, StreamRequest}, utils::logging, @@ -344,7 +344,18 @@ async fn get_finalized_head( Extension(network): Extension>, dataset: DatasetConfig, ) -> Response { - if dataset.hotblocks.is_some() { + if let Some(hotblocks_cfg) = &dataset.hotblocks { + // Traced and traceless variants finalize independently, and a stream may use + // either. Report the minimum of their heads so we never advertise a head one + // variant can't yet serve. + if hotblocks_cfg.dataset_traceless.is_some() { + let traceless_name = traceless_key(&dataset.default_name); + let (traced, traceless) = tokio::join!( + hotblocks.get_finalized_head_opt(&dataset.default_name), + hotblocks.get_finalized_head_opt(&traceless_name), + ); + return min_finalized_head_response(traced, traceless); + } return forward_hotblocks_response( hotblocks .request_finalized_head(&dataset.default_name) @@ -1134,6 +1145,28 @@ where } } +/// Build a `/finalized-head` response with the lower of the traced and traceless heads, +/// so it's streamable by whichever variant a later stream uses. +/// +/// If either variant has no finalized block yet (`None`), the head is `None`: advertising +/// `null` is safer than a head one variant can't serve. An error from either variant is +/// surfaced as-is rather than degrading to the other's (possibly too-high) head. +fn min_finalized_head_response( + traced: Result, HotblocksErr>, + traceless: Result, HotblocksErr>, +) -> Response { + match (traced, traceless) { + (Ok(traced), Ok(traceless)) => { + let head = match (traced, traceless) { + (Some(a), Some(b)) => Some(if a.number <= b.number { a } else { b }), + _ => None, + }; + axum::Json(head).into_response() + } + (Err(e), _) | (_, Err(e)) => forward_hotblocks_response(Err(e)), + } +} + pub(crate) fn forward_hotblocks_response( response: Result, ) -> Response {