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