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/// Resolve an application key's committed ID in the existing default runtime.
110/// Does not construct a manager, open memory, or choose a bucket configuration.
111pub fn default_memory_manager_memory_id(stable_key: &str) -> Result<u8, RuntimeOpenError> {
112    with_existing_default_runtime(|runtime| {
113        runtime
114            .ok_or(RuntimeOpenError::NotBootstrapped)?
115            .memory_id(stable_key)
116    })
117}
118
119/// Verify one consumer's allocation requirements against the existing host
120/// runtime. Does not construct, bootstrap, replay admission or change configuration.
121pub fn verify_default_memory_manager_authority(
122    requirements: &crate::SealedDeclarationSnapshot,
123    authority: &str,
124) -> Result<(), super::RuntimeAdoptionError> {
125    with_existing_default_runtime(|runtime| {
126        runtime
127            .ok_or(RuntimeOpenError::NotBootstrapped)?
128            .verify_authority(requirements, authority)
129    })
130}
131
132/// Bootstrap this thread's default runtime using generic range policy.
133pub fn bootstrap_default_memory_manager()
134-> Result<CommittedAllocations, RuntimeBootstrapError<Infallible>> {
135    bootstrap_default_memory_manager_with_policy(&GenericRangePolicy)
136}
137
138/// Bootstrap this thread's default runtime with caller-supplied policy.
139///
140/// Static declarations are sealed once per linked program. Recovery, policy
141/// evaluation, persistence, and capability publication occur once for this
142/// concrete TLS runtime. Repeated calls must supply the policy identity bound
143/// by the successful bootstrap.
144pub fn bootstrap_default_memory_manager_with_policy<P: RuntimeBootstrapPolicy>(
145    policy: &P,
146) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
147    let declarations = sealed_declaration_snapshot()?;
148    with_default_runtime_mut(|runtime| runtime.bootstrap(&declarations, policy).cloned())
149}
150
151/// Open a committed memory from this thread's default runtime.
152/// Does not construct an absent runtime or select its bucket configuration.
153pub fn open_default_memory_manager_memory(
154    stable_key: &str,
155    id: u8,
156) -> Result<RuntimeMemory<DefaultMemoryImpl>, RuntimeOpenError> {
157    with_existing_default_runtime(|runtime| {
158        runtime
159            .ok_or(RuntimeOpenError::NotBootstrapped)?
160            .open_memory(stable_key, id)
161    })
162}
163
164/// Open a key already committed by the host's default runtime without changing policy.
165/// Does not construct an absent runtime or select its bucket configuration.
166pub fn open_default_memory_manager_memory_by_key(
167    stable_key: &str,
168) -> Result<RuntimeMemory<DefaultMemoryImpl>, RuntimeOpenError> {
169    with_existing_default_runtime(|runtime| {
170        runtime
171            .ok_or(RuntimeOpenError::NotBootstrapped)?
172            .open_memory_by_key(stable_key)
173    })
174}
175
176/// Export this thread's default runtime ledger and live memory sizes.
177pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
178{
179    with_default_runtime(MemoryRuntime::diagnostic_export)
180}
181
182/// Diagnose protected commit recovery for this thread's default runtime.
183pub fn default_memory_manager_commit_recovery_diagnostic()
184-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
185    with_default_runtime(MemoryRuntime::commit_recovery_diagnostic)
186}
187
188/// Build preflight and lifecycle diagnostics for this thread's default runtime.
189pub fn default_memory_manager_doctor_report()
190-> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError> {
191    default_memory_manager_doctor_report_with_policy(&GenericRangePolicy)
192}
193
194/// Build diagnostics for this thread's default runtime under one explicit policy.
195pub fn default_memory_manager_doctor_report_with_policy<P>(
196    policy: &P,
197) -> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError>
198where
199    P: RuntimeBootstrapPolicy,
200    P::Error: Display,
201{
202    let declarations = sealed_declaration_snapshot()?;
203    with_default_runtime(|runtime| Ok(runtime.doctor_report(&declarations, policy)))
204}
205
206#[cfg(test)]
207pub(super) fn with_default_runtime_borrowed(
208    operation: impl FnOnce() -> Result<(), RuntimeStateError>,
209) -> Result<(), RuntimeStateError> {
210    DEFAULT_RUNTIME.with(|runtime| {
211        let _borrow = runtime.borrow_mut();
212        operation()
213    })
214}
215
216/// Measure the existing default runtime without constructing a manager or
217/// initializing backing memory.
218///
219/// Returns `NotBootstrapped` if no runtime exists.
220/// A constructed runtime may be measured before bootstrap with unknown bindings.
221pub fn default_memory_manager_memory_allocations()
222-> Result<super::MemoryAllocations, RuntimeDiagnosticError> {
223    with_existing_default_runtime(|runtime| {
224        runtime
225            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
226            .memory_allocations()
227    })
228}
229
230/// Measure numeric allocation totals in the existing default runtime.
231///
232/// Does not copy binding names or construct per-ID rows. Absence returns
233/// `NotBootstrapped` without initializing memory or choosing configuration.
234pub fn default_memory_manager_memory_allocation_summary()
235-> Result<super::MemoryAllocationSummary, RuntimeDiagnosticError> {
236    with_existing_default_runtime(|runtime| {
237        runtime
238            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
239            .memory_allocation_summary()
240    })
241}
242
243/// Bootstrap the default runtime with an explicit bucket setting and allocation
244/// policy.
245///
246/// The first construction uses this setting; repeated calls and reopened
247/// memory must match it exactly before bootstrap effects. Call this during
248/// bootstrap before any operation that would construct the default runtime.
249/// Use [`super::GenericRangePolicy`] to select the built-in policy, or pass the
250/// host's custom policy. This operation does not adopt a different bound policy.
251pub fn bootstrap_default_memory_manager_with_config<P: RuntimeBootstrapPolicy>(
252    config: super::MemoryManagerConfig,
253    policy: &P,
254) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
255    DEFAULT_RUNTIME
256        .try_with(|runtime| {
257            let mut runtime = runtime
258                .try_borrow_mut()
259                .map_err(|_| RuntimeStateError::ReentrantAccess)?;
260            let runtime = runtime
261                .get_or_insert_with(|| {
262                    MemoryRuntime::new_with_config(DefaultMemoryImpl::default(), config)
263                })
264                .as_mut()
265                .map_err(|error| RuntimeStateError::Construction(*error))?;
266            super::check_bucket_size(runtime.bucket_size_pages, config)
267                .map_err(RuntimeStateError::Construction)?;
268            let declarations = sealed_declaration_snapshot()?;
269            runtime.bootstrap(&declarations, policy).cloned()
270        })
271        .map_err(|_| RuntimeStateError::Unavailable)?
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn observations_leave_an_absent_runtime_absent() {
280        std::thread::spawn(|| {
281            for _ in 0..2 {
282                assert!(!is_default_memory_manager_bootstrapped().unwrap());
283                assert_eq!(
284                    committed_allocations(),
285                    Err(RuntimeOpenError::NotBootstrapped)
286                );
287                assert!(matches!(
288                    default_memory_manager_memory_allocations(),
289                    Err(RuntimeDiagnosticError::NotBootstrapped)
290                ));
291                DEFAULT_RUNTIME.with(|runtime| assert!(runtime.borrow().is_none()));
292            }
293        })
294        .join()
295        .unwrap();
296    }
297
298    #[test]
299    fn observations_preserve_unbootstrapped_configuration() {
300        std::thread::spawn(|| {
301            let config = super::super::MemoryManagerConfig::new(16).unwrap();
302            DEFAULT_RUNTIME.with(|runtime| {
303                *runtime.borrow_mut() = Some(MemoryRuntime::new_with_config(
304                    DefaultMemoryImpl::default(),
305                    config,
306                ));
307            });
308            let before = default_memory_manager_memory_allocations().unwrap();
309            assert!(!is_default_memory_manager_bootstrapped().unwrap());
310            assert_eq!(
311                committed_allocations(),
312                Err(RuntimeOpenError::NotBootstrapped)
313            );
314            assert_eq!(before.bucket_size_pages, 16);
315            assert_eq!(default_memory_manager_memory_allocations().unwrap(), before);
316        })
317        .join()
318        .unwrap();
319    }
320
321    #[test]
322    fn observations_preserve_cached_construction_failure() {
323        std::thread::spawn(|| {
324            let error = RuntimeConstructionError::ForeignMemory {
325                observed_magic: *b"BAD",
326            };
327            DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = Some(Err(error)));
328            assert_eq!(
329                is_default_memory_manager_bootstrapped(),
330                Err(RuntimeStateError::Construction(error))
331            );
332            assert_eq!(
333                committed_allocations(),
334                Err(RuntimeOpenError::State(RuntimeStateError::Construction(
335                    error
336                )))
337            );
338            assert!(matches!(
339                default_memory_manager_memory_allocations(),
340                Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause)))
341                    if cause == error
342            ));
343            for result in [
344                open_default_memory_manager_memory_by_key("app.rows.v1").err(),
345                open_default_memory_manager_memory("app.rows.v1", 100).err(),
346                default_memory_manager_memory_id("app.rows.v1").err(),
347            ] {
348                assert_eq!(result, Some(RuntimeOpenError::State(RuntimeStateError::Construction(error))));
349            }
350            let requirements = crate::SealedDeclarationSnapshot::new(&[], &[], &[]).unwrap();
351            assert_eq!(verify_default_memory_manager_authority(&requirements, "app"), Err(super::super::RuntimeAdoptionError::Open(RuntimeOpenError::State(RuntimeStateError::Construction(error)))));
352            assert!(matches!(default_memory_manager_memory_allocation_summary(), Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause))) if cause == error));
353            DEFAULT_RUNTIME.with(|runtime| {
354                assert!(matches!(runtime.borrow().as_ref(), Some(Err(cause)) if *cause == error));
355            });
356        })
357        .join()
358        .unwrap();
359    }
360    #[test]
361    #[cfg(not(target_arch = "wasm32"))]
362    fn configured_default_prepares_once_and_warm_library_adoption_only_opens() {
363        use crate::registry::{TEST_REGISTRY_LOCK, reset_static_memory_declarations_for_tests};
364        use ic_stable_structures::Memory;
365        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
366        reset_static_memory_declarations_for_tests();
367        crate::register_static_memory_manager_range(
368            100,
369            110,
370            "app",
371            crate::MemoryManagerRangeMode::Allowed,
372            None,
373        )
374        .unwrap();
375        crate::register_memory_request(
376            crate::MemoryRequest::new(
377                "app",
378                "app.main.control.v1",
379                crate::SchemaMetadata::default(),
380            )
381            .unwrap(),
382        )
383        .unwrap();
384        let backing = super::super::admission_tests::seeded();
385        DEFAULT_RUNTIME
386            .with(|runtime| *runtime.borrow_mut() = Some(MemoryRuntime::new(backing.clone())));
387        let policy = super::super::admission_tests::AdmissionPolicy {
388            discover: true,
389            ..Default::default()
390        };
391        let config = super::super::MemoryManagerConfig::new(1).unwrap();
392        let committed = bootstrap_default_memory_manager_with_config(config, &policy).unwrap();
393        assert_eq!(committed.generation(), 2);
394        let before = backing.borrow().clone();
395        let mut marker = [0; 12];
396        open_default_memory_manager_memory_by_key("app.old.journal.v1")
397            .unwrap()
398            .read(0, &mut marker);
399        assert_eq!(&marker, b"pending/debt");
400        assert_eq!(committed_allocations().unwrap(), committed);
401        assert_eq!(
402            bootstrap_default_memory_manager_with_config(config, &policy).unwrap(),
403            committed
404        );
405        assert!(
406            bootstrap_default_memory_manager_with_config(
407                super::super::MemoryManagerConfig::new(2).unwrap(),
408                &policy
409            )
410            .is_err()
411        );
412        assert_eq!(policy.calls.get(), 1);
413        assert_eq!(*backing.borrow(), before);
414        assert_eq!(
415            default_memory_manager_memory_allocations()
416                .unwrap()
417                .bucket_size_pages,
418            1
419        );
420        DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = None);
421        reset_static_memory_declarations_for_tests();
422    }
423}