use std::sync::Arc;
use aion_core::{ActivityId, Event};
use chrono::Utc;
use serde::Deserialize;
use crate::activity::bridge::{ActivityDispatch, ActivityDispatcher};
use crate::durability::FanOutItem;
use crate::error::EngineError;
use crate::registry::Registry;
use crate::runtime::RuntimeHandle;
use crate::runtime::nif_activity_dispatch::{FIRST_DELIVERY_ATTEMPT, spawn_completion_task};
use crate::runtime::nif_context::NifContext;
use crate::runtime::nif_state::{CollectKind, EngineNifState, PendingAwait};
#[derive(Deserialize)]
pub(super) struct ActivitySpec {
name: String,
input: String,
config: String,
}
impl ActivitySpec {
pub(super) fn selects_in_vm(&self) -> bool {
super::nif_activity::config_tier(&self.config).as_deref()
== Some(super::nif_activity::IN_VM_TIER)
}
pub(super) fn spec_name(&self) -> &str {
&self.name
}
}
pub(super) struct CollectDeps {
pub(super) registry: Arc<Registry>,
pub(super) runtime: Arc<RuntimeHandle>,
pub(super) tokio_handle: tokio::runtime::Handle,
pub(super) dispatcher: Option<Arc<dyn ActivityDispatcher>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum OrdinalState {
Completed(String),
Failed(String),
Cancelled,
Pending,
}
pub(super) type RaceSettlement = (u64, Result<String, String>);
#[derive(Debug, thiserror::Error)]
pub(super) enum CollectError {
#[error("{0}")]
Message(String),
#[error(transparent)]
Engine(#[from] EngineError),
}
impl From<String> for CollectError {
fn from(message: String) -> Self {
Self::Message(message)
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum CollectStep {
QuerySentinel(String),
AllCompleted(Vec<String>),
RaceWon(Result<String, String>),
FailFast(String),
ScopeExpired(String),
Suspend,
}
pub(super) fn collect_step(
state: &EngineNifState,
deps: &CollectDeps,
pid: u64,
kind: CollectKind,
specs: &[ActivitySpec],
label: &str,
) -> Result<CollectStep, CollectError> {
if let Some(sentinel) = super::nif_query_pump::take_pending_query_sentinel(state, pid) {
return Ok(CollectStep::QuerySentinel(sentinel));
}
if specs.is_empty() {
return match kind {
CollectKind::All => Ok(CollectStep::AllCompleted(Vec::new())),
CollectKind::Race => Err("expected at least one activity".to_owned().into()),
};
}
let count =
u64::try_from(specs.len()).map_err(|_| "activity list length overflows u64".to_owned())?;
let context = NifContext::new(
pid,
deps.registry.as_ref(),
deps.tokio_handle.clone(),
deps.runtime.signal_delivery(),
)
.map_err(|error| error.error_reason())?;
let pin = pin_or_allocate(state, &context, pid, kind, count)?;
dispatch_unscheduled(
deps,
&context,
specs,
pin.base_ordinal,
pin.first_arrival,
label,
)?;
match kind {
CollectKind::All => settle_all(state, deps, &context, pid, pin.base_ordinal, count),
CollectKind::Race => settle_race(state, deps, &context, pid, pin.base_ordinal, count),
}
}
struct PinResult {
base_ordinal: u64,
first_arrival: bool,
}
fn pin_or_allocate(
state: &EngineNifState,
context: &NifContext,
pid: u64,
kind: CollectKind,
count: u64,
) -> Result<PinResult, String> {
match state.pending_awaits.get(&pid).map(|entry| entry.clone()) {
Some(PendingAwait::Collect {
base_ordinal,
count: pinned_count,
kind: pinned_kind,
}) => {
if pinned_count != count || pinned_kind != kind {
return Err(format!(
"process is pinned to a different collect await \
(pinned {pinned_kind:?} of {pinned_count}, called {kind:?} of {count})"
));
}
Ok(PinResult {
base_ordinal,
first_arrival: false,
})
}
Some(PendingAwait::Sleep { .. }) => {
Err("process is pinned to a pending sleep await".to_owned())
}
Some(PendingAwait::Signal { .. }) => {
Err("process is pinned to a pending signal await".to_owned())
}
Some(PendingAwait::Child { .. }) => {
Err("process is pinned to a pending child await".to_owned())
}
None => {
let base_ordinal = context.allocate_activity_ordinals(count);
state.pending_awaits.insert(
pid,
PendingAwait::Collect {
base_ordinal,
count,
kind,
},
);
Ok(PinResult {
base_ordinal,
first_arrival: true,
})
}
}
}
fn dispatch_unscheduled(
deps: &CollectDeps,
context: &NifContext,
specs: &[ActivitySpec],
base_ordinal: u64,
first_arrival: bool,
label: &str,
) -> Result<(), String> {
let mut fresh: Vec<(u64, &ActivitySpec)> = Vec::new();
let mut stale: Vec<(u64, &ActivitySpec)> = Vec::new();
for (offset, spec) in specs.iter().enumerate() {
let ordinal = base_ordinal + offset_to_u64(offset)?;
match scheduled_activity_type(context.history(), ordinal) {
Some(recorded) => {
if recorded != spec.name {
return Err(format!(
"determinism violation: ordinal {ordinal} recorded activity type \
{recorded:?} but workflow code supplied {:?}",
spec.name
));
}
if first_arrival && recorded_terminal(context.history(), ordinal)?.is_none() {
stale.push((ordinal, spec));
}
}
None => fresh.push((ordinal, spec)),
}
}
if fresh.is_empty() && stale.is_empty() {
return Ok(());
}
let Some(dispatcher) = deps.dispatcher.as_ref() else {
if !fresh.is_empty() {
return Err(
"no activity dispatcher configured — set one via EngineBuilder::activity_dispatcher"
.to_owned(),
);
}
return Ok(());
};
let outbox_enabled = deps.runtime.outbox_enabled();
let workflow_namespace = context.workflow_handle().namespace().to_owned();
let start_time_task_queue = context.start_time_task_queue();
let start_time_task_queue = start_time_task_queue.as_deref();
if outbox_enabled {
if !fresh.is_empty() {
let items = fan_out_items(&fresh, &workflow_namespace, start_time_task_queue, label)?;
context
.record_fan_out_dispatch(Utc::now(), &items)
.map_err(|error| error.error_reason())?;
}
if !stale.is_empty() {
let items =
fan_out_items_recovered(&stale, &workflow_namespace, context.history(), label)?;
context
.rearm_outbox_pending(Utc::now(), &items)
.map_err(|error| error.error_reason())?;
}
} else {
for (ordinal, spec) in &fresh {
let scheduled = fresh_scheduled_activity(spec, start_time_task_queue, label)?;
context
.record_activity_scheduled_started(
Utc::now(),
ActivityId::from_sequence_position(*ordinal),
scheduled,
)
.map_err(|error| error.error_reason())?;
}
}
let namespace = workflow_namespace;
let workflow_id = context.workflow_id().clone();
let empty: &[(u64, &ActivitySpec)] = &[];
let spawn_fresh: &[(u64, &ActivitySpec)] = if outbox_enabled { empty } else { &fresh };
let spawn_stale: &[(u64, &ActivitySpec)] = if outbox_enabled { empty } else { &stale };
for (ordinal, spec) in spawn_fresh.iter().chain(spawn_stale.iter()) {
spawn_completion_task(
&deps.tokio_handle,
Arc::clone(&deps.runtime),
Arc::clone(dispatcher),
super::nif_activity_dispatch::RetryRecorderSeam {
recorder: context.recorder(),
run_id: context.workflow_handle().run_id().clone(),
},
context.pid(),
super::nif_activity::correlation_id(*ordinal),
ActivityDispatch {
namespace: namespace.clone(),
task_queue: super::nif_activity::resolve_task_queue(
&spec.config,
start_time_task_queue,
),
node: super::nif_activity::resolve_node(&spec.config),
workflow_id: workflow_id.clone(),
activity_id: ActivityId::from_sequence_position(*ordinal),
name: spec.name.clone(),
input: spec.input.clone(),
config: spec.config.clone(),
attempt: FIRST_DELIVERY_ATTEMPT,
labels: super::nif_activity::labels_from_config(&spec.config),
},
);
}
Ok(())
}
fn fan_out_items(
members: &[(u64, &ActivitySpec)],
namespace: &str,
start_time_task_queue: Option<&str>,
label: &str,
) -> Result<Vec<FanOutItem>, String> {
members
.iter()
.map(|(ordinal, spec)| {
Ok(FanOutItem {
ordinal: *ordinal,
namespace: namespace.to_owned(),
task_queue: super::nif_activity::resolve_task_queue(
&spec.config,
start_time_task_queue,
),
node: super::nif_activity::resolve_node(&spec.config),
activity_type: spec.name.clone(),
input: payload_from_json_text(&spec.input, label)?,
attempt: FIRST_DELIVERY_ATTEMPT,
})
})
.collect()
}
fn fresh_scheduled_activity(
spec: &ActivitySpec,
start_time_task_queue: Option<&str>,
label: &str,
) -> Result<super::nif_activity::ScheduledActivity, String> {
Ok(super::nif_activity::ScheduledActivity {
activity_type: spec.name.clone(),
input: payload_from_json_text(&spec.input, label)?,
task_queue: super::nif_activity::resolve_task_queue(&spec.config, start_time_task_queue),
node: super::nif_activity::resolve_node(&spec.config),
attempt: FIRST_DELIVERY_ATTEMPT,
})
}
fn fan_out_items_recovered(
members: &[(u64, &ActivitySpec)],
namespace: &str,
history: &[Event],
label: &str,
) -> Result<Vec<FanOutItem>, String> {
members
.iter()
.map(|(ordinal, spec)| {
Ok(FanOutItem {
ordinal: *ordinal,
namespace: namespace.to_owned(),
task_queue: scheduled_task_queue(history, *ordinal)
.unwrap_or_else(|| String::from(aion_core::DEFAULT_TASK_QUEUE)),
node: scheduled_node(history, *ordinal),
activity_type: spec.name.clone(),
input: payload_from_json_text(&spec.input, label)?,
attempt: started_attempt(history, *ordinal).unwrap_or(FIRST_DELIVERY_ATTEMPT),
})
})
.collect()
}
use super::nif_collect_settlement::{
offset_to_u64, payload_from_json_text, recorded_terminal, scheduled_activity_type,
scheduled_node, scheduled_task_queue, settle_all, settle_race, started_attempt,
};
#[cfg(test)]
#[path = "nif_collect_tests/mod.rs"]
mod tests;