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
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-> Result<CommittedAllocations, RuntimeBootstrapError<Infallible>> {
125 bootstrap_default_memory_manager_with_policy(&GenericRangePolicy)
126}
127
128pub 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
143pub 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
156pub 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
168pub 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
181pub 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
194pub fn default_memory_manager_doctor_report()
199-> Result<MemoryRuntimeDoctorReport, RuntimeDiagnosticError> {
200 default_memory_manager_doctor_report_with_policy(&GenericRangePolicy)
201}
202
203pub 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 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
240pub 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
254pub 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
267pub fn bootstrap_default_memory_manager_with_config<P: RuntimeBootstrapPolicy>(
279 config: MemoryManagerConfig,
280 policy: &P,
281) -> Result<CommittedAllocations, RuntimeBootstrapError<P::Error>> {
282 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}