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