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}