Skip to main content

canic_core/dto/
pool.rs

1//! Passive boundary contracts for the Fleet Subnet Root physical-Canister inventory.
2
3use crate::{
4    cdk::types::Cycles,
5    ids::{ComponentInstanceId, FleetSubnetCanisterPoolConfig},
6};
7use candid::{CandidType, Principal};
8use serde::{Deserialize, Serialize};
9
10/// Identifies the durable Component allocation that has claimed one empty Canister.
11#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct CanisterPoolClaim {
13    pub component: ComponentInstanceId,
14    pub operation_id: [u8; 32],
15}
16
17/// Reset outcome retained while a stopped workload is still Registry-owned.
18#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
19pub enum CanisterPoolRecycleReset {
20    Pending,
21    Ready,
22    Failed { reason: String },
23}
24
25/// How one physical Canister entered the root-owned inventory.
26#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
27pub enum CanisterPoolAssetOrigin {
28    InfrastructureStore,
29    Created,
30    Imported,
31    Recycled,
32}
33
34/// Current durable state of one root-owned physical Canister.
35#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36pub enum CanisterPoolAssetStatus {
37    Store,
38    StoreDeletionPending {
39        operation_id: [u8; 32],
40    },
41    PendingReset,
42    Ready,
43    Claimed {
44        claim: CanisterPoolClaim,
45    },
46    Workload {
47        claim: CanisterPoolClaim,
48    },
49    Recycling {
50        claim: CanisterPoolClaim,
51        reset: CanisterPoolRecycleReset,
52    },
53    HandingOff {
54        recipient: Principal,
55    },
56    Failed {
57        reason: String,
58    },
59}
60
61/// Controller-visible inventory row for one root-owned physical Canister.
62#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
63pub struct CanisterPoolAsset {
64    pub canister_id: Principal,
65    pub cycles: Cycles,
66    pub origin: CanisterPoolAssetOrigin,
67    pub status: CanisterPoolAssetStatus,
68    pub added_at_ns: u64,
69    pub updated_at_ns: u64,
70}
71
72/// Durable transfer of one paid asset to replacement authority during root draining.
73#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
74pub struct CanisterPoolHandoff {
75    pub canister_id: Principal,
76    pub recipient: Principal,
77    pub prepared_at_ns: u64,
78}
79
80/// Why one autonomous refill stopped without creating another Canister.
81#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
82pub enum CanisterPoolCreationFailure {
83    UnresolvedAfterLedgerWindow,
84    LedgerCreationFailed,
85    LedgerRejected,
86}
87
88/// Durable controller-visible progress of one autonomous refill.
89#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
90pub enum CanisterPoolCreationProgress {
91    Intent {
92        uncertain_result: bool,
93    },
94    Created {
95        block_index: u64,
96        canister_id: Principal,
97    },
98    Blocked {
99        failure: CanisterPoolCreationFailure,
100    },
101}
102
103/// Exact Cycles Ledger request retained until its principal is in inventory.
104#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub struct CanisterPoolCreation {
106    pub operation_id: [u8; 32],
107    pub cycles_ledger: Principal,
108    pub placement_subnet: Principal,
109    pub root: Principal,
110    pub ledger_amount: Cycles,
111    pub created_at_time_ns: u64,
112    pub progress: CanisterPoolCreationProgress,
113}
114
115/// Bounded controller query for one canonical inventory page.
116#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
117pub struct CanisterPoolStatusRequest {
118    pub start_after: Option<Principal>,
119    pub limit: u16,
120}
121
122/// Exact pool policy and current exclusive root-owned physical inventory.
123#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
124pub struct CanisterPoolResponse {
125    pub config: FleetSubnetCanisterPoolConfig,
126    pub tracked: u32,
127    pub store: u32,
128    pub store_deletion_pending: u32,
129    pub pooled: u32,
130    pub workload: u32,
131    pub surplus: u32,
132    pub ready: u32,
133    pub pending_reset: u32,
134    pub claimed: u32,
135    pub recycling: u32,
136    pub handing_off: u32,
137    pub failed: u32,
138    pub completed_handoffs: u64,
139    pub pending_creation: Option<CanisterPoolCreation>,
140    pub pending_handoff: Option<CanisterPoolHandoff>,
141    pub entries: Vec<CanisterPoolAsset>,
142    pub next_start_after: Option<Principal>,
143}
144
145/// Controller command for explicit pool maintenance.
146#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
147pub enum PoolAdminCommand {
148    Maintain,
149    RetryRefill,
150    Import {
151        canister_id: Principal,
152    },
153    RetryReset {
154        canister_id: Principal,
155    },
156    Handoff {
157        canister_id: Principal,
158        recipient: Principal,
159    },
160}
161
162/// Result of one explicit pool maintenance command.
163#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
164pub enum PoolAdminResponse {
165    Maintained,
166    MaintenancePaused {
167        reason: String,
168    },
169    Created {
170        canister_id: Principal,
171    },
172    RefillWaitingForCycles {
173        available: Cycles,
174        creation_amount: Cycles,
175    },
176    RefillPending {
177        operation_id: [u8; 32],
178        uncertain_result: bool,
179    },
180    RefillBlocked {
181        operation_id: [u8; 32],
182        failure: CanisterPoolCreationFailure,
183    },
184    RefillRetryScheduled {
185        previous_operation_id: [u8; 32],
186    },
187    Imported {
188        canister_id: Principal,
189    },
190    ResetQueued {
191        canister_id: Principal,
192    },
193    ResetReady {
194        canister_id: Principal,
195    },
196    HandedOff {
197        canister_id: Principal,
198        recipient: Principal,
199    },
200    ResetFailed {
201        canister_id: Principal,
202        reason: String,
203    },
204}
205
206// -----------------------------------------------------------------------------
207// Tests
208// -----------------------------------------------------------------------------
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn pool_status_and_admin_contracts_round_trip_through_candid() {
216        let canister_id = Principal::from_slice(&[7; 29]);
217        let response = CanisterPoolResponse {
218            config: FleetSubnetCanisterPoolConfig {
219                minimum_size: 3,
220                maximum_size: 10,
221                canister_cycles: Cycles::new(5_000_000_000_000),
222            },
223            tracked: 1,
224            store: 0,
225            store_deletion_pending: 0,
226            pooled: 1,
227            workload: 0,
228            surplus: 0,
229            ready: 0,
230            pending_reset: 0,
231            claimed: 0,
232            recycling: 0,
233            handing_off: 0,
234            failed: 1,
235            completed_handoffs: 0,
236            pending_creation: Some(CanisterPoolCreation {
237                operation_id: [8; 32],
238                cycles_ledger: Principal::from_slice(&[6; 29]),
239                placement_subnet: Principal::from_slice(&[5; 29]),
240                root: Principal::from_slice(&[4; 29]),
241                ledger_amount: Cycles::new(5_500_000_000_000),
242                created_at_time_ns: 12,
243                progress: CanisterPoolCreationProgress::Blocked {
244                    failure: CanisterPoolCreationFailure::LedgerCreationFailed,
245                },
246            }),
247            pending_handoff: None,
248            entries: vec![CanisterPoolAsset {
249                canister_id,
250                cycles: Cycles::new(4_000_000_000_000),
251                origin: CanisterPoolAssetOrigin::Recycled,
252                status: CanisterPoolAssetStatus::Failed {
253                    reason: "below configured cycles".to_string(),
254                },
255                added_at_ns: 10,
256                updated_at_ns: 11,
257            }],
258            next_start_after: None,
259        };
260        let bytes = candid::encode_one(&response).expect("encode pool status");
261        assert_eq!(
262            candid::decode_one::<CanisterPoolResponse>(&bytes).expect("decode pool status"),
263            response,
264        );
265
266        let command = PoolAdminCommand::Import { canister_id };
267        let bytes = candid::encode_one(&command).expect("encode pool command");
268        assert_eq!(
269            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode pool command"),
270            command,
271        );
272
273        let command = PoolAdminCommand::RetryRefill;
274        let bytes = candid::encode_one(&command).expect("encode refill retry command");
275        assert_eq!(
276            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode refill retry command"),
277            command,
278        );
279    }
280}