use aion_core::{ActivityError, ActivityErrorKind, ActivityId, Event, Payload};
use chrono::Utc;
use crate::durability::{FanOutCompletionResult, FanOutOutcome};
use crate::runtime::nif_activity_dispatch::FIRST_DELIVERY_ATTEMPT;
use crate::runtime::nif_collect::{
CollectDeps, CollectError, CollectStep, OrdinalState, RaceSettlement,
};
use crate::runtime::nif_context::NifContext;
use crate::runtime::nif_state::EngineNifState;
use crate::runtime::nif_timeout::SCOPE_EXPIRED_MESSAGE;
pub(super) fn settle_all(
state: &EngineNifState,
deps: &CollectDeps,
context: &NifContext,
pid: u64,
base_ordinal: u64,
count: u64,
) -> Result<CollectStep, CollectError> {
let mut states = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
for ordinal in base_ordinal..base_ordinal + count {
let recorded = match recorded_terminal(context.history(), ordinal)? {
Some(recorded) => recorded,
None => take_and_record(deps, context, pid, ordinal)?,
};
states.push(recorded);
}
let lowest_failure = states.iter().find_map(|recorded| match recorded {
OrdinalState::Failed(message) => Some(message.clone()),
_ => None,
});
if let Some(message) = lowest_failure {
cancel_pending(deps, context, pid, base_ordinal, &states)?;
state.pending_awaits.remove(&pid);
return Ok(CollectStep::FailFast(message));
}
if states
.iter()
.all(|recorded| matches!(recorded, OrdinalState::Completed(_)))
{
let results = states
.into_iter()
.filter_map(|recorded| match recorded {
OrdinalState::Completed(payload) => Some(payload),
_ => None,
})
.collect();
state.pending_awaits.remove(&pid);
return Ok(CollectStep::AllCompleted(results));
}
if super::nif_timeout::expired_scope_deadline(state, pid, context.history()).is_some() {
cancel_pending(deps, context, pid, base_ordinal, &states)?;
state.pending_awaits.remove(&pid);
return Ok(CollectStep::ScopeExpired(SCOPE_EXPIRED_MESSAGE.to_owned()));
}
if !states
.iter()
.any(|recorded| matches!(recorded, OrdinalState::Pending))
{
state.pending_awaits.remove(&pid);
return Ok(CollectStep::ScopeExpired(SCOPE_EXPIRED_MESSAGE.to_owned()));
}
Ok(CollectStep::Suspend)
}
pub(super) fn settle_race(
state: &EngineNifState,
deps: &CollectDeps,
context: &NifContext,
pid: u64,
base_ordinal: u64,
count: u64,
) -> Result<CollectStep, CollectError> {
let history = context.history();
let mut winner = recorded_race_winner(history, base_ordinal, count)?;
if winner.is_none() {
for ordinal in base_ordinal..base_ordinal + count {
if recorded_terminal(history, ordinal)?.is_some() {
continue;
}
match take_and_record(deps, context, pid, ordinal)? {
OrdinalState::Completed(payload) => {
winner = Some((ordinal, Ok(payload)));
break;
}
OrdinalState::Failed(message) => {
winner = Some((ordinal, Err(message)));
break;
}
OrdinalState::Cancelled | OrdinalState::Pending => {}
}
}
}
if let Some((winner_ordinal, outcome)) = winner {
for ordinal in base_ordinal..base_ordinal + count {
if ordinal == winner_ordinal {
continue;
}
drop_runtime_entries(deps, pid, ordinal)?;
if recorded_terminal(history, ordinal)?.is_none() {
record_cancelled(context, ordinal)?;
}
}
state.pending_awaits.remove(&pid);
return Ok(CollectStep::RaceWon(outcome));
}
if super::nif_timeout::expired_scope_deadline(state, pid, history).is_some() {
for ordinal in base_ordinal..base_ordinal + count {
drop_runtime_entries(deps, pid, ordinal)?;
if recorded_terminal(history, ordinal)?.is_none() {
record_cancelled(context, ordinal)?;
}
}
state.pending_awaits.remove(&pid);
return Ok(CollectStep::ScopeExpired(SCOPE_EXPIRED_MESSAGE.to_owned()));
}
let mut all_cancelled = true;
for ordinal in base_ordinal..base_ordinal + count {
if recorded_terminal(history, ordinal)? != Some(OrdinalState::Cancelled) {
all_cancelled = false;
break;
}
}
if all_cancelled {
state.pending_awaits.remove(&pid);
return Ok(CollectStep::ScopeExpired(SCOPE_EXPIRED_MESSAGE.to_owned()));
}
Ok(CollectStep::Suspend)
}
fn cancel_pending(
deps: &CollectDeps,
context: &NifContext,
pid: u64,
base_ordinal: u64,
states: &[OrdinalState],
) -> Result<(), CollectError> {
for (offset, recorded) in states.iter().enumerate() {
if matches!(recorded, OrdinalState::Pending) {
let ordinal = base_ordinal + offset_to_u64(offset)?;
record_cancelled(context, ordinal)?;
drop_runtime_entries(deps, pid, ordinal)?;
}
}
Ok(())
}
fn take_and_record(
deps: &CollectDeps,
context: &NifContext,
pid: u64,
ordinal: u64,
) -> Result<OrdinalState, CollectError> {
let activity_id = ActivityId::from_sequence_position(ordinal);
let outbox_enabled = deps.runtime.outbox_enabled();
if let Some((payload, attempt)) = deps.runtime.take_activity_result(pid, ordinal)? {
let attempt = attempt.unwrap_or(FIRST_DELIVERY_ATTEMPT);
if outbox_enabled {
let result = context
.record_fan_out_completion(
Utc::now(),
ordinal,
FanOutOutcome::Completed {
result: payload.clone(),
attempt,
},
)
.map_err(|error| error.error_reason())?;
log_unexpected_drop(result, ordinal);
} else {
context
.record_activity_completed(Utc::now(), activity_id, payload.clone(), attempt)
.map_err(|error| error.error_reason())?;
}
return Ok(OrdinalState::Completed(payload_text(&payload)?));
}
if let Some((error, attempt)) = deps.runtime.take_activity_error(pid, ordinal)? {
let attempt = attempt.unwrap_or(FIRST_DELIVERY_ATTEMPT);
if outbox_enabled {
let result = context
.record_fan_out_completion(
Utc::now(),
ordinal,
FanOutOutcome::Failed {
error: terminal_error(&error.message),
attempt,
},
)
.map_err(|inner| inner.error_reason())?;
log_unexpected_drop(result, ordinal);
} else {
context
.record_activity_failed(
Utc::now(),
activity_id,
terminal_error(&error.message),
attempt,
)
.map_err(|inner| inner.error_reason())?;
}
return Ok(OrdinalState::Failed(error.message));
}
Ok(OrdinalState::Pending)
}
fn log_unexpected_drop(result: FanOutCompletionResult, ordinal: u64) {
if result == FanOutCompletionResult::Dropped {
tracing::warn!(
ordinal,
"fan-out completion dropped as duplicate within a single-writer turn (unexpected single-node)"
);
}
}
fn record_cancelled(context: &NifContext, ordinal: u64) -> Result<(), String> {
context
.record_activity_cancelled_and_settle_outbox(Utc::now(), ordinal, FIRST_DELIVERY_ATTEMPT)
.map_err(|error| error.error_reason())
}
fn drop_runtime_entries(deps: &CollectDeps, pid: u64, ordinal: u64) -> Result<(), CollectError> {
drop(deps.runtime.take_activity_result(pid, ordinal)?);
drop(deps.runtime.take_activity_error(pid, ordinal)?);
Ok(())
}
pub(super) fn recorded_terminal(
history: &[Event],
ordinal: u64,
) -> Result<Option<OrdinalState>, String> {
let target = ActivityId::from_sequence_position(ordinal);
for event in history {
match event {
Event::ActivityCompleted {
activity_id,
result,
..
} if *activity_id == target => {
return Ok(Some(OrdinalState::Completed(payload_text(result)?)));
}
Event::ActivityFailed {
activity_id, error, ..
} if *activity_id == target => {
return Ok(Some(OrdinalState::Failed(error.message.clone())));
}
Event::ActivityCancelled { activity_id, .. } if *activity_id == target => {
return Ok(Some(OrdinalState::Cancelled));
}
_ => {}
}
}
Ok(None)
}
fn recorded_race_winner(
history: &[Event],
base_ordinal: u64,
count: u64,
) -> Result<Option<RaceSettlement>, String> {
let in_range = |activity_id: &ActivityId| {
let position = activity_id.sequence_position();
position >= base_ordinal && position < base_ordinal + count
};
for event in history {
match event {
Event::ActivityCompleted {
activity_id,
result,
..
} if in_range(activity_id) => {
return Ok(Some((
activity_id.sequence_position(),
Ok(payload_text(result)?),
)));
}
Event::ActivityFailed {
activity_id, error, ..
} if in_range(activity_id) => {
return Ok(Some((
activity_id.sequence_position(),
Err(error.message.clone()),
)));
}
_ => {}
}
}
Ok(None)
}
pub(super) fn scheduled_activity_type(history: &[Event], ordinal: u64) -> Option<String> {
let target = ActivityId::from_sequence_position(ordinal);
history.iter().find_map(|event| match event {
Event::ActivityScheduled {
activity_id,
activity_type,
..
} if *activity_id == target => Some(activity_type.clone()),
_ => None,
})
}
pub(super) fn scheduled_task_queue(history: &[Event], ordinal: u64) -> Option<String> {
let target = ActivityId::from_sequence_position(ordinal);
history.iter().find_map(|event| match event {
Event::ActivityScheduled {
activity_id,
task_queue,
..
} if *activity_id == target => Some(task_queue.clone()),
_ => None,
})
}
pub(super) fn scheduled_node(history: &[Event], ordinal: u64) -> Option<String> {
let target = ActivityId::from_sequence_position(ordinal);
history.iter().find_map(|event| match event {
Event::ActivityScheduled {
activity_id, node, ..
} if *activity_id == target => node.clone(),
_ => None,
})
}
pub(super) fn started_attempt(history: &[Event], ordinal: u64) -> Option<u32> {
let target = ActivityId::from_sequence_position(ordinal);
history.iter().rev().find_map(|event| match event {
Event::ActivityStarted {
activity_id,
attempt,
..
} if *activity_id == target => Some(*attempt),
_ => None,
})
}
pub(super) fn payload_from_json_text(text: &str, label: &str) -> Result<Payload, String> {
let value = serde_json::from_str(text)
.map_err(|error| format!("{label}: invalid JSON payload: {error}"))?;
Payload::from_json(&value).map_err(|error| format!("{label}: {error}"))
}
fn payload_text(payload: &Payload) -> Result<String, String> {
String::from_utf8(payload.bytes().to_vec())
.map_err(|_| "recorded activity payload is not valid UTF-8".to_owned())
}
fn terminal_error(message: &str) -> ActivityError {
ActivityError {
kind: ActivityErrorKind::Terminal,
message: message.to_owned(),
details: None,
}
}
pub(super) fn offset_to_u64(offset: usize) -> Result<u64, String> {
u64::try_from(offset).map_err(|_| "activity offset overflows u64".to_owned())
}