canic_core/ops/runtime/
cycles.rs

1pub use crate::ops::storage::cycles::CycleTrackerView;
2
3use crate::{
4    cdk::{futures::spawn, utils::time::now_secs},
5    dto::page::{Page, PageRequest},
6    log,
7    log::Topic,
8    ops::{
9        OPS_CYCLE_TRACK_INTERVAL, OPS_INIT_DELAY,
10        config::ConfigOps,
11        ic::{
12            canister_cycle_balance,
13            timer::{TimerId, TimerOps},
14        },
15        storage::{cycles::CycleTrackerStorageOps, env::EnvOps},
16    },
17    types::Cycles,
18};
19use std::{cell::RefCell, time::Duration};
20
21//
22// TIMER
23//
24
25thread_local! {
26    static TIMER: RefCell<Option<TimerId>> = const { RefCell::new(None) };
27
28    static TOPUP_IN_FLIGHT: RefCell<bool> = const { RefCell::new(false) };
29}
30
31///
32/// Constants
33///
34
35const TRACKER_INTERVAL: Duration = OPS_CYCLE_TRACK_INTERVAL;
36
37///
38/// CycleTrackerOps
39///
40
41pub struct CycleTrackerOps;
42
43impl CycleTrackerOps {
44    /// Start recurring tracking every X seconds
45    /// Safe to call multiple times: only one loop will run.
46    pub fn start() {
47        let _ = TimerOps::set_guarded_interval(
48            &TIMER,
49            OPS_INIT_DELAY,
50            "cycles:init",
51            || async {
52                Self::track();
53            },
54            TRACKER_INTERVAL,
55            "cycles:interval",
56            || async {
57                Self::track();
58                let _ = Self::purge();
59            },
60        );
61    }
62
63    /// Stop recurring tracking.
64    pub fn stop() {
65        let _ = TimerOps::clear_guarded(&TIMER);
66    }
67
68    pub fn track() {
69        let ts = now_secs();
70        let cycles = canister_cycle_balance().to_u128();
71
72        if !EnvOps::is_root() {
73            Self::evaluate_policies(cycles);
74        }
75
76        CycleTrackerStorageOps::record(ts, cycles);
77    }
78
79    fn evaluate_policies(cycles: u128) {
80        Self::check_auto_topup(cycles);
81    }
82
83    fn check_auto_topup(cycles: u128) {
84        use crate::ops::rpc::cycles_request;
85
86        let canister_cfg = ConfigOps::current_canister();
87        let Some(topup) = canister_cfg.topup else {
88            return;
89        };
90
91        // If current balance is above the configured threshold, do not request cycles.
92        if cycles >= topup.threshold.to_u128() {
93            return;
94        }
95
96        // Prevent concurrent or overlapping top-up requests.
97        // This avoids spamming root if multiple ticks fire while a request is in flight.
98        let should_request = TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
99            if *in_flight {
100                false
101            } else {
102                *in_flight = true;
103                true
104            }
105        });
106
107        if !should_request {
108            return;
109        }
110
111        // Perform the top-up asynchronously.
112        // The in-flight flag is cleared regardless of success or failure.
113        spawn(async move {
114            let result = cycles_request(topup.amount.to_u128()).await;
115
116            TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
117                *in_flight = false;
118            });
119
120            match result {
121                Ok(res) => log!(
122                    Topic::Cycles,
123                    Ok,
124                    "requested {}, topped up by {}, now {}",
125                    topup.amount,
126                    Cycles::from(res.cycles_transferred),
127                    canister_cycle_balance()
128                ),
129                Err(e) => log!(Topic::Cycles, Error, "failed to request cycles: {e}"),
130            }
131        });
132    }
133
134    /// Purge old entries based on the retention window.
135    #[must_use]
136    pub fn purge() -> bool {
137        let now = now_secs();
138        let purged = CycleTrackerStorageOps::purge(now);
139
140        if purged > 0 {
141            log!(
142                Topic::Cycles,
143                Info,
144                "cycle_tracker: purged {purged} old entries"
145            );
146        }
147
148        purged > 0
149    }
150
151    #[must_use]
152    pub fn page(request: PageRequest) -> Page<(u64, Cycles)> {
153        let entries = CycleTrackerStorageOps::entries(request);
154        let total = CycleTrackerStorageOps::len();
155
156        Page { entries, total }
157    }
158}