canic_core/ops/model/memory/
cycles.rs1pub 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, PageRequest},
18 utils::time::now_secs,
19};
20use candid::CandidType;
21use serde::Serialize;
22use std::{cell::RefCell, time::Duration};
23
24thread_local! {
29 static TIMER: RefCell<Option<TimerId>> = const { RefCell::new(None) };
30}
31
32const TRACKER_INTERVAL_SECS: Duration = OPS_CYCLE_TRACK_INTERVAL;
38
39#[derive(CandidType, Serialize)]
44pub struct CycleTrackerPage {
45 pub entries: CycleTrackerView,
46 pub total: u64,
47}
48
49pub struct CycleTrackerOps;
54
55impl CycleTrackerOps {
56 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 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 if !EnvOps::is_root() {
95 Self::check_auto_topup();
96 }
97
98 CycleTracker::record(ts, cycles)
99 }
100
101 #[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(request: PageRequest) -> CycleTrackerPage {
136 let entries = CycleTracker::entries(request);
137 let total = CycleTracker::len();
138
139 CycleTrackerPage { entries, total }
140 }
141}