v0.3.6-pre.009
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/unit_tests/discovery.rs
|
||||
// version: 2
|
||||
// version: 5
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct PageCall {
|
||||
@@ -15,6 +15,34 @@ struct FakeSource {
|
||||
calls: std::sync::Mutex<std::vec::Vec<PageCall>>,
|
||||
}
|
||||
|
||||
struct PendingSource {
|
||||
calls: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
impl PendingSource {
|
||||
fn new() -> Self {
|
||||
return Self { calls: std::sync::atomic::AtomicUsize::new(0) };
|
||||
}
|
||||
|
||||
fn calls(&self) -> usize {
|
||||
return self.calls.load(std::sync::atomic::Ordering::Acquire);
|
||||
}
|
||||
}
|
||||
|
||||
impl super::SignaturePageSource for PendingSource {
|
||||
fn fetch_signature_page<'a>(
|
||||
&'a self,
|
||||
_role: &'a ksp_onchain_transport_lib::HttpRoleName,
|
||||
_address: &'a ksp_core_lib::Pubkey,
|
||||
_config: ksp_onchain_transport_lib::SolanaSignaturesForAddressConfig,
|
||||
) -> super::SignaturePageFuture<'a> {
|
||||
self.calls.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
||||
return std::boxed::Box::pin(async {
|
||||
return std::future::pending::<ksp_core_lib::Result<std::vec::Vec<super::SignaturePageEntry>>>().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeSource {
|
||||
fn new(pages: std::vec::Vec<std::vec::Vec<super::SignaturePageEntry>>) -> Self {
|
||||
return Self { pages: std::sync::Mutex::new(pages.into()), calls: std::sync::Mutex::new(std::vec::Vec::new()) };
|
||||
@@ -130,7 +158,7 @@ async fn pre_005_latest_paginates_newest_first_and_deduplicates_pages_stably() {
|
||||
std::vec![page_entry('6', 60), page_entry('5', 50), page_entry('5', 50)],
|
||||
std::vec![page_entry('4', 40), page_entry('3', 30)],
|
||||
]);
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
assert!(result.is_ok());
|
||||
let discovery = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -164,7 +192,7 @@ async fn pre_005_before_uses_exclusive_anchor_then_advances_rpc_cursor() {
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let source = FakeSource::new(std::vec![std::vec![page_entry('6', 60), page_entry('5', 50)], std::vec![page_entry('4', 40)]]);
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
let discovery = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
@@ -194,7 +222,7 @@ async fn pre_005_after_keeps_only_nearest_newer_window_and_preserves_rpc_order()
|
||||
std::vec![page_entry('7', 70), page_entry('6', 60), page_entry('5', 50)],
|
||||
std::vec![page_entry('4', 40), page_entry('3', 30)],
|
||||
]);
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
let discovery = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
@@ -223,7 +251,7 @@ async fn pre_005_after_page_bound_is_partial_and_does_not_claim_anchor_completio
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let source = FakeSource::new(std::vec![std::vec![page_entry('7', 70), page_entry('6', 60)], std::vec![page_entry('5', 50), page_entry('4', 40)],]);
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
let discovery = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
@@ -243,7 +271,7 @@ async fn pre_005_latest_page_bound_is_partial_when_full_pages_leave_more_history
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let source = FakeSource::new(std::vec![std::vec![page_entry('7', 70), page_entry('6', 60)]]);
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
let discovery = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
@@ -293,7 +321,7 @@ async fn pre_005_explicit_scope_never_calls_transport_and_preserves_network_scop
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let source = FakeSource::new(std::vec::Vec::new());
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
let discovery = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
@@ -327,7 +355,7 @@ async fn pre_008_before_resume_uses_checkpoint_cursor_instead_of_original_anchor
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let source = FakeSource::new(std::vec![std::vec![page_entry('4', 40)]]);
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
assert!(result.is_ok());
|
||||
let calls = source.calls();
|
||||
assert_eq!(calls.len(), 1);
|
||||
@@ -348,10 +376,36 @@ async fn pre_008_latest_resume_restarts_from_current_latest_without_rpc_cursor()
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let source = FakeSource::new(std::vec![std::vec![page_entry('7', 70)]]);
|
||||
let result = super::discover_with_source(&source, &request).await;
|
||||
let result = super::discover_with_source(&source, &request, std::option::Option::None).await;
|
||||
assert!(result.is_ok());
|
||||
let calls = source.calls();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].before, std::option::Option::None);
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_009_discovery_rpc_wait_is_cancelled_cooperatively() {
|
||||
let scope = crate::BackfillScope::latest_address(ksp_core_lib::Pubkey::new_from_array([9_u8; 32]));
|
||||
let request = match request(scope, 2, 2, 5) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let source = PendingSource::new();
|
||||
let (cancel_sender, cancel_receiver) = tokio::sync::watch::channel(false);
|
||||
let control = crate::BackfillRuntimeControl::new(cancel_sender);
|
||||
let cancellation = crate::BackfillCancellationSignal::new(control.token(), cancel_receiver);
|
||||
let discovery = super::discover_with_source(&source, &request, std::option::Option::Some(&cancellation));
|
||||
let cancel = async {
|
||||
tokio::task::yield_now().await;
|
||||
assert!(control.request_cancellation());
|
||||
};
|
||||
let (result, ()) = tokio::join!(discovery, cancel);
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_CANCELLED);
|
||||
assert_eq!(source.calls(), 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/unit_tests/execution.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FakeDisposition {
|
||||
@@ -164,7 +164,7 @@ async fn pre_008_execution_is_bounded_and_reconciles_out_of_order_durable_comple
|
||||
FakePlan { pending_polls: 2, disposition: FakeDisposition::Durable },
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
]);
|
||||
let result = super::execute_with_processor(&processor, &request, &discovery).await;
|
||||
let result = super::execute_with_processor(&processor, &request, &discovery, std::option::Option::None, std::option::Option::None).await;
|
||||
let batch = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
@@ -196,7 +196,7 @@ async fn pre_008_missing_is_non_fatal_but_blocks_frontier_while_later_candidates
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Missing },
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
]);
|
||||
let batch = match super::execute_with_processor(&processor, &request, &discovery).await {
|
||||
let batch = match super::execute_with_processor(&processor, &request, &discovery, std::option::Option::None, std::option::Option::None).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
@@ -227,7 +227,7 @@ async fn pre_008_conflict_stops_new_admissions_and_drains_already_in_flight_work
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
]);
|
||||
let batch = match super::execute_with_processor(&processor, &request, &discovery).await {
|
||||
let batch = match super::execute_with_processor(&processor, &request, &discovery, std::option::Option::None, std::option::Option::None).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
@@ -263,7 +263,7 @@ async fn pre_008_explicit_resume_skips_only_the_checkpointed_contiguous_prefix()
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
]);
|
||||
let batch = match super::execute_with_processor(&processor, &request, &discovery).await {
|
||||
let batch = match super::execute_with_processor(&processor, &request, &discovery, std::option::Option::None, std::option::Option::None).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
@@ -274,3 +274,47 @@ async fn pre_008_explicit_resume_skips_only_the_checkpointed_contiguous_prefix()
|
||||
assert_eq!(batch.failure_code(), std::option::Option::None);
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_009_cancellation_stops_admission_and_drains_already_admitted_candidate_work() {
|
||||
let request = match explicit_request(&['1', '2', '3', '4'], 2) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let discovery = match discovery(&request, &['1', '2', '3', '4']) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let processor = FakeProcessor::new(std::vec![
|
||||
FakePlan { pending_polls: 12, disposition: FakeDisposition::Durable },
|
||||
FakePlan { pending_polls: 12, disposition: FakeDisposition::Durable },
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
FakePlan { pending_polls: 0, disposition: FakeDisposition::Durable },
|
||||
]);
|
||||
let (cancel_sender, cancel_receiver) = tokio::sync::watch::channel(false);
|
||||
let control = crate::runtime::BackfillRuntimeControl::new(cancel_sender);
|
||||
let signal = crate::runtime::BackfillCancellationSignal::new(control.token(), cancel_receiver);
|
||||
let execution = super::execute_with_processor(&processor, &request, &discovery, std::option::Option::Some(&signal), std::option::Option::None);
|
||||
let cancellation = async {
|
||||
loop {
|
||||
if processor.calls() >= 2 {
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert!(control.request_cancellation());
|
||||
};
|
||||
let (result, ()) = tokio::join!(execution, cancellation);
|
||||
let batch = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(processor.calls(), 2);
|
||||
assert_eq!(batch.admitted_count(), 2);
|
||||
assert_eq!(batch.finished_count(), 2);
|
||||
assert_eq!(batch.durable_count(), 2);
|
||||
assert_eq!(batch.cancelled_count(), 0);
|
||||
assert!(batch.was_cancelled());
|
||||
assert_eq!(batch.checkpoint().completed_prefix(), 2);
|
||||
return;
|
||||
}
|
||||
|
||||
158
crates/ksp-job-backfill-lib/unit_tests/runtime.rs
Normal file
158
crates/ksp-job-backfill-lib/unit_tests/runtime.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
// file: crates/ksp-job-backfill-lib/unit_tests/runtime.rs
|
||||
// version: 2
|
||||
|
||||
use ksp_job_api::JobSnapshotSource; // rust-rules: trait-import
|
||||
|
||||
fn request() -> std::option::Option<crate::BackfillRequest> {
|
||||
let signature = match crate::BackfillSignature::new("1".repeat(crate::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let scope = match crate::BackfillScope::explicit_signatures(std::vec![signature]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let job_id = match ksp_job_api::JobId::new("backfill:runtime-test") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let network = match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return crate::BackfillRequest::new(
|
||||
job_id,
|
||||
network,
|
||||
ksp_onchain_transport_lib::HttpRoleName::new("history"),
|
||||
crate::BackfillCommitment::Confirmed,
|
||||
scope,
|
||||
100,
|
||||
10,
|
||||
1,
|
||||
1,
|
||||
std::option::Option::None,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_009_latest_value_source_coalesces_progress_for_slow_independent_listeners() {
|
||||
let request = match request() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let runtime = match crate::BackfillJobRuntime::new(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let handle = runtime.handle();
|
||||
let listener_a = handle.snapshots();
|
||||
let listener_b = handle.snapshots();
|
||||
let initial = listener_a.current();
|
||||
assert_eq!(initial.sequence().value(), 0);
|
||||
assert_eq!(initial.state(), ksp_job_api::JobState::Created);
|
||||
let first = runtime.publisher.publish_running(crate::BackfillJobPhase::Discovering);
|
||||
assert!(first.is_ok());
|
||||
let second = runtime.publisher.publish_running(crate::BackfillJobPhase::Executing);
|
||||
assert!(second.is_ok());
|
||||
let coalesced = listener_a.wait_for_change(initial.sequence()).await;
|
||||
assert_eq!(coalesced.sequence().value(), 2);
|
||||
assert_eq!(coalesced.snapshot().phase(), crate::BackfillJobPhase::Executing);
|
||||
let listener_b_current = listener_b.current();
|
||||
assert_eq!(listener_b_current.sequence().value(), 2);
|
||||
assert_eq!(listener_b_current.snapshot().phase(), crate::BackfillJobPhase::Executing);
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_009_terminal_snapshot_is_retained_and_late_cancellation_is_rejected() {
|
||||
let request = match request() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let runtime = match crate::BackfillJobRuntime::new(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let handle = runtime.handle();
|
||||
let listener = handle.snapshots();
|
||||
let started = runtime.publisher.publish_running(crate::BackfillJobPhase::Discovering);
|
||||
assert!(started.is_ok());
|
||||
assert_eq!(runtime.control.claim_normal_terminal(), crate::TerminalClaim::Completed);
|
||||
let terminal = runtime.publisher.publish_terminal(ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Complete), std::option::Option::None);
|
||||
assert!(terminal.is_ok());
|
||||
assert!(!handle.cancel());
|
||||
let current = listener.current();
|
||||
assert!(current.state().is_terminal());
|
||||
assert_eq!(current.snapshot().phase(), crate::BackfillJobPhase::Finished);
|
||||
let retained = listener.wait_for_change(current.sequence()).await;
|
||||
assert_eq!(retained.sequence(), current.sequence());
|
||||
assert_eq!(retained.state(), current.state());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_terminal_race_is_first_decision_wins_for_cancellation_vs_completion() {
|
||||
let (cancel_sender, _) = tokio::sync::watch::channel(false);
|
||||
let cancellation_first = crate::BackfillRuntimeControl::new(cancel_sender);
|
||||
assert!(cancellation_first.request_cancellation());
|
||||
assert_eq!(cancellation_first.claim_normal_terminal(), crate::TerminalClaim::Cancelled);
|
||||
assert!(!cancellation_first.request_cancellation());
|
||||
let (cancel_sender, _) = tokio::sync::watch::channel(false);
|
||||
let completion_first = crate::BackfillRuntimeControl::new(cancel_sender);
|
||||
assert_eq!(completion_first.claim_normal_terminal(), crate::TerminalClaim::Completed);
|
||||
assert!(!completion_first.request_cancellation());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_fatal_failure_overrides_pending_cancellation_before_terminal_publication() {
|
||||
let (cancel_sender, _) = tokio::sync::watch::channel(false);
|
||||
let control = crate::BackfillRuntimeControl::new(cancel_sender);
|
||||
assert!(control.request_cancellation());
|
||||
control.claim_failed();
|
||||
assert_eq!(control.claim_normal_terminal(), crate::TerminalClaim::Failed);
|
||||
assert!(!control.request_cancellation());
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_009_long_running_pre_store_future_is_cancelled_cooperatively() {
|
||||
let (cancel_sender, cancel_receiver) = tokio::sync::watch::channel(false);
|
||||
let control = crate::BackfillRuntimeControl::new(cancel_sender);
|
||||
let signal = crate::BackfillCancellationSignal::new(control.token(), cancel_receiver);
|
||||
let operation = std::future::pending::<ksp_core_lib::Result<usize>>();
|
||||
let wait = signal.run_cancellable(operation);
|
||||
let cancel = async {
|
||||
tokio::task::yield_now().await;
|
||||
assert!(control.request_cancellation());
|
||||
};
|
||||
let (result, ()) = tokio::join!(wait, cancel);
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BACKFILL_CANCELLED);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_snapshot_debug_and_public_shape_do_not_include_transport_or_raw_payloads() {
|
||||
let request = match request() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let runtime = match crate::BackfillJobRuntime::new(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let snapshot = runtime.handle().snapshots().current();
|
||||
let debug = format!("{:?}", snapshot.snapshot());
|
||||
for forbidden in ["http://", "https://", "endpoint", "provider", "raw_payload", "transaction_data"] {
|
||||
assert!(!debug.contains(forbidden), "unsafe snapshot diagnostic leaked: {forbidden}");
|
||||
}
|
||||
assert_eq!(snapshot.snapshot().scope_kind(), crate::BackfillScopeKind::ExplicitSignatures);
|
||||
assert_eq!(snapshot.snapshot().candidates_selected(), 0);
|
||||
assert!(snapshot.snapshot().failure_code().is_none());
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user