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