v0.3.12-pre.007

This commit is contained in:
2026-09-09 16:21:23 +02:00
parent 07b9b20eb5
commit ba69a5ad20
13 changed files with 1037 additions and 105 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs
// version: 13
// version: 14
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -14,7 +14,8 @@
//! the validated Yellowstone/HTTP runtime-resource contract now drives one productive supervised source task.
//! Transaction/TransactionStatus/Block updates feed bounded HTTP `getTransaction` hydration and the existing central
//! admission path; BlockMeta/Slot remain continuity-only signals. Hydration is coalesced by network/signature/commitment
//! under bounded in-flight and pending budgets, while reconnect/frontier/replay interpretation remains deferred.
//! under bounded in-flight and pending budgets. A bounded run-local processing frontier now projects hydration pending,
//! oldest pending slot and highest unblocked actually observed slot; reconnect/replay interpretation remains deferred.
mod admission;
mod error;
@@ -104,3 +105,5 @@ pub(crate) use self::persistence::RawTransactionIngestPersistencePort;
pub(crate) use self::persistence::persist_raw_transaction_ingest_acquisition;
/// Private latest-value publisher and checked counter owner shared by the Worker supervisor.
pub(crate) use self::snapshot::RawTransactionIngestSnapshotPublisher;
/// Private latest-value processing-frontier projection emitted by the productive source task.
pub(crate) use self::snapshot::RawTransactionIngestProcessingFrontierProjection;

View File

