Skip to main content

ic_memory/runtime/
default.rs

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