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
50fn 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
76pub 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
85pub 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
99pub 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
109pub 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
122pub 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
129pub 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
145pub 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
157pub 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
170pub 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
183pub 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
193pub 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 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
231pub 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
245pub 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
258pub 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 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}