Skip to main content

ic_storage/adapter/
region.rs

1//! Operation-local collection access within a retained allocation region.
2use super::{Memory, Storable};
3use crate::substrate::{self, DefaultMemoryImpl};
4use ic_memory::{RuntimeGrowError, RuntimeMemory};
5use std::{fmt, marker::PhantomData, ops::Range, ops::RangeBounds, rc::Rc};
6
7const PAGE_BYTES: u64 = 65_536;
8const MAX_PAGES: u64 = 4_194_304;
9
10/// Failure to reserve a governed region's virtual extent, before any writes.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum RegionGrowError {
13    /// The requested relative extent exceeds the selected region.
14    LimitExceeded {
15        /// Requested relative page count.
16        minimum: u64,
17        /// Maximum relative page count.
18        maximum: u64,
19    },
20    /// IC Memory refused to grow the containing allocation.
21    Runtime(RuntimeGrowError),
22}
23
24impl fmt::Display for RegionGrowError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::LimitExceeded { minimum, maximum } => {
28                write!(
29                    f,
30                    "requested region extent {minimum} exceeds {maximum} pages"
31                )
32            }
33            Self::Runtime(error) => error.fmt(f),
34        }
35    }
36}
37
38impl std::error::Error for RegionGrowError {
39    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
40        match self {
41            Self::Runtime(error) => Some(error),
42            Self::LimitExceeded { .. } => None,
43        }
44    }
45}
46
47/// A B-tree view inside one existing, governed allocation lease.
48///
49/// The caller retains its `Rc<RuntimeMemory>` for disjoint application frames.
50/// Each collection operation loads the current upstream root and allocator;
51/// multiple views may interleave operations without retaining stale roots.
52/// Key scans return owned snapshots, so no cached tree or iterator escapes.
53/// Applications own namespace bounds, codec admission and recovery policy.
54///
55/// Construction performs no IO. Inspect retained framing with [`Self::read`]
56/// before calling collection operations, which retain upstream load/panic
57/// contracts. Only [`Self::initialize`] creates a tree, and only in an empty
58/// region. No method reinitializes nonempty memory or exports raw backing.
59/// As with ordinary collections, callers must not reenter collection access
60/// from a codec or mutate the tree's bytes through their retained lease.
61pub struct BTreeMapRegion<K: Storable + Ord + Clone, V: Storable, M: Memory = DefaultMemoryImpl> {
62    memory: RegionMemory<M>,
63    record: PhantomData<(K, V)>,
64}
65
66impl<K: Storable + Ord + Clone, V: Storable, M: Memory> BTreeMapRegion<K, V, M> {
67    /// Select an absolute, half-open range of 65,536-byte allocation pages.
68    ///
69    /// Retains the same allocation lease; no allocation or collection is opened.
70    /// Other owners of the `Rc` may access disjoint frames in that allocation.
71    /// Drop every view and the caller's last `Rc` before reopening the lease.
72    ///
73    /// # Panics
74    /// Panics unless `pages.start < pages.end <= 4_194_304`.
75    #[must_use]
76    pub fn new(memory: Rc<RuntimeMemory<M>>, pages: Range<u64>) -> Self {
77        assert!(pages.start < pages.end && pages.end <= MAX_PAGES);
78        Self {
79            memory: RegionMemory { memory, pages },
80            record: PhantomData,
81        }
82    }
83
84    /// Return the currently allocated relative extent in Wasm pages.
85    #[must_use]
86    pub fn allocated_pages(&self) -> u64 {
87        (&self.memory).size()
88    }
89
90    /// Read relative bytes for the application's retained-framing admission.
91    ///
92    /// # Panics
93    /// Panics before IO if the complete read exceeds the allocated region.
94    /// An empty read is valid at the region's end, but not beyond it.
95    pub fn read(&self, offset: u64, dst: &mut [u8]) {
96        (&self.memory).read(offset, dst);
97    }
98
99    /// Create an upstream tree only when the selected region has zero pages.
100    ///
101    /// Call before reserving extra pages. Retained trees need no initialization:
102    /// admit their framing and use the operations directly.
103    ///
104    /// # Panics
105    /// Panics without writing if the region is nonempty. Empty-region creation
106    /// retains upstream growth and panic contracts.
107    pub fn initialize(&mut self) {
108        assert_eq!(self.allocated_pages(), 0, "region is already nonempty");
109        substrate::BTreeMap::<K, V, _>::new(&self.memory);
110    }
111
112    /// Reserve an absolute relative extent without promising entry capacity.
113    ///
114    /// # Errors
115    /// Returns a region-limit or IC Memory growth error without changing bytes
116    /// or allocation accounting. A satisfied request performs no growth or IO.
117    /// Callers own byte/page budgets and must qualify them for their codecs.
118    /// Mutations retain upstream failure semantics; this is not a transaction
119    /// around an insert that later runs out of space or panics in a codec.
120    pub fn reserve_pages(&mut self, minimum: u64) -> Result<(), RegionGrowError> {
121        self.memory.reserve_pages(minimum)
122    }
123
124    /// Read a record using the current retained root.
125    pub fn get(&self, key: &K) -> Option<V> {
126        self.current_map().get(key)
127    }
128
129    /// Insert using the current retained root and upstream mutation semantics.
130    pub fn insert(&mut self, key: K, value: V) -> Option<V> {
131        self.current_map().insert(key, value)
132    }
133
134    /// Remove using the current retained root and upstream mutation semantics.
135    pub fn remove(&mut self, key: &K) -> Option<V> {
136        self.current_map().remove(key)
137    }
138
139    /// Return the current tree's record count across all namespaces.
140    #[must_use]
141    pub fn len(&self) -> u64 {
142        self.current_map().len()
143    }
144
145    /// Return whether the current tree has no records.
146    #[must_use]
147    pub fn is_empty(&self) -> bool {
148        self.current_map().is_empty()
149    }
150
151    /// Snapshot ordered keys within the caller's namespace/range bounds.
152    ///
153    /// The scan completes before returning; the iterator retains owned keys,
154    /// with no collection cache, memory handle or borrow across later mutations.
155    pub fn keys_range(&self, bounds: impl RangeBounds<K>) -> std::vec::IntoIter<K> {
156        self.keys_range_limited(bounds, usize::MAX)
157    }
158
159    /// Snapshot at most `limit` ordered keys, stopping iteration at that bound.
160    ///
161    /// Unlike `keys_range(bounds).take(limit)`, this bounds collection work before
162    /// allocating the snapshot. Zero returns without loading the tree. Resume
163    /// with an excluded last key; intervening mutations are visible to each call.
164    /// The limit bounds key count, not encoded bytes or instruction cost.
165    pub fn keys_range_limited(
166        &self,
167        bounds: impl RangeBounds<K>,
168        limit: usize,
169    ) -> std::vec::IntoIter<K> {
170        if limit == 0 {
171            return Vec::new().into_iter();
172        }
173        self.current_map()
174            .keys_range(bounds)
175            .take(limit)
176            .collect::<Vec<_>>()
177            .into_iter()
178    }
179
180    fn current_map(&self) -> substrate::BTreeMap<K, V, &RegionMemory<M>> {
181        substrate::BTreeMap::load(&self.memory)
182    }
183}
184
185// Private bounded IO over an admitted lease. Upstream RestrictedMemory does not
186// bound reads/writes and may overgrow an absent region; enforce both here.
187struct RegionMemory<M: Memory> {
188    memory: Rc<RuntimeMemory<M>>,
189    pages: Range<u64>,
190}
191
192impl<M: Memory> RegionMemory<M> {
193    fn reserve_pages(&self, minimum: u64) -> Result<(), RegionGrowError> {
194        let maximum = self.pages.end - self.pages.start;
195        if minimum > maximum {
196            return Err(RegionGrowError::LimitExceeded { minimum, maximum });
197        }
198        if minimum > self.size() {
199            let additional = self.pages.start + minimum - self.memory.size();
200            self.memory
201                .as_ref()
202                .grow(additional)
203                .map_err(RegionGrowError::Runtime)?;
204        }
205        Ok(())
206    }
207
208    fn check_io(&self, offset: u64, count: usize) {
209        let extent = self.size() * PAGE_BYTES;
210        assert!(offset <= extent && count as u64 <= extent - offset);
211    }
212}
213
214impl<M: Memory> Memory for &RegionMemory<M> {
215    fn size(&self) -> u64 {
216        self.memory.size().clamp(self.pages.start, self.pages.end) - self.pages.start
217    }
218
219    fn grow(&self, delta: u64) -> i64 {
220        let previous = self.size();
221        let Some(minimum) = previous.checked_add(delta) else {
222            return -1;
223        };
224        if self.reserve_pages(minimum).is_ok() {
225            // The selected range is bounded by MAX_PAGES.
226            i64::try_from(previous).unwrap()
227        } else {
228            -1
229        }
230    }
231
232    fn read(&self, offset: u64, dst: &mut [u8]) {
233        self.check_io(offset, dst.len());
234        if !dst.is_empty() {
235            self.memory
236                .read(self.pages.start * PAGE_BYTES + offset, dst);
237        }
238    }
239
240    fn write(&self, offset: u64, src: &[u8]) {
241        self.check_io(offset, src.len());
242        if !src.is_empty() {
243            self.memory
244                .write(self.pages.start * PAGE_BYTES + offset, src);
245        }
246    }
247}