canic_core/ops/model/memory/
cycles.rs

1pub use crate::model::memory::cycles::CycleTrackerView;
2
3use crate::{
4    cdk::{
5        futures::spawn,
6        timers::{TimerId, clear_timer, set_timer, set_timer_interval},
7    },
8    interface::ic::canister_cycle_balance,
9    log,
10    log::Topic,
11    model::memory::cycles::CycleTracker,
12    ops::{
13        config::ConfigOps,
14        model::memory::EnvOps,
15        model::{OPS_CYCLE_TRACK_INTERVAL, OPS_INIT_DELAY},
16    },
17    types::Cycles,
18    utils::time::now_secs,
19};
20use candid::CandidType;
21use serde::Serialize;
22use std::{cell::RefCell, time::Duration};
23
24//
25// TIMER
26//
27
28thread_local! {
29    static TIMER: RefCell<Option<TimerId>> = const { RefCell::new(None) };
30}
31
32///
33/// Constants
34///
35
36// Check every 10 minutes
37const TRACKER_INTERVAL_SECS: Duration = OPS_CYCLE_TRACK_INTERVAL;
38
39///
40/// CycleTrackerPage
41///
42
43#[derive(CandidType, Serialize)]
44pub struct CycleTrackerPage {
45    pub entries: CycleTrackerView,
46    pub total: u64,
47}
48
49///
50/// CycleTrackerOps
51///
52
53pub struct CycleTrackerOps;
54
55impl CycleTrackerOps {
56    /// Start recurring tracking every X seconds
57    /// Safe to call multiple times: only one loop will run.
58    pub fn start() {
59        TIMER.with_borrow_mut(|slot| {
60            if slot.is_some() {
61                return;
62            }
63
64            let init = set_timer(OPS_INIT_DELAY, async {
65                let _ = Self::track();
66
67                let interval = set_timer_interval(TRACKER_INTERVAL_SECS, || async {
68                    let _ = Self::track();
69                    let _ = Self::purge();
70                });
71
72                TIMER.with_borrow_mut(|slot| *slot = Some(interval));
73            });
74
75            *slot = Some(init);
76        });
77    }
78
79    /// Stop recurring tracking.
80    pub fn stop() {
81        TIMER.with_borrow_mut(|slot| {
82            if let Some(id) = slot.take() {
83                clear_timer(id);
84            }
85        });
86    }
87
88    #[must_use]
89    pub fn track() -> bool {
90        let ts = now_secs();
91        let cycles = canister_cycle_balance().to_u128();
92
93        // only check for topup on non-root canisters
94        if !EnvOps::is_root() {
95            Self::check_auto_topup();
96        }
97
98        CycleTracker::record(ts, cycles)
99    }
100
101    /// Purge old entries based on the retention window.
102    #[must_use]
103    pub fn purge() -> bool {
104        let now = now_secs();
105        CycleTracker::purge(now) > 0
106    }
107
108    fn check_auto_topup() {
109        use crate::ops::request::cycles_request;
110
111        if let Ok(canister_cfg) = ConfigOps::current_canister()
112            && let Some(topup) = canister_cfg.topup
113        {
114            let cycles = canister_cycle_balance();
115
116            if cycles < topup.threshold {
117                spawn(async move {
118                    match cycles_request(topup.amount.to_u128()).await {
119                        Ok(res) => log!(
120                            Topic::Cycles,
121                            Ok,
122                            "💫 requested {}, topped up by {}, now {}",
123                            topup.amount,
124                            Cycles::from(res.cycles_transferred),
125                            canister_cycle_balance()
126                        ),
127                        Err(e) => log!(Topic::Cycles, Error, "💫 failed to request cycles: {e}"),
128                    }
129                });
130            }
131        }
132    }
133
134    #[must_use]
135    pub fn page(offset: u64, limit: u64) -> CycleTrackerPage {
136        let entries = CycleTracker::entries(offset, limit);
137        let total = CycleTracker::len();
138
139        CycleTrackerPage { entries, total }
140    }
141}