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::{Page, PageRequest},
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,
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 let _ = TimerOps::set_guarded_interval(
48 &TIMER,
49 OPS_INIT_DELAY,
50 "cycles:init",
51 || async {
52 Self::track();
53 },
54 TRACKER_INTERVAL,
55 "cycles:interval",
56 || async {
57 Self::track();
58 let _ = Self::purge();
59 },
60 );
61 }
62
63 pub fn stop() {
65 let _ = TimerOps::clear_guarded(&TIMER);
66 }
67
68 pub fn track() {
69 let ts = now_secs();
70 let cycles = canister_cycle_balance().to_u128();
71
72 if !EnvOps::is_root() {
73 Self::evaluate_policies(cycles);
74 }
75
76 CycleTrackerStorageOps::record(ts, cycles);
77 }
78
79 fn evaluate_policies(cycles: u128) {
80 Self::check_auto_topup(cycles);
81 }
82
83 fn check_auto_topup(cycles: u128) {
84 use crate::ops::rpc::cycles_request;
85
86 let canister_cfg = ConfigOps::current_canister();
87 let Some(topup) = canister_cfg.topup else {
88 return;
89 };
90
91 if cycles >= topup.threshold.to_u128() {
93 return;
94 }
95
96 let should_request = TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
99 if *in_flight {
100 false
101 } else {
102 *in_flight = true;
103 true
104 }
105 });
106
107 if !should_request {
108 return;
109 }
110
111 spawn(async move {
114 let result = cycles_request(topup.amount.to_u128()).await;
115
116 TOPUP_IN_FLIGHT.with_borrow_mut(|in_flight| {
117 *in_flight = false;
118 });
119
120 match result {
121 Ok(res) => log!(
122 Topic::Cycles,
123 Ok,
124 "requested {}, topped up by {}, now {}",
125 topup.amount,
126 Cycles::from(res.cycles_transferred),
127 canister_cycle_balance()
128 ),
129 Err(e) => log!(Topic::Cycles, Error, "failed to request cycles: {e}"),
130 }
131 });
132 }
133
134 #[must_use]
136 pub fn purge() -> bool {
137 let now = now_secs();
138 let purged = CycleTrackerStorageOps::purge(now);
139
140 if purged > 0 {
141 log!(
142 Topic::Cycles,
143 Info,
144 "cycle_tracker: purged {purged} old entries"
145 );
146 }
147
148 purged > 0
149 }
150
151 #[must_use]
152 pub fn page(request: PageRequest) -> Page<(u64, Cycles)> {
153 let entries = CycleTrackerStorageOps::entries(request);
154 let total = CycleTrackerStorageOps::len();
155
156 Page { entries, total }
157 }
158}