v0.3.13-pre.011
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime_resources.rs
|
||||
// version: 22
|
||||
// version: 23
|
||||
|
||||
fn grpc_endpoint(cluster: &str) -> std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||
return grpc_endpoint_with_identity(cluster, "yellowstone-fixture", "fixture-provider");
|
||||
@@ -971,13 +971,13 @@ fn v0_3_13_pre_007_source_inventory_aggregates_frontier_and_lifecycle_conservati
|
||||
source_key: [1_u8; 32],
|
||||
};
|
||||
let second = super::RawTransactionIngestSourceInventoryPublisher { aggregate_sender, entry_index: 1, inventory, source_key: [2_u8; 32] };
|
||||
first.publish(
|
||||
crate::RawTransactionIngestProcessingFrontierProjection::new(1, std::option::Option::Some(50), std::option::Option::Some(45)).with_source_continuity(
|
||||
std::option::Option::Some(crate::RawTransactionIngestSourceState::Active),
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
),
|
||||
assert!(
|
||||
first
|
||||
.publish(
|
||||
crate::RawTransactionIngestProcessingFrontierProjection::new(1, std::option::Option::Some(50), std::option::Option::Some(45))
|
||||
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), 1, 2, 0),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
let first_aggregate = *aggregate_receiver.borrow();
|
||||
assert_eq!(first_aggregate.hydration_pending(), 1);
|
||||
@@ -988,13 +988,13 @@ fn v0_3_13_pre_007_source_inventory_aggregates_frontier_and_lifecycle_conservati
|
||||
assert_eq!(first_aggregate.source_active(), 1);
|
||||
assert_eq!(first_aggregate.source_reconnecting(), 0);
|
||||
assert_eq!(first_aggregate.source_failed(), 0);
|
||||
second.publish(
|
||||
crate::RawTransactionIngestProcessingFrontierProjection::new(2, std::option::Option::Some(42), std::option::Option::Some(40)).with_source_continuity(
|
||||
std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting),
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
),
|
||||
assert!(
|
||||
second
|
||||
.publish(
|
||||
crate::RawTransactionIngestProcessingFrontierProjection::new(2, std::option::Option::Some(42), std::option::Option::Some(40))
|
||||
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Reconnecting), 3, 4, 1),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
let aggregate = *aggregate_receiver.borrow();
|
||||
assert_eq!(aggregate.hydration_pending(), 3);
|
||||
@@ -3146,3 +3146,204 @@ async fn v0_3_13_pre_009_global_hydration_permit_and_leader_cleanup_are_bounded_
|
||||
assert!(matches!(resubscribed, std::result::Result::Ok((true, _))));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_3_13_pre_011_source_inventory_rejects_stale_identity_and_counter_exhaustion() {
|
||||
let mut identity_inventory = super::RawTransactionIngestSourceInventory::new(std::vec![[1_u8; 32]]);
|
||||
let mismatch = identity_inventory.update(0, [2_u8; 32], crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
||||
let mismatch = match mismatch {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(mismatch.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
||||
assert_eq!(mismatch.context()[0].value(), "source.inventory_key_mismatch");
|
||||
let missing = identity_inventory.update(1, [1_u8; 32], crate::RawTransactionIngestProcessingFrontierProjection::empty());
|
||||
let missing = match missing {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(missing.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
||||
assert_eq!(missing.context()[0].value(), "source.inventory_entry_missing");
|
||||
let mut pending_inventory = super::RawTransactionIngestSourceInventory::new(std::vec![[3_u8; 32], [4_u8; 32]]);
|
||||
assert!(
|
||||
pending_inventory
|
||||
.update(
|
||||
0,
|
||||
[3_u8; 32],
|
||||
crate::RawTransactionIngestProcessingFrontierProjection::new(usize::MAX, std::option::Option::Some(10), std::option::Option::None),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
pending_inventory
|
||||
.update(1, [4_u8; 32], crate::RawTransactionIngestProcessingFrontierProjection::new(1, std::option::Option::Some(11), std::option::Option::None),)
|
||||
.is_ok()
|
||||
);
|
||||
let pending_error = match pending_inventory.aggregate() {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(pending_error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_COUNTER_EXHAUSTED);
|
||||
assert_eq!(pending_error.context()[0].value(), "hydration_pending");
|
||||
let mut continuity_inventory = super::RawTransactionIngestSourceInventory::new(std::vec![[5_u8; 32], [6_u8; 32]]);
|
||||
assert!(
|
||||
continuity_inventory
|
||||
.update(
|
||||
0,
|
||||
[5_u8; 32],
|
||||
crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(20), std::option::Option::None)
|
||||
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), u64::MAX, 0, 0),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
continuity_inventory
|
||||
.update(
|
||||
1,
|
||||
[6_u8; 32],
|
||||
crate::RawTransactionIngestProcessingFrontierProjection::new(0, std::option::Option::Some(21), std::option::Option::None)
|
||||
.with_source_continuity(std::option::Option::Some(crate::RawTransactionIngestSourceState::Active), 1, 0, 0),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
let continuity_error = match continuity_inventory.aggregate() {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(continuity_error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_COUNTER_EXHAUSTED);
|
||||
assert_eq!(continuity_error.context()[0].value(), "source_reconnect_total");
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn v0_3_13_pre_011_aborted_hydration_leader_notifies_followers_and_releases_registry_key() {
|
||||
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let registry = std::sync::Arc::new(super::RawTransactionIngestGlobalHydrationRegistry::new(2, 1));
|
||||
let key = super::RawTransactionIngestHydrationKey {
|
||||
commitment: ksp_onchain_transport_lib::SolanaCommitment::Confirmed.as_str(),
|
||||
network,
|
||||
signature: ksp_store_lib::RawTransactionSignature::new([91_u8; 64]),
|
||||
};
|
||||
let subscribed = registry.subscribe_or_lead(&key);
|
||||
let (leader, follower) = match subscribed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(leader);
|
||||
let task_registry = std::sync::Arc::clone(®istry);
|
||||
let task_key = key.clone();
|
||||
let leader_task = tokio::spawn(async move {
|
||||
let _guard = super::RawTransactionIngestHydrationLeaderGuard::new(task_registry, task_key);
|
||||
return std::future::pending::<()>().await;
|
||||
});
|
||||
tokio::task::yield_now().await;
|
||||
leader_task.abort();
|
||||
let _joined = leader_task.await;
|
||||
let published = follower.borrow().clone();
|
||||
match published {
|
||||
std::option::Option::Some(super::RawTransactionIngestSharedHydrationResult::Failed(code)) => {
|
||||
assert_eq!(code, crate::ERROR_CODE_RAW_TRANSACTION_INGEST_SOURCE_FAILED);
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
assert!(matches!(registry.subscribe_or_lead(&key), std::result::Result::Ok((true, _))));
|
||||
return;
|
||||
}
|
||||
|
||||
struct Pre011SourceActiveGuard {
|
||||
active: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Pre011SourceActiveGuard {
|
||||
fn new(active: std::sync::Arc<std::sync::atomic::AtomicUsize>) -> Self {
|
||||
active.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
||||
return Self { active };
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for Pre011SourceActiveGuard {
|
||||
fn drop(&mut self) {
|
||||
self.active.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn v0_3_13_pre_011_aborting_outer_source_supervisor_aborts_nested_source_tasks() {
|
||||
let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let (_stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
||||
let (source_stop_sender, _source_stop_receiver) = tokio::sync::watch::channel(false);
|
||||
let mut children = tokio::task::JoinSet::new();
|
||||
let task_active = std::sync::Arc::clone(&active);
|
||||
let _abort_handle = children.spawn(async move {
|
||||
let _guard = Pre011SourceActiveGuard::new(task_active);
|
||||
return std::future::pending::<ksp_core_lib::Result<()>>().await;
|
||||
});
|
||||
let supervisor = tokio::spawn(super::supervise_live_source_tasks(stop_receiver, source_stop_sender, children));
|
||||
for _ in 0..64 {
|
||||
if active.load(std::sync::atomic::Ordering::Acquire) == 1 {
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 1);
|
||||
supervisor.abort();
|
||||
let _joined = supervisor.await;
|
||||
for _ in 0..64 {
|
||||
if active.load(std::sync::atomic::Ordering::Acquire) == 0 {
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn v0_3_13_pre_011_stop_racing_ready_source_fault_preserves_fault_and_joins_sibling() {
|
||||
let sibling_active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let fault_gate = std::sync::Arc::new(tokio::sync::Notify::new());
|
||||
let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
||||
let (source_stop_sender, source_stop_receiver) = tokio::sync::watch::channel(false);
|
||||
let mut children = tokio::task::JoinSet::new();
|
||||
let task_fault_gate = std::sync::Arc::clone(&fault_gate);
|
||||
let _fault_abort_handle = children.spawn(async move {
|
||||
task_fault_gate.notified().await;
|
||||
return std::result::Result::Err(crate::runtime_error("test.pre_011_ready_source_fault"));
|
||||
});
|
||||
let sibling_active_for_task = std::sync::Arc::clone(&sibling_active);
|
||||
let mut sibling_stop_receiver = source_stop_receiver.clone();
|
||||
let _sibling_abort_handle = children.spawn(async move {
|
||||
let _guard = Pre011SourceActiveGuard::new(sibling_active_for_task);
|
||||
loop {
|
||||
let changed = sibling_stop_receiver.changed().await;
|
||||
if changed.is_err() || *sibling_stop_receiver.borrow() {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
});
|
||||
let supervisor = tokio::spawn(super::supervise_live_source_tasks(stop_receiver, source_stop_sender, children));
|
||||
for _ in 0..64 {
|
||||
if sibling_active.load(std::sync::atomic::Ordering::Acquire) == 1 {
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert_eq!(sibling_active.load(std::sync::atomic::Ordering::Acquire), 1);
|
||||
stop_sender.send_replace(true);
|
||||
fault_gate.notify_one();
|
||||
let result = match supervisor.await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let error = match result {
|
||||
std::result::Result::Ok(()) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(error.context()[0].value(), "test.pre_011_ready_source_fault");
|
||||
assert_eq!(sibling_active.load(std::sync::atomic::Ordering::Acquire), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user