use aion_core::{ActivityId, Event, UnservedActivity, WorkflowId};
use chrono::{DateTime, Utc};
use super::census::classify;
use super::declarations::QueueDeclarationSource;
use super::state::QueueServiceState;
use super::taxonomy::ServiceAddress;
use crate::error::ServerError;
use crate::worker::registry::ConnectedWorkerRegistry;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OpenActivity {
pub activity_id: ActivityId,
pub activity_type: String,
pub task_queue: String,
pub node: Option<String>,
pub attempt: u32,
pub dispatched_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
struct Open {
activity_id: ActivityId,
activity_type: String,
task_queue: String,
node: Option<String>,
attempt: u32,
dispatched_at: DateTime<Utc>,
started: bool,
}
#[must_use]
pub fn open_activities_in_active_segment(history: &[Event]) -> Vec<OpenActivity> {
let segment_start = history
.iter()
.rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
.unwrap_or(0);
let mut open: Vec<Open> = Vec::new();
for event in &history[segment_start..] {
match event {
Event::ActivityScheduled {
envelope,
activity_id,
activity_type,
task_queue,
node,
..
} => {
let entry = Open {
activity_id: activity_id.clone(),
activity_type: activity_type.clone(),
task_queue: task_queue.clone(),
node: node.clone(),
attempt: 0,
dispatched_at: envelope.recorded_at,
started: false,
};
match open
.iter_mut()
.find(|held| &held.activity_id == activity_id)
{
Some(held) => *held = entry,
None => open.push(entry),
}
}
Event::ActivityStarted {
envelope,
activity_id,
attempt,
} => {
if let Some(held) = open
.iter_mut()
.find(|held| &held.activity_id == activity_id)
{
held.attempt = *attempt;
held.dispatched_at = envelope.recorded_at;
held.started = true;
}
}
Event::ActivityCompleted { activity_id, .. }
| Event::ActivityFailed { activity_id, .. }
| Event::ActivityCancelled { activity_id, .. } => {
open.retain(|held| &held.activity_id != activity_id);
}
_ => {}
}
}
open.into_iter()
.filter(|held| held.started)
.map(|held| OpenActivity {
activity_id: held.activity_id,
activity_type: held.activity_type,
task_queue: held.task_queue,
node: held.node,
attempt: held.attempt,
dispatched_at: held.dispatched_at,
})
.collect()
}
pub struct ActivityReachability<'a> {
pub registry: &'a ConnectedWorkerRegistry,
pub declarations: &'a QueueDeclarationSource,
pub state: &'a QueueServiceState,
}
impl ActivityReachability<'_> {
pub fn unserved(
&self,
namespace: &str,
workflow_id: &WorkflowId,
history: &[Event],
) -> Result<Vec<UnservedActivity>, ServerError> {
let mut unserved = Vec::new();
for open in open_activities_in_active_segment(history) {
let address = ServiceAddress {
namespace: namespace.to_owned(),
task_queue: open.task_queue,
activity_type: open.activity_type,
node: open.node,
};
let census = self.registry.pool_census(
&address.namespace,
&address.task_queue,
&address.activity_type,
address.node.as_deref(),
)?;
let declaration = self.declarations.declaration_for(&address.task_queue);
let Some(reason) = classify(declaration, &census) else {
continue;
};
let dispatch_parked = self.state.is_unserved(workflow_id, &open.activity_id)?;
unserved.push(UnservedActivity {
activity_id: open.activity_id,
reason: reason.as_str().to_owned(),
detail: reason.explain(&address),
activity_type: address.activity_type,
task_queue: address.task_queue,
node: address.node,
attempt: open.attempt,
dispatched_at: open.dispatched_at,
workers_in_pool: count(census.workers_in_pool),
workers_serving_activity: count(census.workers_serving_activity),
compatible_workers: count(census.compatible_workers),
dispatch_parked,
});
}
Ok(unserved)
}
}
fn count(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
#[cfg(test)]
#[path = "reachability_tests.rs"]
mod tests;