canic-core 0.26.8

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
pub mod query;

use crate::{
    domain::policy,
    ops::{
        config::ConfigOps,
        ic::{IcOps, mgmt::MgmtOps},
        rpc::request::RequestOps,
        runtime::{env::EnvOps, timer::TimerId},
        storage::cycles::CycleTrackerOps,
    },
    workflow::{
        config::{WORKFLOW_CYCLE_TRACK_INTERVAL, WORKFLOW_INIT_DELAY},
        prelude::*,
        runtime::timer::TimerWorkflow,
    },
};
use std::{cell::RefCell, time::Duration};

thread_local! {
    static TIMER: RefCell<Option<TimerId>> = const { RefCell::new(None) };
    static TOPUP_IN_FLIGHT: RefCell<bool> = const { RefCell::new(false) };
}

const TRACKER_INTERVAL: Duration = WORKFLOW_CYCLE_TRACK_INTERVAL;

///
/// CycleTrackerWorkflow
///

pub struct CycleTrackerWorkflow;

impl CycleTrackerWorkflow {
    /// Start recurring cycle tracking.
    /// Safe to call multiple times.
    pub fn start() {
        Self::start_internal(true);
    }

    // Start the recurring cycle tracker with the requested policy surface.
    fn start_internal(with_auto_topup: bool) {
        let _ = TimerWorkflow::set_guarded_interval(
            &TIMER,
            WORKFLOW_INIT_DELAY,
            "cycles:init",
            move || async move {
                Self::track_internal(with_auto_topup);
            },
            TRACKER_INTERVAL,
            "cycles:interval",
            move || async move {
                Self::track_internal(with_auto_topup);
                let _ = Self::purge();
            },
        );
    }

    // Record cycle balance and optionally evaluate auto-top-up policy.
    fn track_internal(with_auto_topup: bool) {
        let ts = IcOps::now_secs();
        let cycles = MgmtOps::canister_cycle_balance();

        if with_auto_topup && !EnvOps::is_root() {
            Self::evaluate_policies(cycles.clone());
        }

        CycleTrackerOps::record(ts, cycles);
    }

    fn evaluate_policies(cycles: Cycles) {
        Self::check_auto_topup(cycles);
    }

    fn check_auto_topup(cycles: Cycles) {
        let canister_cfg = match ConfigOps::current_canister() {
            Ok(cfg) => cfg,
            Err(err) => {
                log!(Topic::Cycles, Warn, "auto topup skipped: {err}");
                return;
            }
        };
        let Some(plan) = policy::cycles::should_topup(cycles.to_u128(), &canister_cfg) else {
            return;
        };

        let should_request = TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
            if *in_flight {
                false
            } else {
                *in_flight = true;
                true
            }
        });

        if !should_request {
            return;
        }

        IcOps::spawn(async move {
            let result = RequestOps::request_cycles(plan.amount.to_u128()).await;

            TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
                *in_flight = false;
            });

            match result {
                Ok(res) => log!(
                    Topic::Cycles,
                    Ok,
                    "requested {}, topped up by {}, now {}",
                    plan.amount,
                    Cycles::from(res.cycles_transferred),
                    MgmtOps::canister_cycle_balance()
                ),
                Err(e) => log!(Topic::Cycles, Error, "failed to request cycles: {e}"),
            }
        });
    }

    /// Purge old entries based on the retention window.
    #[must_use]
    pub fn purge() -> bool {
        let now = IcOps::now_secs();
        let cutoff = policy::cycles::retention_cutoff(now);
        let purged = CycleTrackerOps::purge_before(cutoff);

        if purged > 0 {
            log!(
                Topic::Cycles,
                Info,
                "cycle_tracker: purged {purged} old entries"
            );
        }

        purged > 0
    }
}