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/// Exact Cycles Ledger receipt retained for one autonomously created pool asset.
35#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36pub struct CanisterPoolCreationReceipt {
37    pub block_index: u64,
38    pub operation_id: [u8; 32],
39    pub cycles_ledger: Principal,
40    pub ledger_amount: Cycles,
41    pub ledger_fee: Cycles,
42    pub readiness_floor: Cycles,
43    pub creation_execution_margin: Cycles,
44    pub management_creation_fee: Cycles,
45    pub first_observed_cycles: Option<Cycles>,
46}
47
48/// Current durable state of one root-owned physical Canister.
49#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
50pub enum CanisterPoolAssetStatus {
51    Store,
52    StoreDeletionPending {
53        operation_id: [u8; 32],
54    },
55    PendingReset,
56    Ready,
57    Claimed {
58        claim: CanisterPoolClaim,
59    },
60    Workload {
61        claim: CanisterPoolClaim,
62    },
63    Recycling {
64        claim: CanisterPoolClaim,
65        reset: CanisterPoolRecycleReset,
66    },
67    HandingOff {
68        recipient: Principal,
69    },
70    Failed {
71        reason: String,
72    },
73}
74
75/// Controller-visible inventory row for one root-owned physical Canister.
76#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77pub struct CanisterPoolAsset {
78    pub canister_id: Principal,
79    pub creation_receipt: Option<CanisterPoolCreationReceipt>,
80    pub cycles: Cycles,
81    pub origin: CanisterPoolAssetOrigin,
82    pub status: CanisterPoolAssetStatus,
83    pub added_at_ns: u64,
84    pub updated_at_ns: u64,
85}
86
87/// Durable transfer of one paid asset to replacement authority during root draining.
88#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
89pub struct CanisterPoolHandoff {
90    pub canister_id: Principal,
91    pub recipient: Principal,
92    pub prepared_at_ns: u64,
93}
94
95/// Why one autonomous refill stopped without creating another Canister.
96#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
97pub enum CanisterPoolCreationFailure {
98    UnresolvedAfterLedgerWindow,
99    LedgerCreationFailed,
100    LedgerRejected,
101}
102
103/// Durable controller-visible progress of one autonomous refill.
104#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub enum CanisterPoolCreationProgress {
106    Intent {
107        uncertain_result: bool,
108    },
109    Created {
110        block_index: u64,
111        canister_id: Principal,
112    },
113    WaitingForFunding {
114        available: Cycles,
115        attempt_count: u32,
116        last_attempt_at_ns: Option<u64>,
117        observed_at_ns: u64,
118        required: Cycles,
119        retry_at_ns: u64,
120        shortfall: Cycles,
121    },
122    Blocked {
123        failure: CanisterPoolCreationFailure,
124    },
125}
126
127/// Exact Cycles Ledger request retained until its principal is in inventory.
128#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
129pub struct CanisterPoolCreation {
130    pub attempt_count: u32,
131    pub operation_id: [u8; 32],
132    pub cycles_ledger: Principal,
133    pub placement_subnet: Principal,
134    pub root: Principal,
135    pub ledger_amount: Cycles,
136    pub ledger_fee: Cycles,
137    pub readiness_floor: Cycles,
138    pub creation_execution_margin: Cycles,
139    pub management_creation_fee: Cycles,
140    pub created_at_time_ns: u64,
141    pub last_attempt_at_ns: Option<u64>,
142    pub progress: CanisterPoolCreationProgress,
143}
144
145/// Bounded controller query for one canonical inventory page.
146#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
147pub struct CanisterPoolStatusRequest {
148    pub start_after: Option<Principal>,
149    pub limit: u16,
150}
151
152/// Selects one pool Canister for an import or reset retry command.
153#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
154pub struct PoolCanisterRequest {
155    pub canister_id: Principal,
156}
157
158/// Selects one pool Canister and its exact handoff recipient.
159#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
160pub struct PoolHandoffRequest {
161    pub canister_id: Principal,
162    pub recipient: Principal,
163}
164
165/// Exact pool policy and current exclusive root-owned physical inventory.
166#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
167pub struct CanisterPoolResponse {
168    pub config: FleetSubnetCanisterPoolConfig,
169    pub tracked: u32,
170    pub store: u32,
171    pub store_deletion_pending: u32,
172    pub pooled: u32,
173    pub workload: u32,
174    pub surplus: u32,
175    pub ready: u32,
176    pub pending_reset: u32,
177    pub claimed: u32,
178    pub recycling: u32,
179    pub handing_off: u32,
180    pub failed: u32,
181    pub completed_handoffs: u64,
182    pub pending_creation: Option<CanisterPoolCreation>,
183    pub pending_handoff: Option<CanisterPoolHandoff>,
184    pub entries: Vec<CanisterPoolAsset>,
185    pub next_start_after: Option<Principal>,
186}
187
188/// Controller command for explicit pool maintenance.
189#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
190pub enum PoolAdminCommand {
191    Maintain,
192    RetryRefill,
193    Import {
194        canister_id: Principal,
195    },
196    RetryReset {
197        canister_id: Principal,
198    },
199    Handoff {
200        canister_id: Principal,
201        recipient: Principal,
202    },
203}
204
205/// Result of one explicit pool maintenance command.
206#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
207pub enum PoolAdminResponse {
208    Maintained,
209    MaintenancePaused {
210        reason: String,
211    },
212    Created {
213        canister_id: Principal,
214    },
215    RefillWaitingForCycles {
216        available: Cycles,
217        attempt_count: u32,
218        creation_amount: Cycles,
219        execution_margin: Cycles,
220        last_attempt_at_ns: Option<u64>,
221        ledger_fee: Cycles,
222        readiness_floor: Cycles,
223        required: Cycles,
224        retry_at_ns: u64,
225        shortfall: Cycles,
226    },
227    RefillPending {
228        operation_id: [u8; 32],
229        uncertain_result: bool,
230    },
231    RefillBlocked {
232        operation_id: [u8; 32],
233        failure: CanisterPoolCreationFailure,
234    },
235    RefillRetryScheduled {
236        previous_operation_id: [u8; 32],
237    },
238    Imported {
239        canister_id: Principal,
240    },
241    ResetQueued {
242        canister_id: Principal,
243    },
244    ResetReady {
245        canister_id: Principal,
246    },
247    HandedOff {
248        canister_id: Principal,
249        recipient: Principal,
250    },
251    ResetFailed {
252        canister_id: Principal,
253        reason: String,
254    },
255}
256
257/// Narrow result of one explicit maintenance pass.
258#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
259pub enum PoolMaintenanceResponse {
260    Maintained,
261    MaintenancePaused {
262        reason: String,
263    },
264    Created {
265        canister_id: Principal,
266    },
267    RefillWaitingForCycles {
268        available: Cycles,
269        attempt_count: u32,
270        creation_amount: Cycles,
271        execution_margin: Cycles,
272        last_attempt_at_ns: Option<u64>,
273        ledger_fee: Cycles,
274        readiness_floor: Cycles,
275        required: Cycles,
276        retry_at_ns: u64,
277        shortfall: Cycles,
278    },
279    RefillPending {
280        operation_id: [u8; 32],
281        uncertain_result: bool,
282    },
283    RefillBlocked {
284        operation_id: [u8; 32],
285        failure: CanisterPoolCreationFailure,
286    },
287    ResetReady {
288        canister_id: Principal,
289    },
290    ResetFailed {
291        canister_id: Principal,
292        reason: String,
293    },
294}
295
296/// Narrow result of importing one existing physical Canister.
297#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
298pub enum PoolImportResponse {
299    Imported {
300        canister_id: Principal,
301    },
302    ResetFailed {
303        canister_id: Principal,
304        reason: String,
305    },
306}
307
308/// Exact result of scheduling another blocked refill attempt.
309#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
310pub struct PoolRefillRetryResponse {
311    pub previous_operation_id: [u8; 32],
312}
313
314/// Exact result of scheduling another reset attempt.
315#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
316pub struct PoolResetRetryResponse {
317    pub canister_id: Principal,
318}
319
320/// Exact result of handing one physical Canister to replacement authority.
321#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
322pub struct PoolHandoffResponse {
323    pub canister_id: Principal,
324    pub recipient: Principal,
325}
326
327// -----------------------------------------------------------------------------
328// Tests
329// -----------------------------------------------------------------------------
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn pool_status_and_admin_contracts_round_trip_through_candid() {
337        let canister_id = Principal::from_slice(&[7; 29]);
338        let response = CanisterPoolResponse {
339            config: FleetSubnetCanisterPoolConfig {
340                minimum_size: 3,
341                maximum_size: 10,
342                canister_cycles: Cycles::new(5_000_000_000_000),
343                creation_execution_margin: Cycles::new(1_000_000_000_000),
344            },
345            tracked: 1,
346            store: 0,
347            store_deletion_pending: 0,
348            pooled: 1,
349            workload: 0,
350            surplus: 0,
351            ready: 0,
352            pending_reset: 0,
353            claimed: 0,
354            recycling: 0,
355            handing_off: 0,
356            failed: 1,
357            completed_handoffs: 0,
358            pending_creation: Some(CanisterPoolCreation {
359                attempt_count: 1,
360                operation_id: [8; 32],
361                cycles_ledger: Principal::from_slice(&[6; 29]),
362                placement_subnet: Principal::from_slice(&[5; 29]),
363                root: Principal::from_slice(&[4; 29]),
364                ledger_amount: Cycles::new(6_500_000_000_000),
365                ledger_fee: Cycles::new(100_000_000),
366                readiness_floor: Cycles::new(5_000_000_000_000),
367                creation_execution_margin: Cycles::new(1_000_000_000_000),
368                management_creation_fee: Cycles::new(500_000_000_000),
369                created_at_time_ns: 12,
370                last_attempt_at_ns: Some(13),
371                progress: CanisterPoolCreationProgress::Blocked {
372                    failure: CanisterPoolCreationFailure::LedgerCreationFailed,
373                },
374            }),
375            pending_handoff: None,
376            entries: vec![CanisterPoolAsset {
377                canister_id,
378                creation_receipt: None,
379                cycles: Cycles::new(4_000_000_000_000),
380                origin: CanisterPoolAssetOrigin::Recycled,
381                status: CanisterPoolAssetStatus::Failed {
382                    reason: "below configured cycles".to_string(),
383                },
384                added_at_ns: 10,
385                updated_at_ns: 11,
386            }],
387            next_start_after: None,
388        };
389        let bytes = candid::encode_one(&response).expect("encode pool status");
390        assert_eq!(
391            candid::decode_one::<CanisterPoolResponse>(&bytes).expect("decode pool status"),
392            response,
393        );
394
395        let command = PoolAdminCommand::Import { canister_id };
396        let bytes = candid::encode_one(&command).expect("encode pool command");
397        assert_eq!(
398            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode pool command"),
399            command,
400        );
401
402        let command = PoolAdminCommand::RetryRefill;
403        let bytes = candid::encode_one(&command).expect("encode refill retry command");
404        assert_eq!(
405            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode refill retry command"),
406            command,
407        );
408    }
409}