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_mut<T, E>(
19    operation: impl FnOnce(&mut 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_mut()
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
39// Observation must not choose a bucket configuration or initialize backing
40// memory. Keep absence distinct from a cached construction failure.
41fn with_existing_default_runtime<T, E>(
42    operation: impl FnOnce(Option<&MemoryRuntime<DefaultMemoryImpl>>) -> Result<T, E>,
43) -> Result<T, E>
44where
45    E: From<RuntimeStateError>,
46{
47    DEFAULT_RUNTIME
48        .try_with(|runtime| {
49            let runtime = runtime
50                .try_borrow()
51                .map_err(|_| E::from(RuntimeStateError::ReentrantAccess))?;
52            let existing = runtime
53                .as_ref()
54                .map(|runtime| {
55                    runtime
56                        .as_ref()
57                        .map_err(|error| E::from(RuntimeStateError::Construction(*error)))
58                })
59                .transpose()?;
60            operation(existing)
61        })
62        .map_err(|_| E::from(RuntimeStateError::Unavailable))?
63}
64
65/// Return whether this thread's default runtime has completed bootstrap.
66///
67/// Does not construct an absent runtime or initialize backing memory. Returns
68/// `false` for an absent or unbootstrapped runtime, preserving construction and
69/// TLS access failures as typed errors.
70pub fn is_default_memory_manager_bootstrapped() -> Result<bool, RuntimeStateError> {
71    with_existing_default_runtime(|runtime| Ok(runtime.is_some_and(MemoryRuntime::is_bootstrapped)))
72}
73
74/// Return this thread's default runtime committed allocation capability.
75///
76/// Does not construct an absent runtime or initialize backing memory. Returns
77/// `NotBootstrapped` for an absent or unbootstrapped runtime. This lookup can
78/// precede configured bootstrap without selecting the default bucket size.
79pub fn committed_allocations() -> Result<CommittedAllocations, RuntimeOpenError> {
80    with_existing_default_runtime(|runtime| {
81        runtime
82            .ok_or(RuntimeOpenError::NotBootstrapped)?
83            .committed_allocations()
84            .cloned()
85    })
86}
87
88/// Resolve an application key's committed ID in the existing default runtime.
89/// Does not construct a manager, open memory, or choose a bucket configuration.
90pub fn default_memory_manager_memory_id(stable_key: &str) -> Result<u8, RuntimeOpenError> {
91    with_existing_default_runtime(|runtime| {
92        runtime
93            .ok_or(RuntimeOpenError::NotBootstrapped)?
94            .memory_id(stable_key)
95    })
96}
97
98/// Verify one consumer's allocation requirements against the existing host
99/// runtime. Does not construct, bootstrap, replay admission or change configuration.
100pub fn verify_default_memory_manager_authority(
101    requirements: &crate::SealedDeclarationSnapshot,
102    authority: &str,
103) -> Result<(), super::RuntimeAdoptionError> {
104    with_existing_default_runtime(|runtime| {
105        runtime
106            .ok_or(RuntimeOpenError::NotBootstrapped)?
107            .verify_authority(requirements, authority)
108    })
109}
110
111/// Bootstrap this thread's default runtime using generic range policy.
112pub fn bootstrap_default_memory_manager()
113-> Result<CommittedAllocations, RuntimeBootstrapError<Infallible>> {
114    bootstrap_default_memory_manager_with_policy(&GenericRangePolicy)
115}
116
117/// Bootstrap this thread's default runtime with caller-supplied policy.
118///
119/// Static declarations are sealed once per linked program. Recovery, policy
120/// evaluation, persistence, and capability publication occur once for this
121/// concrete TLS runtime. Repeated calls must supply the policy identity bound
122/// by the successful bootstrap.
123pub fn bootstrap_default_memory_manager_with_policy<P: RuntimeBootstrapPolicy>(
124    policy: &P,
125) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
126    let declarations = sealed_declaration_snapshot()?;
127    with_default_runtime_mut(|runtime| runtime.bootstrap(&declarations, policy).cloned())
128}
129
130/// Open a committed memory from this thread's default runtime.
131/// Does not construct an absent runtime or select its bucket configuration.
132pub fn open_default_memory_manager_memory(
133    stable_key: &str,
134    id: u8,
135) -> Result<RuntimeMemory<DefaultMemoryImpl>, RuntimeOpenError> {
136    with_existing_default_runtime(|runtime| {
137        runtime
138            .ok_or(RuntimeOpenError::NotBootstrapped)?
139            .open_memory(stable_key, id)
140    })
141}
142
143/// Open a key already committed by the host's default runtime without changing policy.
144/// Does not construct an absent runtime or select its bucket configuration.
145pub fn open_default_memory_manager_memory_by_key(
146    stable_key: &str,
147) -> Result<RuntimeMemory<DefaultMemoryImpl>, RuntimeOpenError> {
148    with_existing_default_runtime(|runtime| {
149        runtime
150            .ok_or(RuntimeOpenError::NotBootstrapped)?
151            .open_memory_by_key(stable_key)
152    })
153}
154
155/// Export this thread's default runtime ledger and live memory sizes.
156///
157/// Returns `NotBootstrapped` for an absent or unbootstrapped runtime without
158/// initializing backing memory or choosing a bucket configuration.
159pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
160{
161    with_existing_default_runtime(|runtime| {
162        runtime
163            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
164            .diagnostic_export()
165    })
166}
167
168/// Diagnose protected commit recovery for this thread's default runtime.
169///
170/// Returns `NotBootstrapped` if no runtime exists without initializing memory
171/// or choosing configuration. An existing runtime can be inspected before bootstrap.
172pub fn default_memory_manager_commit_recovery_diagnostic()
173-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
174    with_existing_default_runtime(|runtime| {
175        runtime
176            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
177            .commit_recovery_diagnostic()
178    })
179}
180
181/// Build preflight and lifecycle diagnostics for this thread's default runtime.
182///
183/// Returns `NotBootstrapped` if no runtime exists without initializing memory
184/// or choosing configuration. An existing runtime can be inspected before bootstrap.
185pub fn default_memory_manager_doctor_report()
186-> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError> {
187    default_memory_manager_doctor_report_with_policy(&GenericRangePolicy)
188}
189
190/// Build diagnostics for this thread's default runtime under one explicit policy.
191///
192/// Returns `NotBootstrapped` if no runtime exists without sealing declarations,
193/// initializing memory or choosing configuration. The policy is evaluated only;
194/// it does not construct or bootstrap the runtime.
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    // Check presence before running registration hooks, but release the TLS
203    // borrow so hooks can inspect the existing runtime during snapshot sealing.
204    with_existing_default_runtime(|runtime| {
205        runtime
206            .ok_or(RuntimeDiagnosticError::NotBootstrapped)
207            .map(|_| ())
208    })?;
209    let declarations = sealed_declaration_snapshot()?;
210    with_existing_default_runtime(|runtime| {
211        Ok(runtime
212            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
213            .doctor_report(&declarations, policy))
214    })
215}
216
217#[cfg(test)]
218pub(super) fn with_default_runtime_borrowed(
219    operation: impl FnOnce() -> Result<(), RuntimeStateError>,
220) -> Result<(), RuntimeStateError> {
221    DEFAULT_RUNTIME.with(|runtime| {
222        let _borrow = runtime.borrow_mut();
223        operation()
224    })
225}
226
227/// Measure the existing default runtime without constructing a manager or
228/// initializing backing memory.
229///
230/// Returns `NotBootstrapped` if no runtime exists.
231/// A constructed runtime may be measured before bootstrap with unknown bindings.
232pub fn default_memory_manager_memory_allocations()
233-> Result<super::MemoryAllocations, RuntimeDiagnosticError> {
234    with_existing_default_runtime(|runtime| {
235        runtime
236            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
237            .memory_allocations()
238    })
239}
240
241/// Measure numeric allocation totals in the existing default runtime.
242///
243/// Does not copy binding names or construct per-ID rows. Absence returns
244/// `NotBootstrapped` without initializing memory or choosing configuration.
245pub fn default_memory_manager_memory_allocation_summary()
246-> Result<super::MemoryAllocationSummary, RuntimeDiagnosticError> {
247    with_existing_default_runtime(|runtime| {
248        runtime
249            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
250            .memory_allocation_summary()
251    })
252}
253
254/// Bootstrap the default runtime with an explicit bucket setting and allocation
255/// policy.
256///
257/// The first construction uses this setting; repeated calls and reopened
258/// memory must match it exactly before bootstrap effects. Call this during
259/// bootstrap before any operation that would construct the default runtime.
260/// Use [`super::GenericRangePolicy`] to select the built-in policy, or pass the
261/// host's custom policy. This operation does not adopt a different bound policy.
262pub fn bootstrap_default_memory_manager_with_config<P: RuntimeBootstrapPolicy>(
263    config: super::MemoryManagerConfig,
264    policy: &P,
265) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
266    DEFAULT_RUNTIME
267        .try_with(|runtime| {
268            let mut runtime = runtime
269                .try_borrow_mut()
270                .map_err(|_| RuntimeStateError::ReentrantAccess)?;
271            let runtime = runtime
272                .get_or_insert_with(|| {
273                    MemoryRuntime::new_with_config(DefaultMemoryImpl::default(), config)
274                })
275                .as_mut()
276                .map_err(|error| RuntimeStateError::Construction(*error))?;
277            super::check_bucket_size(runtime.bucket_size_pages, config)
278                .map_err(RuntimeStateError::Construction)?;
279            let declarations = sealed_declaration_snapshot()?;
280            runtime.bootstrap(&declarations, policy).cloned()
281        })
282        .map_err(|_| RuntimeStateError::Unavailable)?
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    fn diagnostic_observations() -> [Result<(), RuntimeDiagnosticError>; 4] {
290        [
291            default_memory_manager_diagnostic_export().map(|_| ()),
292            default_memory_manager_commit_recovery_diagnostic().map(|_| ()),
293            default_memory_manager_doctor_report().map(|_| ()),
294            default_memory_manager_doctor_report_with_policy(&GenericRangePolicy).map(|_| ()),
295        ]
296    }
297
298    #[test]
299    fn observations_leave_an_absent_runtime_absent() {
300        use crate::registry::{
301            TEST_REGISTRY_LOCK, defer_eager_init, reset_static_memory_declarations_for_tests,
302        };
303        use std::sync::atomic::{AtomicBool, Ordering};
304        static REGISTRATION_RAN: AtomicBool = AtomicBool::new(false);
305        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
306        reset_static_memory_declarations_for_tests();
307        defer_eager_init(|| REGISTRATION_RAN.store(true, Ordering::SeqCst));
308        std::thread::spawn(|| {
309            for _ in 0..2 {
310                assert!(!is_default_memory_manager_bootstrapped().unwrap());
311                assert_eq!(
312                    committed_allocations(),
313                    Err(RuntimeOpenError::NotBootstrapped)
314                );
315                assert!(matches!(
316                    default_memory_manager_memory_allocations(),
317                    Err(RuntimeDiagnosticError::NotBootstrapped)
318                ));
319                for result in diagnostic_observations() {
320                    assert!(matches!(
321                        result,
322                        Err(RuntimeDiagnosticError::NotBootstrapped)
323                    ));
324                }
325                DEFAULT_RUNTIME.with(|runtime| assert!(runtime.borrow().is_none()));
326            }
327        })
328        .join()
329        .unwrap();
330        assert!(!REGISTRATION_RAN.load(Ordering::SeqCst));
331        reset_static_memory_declarations_for_tests();
332    }
333
334    #[test]
335    fn observations_preserve_unbootstrapped_configuration() {
336        use crate::registry::{TEST_REGISTRY_LOCK, reset_static_memory_declarations_for_tests};
337        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
338        reset_static_memory_declarations_for_tests();
339        std::thread::spawn(|| {
340            let config = super::super::MemoryManagerConfig::new(16).unwrap();
341            DEFAULT_RUNTIME.with(|runtime| {
342                *runtime.borrow_mut() = Some(MemoryRuntime::new_with_config(
343                    DefaultMemoryImpl::default(),
344                    config,
345                ));
346            });
347            let before = default_memory_manager_memory_allocations().unwrap();
348            assert!(!is_default_memory_manager_bootstrapped().unwrap());
349            assert_eq!(
350                committed_allocations(),
351                Err(RuntimeOpenError::NotBootstrapped)
352            );
353            assert_eq!(before.bucket_size_pages, 16);
354            assert!(matches!(
355                default_memory_manager_diagnostic_export(),
356                Err(RuntimeDiagnosticError::NotBootstrapped)
357            ));
358            default_memory_manager_commit_recovery_diagnostic().unwrap();
359            assert!(!default_memory_manager_doctor_report().unwrap().bootstrapped);
360            assert!(
361                !default_memory_manager_doctor_report_with_policy(&GenericRangePolicy)
362                    .unwrap()
363                    .bootstrapped
364            );
365            assert_eq!(default_memory_manager_memory_allocations().unwrap(), before);
366        })
367        .join()
368        .unwrap();
369        reset_static_memory_declarations_for_tests();
370    }
371
372    #[test]
373    fn observations_preserve_cached_construction_failure() {
374        std::thread::spawn(|| {
375            let error = RuntimeConstructionError::ForeignMemory {
376                observed_magic: *b"BAD",
377            };
378            DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = Some(Err(error)));
379            for result in diagnostic_observations() {
380                assert!(matches!(result, Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause))) if cause == error));
381            }
382            assert_eq!(
383                is_default_memory_manager_bootstrapped(),
384                Err(RuntimeStateError::Construction(error))
385            );
386            assert_eq!(
387                committed_allocations(),
388                Err(RuntimeOpenError::State(RuntimeStateError::Construction(
389                    error
390                )))
391            );
392            assert!(matches!(
393                default_memory_manager_memory_allocations(),
394                Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause)))
395                    if cause == error
396            ));
397            for result in [
398                open_default_memory_manager_memory_by_key("app.rows.v1").err(),
399                open_default_memory_manager_memory("app.rows.v1", 100).err(),
400                default_memory_manager_memory_id("app.rows.v1").err(),
401            ] {
402                assert_eq!(result, Some(RuntimeOpenError::State(RuntimeStateError::Construction(error))));
403            }
404            let requirements = crate::SealedDeclarationSnapshot::new(&[], &[], &[]).unwrap();
405            assert_eq!(verify_default_memory_manager_authority(&requirements, "app"), Err(super::super::RuntimeAdoptionError::Open(RuntimeOpenError::State(RuntimeStateError::Construction(error)))));
406            assert!(matches!(default_memory_manager_memory_allocation_summary(), Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause))) if cause == error));
407            DEFAULT_RUNTIME.with(|runtime| {
408                assert!(matches!(runtime.borrow().as_ref(), Some(Err(cause)) if *cause == error));
409            });
410        })
411        .join()
412        .unwrap();
413    }
414
415    #[test]
416    fn diagnostic_observations_preserve_reentrant_access_errors() {
417        with_default_runtime_borrowed(|| {
418            for result in diagnostic_observations() {
419                assert!(matches!(
420                    result,
421                    Err(RuntimeDiagnosticError::State(
422                        RuntimeStateError::ReentrantAccess
423                    ))
424                ));
425            }
426            Ok(())
427        })
428        .unwrap();
429    }
430    #[test]
431    #[cfg(not(target_arch = "wasm32"))]
432    fn configured_default_prepares_once_and_warm_library_adoption_only_opens() {
433        use crate::registry::{TEST_REGISTRY_LOCK, reset_static_memory_declarations_for_tests};
434        use ic_stable_structures::Memory;
435        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
436        reset_static_memory_declarations_for_tests();
437        crate::register_static_memory_manager_range(
438            100,
439            110,
440            "app",
441            crate::MemoryManagerRangeMode::Allowed,
442            None,
443        )
444        .unwrap();
445        crate::register_memory_request(
446            crate::MemoryRequest::new(
447                "app",
448                "app.main.control.v1",
449                crate::SchemaMetadata::default(),
450            )
451            .unwrap(),
452        )
453        .unwrap();
454        let backing = super::super::admission_tests::seeded();
455        DEFAULT_RUNTIME
456            .with(|runtime| *runtime.borrow_mut() = Some(MemoryRuntime::new(backing.clone())));
457        let policy = super::super::admission_tests::AdmissionPolicy {
458            discover: true,
459            ..Default::default()
460        };
461        let config = super::super::MemoryManagerConfig::new(1).unwrap();
462        let committed = bootstrap_default_memory_manager_with_config(config, &policy).unwrap();
463        assert_eq!(committed.generation(), 2);
464        let before = backing.borrow().clone();
465        let mut marker = [0; 12];
466        open_default_memory_manager_memory_by_key("app.old.journal.v1")
467            .unwrap()
468            .read(0, &mut marker);
469        assert_eq!(&marker, b"pending/debt");
470        assert_eq!(committed_allocations().unwrap(), committed);
471        assert_eq!(
472            bootstrap_default_memory_manager_with_config(config, &policy).unwrap(),
473            committed
474        );
475        assert!(
476            bootstrap_default_memory_manager_with_config(
477                super::super::MemoryManagerConfig::new(2).unwrap(),
478                &policy
479            )
480            .is_err()
481        );
482        assert_eq!(policy.calls.get(), 1);
483        assert_eq!(*backing.borrow(), before);
484        assert_eq!(
485            default_memory_manager_memory_allocations()
486                .unwrap()
487                .bucket_size_pages,
488            1
489        );
490        DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = None);
491        reset_static_memory_declarations_for_tests();
492    }
493}