ic_storage/adapter/region.rs
1//! Operation-local collection access within a retained allocation region.
2use super::error::{CollectionError as Error, Result as CollectionResult, ensure};
3use super::tree::Tree;
4use super::{Memory, Storable};
5use crate::DefaultMemoryImpl;
6use crate::{RuntimeGrowError, RuntimeMemory, WASM_PAGE_SIZE_BYTES as PAGE_BYTES};
7use std::{marker::PhantomData, ops::Range, ops::RangeBounds, rc::Rc};
8
9const MAX_PAGES: u64 = 4_194_304;
10
11///
12/// `RegionGrowError`
13///
14/// Failure to reserve a governed region's virtual extent, before any writes.
15///
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
18pub enum RegionGrowError {
19 /// The requested relative extent exceeds the selected region.
20 #[error("requested region extent {minimum} exceeds {maximum} pages")]
21 LimitExceeded {
22 /// Requested relative page count.
23 minimum: u64,
24 /// Maximum relative page count.
25 maximum: u64,
26 },
27 /// Storage refused to grow the containing allocation.
28 #[error("{0}")]
29 Runtime(#[source] RuntimeGrowError),
30}
31
32/// A B-tree view inside one existing, governed allocation lease.
33///
34/// The caller retains its `Rc<RuntimeMemory>` for disjoint application frames.
35/// Each collection operation loads the current owned tree root and allocator;
36/// multiple views may interleave operations without retaining stale roots.
37/// Key scans return owned snapshots, so no cached tree or iterator escapes.
38/// Applications own namespace bounds, codec admission and recovery policy.
39///
40/// Construction performs no IO. Inspect retained framing with [`Self::read`]
41/// before calling collection operations, which return collection admission errors. 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 owned 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 /// # Errors
89 /// Rejects a nonempty region or capacity refusal without initializing a replacement.
90 pub fn initialize(&mut self) -> CollectionResult<()> {
91 ensure(self.allocated_pages() == 0, Error::NotEmpty)?;
92 Tree::<K, V, _>::reset(&self.memory)?;
93 Ok(())
94 }
95
96 /// Reserve an absolute relative extent without promising entry capacity.
97 ///
98 /// # Errors
99 /// Returns a region-limit or Storage 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 the host failure contract; 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 ///
110 /// # Errors
111 /// Reports structural, integrity or application decoding failures without writing.
112 pub fn get(&self, key: &K) -> CollectionResult<Option<V>> {
113 self.current_map()?.get(key)
114 }
115
116 /// Insert using the current retained root and staged node updates.
117 ///
118 /// # Errors
119 /// Rejects encoding, decoding, structural or capacity errors before committing writes.
120 pub fn insert(&mut self, key: K, value: V) -> CollectionResult<Option<V>> {
121 self.current_map()?.insert(key, value)
122 }
123
124 /// Remove using the current retained root and staged node updates.
125 ///
126 /// # Errors
127 /// Rejects decoding or structural errors before committing removal.
128 pub fn remove(&mut self, key: &K) -> CollectionResult<Option<V>> {
129 self.current_map()?.remove(key)
130 }
131
132 /// Return the current tree's record count across all namespaces.
133 ///
134 /// # Errors
135 /// Reports invalid region metadata or incompatible codec bounds.
136 pub fn len(&self) -> CollectionResult<u64> {
137 Ok(self.current_map()?.len())
138 }
139
140 /// Return whether the current tree has no records.
141 ///
142 /// # Errors
143 /// Reports invalid region metadata or incompatible codec bounds.
144 pub fn is_empty(&self) -> CollectionResult<bool> {
145 Ok(self.current_map()?.len() == 0)
146 }
147
148 /// Snapshot ordered keys within the caller's namespace/range bounds.
149 ///
150 /// The scan completes before returning; the iterator retains owned keys,
151 /// with no collection cache, memory handle or borrow across later mutations.
152 ///
153 /// # Errors
154 /// Reports invalid ranges, retained structure or key decoding failures.
155 pub fn keys_range(
156 &self,
157 bounds: impl RangeBounds<K>,
158 ) -> CollectionResult<std::vec::IntoIter<K>> {
159 self.keys_range_limited(bounds, usize::MAX)
160 }
161
162 /// Snapshot at most `limit` ordered keys, stopping iteration at that bound.
163 ///
164 /// Unlike `keys_range(bounds).take(limit)`, this bounds collection work before
165 /// allocating the snapshot. Zero returns without loading the tree. Resume
166 /// with an excluded last key; intervening mutations are visible to each call.
167 /// The limit bounds key count, not encoded bytes or instruction cost.
168 ///
169 /// # Errors
170 /// Reports invalid ranges, retained structure or key decoding failures.
171 pub fn keys_range_limited(
172 &self,
173 bounds: impl RangeBounds<K>,
174 limit: usize,
175 ) -> CollectionResult<std::vec::IntoIter<K>> {
176 if limit == 0 {
177 return Ok(Vec::new().into_iter());
178 }
179 Ok(self
180 .current_map()?
181 .keys_range(bounds)
182 .take(limit)
183 .collect::<CollectionResult<Vec<_>>>()?
184 .into_iter())
185 }
186
187 fn current_map(&self) -> CollectionResult<Tree<K, V, &RegionMemory<M>>> {
188 Tree::load(&self.memory)
189 }
190}
191
192// Private bounded IO over an admitted lease. Reads, writes and growth stay
193// inside the selected region, including initially absent regions.
194struct RegionMemory<M: Memory> {
195 memory: Rc<RuntimeMemory<M>>,
196 pages: Range<u64>,
197}
198
199impl<M: Memory> RegionMemory<M> {
200 fn reserve_pages(&self, minimum: u64) -> Result<(), RegionGrowError> {
201 let maximum = self.pages.end - self.pages.start;
202 if minimum > maximum {
203 return Err(RegionGrowError::LimitExceeded { minimum, maximum });
204 }
205 if minimum > self.size() {
206 let additional = self.pages.start + minimum - self.memory.size();
207 self.memory
208 .as_ref()
209 .grow(additional)
210 .map_err(RegionGrowError::Runtime)?;
211 }
212 Ok(())
213 }
214
215 fn check_io(&self, offset: u64, count: usize) {
216 let extent = self.size() * PAGE_BYTES;
217 assert!(offset <= extent && count as u64 <= extent - offset);
218 }
219}
220
221impl<M: Memory> Memory for &RegionMemory<M> {
222 fn size(&self) -> u64 {
223 self.memory.size().clamp(self.pages.start, self.pages.end) - self.pages.start
224 }
225
226 fn grow(&self, delta: u64) -> i64 {
227 let previous = self.size();
228 let Some(minimum) = previous.checked_add(delta) else {
229 return -1;
230 };
231 if self.reserve_pages(minimum).is_ok() {
232 // The selected range is bounded by MAX_PAGES.
233 i64::try_from(previous).unwrap()
234 } else {
235 -1
236 }
237 }
238
239 fn read(&self, offset: u64, dst: &mut [u8]) {
240 self.check_io(offset, dst.len());
241 if !dst.is_empty() {
242 self.memory
243 .read(self.pages.start * PAGE_BYTES + offset, dst);
244 }
245 }
246
247 fn write(&self, offset: u64, src: &[u8]) {
248 self.check_io(offset, src.len());
249 if !src.is_empty() {
250 self.memory
251 .write(self.pages.start * PAGE_BYTES + offset, src);
252 }
253 }
254}