v0.3.6-pre.007
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
// file: crates/ksp-job-backfill-lib/src/error.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Error code used when a signature page violates a bounded discovery invariant.
|
||||
pub const ERROR_CODE_BACKFILL_DISCOVERY_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_backfill", "discovery_invalid");
|
||||
/// Error code used when paginated discovery cannot advance its exclusive RPC cursor safely.
|
||||
pub const ERROR_CODE_BACKFILL_DISCOVERY_STALLED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_backfill", "discovery_stalled");
|
||||
/// Error code used when Store persistence returns an impossible Backfill state or targets a different network.
|
||||
pub const ERROR_CODE_BACKFILL_PERSISTENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_backfill", "persistence_invalid");
|
||||
/// Error code used when deterministic Transport-to-RAW conversion violates the v1 contract.
|
||||
pub const ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_backfill", "raw_conversion_invalid");
|
||||
/// Error code used when one Backfill request violates its bounded admission contract.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -10,13 +10,14 @@
|
||||
//! This tranche owns explicit admission, network-scoped candidate identity, deterministic
|
||||
//! `getSignaturesForAddress` pagination and canonical RAW v1 conversion through observed
|
||||
//! `getTransaction`. Transport retains provider/endpoint selection and retry; Store retains
|
||||
//! durable idempotence and persistence. Persistence, concurrency, checkpointing, cancellation
|
||||
//! durable idempotence through the atomic Store facade. Concurrency, checkpointing, cancellation
|
||||
//! and concrete latest-value snapshots are added by later v0.3.6 tranches.
|
||||
|
||||
mod constants;
|
||||
mod conversion;
|
||||
mod discovery;
|
||||
mod error;
|
||||
mod persistence;
|
||||
mod request;
|
||||
|
||||
/// Result of hydrating one deterministic candidate through observed `getTransaction`.
|
||||
@@ -43,12 +44,22 @@ pub use self::discovery::discover_backfill_candidates;
|
||||
pub use self::error::ERROR_CODE_BACKFILL_DISCOVERY_INVALID;
|
||||
/// Error code used when paginated discovery cannot advance its exclusive RPC cursor safely.
|
||||
pub use self::error::ERROR_CODE_BACKFILL_DISCOVERY_STALLED;
|
||||
/// Error code used when Store persistence returns an impossible Backfill state or targets a different network.
|
||||
pub use self::error::ERROR_CODE_BACKFILL_PERSISTENCE_INVALID;
|
||||
/// Error code used when deterministic Transport-to-RAW conversion violates the v1 contract.
|
||||
pub use self::error::ERROR_CODE_BACKFILL_RAW_CONVERSION_INVALID;
|
||||
/// Error code used when one Backfill request violates its bounded admission contract.
|
||||
pub use self::error::ERROR_CODE_BACKFILL_REQUEST_INVALID;
|
||||
/// Error code used when one transaction signature text violates the bounded Base58-shape contract.
|
||||
pub use self::error::ERROR_CODE_BACKFILL_SIGNATURE_INVALID;
|
||||
/// Canonical entity disposition produced by one Backfill Store persistence attempt.
|
||||
pub use self::persistence::BackfillEntityPersistence;
|
||||
/// Observation disposition produced by one Backfill Store persistence attempt.
|
||||
pub use self::persistence::BackfillObservationPersistence;
|
||||
/// Stable Backfill projection of one hydration persistence result.
|
||||
pub use self::persistence::BackfillPersistenceOutcome;
|
||||
/// Persists one hydrated result through the backend-neutral atomic Store contract.
|
||||
pub use self::persistence::persist_backfill_hydration;
|
||||
/// Commitment levels intentionally admitted by the historical Backfill vertical.
|
||||
pub use self::request::BackfillCommitment;
|
||||
/// Fully explicit bounded request for one historical transaction Backfill Job.
|
||||
|
||||
201
crates/ksp-job-backfill-lib/src/persistence.rs
Normal file
201
crates/ksp-job-backfill-lib/src/persistence.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
// file: crates/ksp-job-backfill-lib/src/persistence.rs
|
||||
// version: 1
|
||||
|
||||
/// Canonical entity disposition produced by one Backfill Store persistence attempt.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum BackfillEntityPersistence {
|
||||
/// The canonical RAW transaction was inserted for the first time.
|
||||
Inserted,
|
||||
/// Identical canonical RAW transaction content was already durable.
|
||||
AlreadyPresent,
|
||||
/// A durable purge tombstone prevented normal Backfill rehydration.
|
||||
SkippedPurged,
|
||||
/// `getTransaction` returned JSON `null`, so no canonical Store write was attempted.
|
||||
Missing,
|
||||
/// Store reported divergent canonical content for the same network-scoped transaction identity.
|
||||
Conflict,
|
||||
}
|
||||
|
||||
/// Observation disposition produced by one Backfill Store persistence attempt.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum BackfillObservationPersistence {
|
||||
/// The deterministic acquisition observation was inserted for the first time.
|
||||
Inserted,
|
||||
/// The same deterministic acquisition observation was already durable.
|
||||
AlreadyPresent,
|
||||
/// Store intentionally recorded no observation because normal persistence skipped a purged entity or detected a conflict.
|
||||
NotRecorded,
|
||||
/// No observation existed because hydration returned `Missing` before persistence.
|
||||
NotApplicable,
|
||||
}
|
||||
|
||||
/// Stable Backfill projection of one hydration persistence result.
|
||||
///
|
||||
/// The canonical transaction identity remains `(network, signature)`. Provider, endpoint and
|
||||
/// transport details can affect the persisted observation but never the transaction identity.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct BackfillPersistenceOutcome {
|
||||
reference: ksp_store_lib::RawTransactionReference,
|
||||
entity: BackfillEntityPersistence,
|
||||
observation: BackfillObservationPersistence,
|
||||
}
|
||||
|
||||
impl BackfillPersistenceOutcome {
|
||||
/// Returns the network-scoped canonical transaction identity classified by this result.
|
||||
#[must_use]
|
||||
pub fn reference(&self) -> &ksp_store_lib::RawTransactionReference {
|
||||
return &self.reference;
|
||||
}
|
||||
|
||||
/// Returns the canonical transaction persistence disposition.
|
||||
#[must_use]
|
||||
pub const fn entity(&self) -> BackfillEntityPersistence {
|
||||
return self.entity;
|
||||
}
|
||||
|
||||
/// Returns the acquisition-observation persistence disposition.
|
||||
#[must_use]
|
||||
pub const fn observation(&self) -> BackfillObservationPersistence {
|
||||
return self.observation;
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists one hydrated Backfill result through the backend-neutral Store facade.
|
||||
///
|
||||
/// Available acquisitions use the existing atomic transaction-plus-observation Store contract in
|
||||
/// `Normal` mode. A durable purge is therefore respected and never rehydrated implicitly.
|
||||
/// `Missing` performs no Store write. Stable Store content conflicts are projected explicitly as
|
||||
/// [`BackfillEntityPersistence::Conflict`] rather than being silently treated as idempotent skips.
|
||||
/// Other Store failures remain errors.
|
||||
pub async fn persist_backfill_hydration(
|
||||
store: &ksp_store_lib::Store,
|
||||
hydration: crate::BackfillHydrationOutcome,
|
||||
) -> ksp_core_lib::Result<BackfillPersistenceOutcome> {
|
||||
return persist_hydration_with_port(store, hydration).await;
|
||||
}
|
||||
|
||||
trait RawTransactionPersistencePort: std::marker::Send + std::marker::Sync {
|
||||
fn network_matches(&self, network: &ksp_store_lib::RawNetworkId) -> bool;
|
||||
|
||||
fn persist_acquisition<'a>(
|
||||
&'a self,
|
||||
transaction: ksp_store_lib::RawTransaction,
|
||||
observation: ksp_store_lib::RawTransactionObservation,
|
||||
mode: ksp_store_lib::RawTransactionAcquisitionMode,
|
||||
) -> ksp_store_lib::StoreApiFuture<'a, ksp_store_lib::Result<ksp_store_lib::RawAcquisitionWriteOutcome>>;
|
||||
}
|
||||
|
||||
impl RawTransactionPersistencePort for ksp_store_lib::Store {
|
||||
fn network_matches(&self, network: &ksp_store_lib::RawNetworkId) -> bool {
|
||||
let snapshot = self.runtime_snapshot();
|
||||
return snapshot.network() == network;
|
||||
}
|
||||
|
||||
fn persist_acquisition<'a>(
|
||||
&'a self,
|
||||
transaction: ksp_store_lib::RawTransaction,
|
||||
observation: ksp_store_lib::RawTransactionObservation,
|
||||
mode: ksp_store_lib::RawTransactionAcquisitionMode,
|
||||
) -> 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);
|
||||
}
|
||||
}
|
||||
|
||||
async fn persist_hydration_with_port<P>(port: &P, hydration: crate::BackfillHydrationOutcome) -> ksp_core_lib::Result<BackfillPersistenceOutcome>
|
||||
where
|
||||
P: RawTransactionPersistencePort,
|
||||
{
|
||||
let reference = hydration.reference().clone();
|
||||
if !port.network_matches(reference.network()) {
|
||||
return std::result::Result::Err(persistence_error("store.network"));
|
||||
}
|
||||
return match hydration {
|
||||
crate::BackfillHydrationOutcome::Missing(_) => std::result::Result::Ok(BackfillPersistenceOutcome {
|
||||
reference,
|
||||
entity: BackfillEntityPersistence::Missing,
|
||||
observation: BackfillObservationPersistence::NotApplicable,
|
||||
}),
|
||||
crate::BackfillHydrationOutcome::Available(acquisition) => {
|
||||
let (transaction, observation) = acquisition.into_parts();
|
||||
persist_available_with_port(port, reference, transaction, observation).await
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async fn persist_available_with_port<P>(
|
||||
port: &P,
|
||||
reference: ksp_store_lib::RawTransactionReference,
|
||||
transaction: ksp_store_lib::RawTransaction,
|
||||
observation: ksp_store_lib::RawTransactionObservation,
|
||||
) -> ksp_core_lib::Result<BackfillPersistenceOutcome>
|
||||
where
|
||||
P: RawTransactionPersistencePort,
|
||||
{
|
||||
if !port.network_matches(reference.network()) {
|
||||
return std::result::Result::Err(persistence_error("store.network"));
|
||||
}
|
||||
if transaction.reference() != &reference || observation.transaction() != &reference {
|
||||
return std::result::Result::Err(persistence_error("acquisition.reference"));
|
||||
}
|
||||
let result = port.persist_acquisition(transaction, observation, ksp_store_lib::RawTransactionAcquisitionMode::Normal).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(outcome) => map_store_outcome(reference, outcome),
|
||||
std::result::Result::Err(error) => {
|
||||
if error.code() == ksp_store_lib::ERROR_CODE_RAW_CONFLICT {
|
||||
return std::result::Result::Ok(BackfillPersistenceOutcome {
|
||||
reference,
|
||||
entity: BackfillEntityPersistence::Conflict,
|
||||
observation: BackfillObservationPersistence::NotRecorded,
|
||||
});
|
||||
}
|
||||
std::result::Result::Err(error)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn map_store_outcome(
|
||||
reference: ksp_store_lib::RawTransactionReference,
|
||||
outcome: ksp_store_lib::RawAcquisitionWriteOutcome,
|
||||
) -> ksp_core_lib::Result<BackfillPersistenceOutcome> {
|
||||
let entity = outcome.entity();
|
||||
let observation = outcome.observation();
|
||||
if entity == ksp_store_lib::RawEntityWriteOutcome::Inserted && observation == ksp_store_lib::RawObservationWriteOutcome::Inserted {
|
||||
return std::result::Result::Ok(BackfillPersistenceOutcome {
|
||||
reference,
|
||||
entity: BackfillEntityPersistence::Inserted,
|
||||
observation: BackfillObservationPersistence::Inserted,
|
||||
});
|
||||
}
|
||||
if entity == ksp_store_lib::RawEntityWriteOutcome::AlreadyPresent && observation == ksp_store_lib::RawObservationWriteOutcome::Inserted {
|
||||
return std::result::Result::Ok(BackfillPersistenceOutcome {
|
||||
reference,
|
||||
entity: BackfillEntityPersistence::AlreadyPresent,
|
||||
observation: BackfillObservationPersistence::Inserted,
|
||||
});
|
||||
}
|
||||
if entity == ksp_store_lib::RawEntityWriteOutcome::AlreadyPresent && observation == ksp_store_lib::RawObservationWriteOutcome::AlreadyPresent {
|
||||
return std::result::Result::Ok(BackfillPersistenceOutcome {
|
||||
reference,
|
||||
entity: BackfillEntityPersistence::AlreadyPresent,
|
||||
observation: BackfillObservationPersistence::AlreadyPresent,
|
||||
});
|
||||
}
|
||||
if entity == ksp_store_lib::RawEntityWriteOutcome::SkippedPurged && observation == ksp_store_lib::RawObservationWriteOutcome::NotRecorded {
|
||||
return std::result::Result::Ok(BackfillPersistenceOutcome {
|
||||
reference,
|
||||
entity: BackfillEntityPersistence::SkippedPurged,
|
||||
observation: BackfillObservationPersistence::NotRecorded,
|
||||
});
|
||||
}
|
||||
return std::result::Result::Err(persistence_error("store.outcome"));
|
||||
}
|
||||
|
||||
fn persistence_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_BACKFILL_PERSISTENCE_INVALID, "invalid Backfill Store persistence state").with_context("field", field);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/persistence.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user