canic_core/ops/model/memory/
cycles.rs

1pub use crate::model::memory::cycles::CycleTrackerView;
2
3use crate::{
4    cdk::{futures::spawn, utils::time::now_secs},
5    interface::ic::{
6        canister_cycle_balance,
7        timer::{Timer, TimerId},
8    },
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        timer::TimerOps,
17    },
18    types::{Cycles, PageRequest},
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 = TimerOps::set(OPS_INIT_DELAY, "cycles:init", async {
65                let _ = Self::track();
66
67                let interval =
68                    TimerOps::set_interval(TRACKER_INTERVAL_SECS, "cycles:interval", || async {
69                        let _ = Self::track();
70                        let _ = Self::purge();
71                    });
72
73                TIMER.with_borrow_mut(|slot| *slot = Some(interval));
74            });
75
76            *slot = Some(init);
77        });
78    }
79
80    /// Stop recurring tracking.
81    pub fn stop() {
82        TIMER.with_borrow_mut(|slot| {
83            if let Some(id) = slot.take() {
84                Timer::clear(id);
85            }
86        });
87    }
88
89    #[must_use]
90    pub fn track() -> bool {
91        let ts = now_secs();
92        let cycles = canister_cycle_balance().to_u128();
93
94        // only check for topup on non-root canisters
95        if !EnvOps::is_root() {
96            Self::check_auto_topup();
97        }
98
99        CycleTracker::record(ts, cycles)
100    }
101
102    /// Purge old entries based on the retention window.
103    #[must_use]
104    pub fn purge() -> bool {
105        let now = now_secs();
106        CycleTracker::purge(now) > 0
107    }
108
109    fn check_auto_topup() {
110        use crate::ops::request::cycles_request;
111
112        if let Ok(canister_cfg) = ConfigOps::current_canister()
113            && let Some(topup) = canister_cfg.topup
114        {
115            let cycles = canister_cycle_balance();
116
117            if cycles < topup.threshold {
118                spawn(async move {
119                    match cycles_request(topup.amount.to_u128()).await {
120                        Ok(res) => log!(
121                            Topic::Cycles,
122                            Ok,
123                            "💫 requested {}, topped up by {}, now {}",
124                            topup.amount,
125                            Cycles::from(res.cycles_transferred),
126                            canister_cycle_balance()
127                        ),
128                        Err(e) => log!(Topic::Cycles, Error, "💫 failed to request cycles: {e}"),
129                    }
130                });
131            }
132        }
133    }
134
135    #[must_use]
136    pub fn page(request: PageRequest) -> CycleTrackerPage {
137        let entries = CycleTracker::entries(request);
138        let total = CycleTracker::len();
139
140        CycleTrackerPage { entries, total }
141    }
142}