rustvello 0.8.0

Distributed task queue and workflow runtime for Rust and Python: typed tasks, retries, priorities, triggers and pluggable backends
//! Recovery and runner maintenance use cases.

use rustvello_core::error::{RustvelloError, RustvelloResult};
use rustvello_proto::config::AppConfig;
use rustvello_proto::identifiers::{InvocationId, RunnerId};
use rustvello_proto::status::InvocationStatus;

use crate::task_catalog::TaskCatalog;

use super::Orchestrator;

impl Orchestrator {
    /// Recover stale pending/running invocations and republish them once.
    pub(crate) async fn recover_stale_invocations(
        &self,
        app_config: &AppConfig,
        task_catalog: &TaskCatalog,
        runner_id: &RunnerId,
    ) -> RustvelloResult<u32> {
        let mut recovered = 0;
        let stale_pending = self
            .backends
            .invocation_control
            .get_stale_pending_invocations(app_config.max_pending_seconds)
            .await?;
        for invocation_id in stale_pending {
            if self
                .recover_one(
                    app_config,
                    task_catalog,
                    runner_id,
                    &invocation_id,
                    InvocationStatus::PendingRecovery,
                )
                .await?
            {
                recovered += 1;
            }
        }

        let stale_running = self
            .backends
            .invocation_control
            .get_stale_running_invocations(app_config.runner_dead_after_seconds)
            .await?;
        for invocation_id in stale_running {
            if self
                .recover_one(
                    app_config,
                    task_catalog,
                    runner_id,
                    &invocation_id,
                    InvocationStatus::RunningRecovery,
                )
                .await?
            {
                recovered += 1;
            }
        }

        Ok(recovered)
    }

    async fn recover_one(
        &self,
        app_config: &AppConfig,
        task_catalog: &TaskCatalog,
        runner_id: &RunnerId,
        invocation_id: &InvocationId,
        recovery_status: InvocationStatus,
    ) -> RustvelloResult<bool> {
        if let Some(publication) = self.publication()? {
            let invocation = self
                .backends
                .state_backend
                .get_invocation(invocation_id)
                .await?;
            let (queue, priority) = task_catalog
                .routing_for(app_config, &invocation.task_id)
                .ok_or_else(|| RustvelloError::TaskNotRegistered {
                    task_id: invocation.task_id.clone(),
                })?;
            let stale_after_seconds = if recovery_status == InvocationStatus::PendingRecovery {
                app_config.max_pending_seconds
            } else {
                app_config.runner_dead_after_seconds
            };
            let recovered = publication
                .change(
                    invocation_id,
                    runner_id,
                    rustvello_core::publication::PublicationChange::Recover {
                        status: recovery_status,
                        stale_after_seconds,
                        route: rustvello_core::publication::PublicationRoute { queue, priority },
                    },
                    false,
                )
                .await?
                .is_some();
            if recovered {
                let arguments = self.get_invocation_arguments(invocation_id).await;
                for status in [recovery_status, InvocationStatus::Rerouted] {
                    self.report_published_status(
                        invocation_id,
                        runner_id,
                        status,
                        &invocation.task_id,
                        arguments.clone(),
                    )
                    .await?;
                }
            }
            return Ok(recovered);
        }
        match self
            .set_invocation_status(invocation_id, recovery_status, runner_id)
            .await
        {
            Ok(_) => {}
            Err(RustvelloError::InvalidStatusTransition { .. }) => return Ok(false),
            Err(error) => return Err(error),
        }

        let invocation = self
            .backends
            .state_backend
            .get_invocation(invocation_id)
            .await?;
        let (queue, priority) = task_catalog
            .routing_for(app_config, &invocation.task_id)
            .ok_or_else(|| RustvelloError::TaskNotRegistered {
                task_id: invocation.task_id.clone(),
            })?;

        self.set_invocation_status(invocation_id, InvocationStatus::Rerouted, runner_id)
            .await?;
        self.backends
            .broker
            .route_invocation_with_options(
                invocation_id,
                Some(&invocation.task_id),
                &queue,
                priority,
            )
            .await?;
        Ok(true)
    }
}