Skip to main content

ic_memory/runtime/
default.rs

1use super::{
2    MemoryManagerConfig, MemoryRuntime, RuntimeBootstrapError, RuntimeConstructionError,
3    RuntimeDiagnosticError, RuntimeMemory, RuntimeOpenError, RuntimeStateError,
4    policy::GenericAllocationPolicy,
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 the host pool and built-in allocation policy.
123pub fn bootstrap_default_memory_manager(
124    pool: &crate::MemoryAllocationPool,
125) -> Result<CommittedAllocations, RuntimeBootstrapError<Infallible>> {
126    bootstrap_default_memory_manager_with_policy(pool, &GenericAllocationPolicy)
127}
128
129/// Bootstrap this thread's default runtime with caller-supplied policy.
130///
131/// Static declarations are sealed once per linked program. Recovery, policy
132/// evaluation, persistence, and capability publication occur once for this
133/// concrete TLS runtime. Repeated calls must supply the canonical host pool
134/// and policy identity bound by the successful bootstrap.
135pub fn bootstrap_default_memory_manager_with_policy<P: RuntimeBootstrapPolicy>(
136    pool: &crate::MemoryAllocationPool,
137    policy: &P,
138) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
139    let declarations = sealed_declaration_snapshot()?;
140    with_default_runtime_mut(None, |runtime| {
141        runtime.bootstrap(&declarations, pool, policy).cloned()
142    })
143}
144
145/// Open a committed memory from this thread's default runtime.
146/// Does not construct an absent runtime or select its bucket configuration.
147pub fn open_default_memory_manager_memory(
148    stable_key: &str,
149) -> Result<RuntimeMemory<DefaultMemoryImpl>, RuntimeOpenError> {
150    with_existing_default_runtime(|runtime| {
151        runtime
152            .ok_or(RuntimeOpenError::NotBootstrapped)?
153            .open_memory(stable_key)
154    })
155}
156
157/// Export this thread's default runtime ledger and live memory sizes.
158///
159/// Returns `NotBootstrapped` for an absent or unbootstrapped runtime without
160/// initializing backing memory or choosing a bucket configuration.
161pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
162{
163    with_existing_default_runtime(|runtime| {
164        runtime
165            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
166            .diagnostic_export()
167    })
168}
169
170/// Diagnose protected commit recovery for this thread's default runtime.
171///
172/// Returns `NotBootstrapped` if no runtime exists without initializing memory
173/// or choosing configuration. An existing runtime can be inspected before bootstrap.
174pub fn default_memory_manager_commit_recovery_diagnostic()
175-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
176    with_existing_default_runtime(|runtime| {
177        runtime
178            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
179            .commit_recovery_diagnostic()
180    })
181}
182
183/// Build preflight and lifecycle diagnostics for this thread's default runtime.
184///
185/// Returns `NotBootstrapped` if no runtime exists without initializing memory
186/// or choosing configuration. An existing runtime can be inspected before bootstrap.
187pub fn default_memory_manager_doctor_report(
188    pool: &crate::MemoryAllocationPool,
189) -> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError> {
190    default_memory_manager_doctor_report_with_policy(pool, &GenericAllocationPolicy)
191}
192
193/// Build diagnostics for this thread's default runtime under one explicit policy.
194///
195/// Returns `NotBootstrapped` if no runtime exists without sealing declarations,
196/// initializing memory or choosing configuration. The policy is evaluated only;
197/// it does not construct or bootstrap the runtime.
198pub fn default_memory_manager_doctor_report_with_policy<P>(
199    pool: &crate::MemoryAllocationPool,
200    policy: &P,
201) -> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError>
202where
203    P: RuntimeBootstrapPolicy,
204    P::Error: Display,
205{
206    // Check presence before running registration hooks, but release the TLS
207    // borrow so hooks can inspect the existing runtime during snapshot sealing.
208    with_existing_default_runtime(|runtime| {
209        runtime
210            .ok_or(RuntimeDiagnosticError::NotBootstrapped)
211            .map(|_| ())
212    })?;
213    let declarations = sealed_declaration_snapshot()?;
214    with_existing_default_runtime(|runtime| {
215        Ok(runtime
216            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
217            .doctor_report(&declarations, pool, policy))
218    })
219}
220
221#[cfg(test)]
222pub(super) fn with_default_runtime_borrowed(
223    operation: impl FnOnce() -> Result<(), RuntimeStateError>,
224) -> Result<(), RuntimeStateError> {
225    DEFAULT_RUNTIME.with(|runtime| {
226        let _borrow = runtime.borrow_mut();
227        operation()
228    })
229}
230
231/// Measure the existing default runtime without constructing a manager or
232/// initializing backing memory.
233///
234/// Returns `NotBootstrapped` if no runtime exists.
235/// A constructed runtime may be measured before bootstrap with unknown bindings.
236pub fn default_memory_manager_memory_allocations()
237-> Result<super::MemoryAllocations, RuntimeDiagnosticError> {
238    with_existing_default_runtime(|runtime| {
239        runtime
240            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
241            .memory_allocations()
242    })
243}
244
245/// Measure numeric allocation totals in the existing default runtime.
246///
247/// Does not copy binding names or construct per-ID rows. Absence returns
248/// `NotBootstrapped` without initializing memory or choosing configuration.
249pub fn default_memory_manager_memory_allocation_summary()
250-> Result<super::MemoryAllocationSummary, RuntimeDiagnosticError> {
251    with_existing_default_runtime(|runtime| {
252        runtime
253            .ok_or(RuntimeDiagnosticError::NotBootstrapped)?
254            .memory_allocation_summary()
255    })
256}
257
258/// Bootstrap the default runtime with an explicit bucket setting and allocation
259/// policy.
260///
261/// The first construction uses this setting; repeated calls and reopened
262/// memory must match it exactly before bootstrap effects. Select this setting
263/// on the first bootstrap; observation and open helpers leave an absent runtime
264/// untouched.
265/// Registration hooks run without a TLS borrow and may observe the configured,
266/// unbootstrapped runtime.
267/// Use [`super::GenericAllocationPolicy`] to select the built-in policy, or pass the
268/// host's custom policy. This operation does not adopt a different bound policy.
269pub fn bootstrap_default_memory_manager_with_config<P: RuntimeBootstrapPolicy>(
270    config: MemoryManagerConfig,
271    pool: &crate::MemoryAllocationPool,
272    policy: &P,
273) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
274    // Reject construction/configuration failures before sealing, but release
275    // the TLS borrow while registration hooks inspect the existing runtime.
276    with_default_runtime_mut(Some(config), |_| Ok::<_, RuntimeStateError>(()))?;
277    let declarations = sealed_declaration_snapshot()?;
278    with_default_runtime_mut(Some(config), |runtime| {
279        runtime.bootstrap(&declarations, pool, policy).cloned()
280    })
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    fn pool() -> crate::MemoryAllocationPool {
287        crate::MemoryAllocationPool::new(
288            vec![crate::MemoryAuthority::new("app", "app.").unwrap()],
289            vec![],
290        )
291        .unwrap()
292    }
293
294    #[test]
295    fn configured_registration_hooks_can_observe_unbootstrapped_runtime() {
296        use crate::registry::{
297            TEST_REGISTRY_LOCK, defer_eager_init, reset_static_memory_declarations_for_tests,
298        };
299        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
300        reset_static_memory_declarations_for_tests();
301        defer_eager_init(|| {
302            assert_eq!(is_default_memory_manager_bootstrapped(), Ok(false));
303            assert_eq!(
304                committed_allocations(),
305                Err(RuntimeOpenError::NotBootstrapped)
306            );
307            let summary = default_memory_manager_memory_allocation_summary().unwrap();
308            assert_eq!(summary.bucket_size_pages, 16);
309            assert_eq!(summary.current_generation, None);
310        });
311        std::thread::spawn(|| {
312            let config = super::super::MemoryManagerConfig::new(16).unwrap();
313            bootstrap_default_memory_manager_with_config(config, &pool(), &GenericAllocationPolicy)
314                .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(&pool()).map(|_| ()),
327            default_memory_manager_doctor_report_with_policy(&pool(), &GenericAllocationPolicy)
328                .map(|_| ()),
329        ]
330    }
331
332    #[test]
333    fn observations_leave_an_absent_runtime_absent() {
334        use crate::registry::{
335            TEST_REGISTRY_LOCK, defer_eager_init, reset_static_memory_declarations_for_tests,
336        };
337        use std::sync::atomic::{AtomicBool, Ordering};
338        static REGISTRATION_RAN: AtomicBool = AtomicBool::new(false);
339        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
340        reset_static_memory_declarations_for_tests();
341        defer_eager_init(|| REGISTRATION_RAN.store(true, Ordering::SeqCst));
342        std::thread::spawn(|| {
343            for _ in 0..2 {
344                assert!(!is_default_memory_manager_bootstrapped().unwrap());
345                assert_eq!(
346                    committed_allocations(),
347                    Err(RuntimeOpenError::NotBootstrapped)
348                );
349                assert!(matches!(
350                    default_memory_manager_memory_allocations(),
351                    Err(RuntimeDiagnosticError::NotBootstrapped)
352                ));
353                for result in diagnostic_observations() {
354                    assert!(matches!(
355                        result,
356                        Err(RuntimeDiagnosticError::NotBootstrapped)
357                    ));
358                }
359                DEFAULT_RUNTIME.with(|runtime| assert!(runtime.borrow().is_none()));
360            }
361        })
362        .join()
363        .unwrap();
364        assert!(!REGISTRATION_RAN.load(Ordering::SeqCst));
365        reset_static_memory_declarations_for_tests();
366    }
367
368    #[test]
369    fn observations_preserve_unbootstrapped_configuration() {
370        use crate::registry::{TEST_REGISTRY_LOCK, reset_static_memory_declarations_for_tests};
371        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
372        reset_static_memory_declarations_for_tests();
373        std::thread::spawn(|| {
374            let config = super::super::MemoryManagerConfig::new(16).unwrap();
375            DEFAULT_RUNTIME.with(|runtime| {
376                *runtime.borrow_mut() = Some(MemoryRuntime::new_with_config(
377                    DefaultMemoryImpl::default(),
378                    config,
379                ));
380            });
381            let before = default_memory_manager_memory_allocations().unwrap();
382            assert!(!is_default_memory_manager_bootstrapped().unwrap());
383            assert_eq!(
384                committed_allocations(),
385                Err(RuntimeOpenError::NotBootstrapped)
386            );
387            assert_eq!(before.bucket_size_pages, 16);
388            assert!(matches!(
389                default_memory_manager_diagnostic_export(),
390                Err(RuntimeDiagnosticError::NotBootstrapped)
391            ));
392            default_memory_manager_commit_recovery_diagnostic().unwrap();
393            assert!(
394                !default_memory_manager_doctor_report(&pool())
395                    .unwrap()
396                    .bootstrapped
397            );
398            assert!(
399                !default_memory_manager_doctor_report_with_policy(
400                    &pool(),
401                    &GenericAllocationPolicy
402                )
403                .unwrap()
404                .bootstrapped
405            );
406            assert_eq!(default_memory_manager_memory_allocations().unwrap(), before);
407        })
408        .join()
409        .unwrap();
410        reset_static_memory_declarations_for_tests();
411    }
412
413    #[test]
414    fn configured_bootstrap_propagates_metadata_growth_refusal() {
415        std::thread::spawn(|| {
416            let error = RuntimeConstructionError::Growth(super::super::RuntimeGrowError::BackingRefused { additional_pages: 1 });
417            DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = Some(Err(error)));
418            assert!(matches!(
419                bootstrap_default_memory_manager_with_config(MemoryManagerConfig::new(16).unwrap(), &pool(), &GenericAllocationPolicy),
420                Err(RuntimeBootstrapError::State(RuntimeStateError::Construction(cause))) if cause == error
421            ));
422            assert_eq!(is_default_memory_manager_bootstrapped(), Err(RuntimeStateError::Construction(error)));
423            DEFAULT_RUNTIME.with(|runtime| assert!(matches!(runtime.borrow().as_ref(), Some(Err(cause)) if *cause == error)));
424        }).join().unwrap();
425    }
426
427    #[test]
428    fn observations_preserve_cached_construction_failure() {
429        std::thread::spawn(|| {
430            let error = RuntimeConstructionError::ForeignMemory {
431                observed_magic: *b"BAD",
432            };
433            DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = Some(Err(error)));
434            for result in diagnostic_observations() {
435                assert!(matches!(result, Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause))) if cause == error));
436            }
437            assert_eq!(
438                is_default_memory_manager_bootstrapped(),
439                Err(RuntimeStateError::Construction(error))
440            );
441            assert_eq!(
442                committed_allocations(),
443                Err(RuntimeOpenError::State(RuntimeStateError::Construction(
444                    error
445                )))
446            );
447            assert!(matches!(
448                default_memory_manager_memory_allocations(),
449                Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause)))
450                    if cause == error
451            ));
452            for result in [
453                open_default_memory_manager_memory("app.rows.v1").err(),
454                default_memory_manager_memory_id("app.rows.v1").err(),
455            ] {
456                assert_eq!(result, Some(RuntimeOpenError::State(RuntimeStateError::Construction(error))));
457            }
458            let requirements = crate::SealedDeclarationSnapshot::new(&[]).unwrap();
459            assert_eq!(verify_default_memory_manager_authority(&requirements, "app"), Err(super::super::RuntimeAdoptionError::Open(RuntimeOpenError::State(RuntimeStateError::Construction(error)))));
460            assert!(matches!(default_memory_manager_memory_allocation_summary(), Err(RuntimeDiagnosticError::State(RuntimeStateError::Construction(cause))) if cause == error));
461            DEFAULT_RUNTIME.with(|runtime| {
462                assert!(matches!(runtime.borrow().as_ref(), Some(Err(cause)) if *cause == error));
463            });
464        })
465        .join()
466        .unwrap();
467    }
468
469    #[test]
470    fn diagnostic_observations_preserve_reentrant_access_errors() {
471        with_default_runtime_borrowed(|| {
472            for result in diagnostic_observations() {
473                assert!(matches!(
474                    result,
475                    Err(RuntimeDiagnosticError::State(
476                        RuntimeStateError::ReentrantAccess
477                    ))
478                ));
479            }
480            Ok(())
481        })
482        .unwrap();
483    }
484    #[test]
485    #[cfg(not(target_arch = "wasm32"))]
486    fn configured_default_prepares_once_and_warm_library_adoption_only_opens() {
487        use crate::registry::{TEST_REGISTRY_LOCK, reset_static_memory_declarations_for_tests};
488        use ic_stable_structures::Memory;
489        let _guard = TEST_REGISTRY_LOCK.lock().unwrap();
490        reset_static_memory_declarations_for_tests();
491        crate::register_memory_request(
492            crate::MemoryRequest::new(
493                "app",
494                "app.main.control.v1",
495                crate::SchemaMetadata::default(),
496            )
497            .unwrap(),
498        )
499        .unwrap();
500        let backing = super::super::admission_tests::seeded();
501        DEFAULT_RUNTIME
502            .with(|runtime| *runtime.borrow_mut() = Some(MemoryRuntime::new(backing.clone())));
503        let policy = super::super::admission_tests::AdmissionPolicy {
504            discover: true,
505            ..Default::default()
506        };
507        let config = super::super::MemoryManagerConfig::new(1).unwrap();
508        let committed =
509            bootstrap_default_memory_manager_with_config(config, &pool(), &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("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, &pool(), &policy).unwrap(),
520            committed
521        );
522        assert!(
523            bootstrap_default_memory_manager_with_config(
524                super::super::MemoryManagerConfig::new(2).unwrap(),
525                &pool(),
526                &policy
527            )
528            .is_err()
529        );
530        assert_eq!(policy.calls.get(), 1);
531        assert_eq!(*backing.borrow(), before);
532        assert_eq!(
533            default_memory_manager_memory_allocations()
534                .unwrap()
535                .bucket_size_pages,
536            1
537        );
538        DEFAULT_RUNTIME.with(|runtime| *runtime.borrow_mut() = None);
539        reset_static_memory_declarations_for_tests();
540    }
541}