0.3.16-pre.008
This commit is contained in:
@@ -6,7 +6,7 @@ resolver = "3"
|
|||||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.16-pre.7.fix.3"
|
version = "0.3.16-pre.8"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-job-backfill-lib/unit_tests/persistence.rs
|
// file: crates/ksp-job-backfill-lib/unit_tests/persistence.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum FakeResponse {
|
enum FakeResponse {
|
||||||
@@ -299,6 +299,31 @@ async fn v0_3_16_pre_007_durable_quarantined_conflict_preserves_observation_proj
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn v0_3_16_pre_008_reobserved_quarantined_conflict_preserves_idempotent_observation() {
|
||||||
|
let (reference, transaction, observation) = match raw_acquisition_parts("devnet", 6, 18) {
|
||||||
|
std::option::Option::Some(parts) => parts,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let network = match raw_network("devnet") {
|
||||||
|
std::option::Option::Some(network) => network,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let response = store_variant_outcome(
|
||||||
|
ksp_store_lib::RawEntityWriteOutcome::AlreadyPresent,
|
||||||
|
ksp_store_lib::RawObservationWriteOutcome::AlreadyPresent,
|
||||||
|
ksp_store_lib::RawTransactionVariantWriteOutcome::QuarantinedConflict,
|
||||||
|
);
|
||||||
|
let port = FakePersistencePort::new(network, &[FakeResponse::Outcome(response)]);
|
||||||
|
let result = super::persist_available_with_port(&port, reference, transaction, observation).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
if let std::result::Result::Ok(result) = result {
|
||||||
|
assert_eq!(result.entity(), crate::BackfillEntityPersistence::Conflict);
|
||||||
|
assert_eq!(result.observation(), crate::BackfillObservationPersistence::AlreadyPresent);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pre_007_legacy_store_content_conflict_error_is_explicit_and_not_idempotent_success() {
|
async fn pre_007_legacy_store_content_conflict_error_is_explicit_and_not_idempotent_success() {
|
||||||
let (reference, transaction, observation) = match raw_acquisition_parts("devnet", 6, 16) {
|
let (reference, transaction, observation) = match raw_acquisition_parts("devnet", 6, 16) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||||
// version: 29
|
// version: 30
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -284,6 +284,9 @@ fn pre_009_live_raw_transaction_proof_is_opt_in_isolated_and_secret_safe() {
|
|||||||
"prove_schema_update_policy",
|
"prove_schema_update_policy",
|
||||||
"prove_concurrent_identical_insert",
|
"prove_concurrent_identical_insert",
|
||||||
"prove_concurrent_divergent_insert",
|
"prove_concurrent_divergent_insert",
|
||||||
|
"prove_conflict_case_rollback",
|
||||||
|
"QuarantinedConflict",
|
||||||
|
"conflict_state_is_exact",
|
||||||
"prove_pagination",
|
"prove_pagination",
|
||||||
"prove_inspection",
|
"prove_inspection",
|
||||||
"prove_retention_and_rehydrate",
|
"prove_retention_and_rehydrate",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
|
// file: crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -12,6 +12,68 @@
|
|||||||
//! drops only the isolated schema it proved absent before the run.
|
//! drops only the isolated schema it proved absent before the run.
|
||||||
|
|
||||||
const LIVE_CANCEL_WAIT: std::time::Duration = std::time::Duration::from_millis(300);
|
const LIVE_CANCEL_WAIT: std::time::Duration = std::time::Duration::from_millis(300);
|
||||||
|
const LIVE_CONFLICT_STATE_SQL: &str = r#"SELECT
|
||||||
|
(SELECT COUNT(*) = 2 FROM ksp_raw_transaction_variants WHERE transaction_signature = $1)
|
||||||
|
AND (SELECT COUNT(*) = 1 FROM ksp_raw_transaction_canonical_selectors WHERE transaction_signature = $1)
|
||||||
|
AND (SELECT COUNT(*) = 2 FROM ksp_raw_transaction_observations WHERE transaction_signature = $1)
|
||||||
|
AND (SELECT COUNT(*) = 2 FROM ksp_raw_transaction_observation_variants WHERE transaction_signature = $1)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ksp_raw_transaction_conflicts AS conflict
|
||||||
|
INNER JOIN ksp_raw_transaction_canonical_selectors AS selector
|
||||||
|
ON selector.transaction_signature = conflict.transaction_signature
|
||||||
|
WHERE conflict.transaction_signature = $1
|
||||||
|
AND conflict.status = 'open'
|
||||||
|
AND conflict.revision = 1
|
||||||
|
AND conflict.canonical_variant_id = selector.canonical_variant_id
|
||||||
|
AND conflict.incoming_variant_id <> conflict.canonical_variant_id
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ksp_raw_transactions AS canonical
|
||||||
|
INNER JOIN ksp_raw_transaction_canonical_selectors AS selector
|
||||||
|
ON selector.transaction_signature = canonical.signature
|
||||||
|
INNER JOIN ksp_raw_transaction_variants AS variant
|
||||||
|
ON variant.transaction_signature = selector.transaction_signature
|
||||||
|
AND variant.variant_id = selector.canonical_variant_id
|
||||||
|
WHERE canonical.signature = $1
|
||||||
|
AND variant.slot = canonical.slot
|
||||||
|
AND variant.block_time_unix_millis IS NOT DISTINCT FROM canonical.block_time_unix_millis
|
||||||
|
AND variant.format_id = canonical.format_id
|
||||||
|
AND variant.format_version = canonical.format_version
|
||||||
|
AND variant.content_hash = canonical.content_hash
|
||||||
|
AND variant.payload = canonical.payload
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM ksp_raw_transaction_observation_variants
|
||||||
|
WHERE observation_key = $2 AND transaction_signature = $1
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM ksp_raw_transaction_observation_variants
|
||||||
|
WHERE observation_key = $3 AND transaction_signature = $1
|
||||||
|
)"#;
|
||||||
|
const LIVE_CONFLICT_ROLLBACK_STATE_SQL: &str = r#"SELECT
|
||||||
|
(SELECT COUNT(*) = 1 FROM ksp_raw_transaction_variants WHERE transaction_signature = $1)
|
||||||
|
AND (SELECT COUNT(*) = 1 FROM ksp_raw_transaction_canonical_selectors WHERE transaction_signature = $1)
|
||||||
|
AND (SELECT COUNT(*) = 0 FROM ksp_raw_transaction_conflicts WHERE transaction_signature = $1)
|
||||||
|
AND (SELECT COUNT(*) = 1 FROM ksp_raw_transaction_observations WHERE transaction_signature = $1)
|
||||||
|
AND (SELECT COUNT(*) = 1 FROM ksp_raw_transaction_observation_variants WHERE transaction_signature = $1)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ksp_raw_transactions AS canonical
|
||||||
|
INNER JOIN ksp_raw_transaction_canonical_selectors AS selector
|
||||||
|
ON selector.transaction_signature = canonical.signature
|
||||||
|
INNER JOIN ksp_raw_transaction_variants AS variant
|
||||||
|
ON variant.transaction_signature = selector.transaction_signature
|
||||||
|
AND variant.variant_id = selector.canonical_variant_id
|
||||||
|
WHERE canonical.signature = $1
|
||||||
|
AND variant.slot = canonical.slot
|
||||||
|
AND variant.block_time_unix_millis IS NOT DISTINCT FROM canonical.block_time_unix_millis
|
||||||
|
AND variant.format_id = canonical.format_id
|
||||||
|
AND variant.format_version = canonical.format_version
|
||||||
|
AND variant.content_hash = canonical.content_hash
|
||||||
|
AND variant.payload = canonical.payload
|
||||||
|
)"#;
|
||||||
const LIVE_INDEX_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
const LIVE_INDEX_EXISTS_SQL: &str = r#"SELECT EXISTS (
|
||||||
SELECT 1 FROM pg_indexes
|
SELECT 1 FROM pg_indexes
|
||||||
WHERE schemaname = current_schema()
|
WHERE schemaname = current_schema()
|
||||||
@@ -214,6 +276,10 @@ async fn run_raw_transaction_scenario(admin: &mut tokio_postgres::Client, uri: &
|
|||||||
if let std::result::Result::Err(error) = divergent_result {
|
if let std::result::Result::Err(error) = divergent_result {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
|
let conflict_rollback_result = prove_conflict_case_rollback(admin, uri).await;
|
||||||
|
if let std::result::Result::Err(error) = conflict_rollback_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
let race_result = prove_retention_races(uri).await;
|
let race_result = prove_retention_races(uri).await;
|
||||||
if let std::result::Result::Err(error) = race_result {
|
if let std::result::Result::Err(error) = race_result {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
@@ -414,23 +480,61 @@ async fn prove_concurrent_divergent_insert(admin: &tokio_postgres::Client, uri:
|
|||||||
};
|
};
|
||||||
let pair = [first_result, second_result];
|
let pair = [first_result, second_result];
|
||||||
let inserted = pair.iter().filter(|value| return matches_inserted(value)).count();
|
let inserted = pair.iter().filter(|value| return matches_inserted(value)).count();
|
||||||
let conflicts = pair
|
let quarantined = pair.iter().filter(|value| return matches_quarantined_conflict(value)).count();
|
||||||
.iter()
|
if inserted != 1 || quarantined != 1 {
|
||||||
.filter(|value| matches!(value, LivePersistResult::BackendError(ksp_store_postgres_lib::PostgresBackendErrorKind::Conflict)))
|
|
||||||
.count();
|
|
||||||
if inserted != 1 || conflicts != 1 {
|
|
||||||
return std::result::Result::Err(LiveFailure::new("concurrent_divergent_outcome"));
|
return std::result::Result::Err(LiveFailure::new("concurrent_divergent_outcome"));
|
||||||
}
|
}
|
||||||
|
let reference = match raw_reference(30) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
let first_key = ksp_store_api::RawObservationKey::new([31; 32]);
|
let first_key = ksp_store_api::RawObservationKey::new([31; 32]);
|
||||||
let second_key = ksp_store_api::RawObservationKey::new([32; 32]);
|
let second_key = ksp_store_api::RawObservationKey::new([32; 32]);
|
||||||
let first_exists = observation_exists(admin, &first_key).await;
|
let state = conflict_state_is_exact(admin, &reference, &first_key, &second_key).await;
|
||||||
let second_exists = observation_exists(admin, &second_key).await;
|
match state {
|
||||||
let count = match (first_exists, second_exists) {
|
std::result::Result::Ok(true) => {},
|
||||||
(std::result::Result::Ok(first_value), std::result::Result::Ok(second_value)) => usize::from(first_value) + usize::from(second_value),
|
std::result::Result::Ok(false) => return std::result::Result::Err(LiveFailure::new("concurrent_divergent_state")),
|
||||||
_ => return std::result::Result::Err(LiveFailure::new("concurrent_divergent_observation_probe")),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prove_conflict_case_rollback(admin: &tokio_postgres::Client, uri: &str) -> std::result::Result<(), LiveFailure> {
|
||||||
|
let seed = persist_once(uri.to_owned(), 82, 8_200, 82, 82).await;
|
||||||
|
match seed {
|
||||||
|
std::result::Result::Ok(LivePersistResult::Outcome(value)) if value.entity() == ksp_store_api::RawEntityWriteOutcome::Inserted => {},
|
||||||
|
_ => return std::result::Result::Err(LiveFailure::new("conflict_rollback_seed")),
|
||||||
|
}
|
||||||
|
let backend = match open_backend(uri, "devnet", true, true).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
if count != 1 {
|
let transaction = match raw_transaction(82, 8_200, 83) {
|
||||||
return std::result::Result::Err(LiveFailure::new("concurrent_divergent_rollback"));
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let observation = match raw_observation(10, 82, 83) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let result = backend.persist_raw_transaction_acquisition(transaction, observation, ksp_store_api::RawTransactionAcquisitionMode::Normal).await;
|
||||||
|
match result {
|
||||||
|
std::result::Result::Err(error) if error.kind() == ksp_store_postgres_lib::PostgresBackendErrorKind::Conflict => {},
|
||||||
|
_ => return std::result::Result::Err(LiveFailure::new("conflict_rollback_expected_failure")),
|
||||||
|
}
|
||||||
|
let close_result = close_backend(backend).await;
|
||||||
|
if let std::result::Result::Err(error) = close_result {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let reference = match raw_reference(82) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let state = conflict_rollback_state_is_exact(admin, &reference).await;
|
||||||
|
match state {
|
||||||
|
std::result::Result::Ok(true) => {},
|
||||||
|
std::result::Result::Ok(false) => return std::result::Result::Err(LiveFailure::new("conflict_rollback_state")),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
}
|
}
|
||||||
return std::result::Result::Ok(());
|
return std::result::Result::Ok(());
|
||||||
}
|
}
|
||||||
@@ -971,6 +1075,16 @@ fn matches_inserted(value: &LivePersistResult) -> bool {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matches_quarantined_conflict(value: &LivePersistResult) -> bool {
|
||||||
|
return matches!(
|
||||||
|
value,
|
||||||
|
LivePersistResult::Outcome(outcome)
|
||||||
|
if outcome.entity() == ksp_store_api::RawEntityWriteOutcome::AlreadyPresent
|
||||||
|
&& outcome.observation() == ksp_store_api::RawObservationWriteOutcome::Inserted
|
||||||
|
&& outcome.transaction_variant() == std::option::Option::Some(ksp_store_api::RawTransactionVariantWriteOutcome::QuarantinedConflict)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn matches_already_present(value: &LivePersistResult) -> bool {
|
fn matches_already_present(value: &LivePersistResult) -> bool {
|
||||||
return matches!(
|
return matches!(
|
||||||
value,
|
value,
|
||||||
@@ -1277,6 +1391,42 @@ async fn observation_exists(client: &tokio_postgres::Client, key: &ksp_store_api
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn conflict_state_is_exact(
|
||||||
|
client: &tokio_postgres::Client,
|
||||||
|
reference: &ksp_store_api::RawTransactionReference,
|
||||||
|
first_key: &ksp_store_api::RawObservationKey,
|
||||||
|
second_key: &ksp_store_api::RawObservationKey,
|
||||||
|
) -> std::result::Result<bool, LiveFailure> {
|
||||||
|
let signature = reference.signature();
|
||||||
|
let signature_bytes: &[u8] = signature.as_bytes();
|
||||||
|
let first_key_bytes: &[u8] = first_key.as_bytes();
|
||||||
|
let second_key_bytes: &[u8] = second_key.as_bytes();
|
||||||
|
let row = match client.query_one(LIVE_CONFLICT_STATE_SQL, &[&signature_bytes, &first_key_bytes, &second_key_bytes]).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("conflict_state_probe")),
|
||||||
|
};
|
||||||
|
return match row.try_get::<usize, bool>(0) {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("conflict_state_decode")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn conflict_rollback_state_is_exact(
|
||||||
|
client: &tokio_postgres::Client,
|
||||||
|
reference: &ksp_store_api::RawTransactionReference,
|
||||||
|
) -> std::result::Result<bool, LiveFailure> {
|
||||||
|
let signature = reference.signature();
|
||||||
|
let signature_bytes: &[u8] = signature.as_bytes();
|
||||||
|
let row = match client.query_one(LIVE_CONFLICT_ROLLBACK_STATE_SQL, &[&signature_bytes]).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return std::result::Result::Err(LiveFailure::new("conflict_rollback_state_probe")),
|
||||||
|
};
|
||||||
|
return match row.try_get::<usize, bool>(0) {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(LiveFailure::new("conflict_rollback_state_decode")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async fn variant_projection_is_exact(
|
async fn variant_projection_is_exact(
|
||||||
client: &tokio_postgres::Client,
|
client: &tokio_postgres::Client,
|
||||||
reference: &ksp_store_api::RawTransactionReference,
|
reference: &ksp_store_api::RawTransactionReference,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/v003_variant_persistence.rs
|
// file: crates/ksp-store-postgres-lib/tests/v003_variant_persistence.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -145,3 +145,50 @@ fn v0_3_16_pre_006_promotion_is_inside_acquisition_transaction_and_preserves_pre
|
|||||||
assert!(promotion.contains("next_transaction_canonical_revision"));
|
assert!(promotion.contains("next_transaction_canonical_revision"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v0_3_16_pre_008_conflict_path_never_mutates_canonical_projection_or_selector() {
|
||||||
|
let source = include_str!("../src/raw_transaction.rs");
|
||||||
|
let start = source.find("ExistingTransactionMatch::ActiveConflict(variant_comparison) => {");
|
||||||
|
let end = source.find("ExistingTransactionMatch::Purged => {");
|
||||||
|
let (start, end) = match (start, end) {
|
||||||
|
(std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => (start, end),
|
||||||
|
_ => panic!("pre.008 conflict branch markers are missing"),
|
||||||
|
};
|
||||||
|
let branch = &source[start..end];
|
||||||
|
assert!(branch.contains("persist_or_reuse_native_transaction_variant"));
|
||||||
|
assert!(branch.contains("open_or_update_raw_transaction_conflict"));
|
||||||
|
assert!(branch.contains("RawTransactionVariantWriteOutcome::QuarantinedConflict"));
|
||||||
|
assert!(!branch.contains("promote_more_complete_transaction_variant"));
|
||||||
|
assert!(!branch.contains("UPDATE_CANONICAL_TRANSACTION_PROJECTION_SQL"));
|
||||||
|
assert!(!branch.contains("UPDATE_TRANSACTION_VARIANT_SELECTOR_SQL"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v0_3_16_pre_008_variant_transition_helpers_cannot_commit_independently() {
|
||||||
|
let source = include_str!("../src/raw_transaction.rs");
|
||||||
|
let promotion_start = source.find("async fn promote_more_complete_transaction_variant(");
|
||||||
|
let promotion_end = source.find("async fn preserve_previous_canonical_variant(");
|
||||||
|
let conflict_start = source.find("async fn open_or_update_raw_transaction_conflict(");
|
||||||
|
let conflict_end = source.find("async fn promote_more_complete_transaction_variant(");
|
||||||
|
let (promotion_start, promotion_end, conflict_start, conflict_end) = match (promotion_start, promotion_end, conflict_start, conflict_end) {
|
||||||
|
(Some(promotion_start), Some(promotion_end), Some(conflict_start), Some(conflict_end))
|
||||||
|
if promotion_start < promotion_end && conflict_start < conflict_end =>
|
||||||
|
{
|
||||||
|
(promotion_start, promotion_end, conflict_start, conflict_end)
|
||||||
|
},
|
||||||
|
_ => panic!("pre.008 transition helper markers are missing"),
|
||||||
|
};
|
||||||
|
let promotion = &source[promotion_start..promotion_end];
|
||||||
|
let conflict = &source[conflict_start..conflict_end];
|
||||||
|
for helper in [promotion, conflict] {
|
||||||
|
assert!(helper.contains("&deadpool_postgres::Transaction<'_>"));
|
||||||
|
assert!(!helper.contains(".commit().await"));
|
||||||
|
}
|
||||||
|
assert!(source.contains("const LOCK_TRANSACTION_SQL: &str = \"SELECT"));
|
||||||
|
assert!(source.contains("WHERE signature = $1 FOR UPDATE\";"));
|
||||||
|
assert!(source.contains("canonical_variant_id = $2::TEXT::NUMERIC AND canonical_revision = $3::TEXT::NUMERIC"));
|
||||||
|
assert!(source.contains("AND revision = $8::TEXT::NUMERIC"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs
|
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs
|
||||||
// version: 14
|
// version: 15
|
||||||
|
|
||||||
struct ActiveTaskGuard {
|
struct ActiveTaskGuard {
|
||||||
active: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
active: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||||
@@ -526,6 +526,50 @@ async fn v0_3_16_pre_007_quarantined_conflict_keeps_worker_running_and_degraded(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn v0_3_16_pre_008_quarantined_conflict_does_not_block_later_identity() {
|
||||||
|
let settings = match settings_with_persistence_concurrency(1) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let network = settings.network().clone();
|
||||||
|
let port = std::sync::Arc::new(RuntimePersistencePort::new(network.clone(), RuntimePortResponse::QuarantinedConflict, true));
|
||||||
|
let runtime_port: super::PersistencePort = port.clone();
|
||||||
|
let handle = match super::start_foundation_with_port_and_source_spawner(
|
||||||
|
settings,
|
||||||
|
tokio::runtime::Handle::current(),
|
||||||
|
std::option::Option::Some(runtime_port),
|
||||||
|
move |children, _stop_receiver, admission_sender| {
|
||||||
|
let _abort_handle = children.spawn(async move {
|
||||||
|
for signature_byte in [20_u8, 21_u8] {
|
||||||
|
let ingress = match runtime_ingress(&network, signature_byte) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Err(crate::runtime_error("test.source_failed")),
|
||||||
|
};
|
||||||
|
if admission_sender.send(ingress).await.is_err() {
|
||||||
|
return std::result::Result::Err(crate::runtime_error("test.source_failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let source = handle.snapshot_source();
|
||||||
|
assert!(wait_for_completed(&port, 2).await);
|
||||||
|
let snapshot = source.current();
|
||||||
|
assert_eq!(snapshot.worker_snapshot().state(), ksp_worker_api::WorkerState::Running);
|
||||||
|
assert_eq!(snapshot.worker_snapshot().health(), ksp_worker_api::WorkerHealth::Degraded);
|
||||||
|
assert_eq!(snapshot.persisted_total(), 2);
|
||||||
|
assert_eq!(snapshot.content_conflict_total(), 2);
|
||||||
|
assert_eq!(snapshot.store_failure_total(), 0);
|
||||||
|
assert!(handle.request_stop());
|
||||||
|
assert!(matches!(handle.wait_terminal().await, std::result::Result::Ok(ksp_worker_api::WorkerState::Stopped)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn pre_007_legacy_store_conflict_error_becomes_terminal_after_private_drain() {
|
async fn pre_007_legacy_store_conflict_error_becomes_terminal_after_private_drain() {
|
||||||
let settings = match settings_with_persistence_concurrency(1) {
|
let settings = match settings_with_persistence_concurrency(1) {
|
||||||
|
|||||||
236
deltas/0.3.16/pre.008.md
Normal file
236
deltas/0.3.16/pre.008.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<!-- file: deltas/0.3.16/pre.008.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.16-pre.008` — hardening technique de la vertical slice variantes/conflits
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.16-pre.007-fix.003 appliqué
|
||||||
|
workspace.package.version = 0.3.16-pre.7.fix.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.007-fix.003` est entièrement propre : format, audits Rust/Markdown, `cargo check --workspace`, Clippy workspace/all-targets/all-features, Store API, Store façade, PostgreSQL, Job Backfill et l'intégralité des suites ciblées du Worker passent. `pre.007` est donc fermé avant ouverture de cette tranche.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
`pre.008` est la tranche de hardening technique finale de la vertical slice `0.3.16` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
concurrence même identité
|
||||||
|
rollback transactionnel promotion/conflit
|
||||||
|
cohérence projection V001 / selector V003
|
||||||
|
conservation du canonique en conflit
|
||||||
|
régressions Job Backfill / Worker
|
||||||
|
preuves live PostgreSQL opt-in actualisées
|
||||||
|
gate workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune nouvelle capacité fonctionnelle, résolution opérateur, retry Store, reconnexion Transport ou Store Desk n'est introduite.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.16-pre.8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hardening PostgreSQL statique
|
||||||
|
|
||||||
|
Les canaris V003 verrouillent désormais explicitement deux invariants supplémentaires :
|
||||||
|
|
||||||
|
1. la branche `ActiveConflict` persiste/réutilise la variante entrante et ouvre/complète le conflict case, mais n'appelle jamais la promotion canonique et ne touche ni `UPDATE_CANONICAL_TRANSACTION_PROJECTION_SQL` ni `UPDATE_TRANSACTION_VARIANT_SELECTOR_SQL` ;
|
||||||
|
2. les helpers de promotion et de conflict case reçoivent la transaction PostgreSQL de l'acquisition et ne peuvent jamais effectuer leur propre `COMMIT`.
|
||||||
|
|
||||||
|
Les gardes existantes restent exigées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
identity row -> FOR UPDATE
|
||||||
|
selector promotion -> expected canonical_variant_id + expected canonical_revision
|
||||||
|
conflict update -> expected revision
|
||||||
|
```
|
||||||
|
|
||||||
|
Ainsi, selector, projection V001, ledger V003, conflict case, observation et mapping restent bornés par la transaction d'acquisition externe.
|
||||||
|
|
||||||
|
## Preuve live PostgreSQL actualisée
|
||||||
|
|
||||||
|
Le test opt-in `postgres_raw_transaction_live.rs` est réaligné sur la sémantique `pre.007`.
|
||||||
|
|
||||||
|
Le scénario de divergence concurrente d'une même identité attend désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1 x InsertedCanonical
|
||||||
|
1 x QuarantinedConflict
|
||||||
|
2 variantes durables
|
||||||
|
2 observations durables
|
||||||
|
2 mappings observation -> variante
|
||||||
|
1 selector canonique
|
||||||
|
1 conflict case Open revision 1
|
||||||
|
projection V001 == variante désignée par le selector
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ordre d'arrivée n'est pas supposé : l'une ou l'autre acquisition peut devenir le premier canonique, mais la seconde doit être quarantinée sans écraser ce canonique.
|
||||||
|
|
||||||
|
### Rollback après ouverture réelle d'un conflit
|
||||||
|
|
||||||
|
Un nouveau scénario live force une erreur après les écritures de divergence mais avant le commit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
canonique existant
|
||||||
|
-> incoming divergent
|
||||||
|
-> variante entrante créée
|
||||||
|
-> conflict case créé
|
||||||
|
-> collision volontaire d'observation avec une autre identité
|
||||||
|
-> erreur avant COMMIT
|
||||||
|
-> rollback implicite de la transaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Après l'échec, la preuve exige exactement l'état antérieur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1 seule variante pour l'identité
|
||||||
|
1 selector
|
||||||
|
0 conflict case
|
||||||
|
1 observation de seed
|
||||||
|
1 mapping de seed
|
||||||
|
projection V001 toujours cohérente avec le selector
|
||||||
|
```
|
||||||
|
|
||||||
|
Cela couvre le risque qu'un conflict case ou une variante orpheline survive à un échec tardif de l'acquisition.
|
||||||
|
|
||||||
|
Le test live reste `#[ignore]`, lit uniquement un URI PostgreSQL dédié depuis stdin, refuse un schéma KSP préexistant et ne devient pas un gate automatique sans ressource opérateur explicite.
|
||||||
|
|
||||||
|
## Régression Job Backfill
|
||||||
|
|
||||||
|
Le Job Backfill ajoute un canari sur une réobservation déjà idempotente d'un conflit durable :
|
||||||
|
|
||||||
|
```text
|
||||||
|
entity = AlreadyPresent
|
||||||
|
observation = AlreadyPresent
|
||||||
|
variant = QuarantinedConflict
|
||||||
|
```
|
||||||
|
|
||||||
|
La projection doit rester :
|
||||||
|
|
||||||
|
```text
|
||||||
|
BackfillEntityPersistence::Conflict
|
||||||
|
BackfillObservationPersistence::AlreadyPresent
|
||||||
|
```
|
||||||
|
|
||||||
|
Le conflit durable n'est donc jamais aplati en simple `AlreadyPresent`, même lorsque l'observation est elle-même idempotente.
|
||||||
|
|
||||||
|
## Régression Worker
|
||||||
|
|
||||||
|
Le Worker ajoute une preuve runtime avec deux identités successives alors que le Store retourne `QuarantinedConflict` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
première identité -> QuarantinedConflict
|
||||||
|
seconde identité -> QuarantinedConflict
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux acquisitions doivent atteindre le Store. Après les deux résultats :
|
||||||
|
|
||||||
|
```text
|
||||||
|
state = Running
|
||||||
|
health = Degraded
|
||||||
|
persisted_total = 2
|
||||||
|
content_conflict_total = 2
|
||||||
|
store_failure_total = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Stop explicite doit ensuite terminer en `Stopped`. Cette preuve verrouille directement l'invariant « un conflit durable n'empêche pas la progression d'une autre identité ».
|
||||||
|
|
||||||
|
## Recalibrage du lifecycle de fermeture
|
||||||
|
|
||||||
|
Le plan 038 est ajusté pour respecter les règles `VER-LIFECYCLE` actuelles, qui sont plus strictes que le découpage historique `pre.008 -> rel.001` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.008 = hardening / gate technique
|
||||||
|
pre.009 = réconciliation documentaire finale
|
||||||
|
pre.010 = préparation minimale de publication
|
||||||
|
rel.001 = mécanique de publication stable
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.008` ne modifie donc ni `CHANGELOG.md`, ni `ROADMAP.md`, ni le prompt de démarrage suivant, et ne fait pas la réconciliation README/USAGE finale.
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-job-backfill-lib/unit_tests/persistence.rs
|
||||||
|
crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||||
|
crates/ksp-store-postgres-lib/tests/postgres_raw_transaction_live.rs
|
||||||
|
crates/ksp-store-postgres-lib/tests/v003_variant_persistence.rs
|
||||||
|
crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs
|
||||||
|
docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichier ajouté
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.16/pre.008.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations exécutées lors de la génération
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
```
|
||||||
|
|
||||||
|
Le toolchain Rust n'est pas installé dans l'environnement de génération. `cargo fmt`, `cargo check`, Clippy et les tests Cargo ne sont donc pas déclarés exécutés ici.
|
||||||
|
|
||||||
|
La preuve PostgreSQL live reste volontairement opt-in et n'est pas déclarée exécutée sans URI isolé fourni par l'opérateur.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
cargo test -p ksp-store-api --all-targets --all-features
|
||||||
|
cargo test -p ksp-store-lib --all-targets --all-features
|
||||||
|
cargo test -p ksp-store-postgres-lib --all-targets --all-features
|
||||||
|
cargo test -p ksp-job-backfill-lib --all-targets --all-features
|
||||||
|
cargo test -p ksp-worker-raw-transaction-ingest-lib --all-targets --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
Le live PostgreSQL peut être exécuté séparément sur une base dédiée vide :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-store-postgres-lib --test postgres_raw_transaction_live -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Il ne doit jamais être lancé contre une base KSP contenant des données utiles.
|
||||||
|
|
||||||
|
## Décisions prises
|
||||||
|
|
||||||
|
- aucune modification runtime n'est nécessaire pour fermer le hardening observé ;
|
||||||
|
- le conflit durable concurrent est un succès quarantiné, pas une erreur backend ;
|
||||||
|
- les deux observations concurrentes divergentes doivent être conservées ;
|
||||||
|
- un échec tardif avant commit doit annuler variante et conflict case nouvellement créés ;
|
||||||
|
- le canonique ne change jamais dans la branche de conflit ;
|
||||||
|
- Backfill conserve explicitement la distinction conflit / observation idempotente ;
|
||||||
|
- le Worker doit continuer à traiter les identités suivantes après une quarantaine ;
|
||||||
|
- le lifecycle final est séparé en `pre.008`, `pre.009`, `pre.010`, puis `rel.001` conformément aux règles actuelles.
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question fonctionnelle bloquante pour `0.3.16`.
|
||||||
|
|
||||||
|
Le test PostgreSQL live reste conditionné à la disponibilité d'un PostgreSQL isolé ; son absence n'autorise pas à prétendre qu'il a été exécuté.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
Après gate technique propre : `0.3.16-pre.009` — réconciliation documentaire finale de `0.3.16`, sans nouveau comportement runtime.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md -->
|
<!-- file: docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Plan `0.3.16` -> `0.3.18` — résilience RAW, variantes, conflits et récupération
|
# Plan `0.3.16` -> `0.3.18` — résilience RAW, variantes, conflits et récupération
|
||||||
|
|
||||||
@@ -990,7 +990,15 @@ Conflit durable minimal + intégration Worker : variante conservée, case ouvert
|
|||||||
|
|
||||||
#### `0.3.16-pre.008`
|
#### `0.3.16-pre.008`
|
||||||
|
|
||||||
Hardening ciblé de la vertical slice : concurrence, rollback transactionnel, régression Job Backfill/Store, gates workspace, validation documentaire et préparation de `0.3.16-rel.001`.
|
Hardening technique ciblé de la vertical slice : concurrence, rollback transactionnel, régressions Store/Job Backfill/Worker, preuves PostgreSQL live opt-in actualisées et gate workspace. Cette tranche ne mélange ni réconciliation documentaire durable ni préparation de publication.
|
||||||
|
|
||||||
|
#### `0.3.16-pre.009`
|
||||||
|
|
||||||
|
Réconciliation documentaire finale de `0.3.16` : plan, validation, README/USAGE concernés et cohérence des contrats durables. Aucun nouveau comportement runtime ni nouveau smoke n'est introduit.
|
||||||
|
|
||||||
|
#### `0.3.16-pre.010`
|
||||||
|
|
||||||
|
Préparation de publication minimale : `CHANGELOG.md`, `ROADMAP.md`, prompt de démarrage de `0.3.17`, version et delta obligatoires uniquement.
|
||||||
|
|
||||||
#### `0.3.16-rel.001`
|
#### `0.3.16-rel.001`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user