canic_memory/
registry.rs

1//! NOTE: All stable registry access is TLS-thread-local.
2//! This ensures atomicity on the IC’s single-threaded execution model.
3use crate::manager::MEMORY_MANAGER;
4use candid::CandidType;
5use canic_cdk::structures::{
6    BTreeMap as StableBTreeMap, DefaultMemoryImpl,
7    memory::{MemoryId, VirtualMemory},
8};
9use canic_core::{impl_storable_bounded, types::BoundedString256, utils::time::now_secs};
10use serde::{Deserialize, Serialize};
11use std::cell::RefCell;
12use thiserror::Error as ThisError;
13
14///
15/// Reserved for the registry system itself
16///
17pub const MEMORY_REGISTRY_ID: u8 = 0;
18pub const MEMORY_RANGES_ID: u8 = 1;
19
20//
21// MEMORY_REGISTRY
22//
23
24thread_local! {
25    static MEMORY_REGISTRY: RefCell<StableBTreeMap<u8, MemoryRegistryEntry, VirtualMemory<DefaultMemoryImpl>>> =
26        RefCell::new(StableBTreeMap::init(
27            MEMORY_MANAGER.with_borrow(|this| {
28                this.get(MemoryId::new(MEMORY_REGISTRY_ID))
29            }),
30        ));
31}
32
33//
34// MEMORY_RANGES
35//
36
37thread_local! {
38    static MEMORY_RANGES: RefCell<StableBTreeMap<String, MemoryRange, VirtualMemory<DefaultMemoryImpl>>> =
39        RefCell::new(StableBTreeMap::init(
40            MEMORY_MANAGER.with_borrow(|mgr| {
41                mgr.get(MemoryId::new(MEMORY_RANGES_ID))
42            }),
43        ));
44}
45
46//
47// PENDING_REGISTRATIONS
48//
49// Queue of memory registrations produced during TLS initialization
50// Each entry is (id, crate_name, label).
51// These are deferred until `flush_pending_registrations()` is called,
52// which validates and inserts them into the global MemoryRegistry.
53//
54
55thread_local! {
56    static PENDING_REGISTRATIONS: RefCell<Vec<(u8, &'static str, &'static str)>> = const {
57        RefCell::new(Vec::new())
58    };
59}
60
61// public as it gets called from macros
62pub fn defer_register(id: u8, crate_name: &'static str, label: &'static str) {
63    PENDING_REGISTRATIONS.with(|q| {
64        q.borrow_mut().push((id, crate_name, label));
65    });
66}
67
68/// Drain (and clear) all pending registrations.
69/// Intended to be called from the ops layer during init/post-upgrade.
70#[must_use]
71pub fn drain_pending_registrations() -> Vec<(u8, &'static str, &'static str)> {
72    PENDING_REGISTRATIONS.with(|q| q.borrow_mut().drain(..).collect())
73}
74
75//
76// PENDING_RANGES
77//
78
79thread_local! {
80    pub static PENDING_RANGES: RefCell<Vec<(&'static str, u8, u8)>> = const {
81        RefCell::new(Vec::new())
82    };
83}
84
85// public as it gets called from macros
86pub fn defer_reserve_range(crate_name: &'static str, start: u8, end: u8) {
87    PENDING_RANGES.with(|q| q.borrow_mut().push((crate_name, start, end)));
88}
89
90/// Drain (and clear) all pending ranges.
91/// Intended to be called from the ops layer during init/post-upgrade.
92#[must_use]
93pub fn drain_pending_ranges() -> Vec<(&'static str, u8, u8)> {
94    PENDING_RANGES.with(|q| q.borrow_mut().drain(..).collect())
95}
96
97///
98/// MemoryRegistryError
99///
100
101#[derive(Debug, ThisError)]
102pub enum MemoryRegistryError {
103    #[error("ID {0} is already registered with type {1}, tried to register type {2}")]
104    AlreadyRegistered(u8, String, String),
105
106    #[error("crate `{0}` already has a reserved range")]
107    DuplicateRange(String),
108
109    #[error("crate `{0}` provided invalid range {1}-{2} (start > end)")]
110    InvalidRange(String, u8, u8),
111
112    #[error("crate `{0}` attempted to register ID {1}, but it is outside its allowed ranges")]
113    OutOfRange(String, u8),
114
115    #[error("crate `{0}` range {1}-{2} overlaps with crate `{3}` range {4}-{5}")]
116    Overlap(String, u8, u8, String, u8, u8),
117
118    #[error("crate `{0}` has not reserved any memory range")]
119    NoRange(String),
120}
121
122///
123/// MemoryRange
124///
125#[derive(Clone, Debug, Deserialize, Serialize)]
126pub struct MemoryRange {
127    pub crate_key: BoundedString256,
128    pub start: u8,
129    pub end: u8,
130    pub created_at: u64,
131}
132
133impl MemoryRange {
134    #[must_use]
135    pub(crate) fn new(crate_key: &str, start: u8, end: u8) -> Self {
136        Self {
137            crate_key: BoundedString256::new(crate_key),
138            start,
139            end,
140            created_at: now_secs(),
141        }
142    }
143
144    #[must_use]
145    pub fn contains(&self, id: u8) -> bool {
146        (self.start..=self.end).contains(&id)
147    }
148}
149
150impl_storable_bounded!(MemoryRange, 320, false);
151
152///
153/// MemoryRegistryEntry
154///
155
156#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
157pub struct MemoryRegistryEntry {
158    pub label: BoundedString256,
159    pub created_at: u64,
160}
161
162impl MemoryRegistryEntry {
163    #[must_use]
164    pub(crate) fn new(label: &str) -> Self {
165        Self {
166            label: BoundedString256::new(label),
167            created_at: now_secs(),
168        }
169    }
170}
171
172impl_storable_bounded!(MemoryRegistryEntry, 320, false);
173
174///
175/// MemoryRegistryView
176///
177
178pub type MemoryRegistryView = Vec<(u8, MemoryRegistryEntry)>;
179
180///
181/// MemoryRegistry
182///
183
184pub struct MemoryRegistry;
185
186impl MemoryRegistry {
187    /// Register an ID, enforcing crate’s allowed range.
188    ///
189    /// Pure domain/model-level function:
190    /// - no logging
191    /// - no unwrap
192    /// - no mapping to `crate::Error`
193    pub fn register(id: u8, crate_name: &str, label: &str) -> Result<(), MemoryRegistryError> {
194        let crate_key = crate_name.to_string();
195
196        // 1. Check reserved range
197        let range = MEMORY_RANGES.with_borrow(|ranges| ranges.get(&crate_key));
198        match range {
199            None => {
200                return Err(MemoryRegistryError::NoRange(crate_key));
201            }
202            Some(r) if !r.contains(id) => {
203                return Err(MemoryRegistryError::OutOfRange(crate_key, id));
204            }
205            Some(_) => {
206                // OK, continue
207            }
208        }
209
210        // 2. Check already registered
211        let existing = MEMORY_REGISTRY.with_borrow(|map| map.get(&id));
212        if let Some(existing) = existing {
213            if existing.label.as_ref() != label {
214                return Err(MemoryRegistryError::AlreadyRegistered(
215                    id,
216                    existing.label.to_string(),
217                    label.to_string(),
218                ));
219            }
220
221            // idempotent case
222            return Ok(());
223        }
224
225        // 3. Insert
226        MEMORY_REGISTRY.with_borrow_mut(|map| {
227            map.insert(id, MemoryRegistryEntry::new(label));
228        });
229
230        Ok(())
231    }
232
233    /// Reserve a block of memory IDs for a crate.
234    ///
235    /// Pure domain/model-level function, no logging or unwrap.
236    pub fn reserve_range(crate_name: &str, start: u8, end: u8) -> Result<(), MemoryRegistryError> {
237        if start > end {
238            return Err(MemoryRegistryError::InvalidRange(
239                crate_name.to_string(),
240                start,
241                end,
242            ));
243        }
244
245        let crate_key = crate_name.to_string();
246
247        // 1. Check for conflicts (existing ranges)
248        let conflict = MEMORY_RANGES.with_borrow(|ranges| {
249            if ranges.contains_key(&crate_key) {
250                return Some(MemoryRegistryError::DuplicateRange(crate_key.clone()));
251            }
252
253            for entry in ranges.iter() {
254                let other_crate = entry.key();
255                let other_range = entry.value();
256
257                if !(end < other_range.start || start > other_range.end) {
258                    return Some(MemoryRegistryError::Overlap(
259                        crate_key.clone(),
260                        start,
261                        end,
262                        other_crate.clone(),
263                        other_range.start,
264                        other_range.end,
265                    ));
266                }
267            }
268
269            None
270        });
271
272        if let Some(err) = conflict {
273            return Err(err);
274        }
275
276        // 2. Insert
277        MEMORY_RANGES.with_borrow_mut(|ranges| {
278            let range = MemoryRange::new(crate_name, start, end);
279            ranges.insert(crate_name.to_string(), range);
280        });
281
282        Ok(())
283    }
284
285    #[must_use]
286    pub fn get(id: u8) -> Option<MemoryRegistryEntry> {
287        MEMORY_REGISTRY.with_borrow(|map| map.get(&id))
288    }
289
290    #[must_use]
291    pub fn export() -> MemoryRegistryView {
292        MEMORY_REGISTRY.with_borrow(|map| {
293            map.iter()
294                .map(|entry| (*entry.key(), entry.value()))
295                .collect()
296        })
297    }
298
299    #[must_use]
300    pub fn export_ranges() -> Vec<(String, MemoryRange)> {
301        MEMORY_RANGES.with_borrow(|ranges| {
302            ranges
303                .iter()
304                .map(|e| (e.key().clone(), e.value()))
305                .collect()
306        })
307    }
308}
309
310#[cfg(test)]
311pub(crate) fn reset_for_tests() {
312    MEMORY_REGISTRY.with_borrow_mut(StableBTreeMap::clear);
313    MEMORY_RANGES.with_borrow_mut(StableBTreeMap::clear);
314    PENDING_REGISTRATIONS.with(|q| q.borrow_mut().clear());
315    PENDING_RANGES.with(|q| q.borrow_mut().clear());
316}
317
318///
319/// TESTS
320///
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn reserve_range_happy_path_and_reject_overlap() {
328        reset_for_tests();
329        MemoryRegistry::reserve_range("crate_a", 10, 20).unwrap();
330
331        // Overlap with existing should error
332        let err = MemoryRegistry::reserve_range("crate_b", 15, 25).unwrap_err();
333        matches!(err, MemoryRegistryError::Overlap(_, _, _, _, _, _));
334
335        // Disjoint should succeed
336        MemoryRegistry::reserve_range("crate_b", 30, 40).unwrap();
337
338        let ranges = MemoryRegistry::export_ranges();
339        assert_eq!(ranges.len(), 2);
340    }
341
342    #[test]
343    fn reserve_range_rejects_invalid_order() {
344        reset_for_tests();
345        let err = MemoryRegistry::reserve_range("crate_a", 5, 4).unwrap_err();
346        matches!(err, MemoryRegistryError::InvalidRange(_, _, _));
347        assert!(MemoryRegistry::export_ranges().is_empty());
348    }
349
350    #[test]
351    fn register_id_requires_range_and_checks_bounds() {
352        reset_for_tests();
353        MemoryRegistry::reserve_range("crate_a", 1, 3).unwrap();
354
355        // Out of range
356        let err = MemoryRegistry::register(5, "crate_a", "Foo").unwrap_err();
357        matches!(err, MemoryRegistryError::OutOfRange(_, _));
358
359        // Happy path
360        MemoryRegistry::register(2, "crate_a", "Foo").unwrap();
361
362        // Idempotent same label
363        MemoryRegistry::register(2, "crate_a", "Foo").unwrap();
364
365        // Different label should error
366        let err = MemoryRegistry::register(2, "crate_a", "Bar").unwrap_err();
367        matches!(err, MemoryRegistryError::AlreadyRegistered(_, _, _));
368
369        let view = MemoryRegistry::export();
370        assert_eq!(view.len(), 1);
371        assert_eq!(view[0].0, 2);
372    }
373
374    #[test]
375    fn pending_queues_drain_in_order() {
376        reset_for_tests();
377        defer_reserve_range("crate_a", 1, 2);
378        defer_reserve_range("crate_b", 3, 4);
379        defer_register(1, "crate_a", "A1");
380        defer_register(3, "crate_b", "B3");
381
382        let ranges = drain_pending_ranges();
383        assert_eq!(ranges, vec![("crate_a", 1, 2), ("crate_b", 3, 4)]);
384        let regs = drain_pending_registrations();
385        assert_eq!(regs, vec![(1, "crate_a", "A1"), (3, "crate_b", "B3")]);
386
387        // queues are empty after drain
388        assert!(drain_pending_ranges().is_empty());
389        assert!(drain_pending_registrations().is_empty());
390    }
391}