use super::trade_dispatch::{ExtraSinks, maybe_reset_at_midnight, maybe_reset_at_week_gap};
use super::types::*;
use crate::ring_buffer::ConcurrentRingBuffer;
use opendeviationbar_core::processor::OpenDeviationBarProcessor;
use opendeviationbar_providers::binance::AdaptiveRateLimiter;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use tokio::sync::watch;
use tokio_util::sync::CancellationToken;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn puck_fill(
symbol: &str,
last_known_id: i64,
first_ws_id: i64,
processors: &mut [(u32, OpenDeviationBarProcessor)],
bar_buffer: &ConcurrentRingBuffer<CompletedBar>,
metrics: &LiveEngineMetrics,
shutdown: &CancellationToken,
rate_limiter: Option<Arc<AdaptiveRateLimiter>>,
forming_tx_map: &HashMap<u32, watch::Sender<Option<FormingBar>>>, event_type: &str, committed_floors: &mut HashMap<u32, i64>, ouroboros_mode: OuroborosMode, extra_sinks: &ExtraSinks, ) -> u64 {
let _ = &extra_sinks; let gap_size = first_ws_id - last_known_id - 1;
tracing::info!(
%symbol, gap_size, last_known_id, first_ws_id, event_type,
"puck: filling gap via parallel REST API"
);
let from_id = last_known_id + 1;
let max_retries: u32 = 3;
let mut trades = Vec::new();
let mut last_err = None;
for attempt in 0..=max_retries {
if attempt > 0 {
let backoff_ms = 1000u64 << (attempt - 1); tracing::warn!(
%symbol, attempt, backoff_ms, event_type, gap_size,
"puck: retrying REST gap-fill after failure"
);
tokio::time::sleep(tokio::time::Duration::from_millis(backoff_ms)).await;
if shutdown.is_cancelled() {
tracing::info!(%symbol, "puck: retry aborted by shutdown");
return 0;
}
}
match opendeviationbar_providers::binance::parallel_fetch::fetch_aggtrades_parallel(
symbol,
from_id,
first_ws_id - 1,
rate_limiter.clone(),
None, None, None, )
.await
{
Ok(t) => {
trades = t;
last_err = None;
break;
}
Err(e) => {
last_err = Some(e);
}
}
}
if let Some(e) = last_err {
tracing::error!(
%symbol, ?e, gap_size, event_type, attempts = max_retries + 1,
"puck: REST gap-fill FAILED after all retries — junction gap UNRECOVERED"
);
return 0;
}
let mut trades_recovered = 0u64;
let symbol_arc: Arc<str> = Arc::from(symbol);
let mut last_days: Vec<i64> = processors.iter().map(|_| -1i64).collect();
let mut last_trade_timestamps_us: Vec<i64> = processors.iter().map(|_| 0i64).collect();
for trade in &trades {
if shutdown.is_cancelled() {
tracing::info!(%symbol, trades_recovered, "gap fill interrupted by shutdown");
break;
}
for (idx, (threshold, processor)) in processors.iter_mut().enumerate() {
if let OuroborosMode::Week { max_gap_us } = ouroboros_mode
&& let Some(orphan) = maybe_reset_at_week_gap(
processor,
trade.timestamp,
&mut last_trade_timestamps_us[idx],
max_gap_us,
)
{
if let Some(&floor) = committed_floors.get(threshold)
&& orphan.last_agg_trade_id > 0
&& orphan.last_agg_trade_id <= floor
{
tracing::debug!(%symbol, threshold = *threshold, last_tid = orphan.last_agg_trade_id, floor, "puck_fill: skipping week-gap orphan (within committed range)");
continue;
}
metrics.bars_emitted.fetch_add(1, Ordering::Relaxed);
let completed = CompletedBar {
symbol: symbol_arc.clone(),
threshold_decimal_bps: *threshold,
bar: orphan,
};
committed_floors.insert(*threshold, completed.bar.last_agg_trade_id);
if !bar_buffer.push(completed.clone()) {
metrics.dropped_bars.fetch_add(1, Ordering::Relaxed);
metrics.backpressure_events.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "clickhouse-sink")]
if let Some(sinks) = extra_sinks {
crate::clickhouse_writer::guards::dispatch_to_sinks(&completed, sinks);
}
}
if let Some(orphan) =
maybe_reset_at_midnight(processor, trade.timestamp, &mut last_days[idx], ouroboros_mode)
{
if let Some(&floor) = committed_floors.get(threshold)
&& orphan.last_agg_trade_id > 0
&& orphan.last_agg_trade_id <= floor
{
tracing::debug!(%symbol, threshold = *threshold, last_tid = orphan.last_agg_trade_id, floor, "puck_fill: skipping orphan (within committed range)");
continue;
}
metrics.bars_emitted.fetch_add(1, Ordering::Relaxed);
let completed = CompletedBar {
symbol: symbol_arc.clone(),
threshold_decimal_bps: *threshold,
bar: orphan,
};
committed_floors.insert(*threshold, completed.bar.last_agg_trade_id);
if !bar_buffer.push(completed.clone()) {
metrics.dropped_bars.fetch_add(1, Ordering::Relaxed);
metrics.backpressure_events.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "clickhouse-sink")]
if let Some(sinks) = extra_sinks {
crate::clickhouse_writer::guards::dispatch_to_sinks(&completed, sinks);
}
}
match processor.process_single_trade(trade) {
Ok(Some(bar)) => {
if let Some(&floor) = committed_floors.get(threshold)
&& bar.last_agg_trade_id > 0
&& bar.last_agg_trade_id <= floor
{
tracing::debug!(%symbol, threshold = *threshold, last_tid = bar.last_agg_trade_id, floor, "puck_fill: skipping bar (within committed range)");
if let Some(tx) = forming_tx_map.get(threshold) {
let _ = tx.send(None);
}
continue;
}
if let Some(tx) = forming_tx_map.get(threshold) {
let _ = tx.send(None);
}
metrics.bars_emitted.fetch_add(1, Ordering::Relaxed);
let completed = CompletedBar {
symbol: symbol_arc.clone(),
threshold_decimal_bps: *threshold,
bar,
};
committed_floors.insert(*threshold, completed.bar.last_agg_trade_id);
if !bar_buffer.push(completed.clone()) {
metrics.dropped_bars.fetch_add(1, Ordering::Relaxed);
metrics.backpressure_events.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "clickhouse-sink")]
if let Some(sinks) = extra_sinks {
crate::clickhouse_writer::guards::dispatch_to_sinks(&completed, sinks);
}
}
Ok(None) => {
if let Some(tx) = forming_tx_map.get(threshold)
&& let Some(incomplete) = processor.get_incomplete_bar()
{
let _ = tx.send(Some(FormingBar {
symbol: symbol_arc.clone(),
threshold_decimal_bps: *threshold,
bar: incomplete,
last_trade_timestamp_us: trade.timestamp,
}));
}
}
Err(e) => {
tracing::warn!(
%symbol, threshold = *threshold, ?e,
"gap-fill trade processing error"
);
}
}
}
trades_recovered += 1;
}
tracing::info!(
%symbol, trades_recovered, gap_size,
"reconnection gap fill complete"
);
trades_recovered
}