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/// Selects one pool Canister for an import or reset retry command.
123#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
124pub struct PoolCanisterRequest {
125    pub canister_id: Principal,
126}
127
128/// Selects one pool Canister and its exact handoff recipient.
129#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
130pub struct PoolHandoffRequest {
131    pub canister_id: Principal,
132    pub recipient: Principal,
133}
134
135/// Exact pool policy and current exclusive root-owned physical inventory.
136#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
137pub struct CanisterPoolResponse {
138    pub config: FleetSubnetCanisterPoolConfig,
139    pub tracked: u32,
140    pub store: u32,
141    pub store_deletion_pending: u32,
142    pub pooled: u32,
143    pub workload: u32,
144    pub surplus: u32,
145    pub ready: u32,
146    pub pending_reset: u32,
147    pub claimed: u32,
148    pub recycling: u32,
149    pub handing_off: u32,
150    pub failed: u32,
151    pub completed_handoffs: u64,
152    pub pending_creation: Option<CanisterPoolCreation>,
153    pub pending_handoff: Option<CanisterPoolHandoff>,
154    pub entries: Vec<CanisterPoolAsset>,
155    pub next_start_after: Option<Principal>,
156}
157
158/// Controller command for explicit pool maintenance.
159#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
160pub enum PoolAdminCommand {
161    Maintain,
162    RetryRefill,
163    Import {
164        canister_id: Principal,
165    },
166    RetryReset {
167        canister_id: Principal,
168    },
169    Handoff {
170        canister_id: Principal,
171        recipient: Principal,
172    },
173}
174
175/// Result of one explicit pool maintenance command.
176#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
177pub enum PoolAdminResponse {
178    Maintained,
179    MaintenancePaused {
180        reason: String,
181    },
182    Created {
183        canister_id: Principal,
184    },
185    RefillWaitingForCycles {
186        available: Cycles,
187        creation_amount: Cycles,
188    },
189    RefillPending {
190        operation_id: [u8; 32],
191        uncertain_result: bool,
192    },
193    RefillBlocked {
194        operation_id: [u8; 32],
195        failure: CanisterPoolCreationFailure,
196    },
197    RefillRetryScheduled {
198        previous_operation_id: [u8; 32],
199    },
200    Imported {
201        canister_id: Principal,
202    },
203    ResetQueued {
204        canister_id: Principal,
205    },
206    ResetReady {
207        canister_id: Principal,
208    },
209    HandedOff {
210        canister_id: Principal,
211        recipient: Principal,
212    },
213    ResetFailed {
214        canister_id: Principal,
215        reason: String,
216    },
217}
218
219/// Narrow result of one explicit maintenance pass.
220#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
221pub enum PoolMaintenanceResponse {
222    Maintained,
223    MaintenancePaused {
224        reason: String,
225    },
226    Created {
227        canister_id: Principal,
228    },
229    RefillWaitingForCycles {
230        available: Cycles,
231        creation_amount: Cycles,
232    },
233    RefillPending {
234        operation_id: [u8; 32],
235        uncertain_result: bool,
236    },
237    RefillBlocked {
238        operation_id: [u8; 32],
239        failure: CanisterPoolCreationFailure,
240    },
241    ResetReady {
242        canister_id: Principal,
243    },
244    ResetFailed {
245        canister_id: Principal,
246        reason: String,
247    },
248}
249
250/// Narrow result of importing one existing physical Canister.
251#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
252pub enum PoolImportResponse {
253    Imported {
254        canister_id: Principal,
255    },
256    ResetFailed {
257        canister_id: Principal,
258        reason: String,
259    },
260}
261
262/// Exact result of scheduling another blocked refill attempt.
263#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
264pub struct PoolRefillRetryResponse {
265    pub previous_operation_id: [u8; 32],
266}
267
268/// Exact result of scheduling another reset attempt.
269#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
270pub struct PoolResetRetryResponse {
271    pub canister_id: Principal,
272}
273
274/// Exact result of handing one physical Canister to replacement authority.
275#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
276pub struct PoolHandoffResponse {
277    pub canister_id: Principal,
278    pub recipient: Principal,
279}
280
281// -----------------------------------------------------------------------------
282// Tests
283// -----------------------------------------------------------------------------
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn pool_status_and_admin_contracts_round_trip_through_candid() {
291        let canister_id = Principal::from_slice(&[7; 29]);
292        let response = CanisterPoolResponse {
293            config: FleetSubnetCanisterPoolConfig {
294                minimum_size: 3,
295                maximum_size: 10,
296                canister_cycles: Cycles::new(5_000_000_000_000),
297            },
298            tracked: 1,
299            store: 0,
300            store_deletion_pending: 0,
301            pooled: 1,
302            workload: 0,
303            surplus: 0,
304            ready: 0,
305            pending_reset: 0,
306            claimed: 0,
307            recycling: 0,
308            handing_off: 0,
309            failed: 1,
310            completed_handoffs: 0,
311            pending_creation: Some(CanisterPoolCreation {
312                operation_id: [8; 32],
313                cycles_ledger: Principal::from_slice(&[6; 29]),
314                placement_subnet: Principal::from_slice(&[5; 29]),
315                root: Principal::from_slice(&[4; 29]),
316                ledger_amount: Cycles::new(5_500_000_000_000),
317                created_at_time_ns: 12,
318                progress: CanisterPoolCreationProgress::Blocked {
319                    failure: CanisterPoolCreationFailure::LedgerCreationFailed,
320                },
321            }),
322            pending_handoff: None,
323            entries: vec![CanisterPoolAsset {
324                canister_id,
325                cycles: Cycles::new(4_000_000_000_000),
326                origin: CanisterPoolAssetOrigin::Recycled,
327                status: CanisterPoolAssetStatus::Failed {
328                    reason: "below configured cycles".to_string(),
329                },
330                added_at_ns: 10,
331                updated_at_ns: 11,
332            }],
333            next_start_after: None,
334        };
335        let bytes = candid::encode_one(&response).expect("encode pool status");
336        assert_eq!(
337            candid::decode_one::<CanisterPoolResponse>(&bytes).expect("decode pool status"),
338            response,
339        );
340
341        let command = PoolAdminCommand::Import { canister_id };
342        let bytes = candid::encode_one(&command).expect("encode pool command");
343        assert_eq!(
344            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode pool command"),
345            command,
346        );
347
348        let command = PoolAdminCommand::RetryRefill;
349        let bytes = candid::encode_one(&command).expect("encode refill retry command");
350        assert_eq!(
351            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode refill retry command"),
352            command,
353        );
354    }
355}