1pub mod install;
2pub mod root_funding;
3#[cfg(feature = "internal-test-fixtures")]
4pub mod topup_fixture;
5
6use crate::{
7 InternalError,
8 cdk::types::Principal,
9 domain::runtime::{
10 FailureSeverity, HealthStatus, ReadinessStatus, RuntimeCheckStatus,
11 RuntimeDiagnosticSeverity, RuntimeFieldVisibility, RuntimeStateDomainStatus, RuntimeStatus,
12 },
13 dto::{
14 error::Error,
15 runtime::{
16 CanicHealthStatus, CanicReadinessStatus, CanicRuntimeStatus, CanisterTimerStatus,
17 RUNTIME_INTROSPECTION_SCHEMA_VERSION, RuntimeAuthStatusSummary,
18 RuntimeBlobStorageStatusSummary, RuntimeBuildInfo, RuntimeCheck, RuntimeDiagnostic,
19 RuntimeFeatureStatus, RuntimeReceiptCapacityStatus, RuntimeStateDomainSummary,
20 RuntimeStateSummary, RuntimeTopologyStatus, RuntimeVisibilityEntry,
21 TimerCallbackPerformanceStatus, TimerMemoryPageExtentStatus,
22 TimerMemoryPageSampleStatus,
23 },
24 },
25 ops::{
26 ic::{IcOps, build_network::BuildNetworkOps},
27 runtime::{
28 env::EnvOps,
29 memory::MemoryRegistryOps,
30 ready::ReadyOps,
31 recent_failure::{RecentFailureInput, RecentFailureOps},
32 },
33 storage::intent::{RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, ReceiptBackedIntentOps},
34 },
35 state_contract::{STATE_MANIFEST_SCHEMA_VERSION, canic_state_descriptors},
36 workflow::runtime::timer::TimerAuthorityWorkflow,
37};
38const RUNTIME_FEATURE_SOURCE: &str = "compile_feature";
39const RUNTIME_FEATURE_FLAGS: [(&str, bool); 11] = [
40 (
41 "auth-chain-key-ecdsa",
42 cfg!(feature = "auth-chain-key-ecdsa"),
43 ),
44 (
45 "auth-chain-key-root-sign",
46 cfg!(feature = "auth-chain-key-root-sign"),
47 ),
48 (
49 "auth-delegated-token-verify",
50 cfg!(feature = "auth-delegated-token-verify"),
51 ),
52 (
53 "auth-issuer-canister-sig-create",
54 cfg!(feature = "auth-issuer-canister-sig-create"),
55 ),
56 (
57 "auth-issuer-canister-sig-verify",
58 cfg!(feature = "auth-issuer-canister-sig-verify"),
59 ),
60 (
61 "auth-local-application-authorization",
62 cfg!(feature = "auth-local-application-authorization"),
63 ),
64 (
65 "auth-root-canister-sig-create",
66 cfg!(feature = "auth-root-canister-sig-create"),
67 ),
68 (
69 "auth-root-canister-sig-verify",
70 cfg!(feature = "auth-root-canister-sig-verify"),
71 ),
72 ("blob-storage", cfg!(feature = "blob-storage")),
73 (
74 "blob-storage-billing",
75 cfg!(feature = "blob-storage-billing"),
76 ),
77 ("sharding", cfg!(feature = "sharding")),
78];
79
80pub struct MemoryRuntimeApi;
85
86impl MemoryRuntimeApi {
87 pub fn bootstrap_registry() -> Result<(), Error> {
89 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
90
91 Ok(())
92 }
93}
94
95pub struct RuntimeIntrospectionApi;
100
101impl RuntimeIntrospectionApi {
102 #[must_use]
104 pub fn health(observed_at_ns: Option<u64>) -> CanicHealthStatus {
105 CanicHealthStatus {
106 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
107 status: HealthStatus::Healthy,
108 observed_at_ns,
109 checks: vec![RuntimeCheck {
110 category: "health".to_string(),
111 code: "canister_responsive".to_string(),
112 status: RuntimeCheckStatus::Pass,
113 subject: "canister".to_string(),
114 detail: "canister returned a health response".to_string(),
115 next: None,
116 source: "runtime_observed".to_string(),
117 }],
118 }
119 }
120
121 #[must_use]
123 pub fn readiness(observed_at_ns: u64) -> CanicReadinessStatus {
124 let ready = ReadyOps::is_ready();
125 let role = EnvOps::canister_role()
126 .ok()
127 .map(crate::ids::CanisterRole::into_string);
128
129 let (status, check_status, detail, next) = if ready {
130 (
131 ReadinessStatus::Ready,
132 RuntimeCheckStatus::Pass,
133 "runtime readiness barrier is marked ready",
134 None,
135 )
136 } else {
137 (
138 ReadinessStatus::NotReady,
139 RuntimeCheckStatus::Fail,
140 "runtime readiness barrier is not ready",
141 Some("wait for bootstrap to complete or inspect the role-owned Overview status"),
142 )
143 };
144
145 let readiness_check = RuntimeCheck {
146 category: "readiness".to_string(),
147 code: "runtime_ready_barrier".to_string(),
148 status: check_status,
149 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
150 detail: detail.to_string(),
151 next: next.map(str::to_string),
152 source: "runtime_observed".to_string(),
153 };
154
155 let blockers = if ready {
156 Vec::new()
157 } else {
158 vec![RuntimeDiagnostic {
159 category: "readiness".to_string(),
160 code: "runtime_not_ready".to_string(),
161 severity: RuntimeDiagnosticSeverity::Blocked,
162 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
163 detail: "runtime readiness barrier has not completed".to_string(),
164 next: Some(
165 "inspect bootstrap status before treating the role as ready".to_string(),
166 ),
167 source: "runtime_observed".to_string(),
168 }]
169 };
170
171 let fixture = crate::workflow::fixture_provisioning::status();
172 let fixture_ready = crate::workflow::fixture_provisioning::is_ready(&fixture);
173 let mut checks = vec![readiness_check];
174 let mut blockers = blockers;
175 checks.push(RuntimeCheck {
176 category: "readiness".to_string(),
177 code: "fixture_receipt".to_string(),
178 status: if fixture_ready {
179 RuntimeCheckStatus::Pass
180 } else {
181 RuntimeCheckStatus::Fail
182 },
183 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
184 detail: "application fixture receipt for the installed selection".to_string(),
185 next: None,
186 source: "application_durable_progress".to_string(),
187 });
188 if !fixture_ready {
189 blockers.push(RuntimeDiagnostic {
190 category: "readiness".to_string(),
191 code: "fixture_not_ready".to_string(),
192 severity: RuntimeDiagnosticSeverity::Blocked,
193 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
194 detail: "required fixture has no validated completion receipt".to_string(),
195 next: Some(
196 "inspect the protected fixture result for progress or failure".to_string(),
197 ),
198 source: "application_durable_progress".to_string(),
199 });
200 }
201 CanicReadinessStatus {
202 fixture,
203 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
204 role,
205 status: if fixture_ready {
206 status
207 } else {
208 ReadinessStatus::NotReady
209 },
210 observed_at_ns,
211 checks,
212 blockers,
213 warnings: Vec::new(),
214 }
215 }
216
217 #[must_use]
219 pub fn runtime_status_for(
220 canister_id: Principal,
221 observed_at_ns: u64,
222 package_name: &str,
223 package_version: &str,
224 canic_version: &str,
225 canister_version: u64,
226 ) -> CanicRuntimeStatus {
227 let readiness = Self::readiness(observed_at_ns);
228 let role = readiness.role.clone();
229 let state = state_summary(role.as_deref());
230 let root = EnvOps::root_pid().ok();
231 let parent = EnvOps::parent_pid().ok();
232 let subnet = EnvOps::subnet_pid().ok();
233 let receipt_capacity_result = runtime_receipt_capacity();
234 let receipt_capacity_status = receipt_capacity_result
235 .as_ref()
236 .ok()
237 .map(|capacity| capacity.status);
238 let timer_observation =
239 timer_status_observation(TimerAuthorityWorkflow::statuses(), observed_at_ns);
240 let status = aggregate_runtime_status(
241 readiness.status,
242 receipt_capacity_status,
243 timer_observation.check.status,
244 );
245 let (receipt_capacity, receipt_failure) = match receipt_capacity_result {
246 Ok(capacity) => (Some(capacity), None),
247 Err(err) => {
248 let code = err.code();
249 (
250 None,
251 Some(RecentFailureInput {
252 occurred_at_ns: observed_at_ns,
253 subsystem: "intent_capacity".to_string(),
254 code: code.to_string(),
255 severity: FailureSeverity::Error,
256 summary: format!("diagnostic={code}"),
257 correlation_id: None,
258 }),
259 )
260 }
261 };
262 let recent_failures = RecentFailureOps::snapshot_with_many(
263 [timer_observation.failure, receipt_failure]
264 .into_iter()
265 .flatten(),
266 );
267
268 CanicRuntimeStatus {
269 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
270 observed_at_ns,
271 canister_id,
272 role,
273 root,
274 build_network: BuildNetworkOps::build_network(),
275 build: RuntimeBuildInfo {
276 package_name: package_name.to_string(),
277 package_version: package_version.to_string(),
278 canic_version: canic_version.to_string(),
279 canister_version,
280 },
281 features: runtime_features(),
282 topology: Some(RuntimeTopologyStatus {
283 root,
284 parent,
285 subnet,
286 source: "runtime_observed".to_string(),
287 }),
288 timers: timer_observation.timers,
289 timer_inventory: timer_observation.check,
290 state,
291 auth: Some(runtime_auth_status()),
292 blob_storage: runtime_blob_storage_status(),
293 receipt_capacity,
294 recent_failures,
295 visibility: runtime_visibility(),
296 readiness,
297 status,
298 }
299 }
300
301 #[must_use]
303 pub fn runtime_status(
304 observed_at_ns: u64,
305 package_name: &str,
306 package_version: &str,
307 canic_version: &str,
308 canister_version: u64,
309 ) -> CanicRuntimeStatus {
310 Self::runtime_status_for(
311 IcOps::canister_self(),
312 observed_at_ns,
313 package_name,
314 package_version,
315 canic_version,
316 canister_version,
317 )
318 }
319}
320
321fn runtime_features() -> Vec<RuntimeFeatureStatus> {
322 RUNTIME_FEATURE_FLAGS
323 .into_iter()
324 .map(|(name, enabled)| runtime_feature_status(name, enabled))
325 .collect()
326}
327
328fn runtime_feature_status(name: &str, enabled: bool) -> RuntimeFeatureStatus {
329 RuntimeFeatureStatus {
330 name: name.to_string(),
331 enabled,
332 visibility: RuntimeFieldVisibility::OperatorOnly,
333 source: RUNTIME_FEATURE_SOURCE.to_string(),
334 }
335}
336
337fn runtime_auth_status() -> RuntimeAuthStatusSummary {
338 RuntimeAuthStatusSummary {
339 auth_features: RUNTIME_FEATURE_FLAGS
340 .into_iter()
341 .filter(|(name, _)| name.starts_with("auth-"))
342 .map(|(name, enabled)| runtime_feature_status(name, enabled))
343 .collect(),
344 }
345}
346
347fn runtime_receipt_capacity() -> Result<RuntimeReceiptCapacityStatus, InternalError> {
348 let capacity = ReceiptBackedIntentOps::receipt_capacity()?;
349 Ok(RuntimeReceiptCapacityStatus {
350 status: receipt_capacity_condition(
351 capacity.remaining_record_headroom,
352 capacity.remaining_resource_total_headroom,
353 ),
354 receipt_records: capacity.total_records,
355 application_receipt_records: capacity.application_records,
356 canic_owned_receipt_records: capacity.canic_owned_records,
357 pending_application_receipt_records: capacity.pending_records,
358 terminal_application_receipt_records: capacity.terminal_records,
359 receipt_record_limit: capacity.record_limit,
360 remaining_receipt_record_headroom: capacity.remaining_record_headroom,
361 resource_total_records: capacity.resource_total_records,
362 resource_total_record_limit: capacity.resource_total_record_limit,
363 remaining_resource_total_headroom: capacity.remaining_resource_total_headroom,
364 warning_headroom_threshold: RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD,
365 reserved_terminal_slots: capacity.reserved_terminal_slots,
366 reserved_terminal_pages: capacity.reserved_terminal_pages,
367 next_terminal_eligibility_at_ns: capacity.next_eligibility_at_ns,
368 source: "intent_storage".to_string(),
369 })
370}
371
372const fn receipt_capacity_condition(
373 remaining_receipt_records: u64,
374 remaining_resource_totals: u64,
375) -> RuntimeCheckStatus {
376 let minimum_headroom = if remaining_receipt_records < remaining_resource_totals {
377 remaining_receipt_records
378 } else {
379 remaining_resource_totals
380 };
381 if minimum_headroom == 0 {
382 RuntimeCheckStatus::Fail
383 } else if minimum_headroom <= RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD {
384 RuntimeCheckStatus::Warn
385 } else {
386 RuntimeCheckStatus::Pass
387 }
388}
389
390const fn aggregate_runtime_status(
391 readiness: ReadinessStatus,
392 receipt_capacity: Option<RuntimeCheckStatus>,
393 timer_inventory: RuntimeCheckStatus,
394) -> RuntimeStatus {
395 if matches!(readiness, ReadinessStatus::NotReady)
396 || matches!(
397 timer_inventory,
398 RuntimeCheckStatus::Fail | RuntimeCheckStatus::NotEvaluated
399 )
400 || matches!(
401 receipt_capacity,
402 None | Some(RuntimeCheckStatus::Fail | RuntimeCheckStatus::NotEvaluated)
403 )
404 {
405 RuntimeStatus::Failing
406 } else if matches!(
407 readiness,
408 ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated
409 ) || matches!(receipt_capacity, Some(RuntimeCheckStatus::Warn))
410 || matches!(timer_inventory, RuntimeCheckStatus::Warn)
411 {
412 RuntimeStatus::Degraded
413 } else {
414 RuntimeStatus::Ok
415 }
416}
417
418fn runtime_blob_storage_status() -> Option<RuntimeBlobStorageStatusSummary> {
419 let blob_storage_enabled = cfg!(feature = "blob-storage");
420 let billing_enabled = cfg!(feature = "blob-storage-billing");
421
422 (blob_storage_enabled || billing_enabled).then(|| RuntimeBlobStorageStatusSummary {
423 blob_storage_features: [
424 ("blob-storage", blob_storage_enabled),
425 ("blob-storage-billing", billing_enabled),
426 ]
427 .into_iter()
428 .map(|(name, enabled)| runtime_feature_status(name, enabled))
429 .collect(),
430 })
431}
432
433struct TimerStatusObservation {
434 check: RuntimeCheck,
435 timers: Vec<CanisterTimerStatus>,
436 failure: Option<RecentFailureInput>,
437}
438
439fn timer_status_observation(
440 snapshots: Result<Vec<ic_timers::TimerSnapshot>, crate::api::timer::TimerError>,
441 observed_at_ns: u64,
442) -> TimerStatusObservation {
443 match snapshots {
444 Ok(snapshots) => TimerStatusObservation {
445 check: RuntimeCheck {
446 category: "runtime".to_string(),
447 code: "timer_inventory_available".to_string(),
448 status: RuntimeCheckStatus::Pass,
449 subject: "shared_timer_registry".to_string(),
450 detail: "the complete canister-local timer inventory was observed".to_string(),
451 next: None,
452 source: "ic_timers".to_string(),
453 },
454 timers: timer_statuses_from(snapshots),
455 failure: None,
456 },
457 Err(error) => TimerStatusObservation {
458 check: RuntimeCheck {
459 category: "runtime".to_string(),
460 code: "timer_inventory_unavailable".to_string(),
461 status: RuntimeCheckStatus::Fail,
462 subject: "shared_timer_registry".to_string(),
463 detail: "the canister-local timer inventory could not be observed".to_string(),
464 next: Some(
465 "retry runtime observation; inspect recent failures if unavailable persists"
466 .to_string(),
467 ),
468 source: "ic_timers".to_string(),
469 },
470 timers: Vec::new(),
471 failure: Some(RecentFailureInput {
472 occurred_at_ns: observed_at_ns,
473 subsystem: "timer_runtime".to_string(),
474 code: "timer_inventory_unavailable".to_string(),
475 severity: FailureSeverity::Error,
476 summary: error.to_string(),
477 correlation_id: None,
478 }),
479 },
480 }
481}
482
483fn timer_statuses_from(snapshots: Vec<ic_timers::TimerSnapshot>) -> Vec<CanisterTimerStatus> {
484 let mut timers = snapshots
485 .into_iter()
486 .map(|snapshot| {
487 let identity = snapshot.identity();
488 let observability = snapshot.observability();
489 let outcomes = observability.outcomes();
490 let counters = observability.counters();
491 let performance = observability.performance();
492 let condition = timer_process_condition(snapshot.process_condition());
493 CanisterTimerStatus {
494 name: identity.name().to_string(),
495 owner: identity.owner().to_string(),
496 subsystem: identity.subsystem().to_string(),
497 scheduling_mode: timer_scheduling_mode(snapshot.scheduling_mode()),
498 registration: timer_registration_status(snapshot.registration_status()),
499 condition,
500 enabled: condition != crate::domain::runtime::TimerProcessCondition::Disabled,
501 generation: snapshot.generation(),
502 next_due_at_ns: snapshot.next_deadline_ns(),
503 last_outcome: outcomes.last_outcome().map(timer_execution_outcome),
504 last_work_count: outcomes.last_work_count().unwrap_or_default(),
505 last_success_at_ns: outcomes.last_success_at_ns(),
506 last_failure_at_ns: outcomes.last_failure_at_ns(),
507 consecutive_expected_failures: outcomes.consecutive_expected_failures(),
508 schedules_since_runtime_start: counters.wakeups_armed(),
509 executions_since_runtime_start: counters.work_started(),
510 successes_since_runtime_start: counters
511 .succeeded()
512 .saturating_add(counters.no_work()),
513 expected_failures_since_runtime_start: counters.retryable_failure(),
514 invariant_failures_since_runtime_start: counters.invariant_failure(),
515 stale_callbacks_since_runtime_start: counters
516 .stale_wakeups()
517 .saturating_add(counters.stale_work()),
518 scheduler_performance: timer_callback_performance_status(
519 performance.scheduler_instructions(),
520 performance.scheduler_memory_pages(),
521 ),
522 work_performance: timer_callback_performance_status(
523 performance.work_instructions(),
524 performance.work_memory_pages(),
525 ),
526 }
527 })
528 .collect::<Vec<_>>();
529 timers.sort_by(|left, right| {
530 left.owner
531 .cmp(&right.owner)
532 .then_with(|| left.subsystem.cmp(&right.subsystem))
533 .then_with(|| left.name.cmp(&right.name))
534 });
535 timers
536}
537
538fn timer_callback_performance_status(
539 instructions: ic_timers::MeasurementSummary,
540 memory: ic_timers::MemoryPageSummary,
541) -> TimerCallbackPerformanceStatus {
542 TimerCallbackPerformanceStatus {
543 instruction_samples_since_runtime_start: instructions.samples(),
544 instructions_latest: instructions.latest(),
545 instructions_maximum: instructions.maximum(),
546 instructions_total_since_runtime_start: instructions.total(),
547 memory_page_samples_since_runtime_start: memory.samples(),
548 memory_pages_latest: memory.latest().map(timer_memory_page_sample_status),
549 maximum_wasm_memory_growth_pages: memory.maximum_wasm_growth_pages(),
550 maximum_stable_memory_growth_pages: memory.maximum_stable_growth_pages(),
551 }
552}
553
554const fn timer_memory_page_sample_status(
555 sample: ic_timers::MemoryPageSample,
556) -> TimerMemoryPageSampleStatus {
557 TimerMemoryPageSampleStatus {
558 start: timer_memory_page_extent_status(sample.start()),
559 end: timer_memory_page_extent_status(sample.end()),
560 }
561}
562
563const fn timer_memory_page_extent_status(
564 extent: ic_timers::MemoryPageExtent,
565) -> TimerMemoryPageExtentStatus {
566 TimerMemoryPageExtentStatus {
567 wasm_pages: extent.wasm_pages(),
568 stable_pages: extent.stable_pages(),
569 }
570}
571
572const fn timer_scheduling_mode(
573 value: ic_timers::TimerSchedulingMode,
574) -> crate::domain::runtime::TimerSchedulingMode {
575 use crate::domain::runtime::TimerSchedulingMode as Canic;
576 use ic_timers::TimerSchedulingMode as Shared;
577 match value {
578 Shared::Once => Canic::Once,
579 Shared::AfterCompletion => Canic::AfterCompletion,
580 Shared::Deadline => Canic::Deadline,
581 Shared::Retry => Canic::Retry,
582 Shared::Continuation => Canic::Continuation,
583 Shared::Watchdog => Canic::Watchdog,
584 }
585}
586
587const fn timer_registration_status(
588 value: ic_timers::TimerRegistrationStatus,
589) -> crate::domain::runtime::TimerRegistrationStatus {
590 use crate::domain::runtime::TimerRegistrationStatus as Canic;
591 match value {
592 ic_timers::TimerRegistrationStatus::Unregistered => Canic::Unregistered,
593 ic_timers::TimerRegistrationStatus::Scheduled => Canic::Scheduled,
594 ic_timers::TimerRegistrationStatus::Running => Canic::Running,
595 }
596}
597
598const fn timer_process_condition(
599 value: ic_timers::TimerProcessCondition,
600) -> crate::domain::runtime::TimerProcessCondition {
601 use crate::domain::runtime::TimerProcessCondition as Canic;
602 match value {
603 ic_timers::TimerProcessCondition::Disabled => Canic::Disabled,
604 ic_timers::TimerProcessCondition::Idle => Canic::Idle,
605 ic_timers::TimerProcessCondition::Active => Canic::Active,
606 ic_timers::TimerProcessCondition::Retrying => Canic::Retrying,
607 ic_timers::TimerProcessCondition::Failed => Canic::Failed,
608 }
609}
610
611const fn timer_execution_outcome(
612 value: ic_timers::TimerLastOutcome,
613) -> crate::domain::runtime::TimerExecutionOutcome {
614 use crate::domain::runtime::TimerExecutionOutcome as Canic;
615 match value {
616 ic_timers::TimerLastOutcome::Completed(completion) => match completion {
617 ic_timers::TimerCompletionOutcome::Success => Canic::Success,
618 ic_timers::TimerCompletionOutcome::NoWork => Canic::NoWork,
619 ic_timers::TimerCompletionOutcome::RetryableFailure => Canic::RetryableFailure,
620 ic_timers::TimerCompletionOutcome::InvariantFailure => Canic::InvariantFailure,
621 },
622 ic_timers::TimerLastOutcome::Unacknowledged => Canic::Unacknowledged,
623 }
624}
625
626fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
627 let memory_ids = MemoryRegistryOps::ledger_snapshot()
628 .ok()?
629 .memories
630 .into_iter()
631 .map(|memory| memory.memory_manager_id)
632 .collect::<std::collections::BTreeSet<_>>();
633 state_summary_for_memory_ids(role, &memory_ids)
634}
635
636fn state_summary_for_memory_ids(
637 role: Option<&str>,
638 memory_ids: &std::collections::BTreeSet<u8>,
639) -> Option<RuntimeStateSummary> {
640 role?;
641 let mut domains = canic_state_descriptors()
642 .into_iter()
643 .flat_map(|descriptor| descriptor.state)
644 .filter(|domain| domain.memory_id.is_some_and(|id| memory_ids.contains(&id)))
645 .map(|domain| RuntimeStateDomainSummary {
646 domain: domain.domain,
647 version: domain.version,
648 storage: domain.storage.as_str().to_string(),
649 memory_id: domain.memory_id,
650 status: RuntimeStateDomainStatus::Ok,
651 })
652 .collect::<Vec<_>>();
653 domains.sort_by(|left, right| left.domain.cmp(&right.domain));
654
655 if domains.is_empty() {
656 return None;
657 }
658
659 Some(RuntimeStateSummary {
660 manifest_schema_version: u32::from(STATE_MANIFEST_SCHEMA_VERSION),
661 domains,
662 total_stable_memory_pages: None,
663 })
664}
665
666fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
667 [
668 ("schema_version", RuntimeFieldVisibility::PublicSafe),
669 ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
670 ("canister_id", RuntimeFieldVisibility::OperatorOnly),
671 ("role", RuntimeFieldVisibility::OperatorOnly),
672 ("root", RuntimeFieldVisibility::OperatorOnly),
673 ("build_network", RuntimeFieldVisibility::OperatorOnly),
674 ("build", RuntimeFieldVisibility::OperatorOnly),
675 ("features", RuntimeFieldVisibility::OperatorOnly),
676 ("topology", RuntimeFieldVisibility::ControllerOnly),
677 ("timers", RuntimeFieldVisibility::OperatorOnly),
678 ("timer_inventory", RuntimeFieldVisibility::OperatorOnly),
679 ("state", RuntimeFieldVisibility::OperatorOnly),
680 ("auth", RuntimeFieldVisibility::OperatorOnly),
681 ("blob_storage", RuntimeFieldVisibility::FeatureGated),
682 ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
683 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
684 ("readiness", RuntimeFieldVisibility::OperatorOnly),
685 ("status", RuntimeFieldVisibility::OperatorOnly),
686 ("visibility", RuntimeFieldVisibility::OperatorOnly),
687 ]
688 .into_iter()
689 .map(|(field, visibility)| RuntimeVisibilityEntry {
690 field: field.to_string(),
691 visibility,
692 })
693 .collect()
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use crate::ids::IntentResourceKey;
700 use crate::ops::runtime::bootstrap::{BootstrapPhaseLabel, BootstrapStatusOps};
701 use crate::ops::runtime::recent_failure::RecentFailureOps;
702 use crate::ops::storage::intent::{
703 INTENT_RESOURCE_TOTAL_RECORD_LIMIT, IntentStoreOps, RECEIPT_BACKED_INTENT_RECORD_LIMIT,
704 };
705 use crate::storage::stable::intent::{IntentResourceTotalsRecord, IntentStore};
706
707 #[test]
708 fn health_is_minimal_and_schema_versioned() {
709 let health = RuntimeIntrospectionApi::health(Some(42));
710
711 assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
712 assert_eq!(health.status, HealthStatus::Healthy);
713 assert_eq!(health.observed_at_ns, Some(42));
714 assert_eq!(health.checks.len(), 1);
715 assert_eq!(health.checks[0].code, "canister_responsive");
716 }
717
718 #[test]
719 fn unavailable_timer_inventory_is_explicit_and_fails_runtime_status() {
720 let observation =
721 timer_status_observation(Err(crate::api::timer::TimerError::CustodyBusy), 101);
722
723 assert_eq!(observation.check.status, RuntimeCheckStatus::Fail);
724 assert!(observation.timers.is_empty());
725 assert_eq!(
726 observation
727 .failure
728 .as_ref()
729 .map(|failure| failure.code.as_str()),
730 Some("timer_inventory_unavailable")
731 );
732 assert_eq!(
733 aggregate_runtime_status(
734 ReadinessStatus::Ready,
735 Some(RuntimeCheckStatus::Pass),
736 observation.check.status,
737 ),
738 RuntimeStatus::Failing
739 );
740 }
741
742 #[test]
743 fn runtime_status_embeds_guarded_readiness_and_build_info() {
744 let status = RuntimeIntrospectionApi::runtime_status_for(
745 Principal::anonymous(),
746 100,
747 "test-canister",
748 "1.2.3",
749 "0.81.0",
750 7,
751 );
752
753 assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
754 assert_eq!(status.observed_at_ns, 100);
755 assert_eq!(status.canister_id, Principal::anonymous());
756 assert_eq!(status.build_network, BuildNetworkOps::build_network());
757 assert_eq!(status.build.package_name, "test-canister");
758 assert_eq!(status.build.package_version, "1.2.3");
759 assert_eq!(status.build.canic_version, "0.81.0");
760 assert_eq!(status.build.canister_version, 7);
761 assert_eq!(status.readiness.observed_at_ns, 100);
762 assert!(
763 status
764 .visibility
765 .iter()
766 .any(|entry| entry.field == "topology"
767 && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
768 );
769 }
770
771 #[test]
772 fn runtime_status_classifies_each_top_level_field_visibility() {
773 let status = RuntimeIntrospectionApi::runtime_status_for(
774 Principal::anonymous(),
775 100,
776 "test-canister",
777 "1.2.3",
778 "0.81.0",
779 7,
780 );
781 let expected = [
782 ("schema_version", RuntimeFieldVisibility::PublicSafe),
783 ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
784 ("canister_id", RuntimeFieldVisibility::OperatorOnly),
785 ("role", RuntimeFieldVisibility::OperatorOnly),
786 ("root", RuntimeFieldVisibility::OperatorOnly),
787 ("build_network", RuntimeFieldVisibility::OperatorOnly),
788 ("build", RuntimeFieldVisibility::OperatorOnly),
789 ("features", RuntimeFieldVisibility::OperatorOnly),
790 ("topology", RuntimeFieldVisibility::ControllerOnly),
791 ("timers", RuntimeFieldVisibility::OperatorOnly),
792 ("timer_inventory", RuntimeFieldVisibility::OperatorOnly),
793 ("state", RuntimeFieldVisibility::OperatorOnly),
794 ("auth", RuntimeFieldVisibility::OperatorOnly),
795 ("blob_storage", RuntimeFieldVisibility::FeatureGated),
796 ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
797 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
798 ("readiness", RuntimeFieldVisibility::OperatorOnly),
799 ("status", RuntimeFieldVisibility::OperatorOnly),
800 ("visibility", RuntimeFieldVisibility::OperatorOnly),
801 ];
802
803 assert_eq!(status.visibility.len(), expected.len());
804 for (index, (field, visibility)) in expected.into_iter().enumerate() {
805 assert_eq!(status.visibility[index].field, field);
806 assert_eq!(status.visibility[index].visibility, visibility);
807 }
808 }
809
810 #[test]
811 fn runtime_status_projects_empty_receipt_capacity() {
812 IntentStoreOps::reset_for_tests();
813
814 let status = RuntimeIntrospectionApi::runtime_status_for(
815 Principal::anonymous(),
816 100,
817 "test-canister",
818 "1.2.3",
819 "0.96.6",
820 7,
821 );
822 let capacity = status.receipt_capacity.expect("receipt capacity");
823
824 assert_eq!(capacity.status, RuntimeCheckStatus::Pass);
825 assert_eq!(capacity.receipt_records, 0);
826 assert_eq!(
827 capacity.receipt_record_limit,
828 RECEIPT_BACKED_INTENT_RECORD_LIMIT
829 );
830 assert_eq!(capacity.resource_total_records, 0);
831 assert_eq!(
832 capacity.resource_total_record_limit,
833 INTENT_RESOURCE_TOTAL_RECORD_LIMIT
834 );
835 assert_eq!(
836 capacity.warning_headroom_threshold,
837 RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD
838 );
839 assert_eq!(capacity.source, "intent_storage");
840 }
841
842 #[test]
843 fn receipt_capacity_condition_has_exact_warning_and_failure_boundaries() {
844 assert_eq!(
845 receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD + 1, u64::MAX,),
846 RuntimeCheckStatus::Pass
847 );
848 assert_eq!(
849 receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, u64::MAX),
850 RuntimeCheckStatus::Warn
851 );
852 assert_eq!(
853 receipt_capacity_condition(u64::MAX, 1),
854 RuntimeCheckStatus::Warn
855 );
856 assert_eq!(
857 receipt_capacity_condition(u64::MAX, 0),
858 RuntimeCheckStatus::Fail
859 );
860 assert_eq!(
861 aggregate_runtime_status(
862 ReadinessStatus::Ready,
863 Some(RuntimeCheckStatus::Warn),
864 RuntimeCheckStatus::Pass,
865 ),
866 RuntimeStatus::Degraded
867 );
868 assert_eq!(
869 aggregate_runtime_status(ReadinessStatus::Ready, None, RuntimeCheckStatus::Pass,),
870 RuntimeStatus::Failing
871 );
872 assert_eq!(
873 aggregate_runtime_status(
874 ReadinessStatus::Ready,
875 Some(RuntimeCheckStatus::Pass),
876 RuntimeCheckStatus::Fail,
877 ),
878 RuntimeStatus::Failing
879 );
880 }
881
882 #[test]
883 fn runtime_status_fails_closed_with_typed_capacity_diagnostic() {
884 IntentStoreOps::reset_for_tests();
885 RecentFailureOps::reset();
886 for value in 0..=INTENT_RESOURCE_TOTAL_RECORD_LIMIT {
887 IntentStore::set_totals(
888 IntentResourceKey::new(format!("runtime-capacity:{value}")),
889 IntentResourceTotalsRecord {
890 reserved_qty: 0,
891 committed_qty: 1,
892 pending_count: 0,
893 },
894 );
895 }
896
897 let status = RuntimeIntrospectionApi::runtime_status_for(
898 Principal::anonymous(),
899 100,
900 "test-canister",
901 "1.2.3",
902 "0.96.6",
903 7,
904 );
905
906 assert_eq!(status.status, RuntimeStatus::Failing);
907 assert!(status.receipt_capacity.is_none());
908 let expected_code = crate::diagnostics::codes::CAPACITY_LIMIT.to_string();
909 let failure = status
910 .recent_failures
911 .iter()
912 .find(|failure| failure.code == expected_code)
913 .expect("current capacity failure diagnostic");
914 assert_eq!(failure.subsystem, "intent_capacity");
915 assert_eq!(failure.code, expected_code);
916 assert_eq!(failure.severity, FailureSeverity::Error);
917 assert_eq!(
918 failure.summary,
919 format!("diagnostic={}", crate::diagnostics::codes::CAPACITY_LIMIT)
920 );
921 assert!(RecentFailureOps::snapshot().is_empty());
922
923 IntentStoreOps::reset_for_tests();
924 RecentFailureOps::reset();
925 }
926
927 #[test]
928 fn runtime_status_reports_compile_features_deterministically() {
929 let status = RuntimeIntrospectionApi::runtime_status_for(
930 Principal::anonymous(),
931 100,
932 "test-canister",
933 "1.2.3",
934 "0.81.0",
935 7,
936 );
937 assert_eq!(status.features.len(), RUNTIME_FEATURE_FLAGS.len());
938 for (index, (name, enabled)) in RUNTIME_FEATURE_FLAGS.into_iter().enumerate() {
939 assert_eq!(status.features[index].name, name);
940 assert_eq!(status.features[index].enabled, enabled);
941 assert_eq!(
942 status.features[index].visibility,
943 RuntimeFieldVisibility::OperatorOnly
944 );
945 assert_eq!(status.features[index].source, RUNTIME_FEATURE_SOURCE);
946 }
947 }
948
949 #[test]
950 fn runtime_status_reports_auth_and_blob_storage_feature_summaries() {
951 let status = RuntimeIntrospectionApi::runtime_status_for(
952 Principal::anonymous(),
953 100,
954 "test-canister",
955 "1.2.3",
956 "0.81.0",
957 7,
958 );
959
960 let auth = status.auth.expect("auth feature summary");
961 assert!(
962 auth.auth_features
963 .windows(2)
964 .all(|features| features[0].name <= features[1].name)
965 );
966 assert_runtime_feature(
967 &auth.auth_features,
968 "auth-chain-key-ecdsa",
969 cfg!(feature = "auth-chain-key-ecdsa"),
970 );
971 assert_runtime_feature(
972 &auth.auth_features,
973 "auth-delegated-token-verify",
974 cfg!(feature = "auth-delegated-token-verify"),
975 );
976 assert_runtime_feature(
977 &auth.auth_features,
978 "auth-issuer-canister-sig-create",
979 cfg!(feature = "auth-issuer-canister-sig-create"),
980 );
981
982 if cfg!(any(
983 feature = "blob-storage",
984 feature = "blob-storage-billing"
985 )) {
986 let blob_storage = status.blob_storage.expect("blob-storage feature summary");
987 assert_runtime_feature(
988 &blob_storage.blob_storage_features,
989 "blob-storage",
990 cfg!(feature = "blob-storage"),
991 );
992 assert_runtime_feature(
993 &blob_storage.blob_storage_features,
994 "blob-storage-billing",
995 cfg!(feature = "blob-storage-billing"),
996 );
997 } else {
998 assert!(status.blob_storage.is_none());
999 }
1000 }
1001
1002 fn assert_runtime_feature(
1003 features: &[RuntimeFeatureStatus],
1004 name: &str,
1005 expected_enabled: bool,
1006 ) {
1007 let feature = features
1008 .iter()
1009 .find(|feature| feature.name == name)
1010 .unwrap_or_else(|| panic!("expected runtime feature {name}"));
1011
1012 assert_eq!(feature.enabled, expected_enabled);
1013 assert_eq!(feature.visibility, RuntimeFieldVisibility::OperatorOnly);
1014 assert_eq!(feature.source, RUNTIME_FEATURE_SOURCE);
1015 }
1016
1017 #[test]
1018 fn state_summary_joins_runtime_memory_ids_to_owner_metadata() {
1019 let summary = state_summary_for_memory_ids(
1020 Some("root"),
1021 &std::collections::BTreeSet::from([
1022 crate::role_contract::allocation::memory::runtime::RUNTIME_BINDINGS_ID,
1023 ]),
1024 )
1025 .expect("runtime state declarations");
1026
1027 assert_eq!(
1028 summary.manifest_schema_version,
1029 u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
1030 );
1031 assert!(summary.total_stable_memory_pages.is_none());
1032 assert!(summary.domains.iter().any(|domain| {
1033 domain.domain == "runtime_bindings"
1034 && domain.storage == "stable_memory"
1035 && domain.status == RuntimeStateDomainStatus::Ok
1036 }));
1037 assert!(state_summary_for_memory_ids(None, &std::collections::BTreeSet::new()).is_none());
1038 }
1039
1040 #[test]
1041 fn runtime_status_includes_recent_failure_snapshot() {
1042 RecentFailureOps::reset();
1043 RecentFailureOps::record(RecentFailureInput {
1044 occurred_at_ns: 77,
1045 subsystem: "runtime".to_string(),
1046 code: "readiness_failed".to_string(),
1047 severity: FailureSeverity::Error,
1048 summary: "bounded failure summary".to_string(),
1049 correlation_id: Some("runtime-check".to_string()),
1050 });
1051
1052 let status = RuntimeIntrospectionApi::runtime_status_for(
1053 Principal::anonymous(),
1054 100,
1055 "test-canister",
1056 "1.2.3",
1057 "0.81.0",
1058 7,
1059 );
1060
1061 let failure = status
1062 .recent_failures
1063 .iter()
1064 .find(|failure| failure.code == "readiness_failed")
1065 .expect("retained recent failure");
1066 assert_eq!(failure.occurred_at_ns, 77);
1067 assert_eq!(failure.subsystem, "runtime");
1068
1069 RecentFailureOps::reset();
1070 }
1071
1072 #[test]
1073 fn runtime_status_includes_bootstrap_failure_metadata() {
1074 RecentFailureOps::reset();
1075 BootstrapStatusOps::set_phase(BootstrapPhaseLabel::ROOT_INIT);
1076 BootstrapStatusOps::mark_failed("raw bootstrap failure detail");
1077
1078 let status = RuntimeIntrospectionApi::runtime_status_for(
1079 Principal::anonymous(),
1080 100,
1081 "test-canister",
1082 "1.2.3",
1083 "0.81.0",
1084 7,
1085 );
1086
1087 let failure = status
1088 .recent_failures
1089 .iter()
1090 .find(|failure| failure.code == "bootstrap_failed")
1091 .expect("bootstrap failure metadata");
1092
1093 assert_eq!(failure.subsystem, "runtime_bootstrap");
1094 assert_eq!(failure.severity, FailureSeverity::Error);
1095 assert_eq!(failure.correlation_id.as_deref(), Some("root:init"));
1096 assert!(
1097 !failure.summary.contains("raw bootstrap failure detail"),
1098 "runtime status recent failures should not mirror raw bootstrap errors"
1099 );
1100
1101 RecentFailureOps::reset();
1102 }
1103}