use std::sync::{Arc, Weak};
use aion_core::{ContentType, Payload, RunId, WorkflowId, WorkflowStatus};
use aion_package::ContentHash;
use aion_store::InMemoryStore;
use super::*;
use crate::durability::Recorder;
use crate::engine_seam::{
EngineHandle, WorkflowMailboxMessage, WorkflowProcessHandle, WorkflowResidency,
};
use crate::query::QueryError;
use crate::registry::{
CompletionNotifier, HandleResidency, Registry, WorkflowHandle, WorkflowHandleParts,
};
use crate::runtime::nif_query::{is_query_registered, register_query_impl};
use crate::runtime::{RuntimeConfig, RuntimeHandle};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const PREDECESSOR_PID: u64 = 4_001;
const SUCCESSOR_PID: u64 = 4_002;
fn handle_for(
store: &Arc<InMemoryStore>,
workflow_id: &WorkflowId,
run_id: &RunId,
pid: u64,
cached_status: WorkflowStatus,
) -> WorkflowHandle {
WorkflowHandle::new(WorkflowHandleParts {
workflow_id: workflow_id.clone(),
run_id: run_id.clone(),
pid,
workflow_type: "checkout".to_owned(),
namespace: String::from("default"),
loaded_version: ContentHash::from_bytes([9; 32]),
cached_status,
residency: HandleResidency::Resident,
recorder: Recorder::resume_at(workflow_id.clone(), Arc::clone(store) as _, 0),
completion: CompletionNotifier::new(),
})
}
fn seam(registry: &Arc<Registry>) -> QueryMailboxEngine {
QueryMailboxEngine::new(Arc::clone(registry), Weak::new(), Weak::new())
}
fn live_seam() -> Result<(Arc<RuntimeHandle>, QueryMailboxEngine), crate::EngineError> {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(
Some(1),
crate::runtime::config::TEST_STOP_DRAIN_TIMEOUT,
))?);
let mailbox = QueryMailboxEngine::new(
Arc::new(Registry::default()),
Arc::downgrade(runtime.nif_state()),
Arc::downgrade(&runtime),
);
Ok((runtime, mailbox))
}
#[test]
fn a_continued_workflow_resolves_to_its_current_run() -> TestResult {
let registry = Arc::new(Registry::default());
let store = Arc::new(InMemoryStore::default());
let workflow_id = WorkflowId::new_v4();
let predecessor_run = RunId::new_v4();
let successor_run = RunId::new_v4();
registry.insert(
(workflow_id.clone(), predecessor_run.clone()),
handle_for(
&store,
&workflow_id,
&predecessor_run,
PREDECESSOR_PID,
WorkflowStatus::ContinuedAsNew,
),
)?;
registry.insert(
(workflow_id.clone(), successor_run.clone()),
handle_for(
&store,
&workflow_id,
&successor_run,
SUCCESSOR_PID,
WorkflowStatus::Running,
),
)?;
assert_eq!(
seam(®istry).resolve_workflow(&workflow_id)?,
WorkflowResidency::Resident(WorkflowProcessHandle::new(SUCCESSOR_PID)),
"the seam must name the current run, not whichever handle a HashMap scan yielded"
);
Ok(())
}
#[test]
fn a_stale_predecessor_handle_without_an_index_entry_is_unknown() -> TestResult {
let registry = Arc::new(Registry::default());
let store = Arc::new(InMemoryStore::default());
let workflow_id = WorkflowId::new_v4();
let predecessor_run = RunId::new_v4();
let successor_run = RunId::new_v4();
registry.insert(
(workflow_id.clone(), predecessor_run.clone()),
handle_for(
&store,
&workflow_id,
&predecessor_run,
PREDECESSOR_PID,
WorkflowStatus::ContinuedAsNew,
),
)?;
registry.insert(
(workflow_id.clone(), successor_run.clone()),
handle_for(
&store,
&workflow_id,
&successor_run,
SUCCESSOR_PID,
WorkflowStatus::Running,
),
)?;
registry.remove(&workflow_id, &successor_run)?;
assert!(
registry.get(&workflow_id, &predecessor_run)?.is_some(),
"premise: the predecessor's handle outlives the successor's removal"
);
assert_eq!(
seam(®istry).resolve_workflow(&workflow_id)?,
WorkflowResidency::Unknown,
"no current run means Unknown, not the stale predecessor's Terminal"
);
Ok(())
}
#[test]
fn the_live_run_index_follows_registration_order() -> TestResult {
let registry = Arc::new(Registry::default());
let store = Arc::new(InMemoryStore::default());
let workflow_id = WorkflowId::new_v4();
let predecessor_run = RunId::new_v4();
let successor_run = RunId::new_v4();
registry.insert(
(workflow_id.clone(), successor_run.clone()),
handle_for(
&store,
&workflow_id,
&successor_run,
SUCCESSOR_PID,
WorkflowStatus::Running,
),
)?;
registry.insert(
(workflow_id.clone(), predecessor_run.clone()),
handle_for(
&store,
&workflow_id,
&predecessor_run,
PREDECESSOR_PID,
WorkflowStatus::ContinuedAsNew,
),
)?;
assert_eq!(
registry.live_run_pid(&workflow_id)?,
Some((predecessor_run, PREDECESSOR_PID)),
"the index names the LAST run registered, whatever its age"
);
assert_eq!(
seam(®istry).resolve_workflow(&workflow_id)?,
WorkflowResidency::Terminal,
"so a superseded run registered last would be the run this seam answers for"
);
Ok(())
}
#[test]
fn cleanup_started_while_pid_is_live_drops_the_query_reply() -> TestResult {
let (runtime, mailbox) = live_seam()?;
let pid = runtime.spawn_test_process()?;
let state = runtime.nif_state();
register_query_impl(state, "state", "{}", Some(pid))?;
assert!(
is_query_registered(state, pid, "state")?,
"fixture control: the query handler must be registered before cleanup"
);
state.cleanup_process(pid);
assert!(
runtime.is_live(pid),
"fixture control: cleanup must precede scheduler pid retirement"
);
assert!(
runtime.process_cleanup_started(pid),
"fixture control: cleanup must stamp the exit tombstone"
);
assert!(
!is_query_registered(state, pid, "state")?,
"fixture control: cleanup must remove the registered handler"
);
let (reply_to, reply_from) = tokio::sync::oneshot::channel();
mailbox.deliver_workflow_message(
WorkflowProcessHandle::new(pid),
WorkflowMailboxMessage::Query {
name: "state".to_owned(),
payload: Payload::new(ContentType::Json, b"{}".to_vec()),
reply_to,
},
)?;
assert_eq!(
reply_from.blocking_recv()?,
Err(QueryError::ReplyDropped),
"completion cleanup must yield ReplyDropped, never UnknownQuery"
);
runtime.shutdown()?;
Ok(())
}
#[test]
fn live_pid_without_cleanup_reports_unknown_query() -> TestResult {
let (runtime, mailbox) = live_seam()?;
let pid = runtime.spawn_test_process()?;
assert!(runtime.is_live(pid));
assert!(!runtime.process_cleanup_started(pid));
let (reply_to, reply_from) = tokio::sync::oneshot::channel();
mailbox.deliver_workflow_message(
WorkflowProcessHandle::new(pid),
WorkflowMailboxMessage::Query {
name: "missing".to_owned(),
payload: Payload::new(ContentType::Json, b"{}".to_vec()),
reply_to,
},
)?;
assert_eq!(
reply_from.blocking_recv()?,
Err(QueryError::UnknownQuery("missing".to_owned()))
);
runtime.shutdown()?;
Ok(())
}
#[test]
fn retired_pid_before_cleanup_drops_the_query_reply() -> TestResult {
let (runtime, mailbox) = live_seam()?;
let pid = runtime.spawn_test_process()?;
runtime.cancel_pid(pid)?;
assert!(
!runtime.is_live(pid),
"fixture control: scheduler retirement must win this interleaving"
);
assert!(
!runtime.process_cleanup_started(pid),
"fixture control: Aion cleanup must not have started without a monitor"
);
let (reply_to, reply_from) = tokio::sync::oneshot::channel();
mailbox.deliver_workflow_message(
WorkflowProcessHandle::new(pid),
WorkflowMailboxMessage::Query {
name: "state".to_owned(),
payload: Payload::new(ContentType::Json, b"{}".to_vec()),
reply_to,
},
)?;
assert_eq!(
reply_from.blocking_recv()?,
Err(QueryError::ReplyDropped),
"scheduler retirement before cleanup must remain ReplyDropped"
);
runtime.shutdown()?;
Ok(())
}
#[test]
fn cleanup_started_while_pid_is_live_classifies_wake_failure_as_completion() -> TestResult {
let (runtime, _mailbox) = live_seam()?;
let pid = runtime.spawn_test_process()?;
runtime.nif_state().cleanup_process(pid);
assert!(
runtime.is_live(pid),
"fixture control: cleanup must precede scheduler pid retirement"
);
assert!(runtime.process_cleanup_started(pid));
assert!(
query_reply_was_dropped_by_completion(&runtime, pid),
"wake-marker failure after cleanup starts must drop the reply, never report an engine fault"
);
runtime.shutdown()?;
Ok(())
}