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