canic_core/ops/runtime/
cycles.rs1pub use crate::ops::storage::cycles::CycleTrackerView;
2
3use crate::{
4 cdk::{futures::spawn, utils::time::now_secs},
5 dto::Page,
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, PageRequest},
18};
19use std::{cell::RefCell, time::Duration};
20
21thread_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
31const TRACKER_INTERVAL: Duration = OPS_CYCLE_TRACK_INTERVAL;
36
37pub struct CycleTrackerOps;
42
43impl CycleTrackerOps {
44 pub fn start() {
47 TIMER.with_borrow_mut(|slot| {
48 if slot.is_some() {
49 return;
50 }
51
52 let init = TimerOps::set(OPS_INIT_DELAY, "cycles:init", async {
53 Self::track();
54
55 let interval =
56 TimerOps::set_interval(TRACKER_INTERVAL, "cycles:interval", || async {
57 Self::track();
58 let _ = Self::purge();
59 });
60
61 TIMER.with_borrow_mut(|slot| *slot = Some(interval));
62 });
63
64 *slot = Some(init);
65 });
66 }
67
68 pub fn stop() {
70 TIMER.with_borrow_mut(|slot| {
71 if let Some(id) = slot.take() {
72 TimerOps::clear(id);
73 }
74 });
75 }
76
77 pub fn track() {
78 let ts = now_secs();
79 let cycles = canister_cycle_balance().to_u128();
80
81 if !EnvOps::is_root() {
82 Self::evaluate_policies(cycles);
83 }
84
85 CycleTrackerStorageOps::record(ts, cycles);
86 }
87
88 fn evaluate_policies(cycles: u128) {
89 Self::check_auto_topup(cycles);
90 }
91
92 fn check_auto_topup(cycles: u128) {
93 use crate::ops::rpc::cycles_request;
94
95 let Ok(canister_cfg) = ConfigOps::current_canister() else {
98 return;
99 };
100 let Some(topup) = canister_cfg.topup else {
101 return;
102 };
103
104 if cycles >= topup.threshold.to_u128() {
106 return;
107 }
108
109 let should_request = TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
112 if *in_flight {
113 false
114 } else {
115 *in_flight = true;
116 true
117 }
118 });
119
120 if !should_request {
121 return;
122 }
123
124 spawn(async move {
127 let result = cycles_request(topup.amount.to_u128()).await;
128
129 TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
130 *in_flight = false;
131 });
132
133 match result {
134 Ok(res) => log!(
135 Topic::Cycles,
136 Ok,
137 "requested {}, topped up by {}, now {}",
138 topup.amount,
139 Cycles::from(res.cycles_transferred),
140 canister_cycle_balance()
141 ),
142 Err(e) => log!(Topic::Cycles, Error, "failed to request cycles: {e}"),
143 }
144 });
145 }
146
147 #[must_use]
149 pub fn purge() -> bool {
150 let now = now_secs();
151 let purged = CycleTrackerStorageOps::purge(now);
152
153 if purged > 0 {
154 log!(
155 Topic::Cycles,
156 Info,
157 "cycle_tracker: purged {purged} old entries"
158 );
159 }
160
161 purged > 0
162 }
163
164 #[must_use]
165 pub fn page(request: PageRequest) -> Page<(u64, Cycles)> {
166 let entries = CycleTrackerStorageOps::entries(request);
167 let total = CycleTrackerStorageOps::len();
168
169 Page { entries, total }
170 }
171}