Skip to main content

ic_memory/runtime/
default.rs

1use super::{
2    MemoryRuntime, RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError,
3    RuntimeMemory, RuntimeOpenError, RuntimeStateError, policy::GenericRangePolicy,
4};
5use crate::{
6    CommittedAllocations, DiagnosticExport, MemoryRuntimeDoctorReport, RuntimeBootstrapPolicy,
7    physical::CommitStoreDiagnostic, registry::sealed_declaration_snapshot,
8};
9use ic_stable_structures::DefaultMemoryImpl;
10use std::{cell::RefCell, convert::Infallible, fmt::Display};
11
12thread_local! {
13    static DEFAULT_RUNTIME:
14        RefCell<Option<Result<MemoryRuntime<DefaultMemoryImpl>, RuntimeConstructionError>>> =
15        const { RefCell::new(None) };
16}
17
18fn with_default_runtime<T, E>(
19    operation: impl FnOnce(&MemoryRuntime<DefaultMemoryImpl>) -> Result<T, E>,
20) -> Result<T, E>
21where
22    E: From<RuntimeStateError>,
23{
24    match DEFAULT_RUNTIME.try_with(|runtime| {
25        let mut runtime = runtime
26            .try_borrow_mut()
27            .map_err(|_| E::from(RuntimeStateError::ReentrantAccess))?;
28        let runtime = runtime
29            .get_or_insert_with(|| MemoryRuntime::new(DefaultMemoryImpl::default()))
30            .as_ref()
31            .map_err(|error| E::from(RuntimeStateError::Construction(*error)))?;
32        operation(runtime)
33    }) {
34        Ok(result) => result,
35        Err(_) => Err(E::from(RuntimeStateError::Unavailable)),
36    }
37}
38
39fn with_default_runtime_mut<T, E>(
40    operation: impl FnOnce(&mut MemoryRuntime<DefaultMemoryImpl>) -> Result<T, E>,
41) -> Result<T, E>
42where
43    E: From<RuntimeStateError>,
44{
45    match DEFAULT_RUNTIME.try_with(|runtime| {
46        let mut runtime = runtime
47            .try_borrow_mut()
48            .map_err(|_| E::from(RuntimeStateError::ReentrantAccess))?;
49        let runtime = runtime
50            .get_or_insert_with(|| MemoryRuntime::new(DefaultMemoryImpl::default()))
51            .as_mut()
52            .map_err(|error| E::from(RuntimeStateError::Construction(*error)))?;
53        operation(runtime)
54    }) {
55        Ok(result) => result,
56        Err(_) => Err(E::from(RuntimeStateError::Unavailable)),
57    }
58}
59
60// Observation must not choose a bucket configuration or initialize backing
61// memory. Keep absence distinct from a cached construction failure.
62fn with_existing_default_runtime<T, E>(
63    operation: impl FnOnce(Option<&MemoryRuntime<DefaultMemoryImpl>>) -> Result<T, E>,
64) -> Result<T, E>
65where
66    E: From<RuntimeStateError>,
67{
68    DEFAULT_RUNTIME
69        .try_with(|runtime| {
70            let runtime = runtime
71                .try_borrow()
72                .map_err(|_| E::from(RuntimeStateError::ReentrantAccess))?;
73            let existing = runtime
74                .as_ref()
75                .map(|runtime| {
76                    runtime
77                        .as_ref()
78                        .map_err(|error| E::from(RuntimeStateError::Construction(*error)))
79                })
80                .transpose()?;
81            operation(existing)
82        })
83        .map_err(|_| E::from(RuntimeStateError::Unavailable))?
84}
85
86/// Return whether this thread's default runtime has completed bootstrap.
87///
88/// Does not construct an absent runtime or initialize backing memory. Returns
89/// `false` for an absent or unbootstrapped runtime, preserving construction and
90/// TLS access failures as typed errors.
91pub fn is_default_memory_manager_bootstrapped() -> Result<bool, RuntimeStateError> {
92    with_existing_default_runtime(|runtime| Ok(runtime.is_some_and(MemoryRuntime::is_bootstrapped)))
93}
94
95/// Return this thread's default runtime committed allocation capability.
96///
97/// Does not construct an absent runtime or initialize backing memory. Returns
98/// `NotBootstrapped` for an absent or unbootstrapped runtime. This lookup can
99/// precede configured bootstrap without selecting the default bucket size.
100pub fn committed_allocations() -> Result<CommittedAllocations, RuntimeOpenError> {
101    with_existing_default_runtime(|runtime| {
102        runtime
103            .ok_or(RuntimeOpenError::NotBootstrapped)?
104            .committed_allocations()
105            .cloned()
106    })
107}
108
109/// Bootstrap this thread's default runtime using generic range policy.
110pub fn bootstrap_default_memory_manager()
111-> Result<CommittedAllocations, RuntimeBootstrapError<Infallible>> {
112    bootstrap_default_memory_manager_with_policy(&GenericRangePolicy)
113}
114
115/// Bootstrap this thread's default runtime with caller-supplied policy.
116///
117/// Static declarations are sealed once per linked program. Recovery, policy
118/// evaluation, persistence, and capability publication occur once for this
119/// concrete TLS runtime. Repeated calls must supply the policy identity bound
120/// by the successful bootstrap.
121pub fn bootstrap_default_memory_manager_with_policy<P: RuntimeBootstrapPolicy>(
122    policy: &P,
123) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
124    let declarations = sealed_declaration_snapshot()?;
125    with_default_runtime_mut(|runtime| runtime.bootstrap(&declarations, policy).cloned())
126}
127
128/// Open a committed memory from this thread's default runtime.
129pub fn open_default_memory_manager_memory(
130    stable_key: &str,
131    id: u8,
132) -> Result<RuntimeMemory<DefaultMemoryImpl>, RuntimeOpenError> {
133    with_default_runtime(|runtime| runtime.open_memory(stable_key, id))
134}
135
136/// Export this thread's default runtime ledger and live memory sizes.
137pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
138{
139    with_default_runtime(MemoryRuntime::diagnostic_export)
140}
141
142/// Diagnose protected commit recovery for this thread's default runtime.
143pub fn default_memory_manager_commit_recovery_diagnostic()
144-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
145    with_default_runtime(MemoryRuntime::commit_recovery_diagnostic)
146}
147
148/// Build preflight and lifecycle diagnostics for this thread's default runtime.
149pub fn default_memory_manager_doctor_report()
150-> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError> {
151    default_memory_manager_doctor_report_with_policy(&GenericRangePolicy)
152}
153
154/// Build diagnostics for this thread's default runtime under one explicit policy.
155pub fn default_memory_manager_doctor_report_with_policy<P>(
156    policy: &P,
157) -> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError>
158where
159    P: RuntimeBootstrapPolicy,
160    P::Error: Display,
161{
162    let declarations = sealed_declaration_snapshot()?;
163    with_default_runtime(|runtime| Ok(runtime.doctor_report(&declarations, policy)))
164}
165
166#[cfg(test)]
167pub(super) fn with_default_runtime_borrowed(
168    operation: impl FnOnce() -> Result<(), RuntimeStateError>,
169) -> Result<(), RuntimeStateError> {
170    DEFAULT_RUNTIME.with(|runtime| {
171        let _borrow = runtime.borrow_mut();
172        operation()
173    })
174}
175
176/// Measure the existing default runtime without constructing a manager or
177/// initializing backing memory.
178///
179/// Returns `NotBootstrapped` if no runtime exists.
180/// A constructed runtime may be measured before bootstrap with unknown bindings.
181pub fn default_memory_manager_memory_allocations()
182-> Result<super::MemoryAllocations, RuntimeDiagnosticError> {
183    with_existing_default_runtime(|runtime| {
184        runtime
185            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
186            .memory_allocations()
187    })
188}
189
190/// Bootstrap the default runtime with an explicit bucket setting and allocation
191/// policy.
192///
193/// The first construction uses this setting; repeated calls and reopened
194/// memory must match it exactly before bootstrap effects. Call this during
195/// bootstrap before any operation that would construct the default runtime.
196/// Use [`super::GenericRangePolicy`] to select the built-in policy, or pass the
197/// host's custom policy. This operation does not adopt a different bound policy.
198pub fn bootstrap_default_memory_manager_with_config<P: RuntimeBootstrapPolicy>(
199    config: super::MemoryManagerConfig,
200    policy: &P,
201) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
202    DEFAULT_RUNTIME
203        .try_with(|runtime| {
204            let mut runtime = runtime
205                .try_borrow_mut()
206                .map_err(|_| RuntimeStateError::ReentrantAccess)?;
207            let runtime = runtime
208                .get_or_insert_with(|| {
209                    MemoryRuntime::new_with_config(DefaultMemoryImpl::default(), config)
210                })
211                .as_mut()
212                .map_err(|error| RuntimeStateError::Construction(*error))?;
213            super::check_bucket_size(runtime.bucket_size_pages, config)
214                .map_err(RuntimeStateError::Construction)?;
215            let declarations = sealed_declaration_snapshot()?;
216            runtime.bootstrap(&declarations, policy).cloned()
217        })
218        .map_err(|_| RuntimeStateError::Unavailable)?
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn observations_leave_an_absent_runtime_absent() {
227        std::thread::spawn(|| {
228            for _ in 0..2 {
229                assert!(!is_default_memory_manager_bootstrapped().unwrap());
230                assert_eq!(
231                    committed_allocations(),
232                    Err(RuntimeOpenError::NotBootstrapped)
233                );
234                assert!(matches!(
235                    default_memory_manager_memory_allocations(),
236                    Err(RuntimeDiagnosticError::NotBootstrapped)
237                ));
238                DEFAULT_RUNTIME.with(|runtime| assert!(runtime.borrow().is_none()));
239            }
240        })
241        .join()
242        .unwrap();
243    }
244
245    #[test]
246    fn observations_preserve_unbootstrapped_configuration() {
247        std::thread::spawn(|| {
248            let config = super::super::MemoryManagerConfig::new(16).unwrap();
249            DEFAULT_RUNTIME.with(|runtime| {
250                *runtime.borrow_mut() = Some(MemoryRuntime::new_with_config(
251                    DefaultMemoryImpl::default(),
252                    config,
253                ));
254            });
255            let before = default_memory_manager_memory_allocations().unwrap();
256            assert!(!is_default_memory_manager_bootstrapped().unwrap());
257            assert_eq!(
258                committed_allocations(),
259                Err(RuntimeOpenError::NotBootstrapped)
260            );
261            assert_eq!(before.bucket_size_pages, 16);
262            assert_eq!(default_memory_manager_memory_allocations().unwrap(), before);
263        })
264        .join()
265        .unwrap();
266    }
267
268    #[test]
269    fn observations_preserve_cached_construction_failure() {
270        std::thread::spawn(|| {
271            let error = RuntimeConstructionError::ForeignMemory {
272                observed_magic: *b"BAD",
273            };
274            DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = Some(Err(error)));
275            assert_eq!(
276                is_default_memory_manager_bootstrapped(),
277                Err(RuntimeStateError::Construction(error))
278            );
279            assert_eq!(
280                committed_allocations(),
281                Err(RuntimeOpenError::State(RuntimeStateError::Construction(
282                    error
283                )))
284            );
285            assert!(matches!(
286                default_memory_manager_memory_allocations(),
287                Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause)))
288                    if cause == error
289            ));
290            DEFAULT_RUNTIME.with(|runtime| {
291                assert!(matches!(runtime.borrow().as_ref(), Some(Err(cause)) if *cause == error));
292            });
293        })
294        .join()
295        .unwrap();
296    }
297}