use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use a3s_boot::{BootError, Queue, QueueJob, QueueJobOptions, QueueJobReceipt, QueueRetryPolicy};
use async_trait::async_trait;
use sha2::{Digest, Sha256};
use crate::engine::FlowEngine;
use crate::error::{FlowError, Result};
use super::runner::handle_flow_task;
use super::{FlowTask, FlowTaskDispatcher};
const DEFAULT_FLOW_JOB_NAME: &str = "a3s.flow.task";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum BootFlowTaskDeduplication {
#[default]
Disabled,
UntilTerminal,
UntilTerminalOrTtl(Duration),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BootFlowTaskPolicy {
retry_policy: QueueRetryPolicy,
timeout: Option<Duration>,
max_stalled_count: u32,
remove_on_complete: bool,
remove_on_fail: bool,
deduplication: BootFlowTaskDeduplication,
}
impl Default for BootFlowTaskPolicy {
fn default() -> Self {
Self {
retry_policy: QueueRetryPolicy::none(),
timeout: None,
max_stalled_count: 1,
remove_on_complete: false,
remove_on_fail: false,
deduplication: BootFlowTaskDeduplication::Disabled,
}
}
}
impl BootFlowTaskPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn with_retry_policy(mut self, retry_policy: QueueRetryPolicy) -> Self {
self.retry_policy = retry_policy;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_max_stalled_count(mut self, max_stalled_count: u32) -> Self {
self.max_stalled_count = max_stalled_count;
self
}
pub fn remove_on_complete(mut self, remove: bool) -> Self {
self.remove_on_complete = remove;
self
}
pub fn remove_on_fail(mut self, remove: bool) -> Self {
self.remove_on_fail = remove;
self
}
pub fn with_deduplication(mut self, deduplication: BootFlowTaskDeduplication) -> Self {
self.deduplication = deduplication;
self
}
pub fn retry_policy(&self) -> &QueueRetryPolicy {
&self.retry_policy
}
pub fn timeout(&self) -> Option<Duration> {
self.timeout
}
pub fn max_stalled_count(&self) -> u32 {
self.max_stalled_count
}
pub fn removes_completed_jobs(&self) -> bool {
self.remove_on_complete
}
pub fn removes_failed_jobs(&self) -> bool {
self.remove_on_fail
}
pub fn deduplication(&self) -> BootFlowTaskDeduplication {
self.deduplication
}
fn validate(&self) -> Result<()> {
if matches!(
self.deduplication,
BootFlowTaskDeduplication::UntilTerminalOrTtl(ttl) if ttl.is_zero()
) {
return Err(FlowError::InvalidWorkerConfiguration(
"Boot Flow task deduplication TTL must be greater than zero".to_string(),
));
}
Ok(())
}
fn job_options_for(&self, job_name: &str, task: &FlowTask) -> QueueJobOptions {
let mut options = QueueJobOptions::new()
.with_retry_policy(self.retry_policy.clone())
.with_max_stalled_count(self.max_stalled_count)
.remove_on_complete(self.remove_on_complete)
.remove_on_fail(self.remove_on_fail);
if let Some(timeout) = self.timeout {
options = options.with_timeout(timeout);
}
let ttl = match self.deduplication {
BootFlowTaskDeduplication::Disabled => return options,
BootFlowTaskDeduplication::UntilTerminal => None,
BootFlowTaskDeduplication::UntilTerminalOrTtl(ttl) => Some(ttl),
};
options = options.with_deduplication_id(flow_task_deduplication_id(job_name, task));
if let Some(deduplication) = options.deduplication.as_mut() {
deduplication.ttl = ttl;
deduplication.keep_last_if_active = flow_task_needs_active_successor(task);
}
options
}
}
#[derive(Clone)]
pub struct BootFlowTaskManager {
engine: FlowEngine,
queue: Arc<Queue>,
job_name: String,
task_policy: BootFlowTaskPolicy,
}
impl fmt::Debug for BootFlowTaskManager {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BootFlowTaskManager")
.field("queue", &self.queue.name())
.field("job_name", &self.job_name)
.field("task_policy", &self.task_policy)
.finish_non_exhaustive()
}
}
impl BootFlowTaskManager {
pub fn new(engine: FlowEngine, queue: Arc<Queue>) -> Self {
Self {
engine,
queue,
job_name: DEFAULT_FLOW_JOB_NAME.to_string(),
task_policy: BootFlowTaskPolicy::new(),
}
}
pub fn with_job_name(mut self, job_name: impl Into<String>) -> Result<Self> {
let job_name = job_name.into().trim().to_string();
if job_name.is_empty() {
return Err(FlowError::InvalidWorkerConfiguration(
"Boot Flow job name cannot be empty".to_string(),
));
}
self.job_name = job_name;
Ok(self)
}
pub fn with_task_policy(mut self, task_policy: BootFlowTaskPolicy) -> Result<Self> {
task_policy.validate()?;
self.task_policy = task_policy;
Ok(self)
}
pub fn engine(&self) -> &FlowEngine {
&self.engine
}
pub fn queue(&self) -> Arc<Queue> {
Arc::clone(&self.queue)
}
pub fn job_name(&self) -> &str {
&self.job_name
}
pub fn task_policy(&self) -> &BootFlowTaskPolicy {
&self.task_policy
}
pub fn job_options_for(&self, task: &FlowTask) -> QueueJobOptions {
self.task_policy.job_options_for(&self.job_name, task)
}
pub fn register(&self) -> Result<()> {
let engine = self.engine.clone();
self.queue
.process(self.job_name.clone(), move |job: QueueJob, _context| {
let engine = engine.clone();
async move {
let task = job.data_as::<FlowTask>()?;
handle_flow_task(&engine, task).await.map_err(|error| {
BootError::Internal(format!("A3S Flow task handling failed: {error}"))
})?;
Ok(())
}
})
.map_err(boot_error)
}
pub async fn enqueue_with_receipt(&self, task: FlowTask) -> Result<QueueJobReceipt> {
let options = self.job_options_for(&task);
self.enqueue_with_options(task, options).await
}
pub async fn enqueue_with_options(
&self,
task: FlowTask,
options: QueueJobOptions,
) -> Result<QueueJobReceipt> {
self.queue
.enqueue_with_options(self.job_name.clone(), &task, options)
.await
.map_err(boot_error)
}
}
#[async_trait]
impl FlowTaskDispatcher for BootFlowTaskManager {
async fn dispatch(&self, task: FlowTask) -> Result<()> {
self.enqueue_with_receipt(task).await.map(|_| ())
}
}
fn boot_error(error: BootError) -> FlowError {
FlowError::TaskManagement(format!("A3S Boot queue error: {error}"))
}
fn flow_task_deduplication_id(job_name: &str, task: &FlowTask) -> String {
let mut hasher = Sha256::new();
hash_deduplication_field(&mut hasher, job_name);
let kind = match task {
FlowTask::DriveRun { run_id } => {
hash_deduplication_field(&mut hasher, run_id);
"drive_run"
}
FlowTask::ResumeWait { run_id, wait_id } => {
hash_deduplication_field(&mut hasher, run_id);
hash_deduplication_field(&mut hasher, wait_id);
"resume_wait"
}
FlowTask::ResumeHook {
run_id, hook_id, ..
} => {
hash_deduplication_field(&mut hasher, run_id);
hash_deduplication_field(&mut hasher, hook_id);
"resume_hook"
}
FlowTask::ResumeHookByToken { token, .. } => {
hash_deduplication_field(&mut hasher, token);
"resume_hook_by_token"
}
FlowTask::DisposeHook { run_id, hook_id } => {
hash_deduplication_field(&mut hasher, run_id);
hash_deduplication_field(&mut hasher, hook_id);
"dispose_hook"
}
FlowTask::DisposeHookByToken { token } => {
hash_deduplication_field(&mut hasher, token);
"dispose_hook_by_token"
}
FlowTask::ResumeScheduledRun { run_id, .. } => {
hash_deduplication_field(&mut hasher, run_id);
"resume_scheduled_run"
}
FlowTask::ResumeDueWaits { .. } => "resume_due_waits",
FlowTask::ResumeDueRetries { .. } => "resume_due_retries",
};
hash_deduplication_field(&mut hasher, kind);
format!("a3s-flow:{kind}:{:x}", hasher.finalize())
}
fn hash_deduplication_field(hasher: &mut Sha256, value: &str) {
let length = u64::try_from(value.len()).unwrap_or(u64::MAX);
hasher.update(length.to_be_bytes());
hasher.update(value.as_bytes());
}
fn flow_task_needs_active_successor(task: &FlowTask) -> bool {
matches!(
task,
FlowTask::DriveRun { .. }
| FlowTask::ResumeScheduledRun { .. }
| FlowTask::ResumeDueWaits { .. }
| FlowTask::ResumeDueRetries { .. }
)
}