@@ -1,8 +1,9 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs
// version: 9
// version: 10
type PersistencePort = std::sync::Arc<dyn crate::RawTransactionIngestPersistencePort + 'static>;
type PersistenceTasks = tokio::task::JoinSet<ksp_core_lib::Result<crate::RawTransactionIngestPersistenceOutcome>>;
type ProcessingFrontierReceiver = tokio::sync::watch::Receiver<crate::RawTransactionIngestProcessingFrontierProjection>;
type SourceTasks = tokio::task::JoinSet<ksp_core_lib::Result<()>>;
/// Runtime-neutral boxed future resolving after one RAW transaction ingest Worker has fully reached a terminal lifecycle state.
@@ -115,11 +116,20 @@ impl crate::RawTransactionIngestWorker {
}
let source = runtime_resources.into_yellowstone_source();
let source_settings = settings.clone();
return start_foundation_with_source_spawner(settings, runtime, std::option::Option::Some(store), move |children, stop_receiver, admission_sender| {
let _abort_handle = children.spawn(async move {
return source.run(source_settings, stop_receiver, admission_sender).await;
});
});
let (processing_frontier_sender, processing_frontier_receiver) =
tokio::sync::watch::channel(crate::RawTransactionIngestProcessingFrontierProjection::empty());
let port: PersistencePort = store;
return start_foundation_with_port_source_spawner_and_frontier(
settings,
runtime,
std::option::Option::Some(port),
std::option::Option::Some(processing_frontier_receiver),
move |children, stop_receiver, admission_sender| {
let _abort_handle = children.spawn(async move {
return source.run(source_settings, stop_receiver, admission_sender, processing_frontier_sender).await;
});
},
);
}
}
@@ -179,6 +189,7 @@ async fn drain_admission_and_persistence(
persistence: &mut PersistenceTasks,
port: &std::option::Option<PersistencePort>,
snapshots: &mut crate::RawTransactionIngestSnapshotPublisher,
processing_frontier_receiver: &mut std::option::Option<ProcessingFrontierReceiver>,
) -> std::option::Option<ksp_core_lib::ErrorCode> {
let mut fault = std::option::Option::None;
admission.close();
@@ -344,6 +355,7 @@ async fn run_supervisor<Spawner>(
port: std::option::Option<PersistencePort>,
mut stop_receiver: tokio::sync::watch::Receiver<bool>,
mut snapshots: crate::RawTransactionIngestSnapshotPublisher,
mut processing_frontier_receiver: std::option::Option<ProcessingFrontierReceiver>,
source_spawner: Spawner,
) where
Spawner:
@@ -380,6 +392,7 @@ async fn run_supervisor<Spawner>(
&mut persistence,
&port,
&mut snapshots,
&mut processing_frontier_receiver,
)
.await;
source_stop_sender.send_replace(true);
@@ -431,6 +444,26 @@ fn start_foundation_with_port_and_source_spawner<Spawner>(
port: std::option::Option<PersistencePort>,
source_spawner: Spawner,
) -> ksp_core_lib::Result<crate::RawTransactionIngestHandle>
where
Spawner:
FnOnce(&mut SourceTasks, tokio::sync::watch::Receiver<bool>, tokio::sync::mpsc::Sender<crate::RawTransactionIngress>) + std::marker::Send + 'static,
{
return start_foundation_with_port_source_spawner_and_frontier(
settings,
runtime,
port,
std::option::Option::None,
source_spawner,
);
}
fn start_foundation_with_port_source_spawner_and_frontier<Spawner>(
settings: crate::RawTransactionIngestSettings,
runtime: tokio::runtime::Handle,
port: std::option::Option<PersistencePort>,
processing_frontier_receiver: std::option::Option<ProcessingFrontierReceiver>,
source_spawner: Spawner,
) -> ksp_core_lib::Result<crate::RawTransactionIngestHandle>
where
Spawner:
FnOnce(&mut SourceTasks, tokio::sync::watch::Receiver<bool>, tokio::sync::mpsc::Sender<crate::RawTransactionIngress>) + std::marker::Send + 'static,
@@ -447,7 +480,15 @@ where
let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
let (snapshots, snapshot_source) = crate::RawTransactionIngestSnapshotPublisher::new(&settings, &lifecycle);
let handle = crate::RawTransactionIngestHandle { snapshots: snapshot_source, stop_sender, stop_token };
std::mem::drop(runtime.spawn(run_supervisor(settings, lifecycle, port, stop_receiver, snapshots, source_spawner)));
std::mem::drop(runtime.spawn(run_supervisor(
settings,
lifecycle,
port,
stop_receiver,
snapshots,
processing_frontier_receiver,
source_spawner,
)));
return std::result::Result::Ok(handle);
}
@@ -504,6 +545,25 @@ async fn supervise_until_stop(
break;
}
}
processing_frontier = wait_processing_frontier(processing_frontier_receiver) => {
match processing_frontier {
std::option::Option::Some(projection) => {
let published = snapshots.record_processing_frontier(
lifecycle.state(),
admission.queue_depth(),
persistence.len(),
projection,
);
if let std::result::Result::Err(error) = published {
source_stop_sender.send_replace(true);
return std::option::Option::Some(error.code());
}
},
std::option::Option::None => {
*processing_frontier_receiver = std::option::Option::None;
},
}
}
joined = children.join_next(), if !children.is_empty() => {
let source_fault = match joined {
std::option::Option::Some(value) => source_completion(value, lifecycle.state(), admission.queue_depth(), persistence.len(), snapshots),
@@ -571,6 +631,21 @@ async fn supervise_until_stop(
return std::option::Option::None;
}
async fn wait_processing_frontier(
receiver: &mut std::option::Option<ProcessingFrontierReceiver>,
) -> std::option::Option<crate::RawTransactionIngestProcessingFrontierProjection> {
let receiver = match receiver.as_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::future::pending::<std::option::Option<crate::RawTransactionIngestProcessingFrontierProjection>>().await;
},
};
if receiver.changed().await.is_err() {
return std::option::Option::None;
}
return std::option::Option::Some(*receiver.borrow_and_update());
}
fn validate_store_network(settings: &crate::RawTransactionIngestSettings, store_network: &ksp_store_lib::RawNetworkId) -> ksp_core_lib::Result<()> {
if settings.network() != store_network {
return std::result::Result::Err(crate::runtime_error("start.store_network_mismatch"));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime_resources.rs
// version: 9
// version: 10
use sha2::Digest; // rust-rules: trait-import
@@ -188,7 +188,10 @@ trait RawTransactionIngestYellowstoneBlockView {
fn transaction_count(&self) -> usize;
fn transaction_identity(&self, position: usize) -> ksp_core_lib::Result<(ksp_onchain_transport_lib::YellowstoneTransactionSignature, u64)>;
fn transaction_identity(
&self,
position: usize,
) -> ksp_core_lib::Result<(ksp_onchain_transport_lib::YellowstoneTransactionSignature, u64)>;
}
impl RawTransactionIngestYellowstoneBlockView for ksp_onchain_transport_lib::YellowstoneBlockUpdate {
@@ -208,7 +211,10 @@ impl RawTransactionIngestYellowstoneBlockView for ksp_onchain_transport_lib::Yel
return ksp_onchain_transport_lib::YellowstoneBlockUpdate::transactions(self).len();
}
fn transaction_identity(&self, position: usize) -> ksp_core_lib::Result<(ksp_onchain_transport_lib::YellowstoneTransactionSignature, u64)> {
fn transaction_identity(
&self,
position: usize,
) -> ksp_core_lib::Result<(ksp_onchain_transport_lib::YellowstoneTransactionSignature, u64)> {
let transaction = match ksp_onchain_transport_lib::YellowstoneBlockUpdate::transactions(self).get(position) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.block_transaction_missing")),
@@ -286,7 +292,9 @@ impl RawTransactionIngestYellowstoneContinuityView for ksp_onchain_transport_lib
impl std::convert::From<(&crate::RawTransactionIngestYellowstoneSource, &ksp_onchain_transport_lib::YellowstoneTransactionUpdate)>
for RawTransactionIngestSourceSignal
{
fn from(value: (&crate::RawTransactionIngestYellowstoneSource, &ksp_onchain_transport_lib::YellowstoneTransactionUpdate)) -> Self {
fn from(
value: (&crate::RawTransactionIngestYellowstoneSource, &ksp_onchain_transport_lib::YellowstoneTransactionUpdate),
) -> Self {
return project_yellowstone_signal(value.0, value.1);
}
}
@@ -294,7 +302,9 @@ impl std::convert::From<(&crate::RawTransactionIngestYellowstoneSource, &ksp_onc
impl std::convert::From<(&crate::RawTransactionIngestYellowstoneSource, &ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate)>
for RawTransactionIngestSourceSignal
{
fn from(value: (&crate::RawTransactionIngestYellowstoneSource, &ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate)) -> Self {
fn from(
value: (&crate::RawTransactionIngestYellowstoneSource, &ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate),
) -> Self {
return project_yellowstone_signal(value.0, value.1);
}
}
@@ -367,6 +377,7 @@ impl crate::RawTransactionIngestYellowstoneSource {
settings: crate::RawTransactionIngestSettings,
mut stop_receiver: tokio::sync::watch::Receiver<bool>,
admission_sender: tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
processing_frontier_sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
) -> ksp_core_lib::Result<()> {
let opened = tokio::select! {
biased;
@@ -380,6 +391,7 @@ impl crate::RawTransactionIngestYellowstoneSource {
std::result::Result::Err(error) => return std::result::Result::Err(source_transport_error(error.code())),
};
let mut coordinator = RawTransactionIngestHydrationCoordinator::new(&settings);
let mut processing_frontier = RawTransactionIngestProcessingFrontierReporter::new(processing_frontier_sender);
let mut fault = std::option::Option::None;
loop {
if *stop_receiver.borrow() {
@@ -407,7 +419,16 @@ impl crate::RawTransactionIngestYellowstoneSource {
break;
},
};
let handled = coordinator.handle_joined(joined, &self, &settings, &admission_sender, &mut stop_receiver).await;
let handled = coordinator
.handle_joined(
joined,
&self,
&settings,
&admission_sender,
&mut stop_receiver,
&mut processing_frontier,
)
.await;
match handled {
std::result::Result::Ok(true) => {},
std::result::Result::Ok(false) => break,
@@ -429,7 +450,12 @@ impl crate::RawTransactionIngestYellowstoneSource {
break;
},
};
if let std::result::Result::Err(error) = route_yellowstone_update(&self, &mut coordinator, update) {
if let std::result::Result::Err(error) = route_yellowstone_update(
&self,
&mut coordinator,
&mut processing_frontier,
update,
) {
fault = std::option::Option::Some(error);
break;
}
@@ -565,7 +591,9 @@ fn ingestion_filter_count(request: &ksp_onchain_transport_lib::YellowstoneSubscr
return request.transaction_filter_count().saturating_add(request.transaction_status_filter_count()).saturating_add(request.block_filter_count());
}
fn matched_filter_direct_id(filters: &[ksp_onchain_transport_lib::YellowstoneSubscribeFilterName]) -> std::option::Option<ksp_store_lib::RawProvenanceCode> {
fn matched_filter_direct_id(
filters: &[ksp_onchain_transport_lib::YellowstoneSubscribeFilterName],
) -> std::option::Option<ksp_store_lib::RawProvenanceCode> {
let mut names = filters.iter().map(ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::as_str).collect::<std::vec::Vec<_>>();
names.sort_unstable();
names.dedup();
@@ -687,6 +715,7 @@ fn project_yellowstone_signal<T: RawTransactionIngestYellowstoneSignalView>(
fn route_yellowstone_update(
source: &crate::RawTransactionIngestYellowstoneSource,
coordinator: &mut RawTransactionIngestHydrationCoordinator,
processing_frontier: &mut RawTransactionIngestProcessingFrontierReporter,
update: ksp_onchain_transport_lib::YellowstoneSubscribeUpdate,
) -> ksp_core_lib::Result<()> {
match update {
@@ -695,14 +724,24 @@ fn route_yellowstone_update(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return coordinator.queue_signal(source, RawTransactionIngestSourceSignal::from((source, value.as_ref())), received_at);
return coordinator.queue_signal(
source,
RawTransactionIngestSourceSignal::from((source, value.as_ref())),
received_at,
processing_frontier,
);
},
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::TransactionStatus(value) => {
let received_at = match current_raw_timestamp() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return coordinator.queue_signal(source, RawTransactionIngestSourceSignal::from((source, &value)), received_at);
return coordinator.queue_signal(
source,
RawTransactionIngestSourceSignal::from((source, &value)),
received_at,
processing_frontier,
);
},
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Block(value) => {
let received_at = match current_raw_timestamp() {
@@ -714,18 +753,26 @@ fn route_yellowstone_update(
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for signal in signals {
if let std::result::Result::Err(error) = coordinator.queue_signal(source, signal, received_at) {
if let std::result::Result::Err(error) =
coordinator.queue_signal(source, signal, received_at, processing_frontier)
{
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(());
},
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::BlockMeta(value) => {
let _continuity_signal = project_yellowstone_continuity_signal(source, &value);
let continuity_signal = project_yellowstone_continuity_signal(source, &value);
if let std::result::Result::Err(error) = processing_frontier.observe_settled(continuity_signal.slot) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
},
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Slot(value) => {
let _continuity_signal = project_yellowstone_continuity_signal(source, &value);
let continuity_signal = project_yellowstone_continuity_signal(source, &value);
if let std::result::Result::Err(error) = processing_frontier.observe_settled(continuity_signal.slot) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
},
ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Account(_)
@@ -757,6 +804,178 @@ fn source_transport_error(code: ksp_core_lib::ErrorCode) -> ksp_core_lib::Error
.with_context("transport_code", code.code());
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct RawTransactionIngestProcessingSlotState {
pending: usize,
settled: u64,
}
struct RawTransactionIngestProcessingFrontier {
pending_total: usize,
slots: std::collections::BTreeMap<u64, RawTransactionIngestProcessingSlotState>,
}
impl RawTransactionIngestProcessingFrontier {
fn new() -> Self {
return Self { pending_total: 0, slots: std::collections::BTreeMap::new() };
}
fn observe_pending(&mut self, slot: u64) -> ksp_core_lib::Result<()> {
let pending_total = match self.pending_total.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.frontier_pending_counter_exhausted")),
};
let state = self.slots.entry(slot).or_insert(RawTransactionIngestProcessingSlotState { pending: 0, settled: 0 });
state.pending = match state.pending.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.frontier_slot_pending_counter_exhausted")),
};
self.pending_total = pending_total;
self.compact();
return std::result::Result::Ok(());
}
fn observe_settled(&mut self, slot: u64) -> ksp_core_lib::Result<()> {
let state = self.slots.entry(slot).or_insert(RawTransactionIngestProcessingSlotState { pending: 0, settled: 0 });
state.settled = match state.settled.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.frontier_slot_settled_counter_exhausted")),
};
self.compact();
return std::result::Result::Ok(());
}
fn settle_pending(&mut self, slot: u64) -> ksp_core_lib::Result<()> {
let state = match self.slots.get_mut(&slot) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.frontier_pending_slot_missing")),
};
if state.pending == 0 || self.pending_total == 0 {
return std::result::Result::Err(crate::runtime_error("source.frontier_pending_counter_invalid"));
}
state.pending -= 1;
self.pending_total -= 1;
state.settled = match state.settled.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.frontier_slot_settled_counter_exhausted")),
};
self.compact();
return std::result::Result::Ok(());
}
fn projection(&self) -> crate::RawTransactionIngestProcessingFrontierProjection {
let oldest_pending_slot = self.slots.iter().find_map(|(slot, state)| {
if state.pending == 0 {
return std::option::Option::None;
}
return std::option::Option::Some(*slot);
});
let processing_frontier_slot = match oldest_pending_slot {
std::option::Option::Some(pending_slot) => self.slots.range(..pending_slot).rev().find_map(|(slot, state)| {
if state.pending == 0 && state.settled > 0 {
return std::option::Option::Some(*slot);
}
return std::option::Option::None;
}),
std::option::Option::None => self.slots.iter().rev().find_map(|(slot, state)| {
if state.pending == 0 && state.settled > 0 {
return std::option::Option::Some(*slot);
}
return std::option::Option::None;
}),
};
return crate::RawTransactionIngestProcessingFrontierProjection::new(self.pending_total, processing_frontier_slot, oldest_pending_slot);
}
fn compact(&mut self) {
let pending_slots = self
.slots
.iter()
.filter_map(|(slot, state)| {
if state.pending > 0 {
return std::option::Option::Some(*slot);
}
return std::option::Option::None;
})
.collect::<std::vec::Vec<_>>();
let mut settled_interval_highs = std::collections::BTreeMap::<std::option::Option<u64>, u64>::new();
for (slot, state) in &self.slots {
if state.pending > 0 || state.settled == 0 {
continue;
}
let upper_pending = pending_slots.iter().find_map(|pending_slot| {
if pending_slot > slot {
return std::option::Option::Some(*pending_slot);
}
return std::option::Option::None;
});
match settled_interval_highs.entry(upper_pending) {
std::collections::btree_map::Entry::Occupied(mut entry) => {
if *slot > *entry.get() {
entry.insert(*slot);
}
},
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(*slot);
},
}
}
let mut retained = pending_slots.into_iter().collect::<std::collections::BTreeSet<_>>();
for slot in settled_interval_highs.into_values() {
retained.insert(slot);
}
self.slots.retain(|slot, _state| {
return retained.contains(slot);
});
return;
}
#[cfg(test)]
fn slot_state_count(&self) -> usize {
return self.slots.len();
}
}
struct RawTransactionIngestProcessingFrontierReporter {
frontier: RawTransactionIngestProcessingFrontier,
sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>,
}
impl RawTransactionIngestProcessingFrontierReporter {
fn new(sender: tokio::sync::watch::Sender<crate::RawTransactionIngestProcessingFrontierProjection>) -> Self {
return Self { frontier: RawTransactionIngestProcessingFrontier::new(), sender };
}
fn observe_pending(&mut self, slot: u64) -> ksp_core_lib::Result<()> {
if let std::result::Result::Err(error) = self.frontier.observe_pending(slot) {
return std::result::Result::Err(error);
}
self.publish();
return std::result::Result::Ok(());
}
fn observe_settled(&mut self, slot: u64) -> ksp_core_lib::Result<()> {
if let std::result::Result::Err(error) = self.frontier.observe_settled(slot) {
return std::result::Result::Err(error);
}
self.publish();
return std::result::Result::Ok(());
}
fn settle_pending(&mut self, slot: u64) -> ksp_core_lib::Result<()> {
if let std::result::Result::Err(error) = self.frontier.settle_pending(slot) {
return std::result::Result::Err(error);
}
self.publish();
return std::result::Result::Ok(());
}
fn publish(&self) {
self.sender.send_replace(self.frontier.projection());
return;
}
}
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
struct RawTransactionIngestHydrationKey {
commitment: &'static str,
@@ -812,6 +1031,7 @@ impl RawTransactionIngestHydrationCoordinator {
source: &crate::RawTransactionIngestYellowstoneSource,
signal: RawTransactionIngestSourceSignal,
received_at: ksp_store_lib::RawTimestamp,
processing_frontier: &mut RawTransactionIngestProcessingFrontierReporter,
) -> ksp_core_lib::Result<()> {
if self.pending_signal_count >= self.max_pending_signals {
return std::result::Result::Err(crate::runtime_error("source.hydration_pending_saturated"));
@@ -820,6 +1040,7 @@ impl RawTransactionIngestHydrationCoordinator {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let signal_slot = signal.slot;
match self.pending.entry(key) {
std::collections::btree_map::Entry::Occupied(mut entry) => {
entry.get_mut().signals.push(RawTransactionIngestPendingSignal { received_at, signal });
@@ -835,17 +1056,23 @@ impl RawTransactionIngestHydrationCoordinator {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("source.hydration_pending_counter_exhausted")),
};
if let std::result::Result::Err(error) = processing_frontier.observe_pending(signal_slot) {
return std::result::Result::Err(error);
}
return self.start_hydrations(source);
}
fn start_hydrations(&mut self, source: &crate::RawTransactionIngestYellowstoneSource) -> ksp_core_lib::Result<()> {
while self.tasks.len() < self.max_in_flight {
let key = self.pending.iter().find_map(|(key, pending)| {
if pending.in_flight {
return std::option::Option::None;
}
return std::option::Option::Some(key.clone());
});
let key = self
.pending
.iter()
.find_map(|(key, pending)| {
if pending.in_flight {
return std::option::Option::None;
}
return std::option::Option::Some(key.clone());
});
let key = match key {
std::option::Option::Some(value) => value,
std::option::Option::None => break,
@@ -877,6 +1104,7 @@ impl RawTransactionIngestHydrationCoordinator {
settings: &crate::RawTransactionIngestSettings,
admission_sender: &tokio::sync::mpsc::Sender<crate::RawTransactionIngress>,
stop_receiver: &mut tokio::sync::watch::Receiver<bool>,
processing_frontier: &mut RawTransactionIngestProcessingFrontierReporter,
) -> ksp_core_lib::Result<bool> {
let fetched = match joined {
std::result::Result::Ok(std::result::Result::Ok(value)) => value,
@@ -892,6 +1120,7 @@ impl RawTransactionIngestHydrationCoordinator {
}
self.pending_signal_count -= pending.signals.len();
for pending_signal in pending.signals {
let signal_slot = pending_signal.signal.slot;
let ingress = finalize_yellowstone_hydration(source, settings, pending_signal.signal, pending_signal.received_at, &fetched.observed);
let ingress = match ingress {
std::result::Result::Ok(value) => value,
@@ -899,7 +1128,12 @@ impl RawTransactionIngestHydrationCoordinator {
};
let ingress = match ingress {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
std::option::Option::None => {
if let std::result::Result::Err(error) = processing_frontier.settle_pending(signal_slot) {
return std::result::Result::Err(error);
}
continue;
},
};
let sent = tokio::select! {
biased;
@@ -914,6 +1148,9 @@ impl RawTransactionIngestHydrationCoordinator {
}
return std::result::Result::Err(crate::runtime_error("source.admission_closed"));
}
if let std::result::Result::Err(error) = processing_frontier.settle_pending(signal_slot) {
return std::result::Result::Err(error);
}
}
if let std::result::Result::Err(error) = self.start_hydrations(source) {
return std::result::Result::Err(error);
@@ -1039,7 +1276,14 @@ fn finalize_yellowstone_hydration(
if embedded_signature != signal.signature {
return std::result::Result::Err(crate::runtime_error("hydration.signature_mismatch"));
}
let provenance = build_hydration_provenance(settings, &signal, observed.provider().as_str(), observed.endpoint_name(), commitment, received_at);
let provenance = build_hydration_provenance(
settings,
&signal,
observed.provider().as_str(),
observed.endpoint_name(),
commitment,
received_at,
);
let provenance = match provenance {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -1060,7 +1304,12 @@ fn finalize_yellowstone_hydration(
}),
map_hydration_wire_field(transaction.transaction_index(), |value| return *value),
);
return std::result::Result::Ok(std::option::Option::Some(crate::RawTransactionIngress { material, network: signal.network, provenance, source_key }));
return std::result::Result::Ok(std::option::Option::Some(crate::RawTransactionIngress {
material,
network: signal.network,
provenance,
source_key,
}));
}
#[cfg(test)]
@@ -1085,7 +1334,14 @@ async fn hydrate_yellowstone_signal(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fetched = fetch_yellowstone_hydration(source.http_pool.clone(), source.hydration_role.clone(), source.network.clone(), key, commitment).await;
let fetched = fetch_yellowstone_hydration(
source.http_pool.clone(),
source.hydration_role.clone(),
source.network.clone(),
key,
commitment,
)
.await;
let fetched = match fetched {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -1135,12 +1391,17 @@ fn build_hydration_provenance(
std::result::Result::Err(error) => return std::result::Result::Err(error),
},
};
let mut provenance =
ksp_store_lib::RawAcquisitionProvenance::new(provider, protocol, acquisition_method, ksp_store_lib::RawAcquisitionOrigin::Live, received_at)
.with_endpoint_id(endpoint_id)
.with_commitment(commitment)
.with_capture_session_id(capture_session)
.with_filter_id(filter_id);
let mut provenance = ksp_store_lib::RawAcquisitionProvenance::new(
provider,
protocol,
acquisition_method,
ksp_store_lib::RawAcquisitionOrigin::Live,
received_at,
)
.with_endpoint_id(endpoint_id)
.with_commitment(commitment)
.with_capture_session_id(capture_session)
.with_filter_id(filter_id);
if let std::option::Option::Some(observed_at) = representable_observed_at(signal.created_at, received_at) {
provenance = match provenance.try_with_observed_at(observed_at) {
std::result::Result::Ok(value) => value,

View File

@@ -1,10 +1,49 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/snapshot.rs
// version: 2
// version: 3
/// Runtime-neutral boxed future resolving to one newer concrete RAW transaction ingest Worker snapshot.
pub type RawTransactionIngestSnapshotFuture<'a> =
std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = crate::RawTransactionIngestSnapshot> + std::marker::Send + 'a>>;
/// Private latest-value processing-frontier projection emitted by the productive source task.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RawTransactionIngestProcessingFrontierProjection {
hydration_pending: usize,
processing_frontier_slot: std::option::Option<u64>,
oldest_pending_slot: std::option::Option<u64>,
}
impl crate::RawTransactionIngestProcessingFrontierProjection {
/// Returns the empty run-local processing projection used before the source observes work.
pub(crate) const fn empty() -> Self {
return Self { hydration_pending: 0, processing_frontier_slot: std::option::Option::None, oldest_pending_slot: std::option::Option::None };
}
/// Creates one run-local processing projection from bounded source-owned state.
pub(crate) const fn new(
hydration_pending: usize,
processing_frontier_slot: std::option::Option<u64>,
oldest_pending_slot: std::option::Option<u64>,
) -> Self {
return Self { hydration_pending, processing_frontier_slot, oldest_pending_slot };
}
/// Returns the number of source signals still pending hydration/admission processing.
pub(crate) const fn hydration_pending(&self) -> usize {
return self.hydration_pending;
}
/// Returns the highest actually observed slot not blocked by older pending source work.
pub(crate) const fn processing_frontier_slot(&self) -> std::option::Option<u64> {
return self.processing_frontier_slot;
}
/// Returns the oldest actually observed slot that still owns pending source work.
pub(crate) const fn oldest_pending_slot(&self) -> std::option::Option<u64> {
return self.oldest_pending_slot;
}
}
/// Complete safe latest-value snapshot of one continuous RAW transaction ingest Worker.
#[derive(Clone, Eq, PartialEq)]
pub struct RawTransactionIngestSnapshot {
@@ -25,6 +64,9 @@ pub struct RawTransactionIngestSnapshot {
store_failure_total: u64,
source_failure_total: u64,
backpressure_wait_total: u64,
hydration_pending: usize,
processing_frontier_slot: std::option::Option<u64>,
oldest_pending_slot: std::option::Option<u64>,
}
impl crate::RawTransactionIngestSnapshot {
@@ -129,6 +171,24 @@ impl crate::RawTransactionIngestSnapshot {
pub const fn backpressure_wait_total(&self) -> u64 {
return self.backpressure_wait_total;
}
/// Returns the latest number of source signals pending hydration/admission processing.
#[must_use]
pub const fn hydration_pending(&self) -> usize {
return self.hydration_pending;
}
/// Returns the run-local processing frontier over actually observed source work.
#[must_use]
pub const fn processing_frontier_slot(&self) -> std::option::Option<u64> {
return self.processing_frontier_slot;
}
/// Returns the oldest actually observed slot that still owns pending source work.
#[must_use]
pub const fn oldest_pending_slot(&self) -> std::option::Option<u64> {
return self.oldest_pending_slot;
}
}
impl std::fmt::Debug for crate::RawTransactionIngestSnapshot {
@@ -152,6 +212,9 @@ impl std::fmt::Debug for crate::RawTransactionIngestSnapshot {
.field("store_failure_total", &self.store_failure_total)
.field("source_failure_total", &self.source_failure_total)
.field("backpressure_wait_total", &self.backpressure_wait_total)
.field("hydration_pending", &self.hydration_pending)
.field("processing_frontier_slot", &self.processing_frontier_slot)
.field("oldest_pending_slot", &self.oldest_pending_slot)
.finish();
}
}
@@ -265,6 +328,9 @@ impl crate::RawTransactionIngestSnapshotPublisher {
store_failure_total: 0,
source_failure_total: 0,
backpressure_wait_total: 0,
hydration_pending: 0,
processing_frontier_slot: std::option::Option::None,
oldest_pending_slot: std::option::Option::None,
};
let (sender, receiver) = tokio::sync::watch::channel(snapshot.clone());
return (Self { sender, snapshot }, crate::RawTransactionIngestSnapshotSource { receiver });
@@ -346,6 +412,20 @@ impl crate::RawTransactionIngestSnapshotPublisher {
return self.publish(state, admission_queue_depth, in_flight_persistence);
}
/// Publishes one latest run-local processing-frontier projection emitted by the source task.
pub(crate) fn record_processing_frontier(
&mut self,
state: ksp_worker_api::WorkerState,
admission_queue_depth: usize,
in_flight_persistence: usize,
projection: crate::RawTransactionIngestProcessingFrontierProjection,
) -> ksp_core_lib::Result<()> {
self.snapshot.hydration_pending = projection.hydration_pending();
self.snapshot.processing_frontier_slot = projection.processing_frontier_slot();
self.snapshot.oldest_pending_slot = projection.oldest_pending_slot();
return self.publish(state, admission_queue_depth, in_flight_persistence);
}
/// Records one failed private source task without retaining provider-specific error material.
pub(crate) fn record_source_failure(
&mut self,
@@ -457,7 +537,7 @@ impl crate::RawTransactionIngestSnapshotPublisher {
sequence,
state,
health_for_state(state, self.snapshot.worker.health()),
activity_for_state(state, admission_queue_depth, in_flight_persistence),
activity_for_state(state, admission_queue_depth, in_flight_persistence, self.snapshot.hydration_pending),
);
self.snapshot.worker = worker;
self.snapshot.admission_queue_depth = admission_queue_depth;
@@ -467,8 +547,13 @@ impl crate::RawTransactionIngestSnapshotPublisher {
}
}
fn activity_for_state(state: ksp_worker_api::WorkerState, admission_queue_depth: usize, in_flight_persistence: usize) -> ksp_worker_api::WorkerActivity {
if admission_queue_depth > 0 || in_flight_persistence > 0 {
fn activity_for_state(
state: ksp_worker_api::WorkerState,
admission_queue_depth: usize,
in_flight_persistence: usize,
hydration_pending: usize,
) -> ksp_worker_api::WorkerActivity {
if admission_queue_depth > 0 || in_flight_persistence > 0 || hydration_pending > 0 {
return ksp_worker_api::WorkerActivity::Active;
}
return match state {