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.growth.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 configured_bootstrap_propagates_metadata_growth_refusal() {
407        std::thread::spawn(|| {
408            let error = RuntimeConstructionError::Growth(super::super::RuntimeGrowError::BackingRefused { additional_pages: 1 });
409            DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = Some(Err(error)));
410            assert!(matches!(
411                bootstrap_default_memory_manager_with_config(MemoryManagerConfig::new(16).unwrap(), &GenericRangePolicy),
412                Err(RuntimeBootstrapError::State(RuntimeStateError::Construction(cause))) if cause == error
413            ));
414            assert_eq!(is_default_memory_manager_bootstrapped(), Err(RuntimeStateError::Construction(error)));
415            DEFAULT_RUNTIME.with(|runtime| assert!(matches!(runtime.borrow().as_ref(), Some(Err(cause)) if *cause == error)));
416        }).join().unwrap();
417    }
418
419    #[test]
420    fn observations_preserve_cached_construction_failure() {
421        std::thread::spawn(|| {
422            let error = RuntimeConstructionError::ForeignMemory {
423                observed_magic: *b"BAD",
424            };
425            DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = Some(Err(error)));
426            for result in diagnostic_observations() {
427                assert!(matches!(result, Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause))) if cause == error));
428            }
429            assert_eq!(
430                is_default_memory_manager_bootstrapped(),
431                Err(RuntimeStateError::Construction(error))
432            );
433            assert_eq!(
434                committed_allocations(),
435                Err(RuntimeOpenError::State(RuntimeStateError::Construction(
436                    error
437                )))
438            );
439            assert!(matches!(
440                default_memory_manager_memory_allocations(),
441                Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause)))
442                    if cause == error
443            ));
444            for result in [
445                open_default_memory_manager_memory_by_key("app.rows.v1").err(),
446                open_default_memory_manager_memory("app.rows.v1", 100).err(),
447                default_memory_manager_memory_id("app.rows.v1").err(),
448            ] {
449                assert_eq!(result, Some(RuntimeOpenError::State(RuntimeStateError::Construction(error))));
450            }
451            let requirements = crate::SealedDeclarationSnapshot::new(&[], &[], &[]).unwrap();
452            assert_eq!(verify_default_memory_manager_authority(&requirements, "app"), Err(super::super::RuntimeAdoptionError::Open(RuntimeOpenError::State(RuntimeStateError::Construction(error)))));
453            assert!(matches!(default_memory_manager_memory_allocation_summary(), Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause))) if cause == error));
454            DEFAULT_RUNTIME.with(|runtime| {
455                assert!(matches!(runtime.borrow().as_ref(), Some(Err(cause)) if *cause == error));
456            });
457        })
458        .join()
459        .unwrap();
460    }
461
462    #[test]
463    fn diagnostic_observations_preserve_reentrant_access_errors() {
464        with_default_runtime_borrowed(|| {
465            for result in diagnostic_observations() {
466                assert!(matches!(
467                    result,
468                    Err(RuntimeDiagnosticError::State(
469                        RuntimeStateError::ReentrantAccess
470                    ))
471                ));
472            }
473            Ok(())
474        })
475        .unwrap();
476    }
477    #[test]
478    #[cfg(not(target_arch = "wasm32"))]
479    fn configured_default_prepares_once_and_warm_library_adoption_only_opens() {
480        use crate::registry::{TEST_REGISTRY_LOCK, reset_static_memory_declarations_for_tests};
481        use ic_stable_structures::Memory;
482        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
483        reset_static_memory_declarations_for_tests();
484        crate::register_static_memory_manager_range(
485            100,
486            110,
487            "app",
488            crate::MemoryManagerRangeMode::Allowed,
489            None,
490        )
491        .unwrap();
492        crate::register_memory_request(
493            crate::MemoryRequest::new(
494                "app",
495                "app.main.control.v1",
496                crate::SchemaMetadata::default(),
497            )
498            .unwrap(),
499        )
500        .unwrap();
501        let backing = super::super::admission_tests::seeded();
502        DEFAULT_RUNTIME
503            .with(|runtime| *runtime.borrow_mut() = Some(MemoryRuntime::new(backing.clone())));
504        let policy = super::super::admission_tests::AdmissionPolicy {
505            discover: true,
506            ..Default::default()
507        };
508        let config = super::super::MemoryManagerConfig::new(1).unwrap();
509        let committed = bootstrap_default_memory_manager_with_config(config, &policy).unwrap();
510        assert_eq!(committed.generation(), 2);
511        let before = backing.borrow().clone();
512        let mut marker = [0; 12];
513        open_default_memory_manager_memory_by_key("app.old.journal.v1")
514            .unwrap()
515            .read(0, &mut marker);
516        assert_eq!(&marker, b"pending/debt");
517        assert_eq!(committed_allocations().unwrap(), committed);
518        assert_eq!(
519            bootstrap_default_memory_manager_with_config(config, &policy).unwrap(),
520            committed
521        );
522        assert!(
523            bootstrap_default_memory_manager_with_config(
524                super::super::MemoryManagerConfig::new(2).unwrap(),
525                &policy
526            )
527            .is_err()
528        );
529        assert_eq!(policy.calls.get(), 1);
530        assert_eq!(*backing.borrow(), before);
531        assert_eq!(
532            default_memory_manager_memory_allocations()
533                .unwrap()
534                .bucket_size_pages,
535            1
536        );
537        DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = None);
538        reset_static_memory_declarations_for_tests();
539    }
540}