159 lines
6.8 KiB
Rust
159 lines
6.8 KiB
Rust
// file: crates/ksp-job-backfill-lib/unit_tests/runtime.rs
|
|
// version: 3
|
|
|
|
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(), super::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 = super::BackfillRuntimeControl::new(cancel_sender);
|
|
assert!(cancellation_first.request_cancellation());
|
|
assert_eq!(cancellation_first.claim_normal_terminal(), super::TerminalClaim::Cancelled);
|
|
assert!(!cancellation_first.request_cancellation());
|
|
let (cancel_sender, _) = tokio::sync::watch::channel(false);
|
|
let completion_first = super::BackfillRuntimeControl::new(cancel_sender);
|
|
assert_eq!(completion_first.claim_normal_terminal(), super::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 = super::BackfillRuntimeControl::new(cancel_sender);
|
|
assert!(control.request_cancellation());
|
|
control.claim_failed();
|
|
assert_eq!(control.claim_normal_terminal(), super::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 = super::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;
|
|
}
|