v0.3.13-pre.008

This commit is contained in:
2026-09-10 17:52:35 +02:00
parent 8bb530e9b4
commit 1f2720463b
18 changed files with 899 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/persistence.rs
// version: 1
// version: 2
/// Canonical entity disposition produced by one Worker Store persistence attempt.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -56,6 +56,12 @@ pub(crate) trait RawTransactionIngestPersistencePort: std::marker::Send + std::m
observation: ksp_store_lib::RawTransactionObservation,
mode: ksp_store_lib::RawTransactionAcquisitionMode,
) -> ksp_store_lib::StoreApiFuture<'a, ksp_store_lib::Result<ksp_store_lib::RawAcquisitionWriteOutcome>>;
/// Records one additional observation for an already durable canonical RAW transaction.
fn record_observation<'a>(
&'a self,
observation: ksp_store_lib::RawTransactionObservation,
) -> ksp_store_lib::StoreApiFuture<'a, ksp_store_lib::Result<ksp_store_lib::RawObservationWriteOutcome>>;
}
impl crate::RawTransactionIngestPersistencePort for ksp_store_lib::Store {
@@ -71,6 +77,65 @@ impl crate::RawTransactionIngestPersistencePort for ksp_store_lib::Store {
) -> ksp_store_lib::StoreApiFuture<'a, ksp_store_lib::Result<ksp_store_lib::RawAcquisitionWriteOutcome>> {
return ksp_store_lib::RawTransactionWrite::persist_raw_transaction_acquisition(self, transaction, observation, mode);
}
fn record_observation<'a>(
&'a self,
observation: ksp_store_lib::RawTransactionObservation,
) -> ksp_store_lib::StoreApiFuture<'a, ksp_store_lib::Result<ksp_store_lib::RawObservationWriteOutcome>> {
return ksp_store_lib::RawTransactionObservationWrite::record_raw_transaction_observation(self, observation);
}
}
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
struct RawTransactionIngestPersistenceKey {
network: ksp_store_lib::RawNetworkId,
signature: ksp_store_lib::RawTransactionSignature,
}
/// Private bounded run-local cache serializing repeated canonical identities before Store writes.
pub(crate) struct RawTransactionIngestPersistenceConvergence {
max_entries: usize,
entries: std::sync::Mutex<
std::collections::BTreeMap<RawTransactionIngestPersistenceKey, std::sync::Arc<tokio::sync::Mutex<std::option::Option<ksp_store_lib::RawContentHash>>>>,
>,
}
impl crate::RawTransactionIngestPersistenceConvergence {
/// Creates one bounded run-local convergence cache using the caller-provided effective runtime bound.
#[must_use]
pub(crate) fn new(max_entries: usize) -> Self {
return Self { max_entries, entries: std::sync::Mutex::new(std::collections::BTreeMap::new()) };
}
fn entry(
&self,
key: RawTransactionIngestPersistenceKey,
) -> std::option::Option<std::sync::Arc<tokio::sync::Mutex<std::option::Option<ksp_store_lib::RawContentHash>>>> {
let mut entries = match self.entries.lock() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(poisoned) => poisoned.into_inner(),
};
if let std::option::Option::Some(entry) = entries.get(&key) {
return std::option::Option::Some(std::sync::Arc::clone(entry));
}
if entries.len() >= self.max_entries {
let removable = entries.iter().find_map(|(candidate, entry)| {
if std::sync::Arc::strong_count(entry) == 1 {
return std::option::Option::Some(candidate.clone());
}
return std::option::Option::None;
});
if let std::option::Option::Some(removable) = removable {
entries.remove(&removable);
}
}
if entries.len() >= self.max_entries {
return std::option::Option::None;
}
let entry = std::sync::Arc::new(tokio::sync::Mutex::new(std::option::Option::None));
entries.insert(key, std::sync::Arc::clone(&entry));
return std::option::Option::Some(entry);
}
}
/// Persists one already-canonical Worker acquisition through the private Store port in `Normal` mode.
@@ -96,6 +161,52 @@ where
};
}
/// Persists one canonical acquisition through the bounded cross-source convergence cache.
pub(crate) async fn persist_raw_transaction_ingest_converged_acquisition<P>(
port: &P,
acquisition: ksp_raw_transaction_lib::RawTransactionAcquisition,
convergence: &crate::RawTransactionIngestPersistenceConvergence,
) -> ksp_core_lib::Result<crate::RawTransactionIngestPersistenceOutcome>
where
P: crate::RawTransactionIngestPersistencePort + ?Sized,
{
let reference = acquisition.transaction().reference().clone();
if !port.network_matches(reference.network()) {
return std::result::Result::Err(crate::runtime_error("persistence.store_network_mismatch"));
}
if acquisition.observation().transaction() != &reference {
return std::result::Result::Err(crate::runtime_error("persistence.acquisition_reference_mismatch"));
}
let content_hash = acquisition.transaction().payload().content_hash();
let key = RawTransactionIngestPersistenceKey { network: reference.network().clone(), signature: reference.signature() };
let entry = convergence.entry(key);
let entry = match entry {
std::option::Option::Some(value) => value,
std::option::Option::None => return persist_raw_transaction_ingest_acquisition(port, acquisition).await,
};
let mut known_hash = entry.lock().await;
if let std::option::Option::Some(known_hash_value) = *known_hash {
if known_hash_value != content_hash {
return std::result::Result::Err(crate::content_conflict_error());
}
let (_transaction, observation) = acquisition.into_parts();
let result = port.record_observation(observation).await;
return match result {
std::result::Result::Ok(outcome) => map_additional_observation_outcome(outcome),
std::result::Result::Err(error) => map_store_error(error),
};
}
let outcome = persist_raw_transaction_ingest_acquisition(port, acquisition).await;
let outcome = match outcome {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if outcome.entity() != crate::RawTransactionIngestEntityPersistence::SkippedPurged {
*known_hash = std::option::Option::Some(content_hash);
}
return std::result::Result::Ok(outcome);
}
fn map_store_error(error: ksp_core_lib::Error) -> ksp_core_lib::Result<crate::RawTransactionIngestPersistenceOutcome> {
if error.code() == ksp_store_lib::ERROR_CODE_RAW_CONFLICT {
return std::result::Result::Err(crate::content_conflict_error());
@@ -133,6 +244,24 @@ fn map_store_outcome(outcome: ksp_store_lib::RawAcquisitionWriteOutcome) -> ksp_
return std::result::Result::Err(crate::runtime_error("persistence.store_outcome_invalid"));
}
fn map_additional_observation_outcome(
outcome: ksp_store_lib::RawObservationWriteOutcome,
) -> ksp_core_lib::Result<crate::RawTransactionIngestPersistenceOutcome> {
return match outcome {
ksp_store_lib::RawObservationWriteOutcome::Inserted => std::result::Result::Ok(crate::RawTransactionIngestPersistenceOutcome {
entity: crate::RawTransactionIngestEntityPersistence::AlreadyPresent,
observation: crate::RawTransactionIngestObservationPersistence::Inserted,
}),
ksp_store_lib::RawObservationWriteOutcome::AlreadyPresent => std::result::Result::Ok(crate::RawTransactionIngestPersistenceOutcome {
entity: crate::RawTransactionIngestEntityPersistence::AlreadyPresent,
observation: crate::RawTransactionIngestObservationPersistence::AlreadyPresent,
}),
ksp_store_lib::RawObservationWriteOutcome::NotRecorded => {
std::result::Result::Err(crate::runtime_error("persistence.additional_observation_not_recorded"))
},
};
}
#[cfg(test)]
#[path = "../unit_tests/persistence.rs"]
mod tests;