1use std::backtrace::Backtrace;
16use std::error::Error;
17use std::fmt;
18use std::panic::Location;
19use std::sync::Arc;
20
21use rocketmq_error::CanonicalCondition;
22use rocketmq_error::DiagnosticView;
23use rocketmq_error::Error as CanonicalError;
24use rocketmq_error::ErrorCode;
25use rocketmq_error::ErrorContext;
26use rocketmq_error::ErrorDescriptor;
27use rocketmq_error::PublicErrorView;
28use rocketmq_error::RecoveryHint;
29use rocketmq_error::SharedError;
30
31use crate::resource_budget::BudgetDimension;
32
33pub type RuntimeResult<T> = Result<T, RuntimeError>;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47#[non_exhaustive]
48pub enum RuntimeOperation {
49 BuildTokioRuntime,
51 LoadConfigFile,
53 DeserializeConfigFile,
55 DetectProcessMemoryLimit,
57 ResolveManagedMemoryBudget,
59 ReadFile,
61 ReadFileBackup,
63 PersistMetadata,
65 MetadataCreateParent,
67 MetadataCreateTemporary,
69 MetadataWriteTemporary,
71 MetadataSyncTemporary,
73 MetadataReplaceTarget,
75 MetadataSyncParent,
77 MetadataRemoveTemporary,
79 WaitForDurableMetadata,
81 AdmitMetadataSnapshot,
83 AdmitMetadataOperation,
85 AdmitMetadataBytes,
87 MetadataWorkerStopped,
89 MetadataIoClosed,
91 RegisterSigtermHandler,
93 RegisterSigintHandler,
95 WaitForSignal,
97 CreateTaskGroupChild,
99 SpawnTaskGroupTask,
101 SpawnServiceTask,
103 SpawnSchedulerTask,
105 RuntimeContext,
107 StartServiceLifecycle,
109 ServiceLifecycleTaskGroup,
111 BindServiceHealthProbe,
113 InspectServiceHealthProbe,
115 MarkServiceReady,
117 SuspendServiceReadiness,
119 RestoreServiceReadiness,
121 ShutdownRuntimeBlocking,
123 BlockingQueueAdmission,
125 RunBlockingTask,
127 BlockingTaskDeadline,
129 BlockingTask,
131 BlockingExecutorKind,
133 ShutdownBlockingExecutor,
135 ClearCompletedSchedules,
137 RegisterScheduledTask,
139 SpawnOperation,
141 OperationOwner,
143 MetadataIo,
145 ServiceLifecycleEnvironment,
147 ServiceLifecycleProbeAddress,
149 ServiceLifecycleDuration,
151 ServiceLifecycleDurationRange,
153 ServiceLifecycleDrainMethods,
155 MetadataResourceTarget,
157 External(ExternalOperation),
159}
160
161impl RuntimeOperation {
162 #[must_use]
171 pub const fn external(label: &'static str) -> Self {
172 Self::External(ExternalOperation::new(label))
173 }
174
175 const fn diagnostic_label(self) -> &'static str {
176 match self {
177 Self::BuildTokioRuntime => "build-tokio-runtime",
178 Self::LoadConfigFile => "load-config-file",
179 Self::DeserializeConfigFile => "deserialize-config-file",
180 Self::DetectProcessMemoryLimit => "detect-process-memory-limit",
181 Self::ResolveManagedMemoryBudget => "resolve-managed-memory-budget",
182 Self::ReadFile => "read-file",
183 Self::ReadFileBackup => "read-file-backup",
184 Self::PersistMetadata => "metadata-persistence",
185 Self::MetadataCreateParent => "metadata-create-parent",
186 Self::MetadataCreateTemporary => "metadata-create-temporary",
187 Self::MetadataWriteTemporary => "metadata-write-temporary",
188 Self::MetadataSyncTemporary => "metadata-sync-temporary",
189 Self::MetadataReplaceTarget => "metadata-replace-target",
190 Self::MetadataSyncParent => "metadata-sync-parent",
191 Self::MetadataRemoveTemporary => "metadata-remove-temporary",
192 Self::WaitForDurableMetadata => "wait-for-durable-metadata",
193 Self::AdmitMetadataSnapshot => "admit-metadata-snapshot",
194 Self::AdmitMetadataOperation => "admit-metadata-operation",
195 Self::AdmitMetadataBytes => "admit-metadata-bytes",
196 Self::MetadataWorkerStopped => "metadata-worker-stopped",
197 Self::MetadataIoClosed => "metadata-io-closed",
198 Self::RegisterSigtermHandler => "register-sigterm-handler",
199 Self::RegisterSigintHandler => "register-sigint-handler",
200 Self::WaitForSignal => "wait-for-signal",
201 Self::CreateTaskGroupChild => "create-task-group-child",
202 Self::SpawnTaskGroupTask => "spawn-task-group-task",
203 Self::SpawnServiceTask => "spawn-service-task",
204 Self::SpawnSchedulerTask => "spawn-scheduler-task",
205 Self::RuntimeContext => "runtime-context",
206 Self::StartServiceLifecycle => "start-service-lifecycle",
207 Self::ServiceLifecycleTaskGroup => "service-lifecycle-task-group",
208 Self::BindServiceHealthProbe => "bind-service-health-probe",
209 Self::InspectServiceHealthProbe => "inspect-service-health-probe",
210 Self::MarkServiceReady => "mark-service-ready",
211 Self::SuspendServiceReadiness => "suspend-service-readiness",
212 Self::RestoreServiceReadiness => "restore-service-readiness",
213 Self::ShutdownRuntimeBlocking => "shutdown-runtime-blocking",
214 Self::BlockingQueueAdmission => "blocking-queue-admission",
215 Self::RunBlockingTask => "run-blocking-task",
216 Self::BlockingTaskDeadline => "blocking-task-deadline",
217 Self::BlockingTask => "blocking-task",
218 Self::BlockingExecutorKind => "blocking-executor-kind",
219 Self::ShutdownBlockingExecutor => "shutdown-blocking-executor",
220 Self::ClearCompletedSchedules => "clear-completed-schedules",
221 Self::RegisterScheduledTask => "register-scheduled-task",
222 Self::SpawnOperation => "spawn-operation",
223 Self::OperationOwner => "operation-owner",
224 Self::MetadataIo => "metadata-io",
225 Self::ServiceLifecycleEnvironment => "service-lifecycle-environment",
226 Self::ServiceLifecycleProbeAddress => "service-lifecycle-probe-address",
227 Self::ServiceLifecycleDuration => "service-lifecycle-duration",
228 Self::ServiceLifecycleDurationRange => "service-lifecycle-duration-range",
229 Self::ServiceLifecycleDrainMethods => "service-lifecycle-drain-methods",
230 Self::MetadataResourceTarget => "metadata-resource-target",
231 Self::External(operation) => operation.label(),
232 }
233 }
234}
235
236impl fmt::Display for RuntimeOperation {
237 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238 formatter.write_str(self.diagnostic_label())
239 }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub struct ExternalOperation(&'static str);
250
251impl ExternalOperation {
252 pub const MAX_LABEL_BYTES: usize = 64;
254
255 #[must_use]
267 pub const fn new(label: &'static str) -> Self {
268 assert!(
269 is_valid_external_label(label),
270 "external operation labels are 1-64 bytes of lowercase ASCII letters, digits, and inner hyphens"
271 );
272 Self(label)
273 }
274
275 #[must_use]
277 pub const fn label(self) -> &'static str {
278 self.0
279 }
280}
281
282const fn is_valid_external_label(label: &str) -> bool {
283 let bytes = label.as_bytes();
284 if bytes.is_empty() || bytes.len() > ExternalOperation::MAX_LABEL_BYTES {
285 return false;
286 }
287 if bytes[0] == b'-' || bytes[bytes.len() - 1] == b'-' {
288 return false;
289 }
290 let mut index = 0;
291 while index < bytes.len() {
292 let byte = bytes[index];
293 if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') {
294 return false;
295 }
296 index += 1;
297 }
298 true
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306#[non_exhaustive]
307pub enum RuntimeContractPolicy {
308 MaxBlockingThreadsWithinSupportedRange,
310 WorkerThreadsPositive,
312 MaxBlockingThreadsPositive,
314 ThreadNameNotBlank,
316 ThreadStackSizePositive,
318 BlockingMaxConcurrencyPositive,
320 BlockingMaxQueueDepthPositive,
322 BlockingTimeoutRepresentable,
324 BlockingGlobalCapacityCoversLanes,
326 ServiceContextScopeNotBlank,
328 ConfiguredMemoryLimitPositive,
330 MemoryFractionPositiveAndBounded,
332 ManagedMemoryRatioPositiveAndBounded,
334 ManagedMemoryHeadroomBelowEffectiveLimit,
336 ManagedMemoryBudgetPositive,
338 CriticalFailureSubscriptionCapacityPositive,
340 MetadataMaxPendingOperationsPositive,
342 MetadataMaxPendingBytesPositive,
344 MetadataBlockingTaskTimeoutPositive,
346}
347
348impl RuntimeContractPolicy {
349 const fn diagnostic_label(self) -> &'static str {
350 match self {
351 Self::MaxBlockingThreadsWithinSupportedRange => "max-blocking-threads-within-supported-range",
352 Self::WorkerThreadsPositive => "worker-threads-positive",
353 Self::MaxBlockingThreadsPositive => "max-blocking-threads-positive",
354 Self::ThreadNameNotBlank => "thread-name-not-blank",
355 Self::ThreadStackSizePositive => "thread-stack-size-positive",
356 Self::BlockingMaxConcurrencyPositive => "blocking-max-concurrency-positive",
357 Self::BlockingMaxQueueDepthPositive => "blocking-max-queue-depth-positive",
358 Self::BlockingTimeoutRepresentable => "blocking-timeout-representable",
359 Self::BlockingGlobalCapacityCoversLanes => "blocking-global-capacity-covers-lanes",
360 Self::ServiceContextScopeNotBlank => "service-context-scope-not-blank",
361 Self::ConfiguredMemoryLimitPositive => "configured-limit-must-be-positive",
362 Self::MemoryFractionPositiveAndBounded => "fraction-must-be-positive-and-bounded",
363 Self::ManagedMemoryRatioPositiveAndBounded => "managed-memory-ratio-positive-and-bounded",
364 Self::ManagedMemoryHeadroomBelowEffectiveLimit => "managed-memory-headroom-below-effective-limit",
365 Self::ManagedMemoryBudgetPositive => "managed-memory-budget-positive",
366 Self::CriticalFailureSubscriptionCapacityPositive => "critical-failure-subscription-capacity-positive",
367 Self::MetadataMaxPendingOperationsPositive => "max-pending-operations-positive",
368 Self::MetadataMaxPendingBytesPositive => "max-pending-bytes-positive",
369 Self::MetadataBlockingTaskTimeoutPositive => "blocking-task-timeout-positive",
370 }
371 }
372}
373
374impl fmt::Display for RuntimeContractPolicy {
375 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
376 formatter.write_str(self.diagnostic_label())
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
386pub enum RuntimeContractViolation {
387 #[error("runtime configuration violates {policy}")]
389 InvalidConfiguration {
390 policy: RuntimeContractPolicy,
392 },
393 #[error("resource budget name must not be blank")]
395 EmptyBudgetName,
396 #[error("resource budget name must not contain '/'")]
398 InvalidBudgetName,
399 #[error("resource budget capacity is zero")]
401 ZeroBudgetCapacity {
402 dimension: BudgetDimension,
404 },
405 #[error("resource budget rate must be positive")]
407 ZeroBudgetRate,
408 #[error("resource budget maximum age must be positive")]
410 ZeroBudgetMaxAge,
411 #[error("resource budget reserve exceeds capacity")]
413 ReserveExceedsBudgetCapacity {
414 dimension: BudgetDimension,
416 },
417 #[error("resource budget reserve requires capacity")]
419 ReserveWithoutBudgetCapacity {
420 dimension: BudgetDimension,
422 },
423 #[error("resource budget child limit exceeds parent")]
425 ChildBudgetExceedsParent {
426 dimension: BudgetDimension,
428 },
429 #[error("resource budget child maximum age exceeds parent")]
431 ChildBudgetMaxAgeExceedsParent,
432 #[error("resource permit target belongs to a different budget tree")]
434 PermitTargetInDifferentTree,
435 #[error("process memory limit violates {policy}")]
437 InvalidMemoryLimit {
438 policy: RuntimeContractPolicy,
440 },
441 #[error("schedule definition violates {policy}")]
443 InvalidSchedule {
444 policy: RuntimeContractPolicy,
446 },
447 #[error("metadata I/O configuration violates {policy}")]
449 InvalidMetadataConfiguration {
450 policy: RuntimeContractPolicy,
452 },
453}
454
455impl RuntimeContractViolation {
456 #[must_use]
458 pub const fn condition(&self) -> CanonicalCondition {
459 CanonicalCondition::InvalidArgument
460 }
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
470#[non_exhaustive]
471pub enum RuntimeErrorKind {
472 Closed,
476 Poisoned,
478 TimedOut,
480 CapacityExhausted,
482 Unsupported,
485 ContextUnavailable,
488 Configuration,
490 Io,
492 Internal,
494}
495
496#[derive(Clone)]
502pub struct RuntimeError {
503 error: SharedError,
504 kind: RuntimeErrorKind,
505 operation: RuntimeOperation,
506 component: &'static str,
507}
508
509impl RuntimeError {
510 #[must_use]
512 #[track_caller]
513 fn new(
514 descriptor: &'static ErrorDescriptor,
515 kind: RuntimeErrorKind,
516 operation: RuntimeOperation,
517 component: &'static str,
518 ) -> Self {
519 let error = CanonicalError::new(descriptor).with_context(runtime_context(operation, false));
520 Self {
521 error: Arc::new(error),
522 kind,
523 operation,
524 component,
525 }
526 }
527
528 #[must_use]
530 #[track_caller]
531 fn caused_by(
532 descriptor: &'static ErrorDescriptor,
533 kind: RuntimeErrorKind,
534 operation: RuntimeOperation,
535 component: &'static str,
536 source: impl Error + Send + Sync + 'static,
537 ) -> Self {
538 let error = CanonicalError::caused_by(descriptor, source).with_context(runtime_context(operation, true));
539 Self {
540 error: Arc::new(error),
541 kind,
542 operation,
543 component,
544 }
545 }
546
547 #[must_use]
552 #[track_caller]
553 pub(crate) fn within(operation: RuntimeOperation, source: RuntimeError) -> Self {
554 Self::caused_by(source.descriptor(), source.kind, operation, source.component, source)
555 }
556
557 #[must_use]
559 pub fn descriptor(&self) -> &'static ErrorDescriptor {
560 self.error.descriptor()
561 }
562
563 #[must_use]
565 pub fn code(&self) -> ErrorCode {
566 self.error.code()
567 }
568
569 #[must_use]
571 pub fn condition(&self) -> CanonicalCondition {
572 self.error.condition()
573 }
574
575 #[must_use]
577 pub fn recovery_hint(&self) -> RecoveryHint {
578 self.error.recovery_hint()
579 }
580
581 #[must_use]
583 pub const fn kind(&self) -> RuntimeErrorKind {
584 self.kind
585 }
586
587 #[must_use]
589 pub const fn operation(&self) -> RuntimeOperation {
590 self.operation
591 }
592
593 #[must_use]
595 pub fn component(&self) -> &str {
596 self.component
597 }
598
599 #[must_use]
601 pub fn context(&self) -> &ErrorContext {
602 self.error.context()
603 }
604
605 #[must_use]
607 pub fn location(&self) -> &'static Location<'static> {
608 self.error.location()
609 }
610
611 #[must_use]
613 pub fn backtrace(&self) -> Option<&Backtrace> {
614 self.error.backtrace()
615 }
616
617 pub fn public_view(&self) -> Result<PublicErrorView<'_>, rocketmq_error::ViewContextViolation> {
624 self.error.public_view()
625 }
626
627 pub fn diagnostic_view(&self) -> Result<DiagnosticView<'_>, rocketmq_error::ViewContextViolation> {
634 self.error.diagnostic_view()
635 }
636
637 #[must_use]
639 #[track_caller]
640 pub fn configuration(operation: RuntimeOperation) -> Self {
641 Self::new(
642 &rocketmq_error::RUNTIME_CONFIGURATION_FAILED,
643 RuntimeErrorKind::Configuration,
644 operation,
645 "runtime",
646 )
647 }
648
649 #[must_use]
651 #[track_caller]
652 pub fn configuration_failure(operation: RuntimeOperation, source: impl Error + Send + Sync + 'static) -> Self {
653 Self::caused_by(
654 &rocketmq_error::RUNTIME_CONFIGURATION_FAILED,
655 RuntimeErrorKind::Configuration,
656 operation,
657 "runtime",
658 source,
659 )
660 }
661
662 #[must_use]
664 #[track_caller]
665 pub fn build(operation: RuntimeOperation, source: std::io::Error) -> Self {
666 Self::caused_by(
667 &rocketmq_error::RUNTIME_BUILD_FAILED,
668 RuntimeErrorKind::Io,
669 operation,
670 "runtime",
671 source,
672 )
673 }
674
675 #[must_use]
677 #[track_caller]
678 pub fn io(operation: RuntimeOperation, source: std::io::Error) -> Self {
679 Self::caused_by(
680 &rocketmq_error::RUNTIME_IO_FAILED,
681 RuntimeErrorKind::Io,
682 operation,
683 "runtime",
684 source,
685 )
686 }
687
688 #[must_use]
690 #[track_caller]
691 pub fn context_unavailable(operation: RuntimeOperation) -> Self {
692 Self::new(
693 &rocketmq_error::RUNTIME_CONTEXT_UNAVAILABLE,
694 RuntimeErrorKind::ContextUnavailable,
695 operation,
696 "runtime",
697 )
698 }
699
700 #[must_use]
707 #[track_caller]
708 pub fn closed(operation: RuntimeOperation) -> Self {
709 Self::new(
710 &rocketmq_error::RUNTIME_CONTEXT_UNAVAILABLE,
711 RuntimeErrorKind::Closed,
712 operation,
713 "runtime",
714 )
715 }
716
717 #[must_use]
719 #[track_caller]
720 pub(crate) fn poisoned(operation: RuntimeOperation) -> Self {
721 Self::new(
722 &rocketmq_error::RUNTIME_CONTEXT_UNAVAILABLE,
723 RuntimeErrorKind::Poisoned,
724 operation,
725 "runtime",
726 )
727 }
728
729 #[must_use]
731 #[track_caller]
732 pub fn capacity(operation: RuntimeOperation) -> Self {
733 Self::new(
734 &rocketmq_error::RUNTIME_CAPACITY_EXHAUSTED,
735 RuntimeErrorKind::CapacityExhausted,
736 operation,
737 "runtime",
738 )
739 }
740
741 #[must_use]
743 #[track_caller]
744 pub fn timed_out(operation: RuntimeOperation) -> Self {
745 Self::new(
746 &rocketmq_error::RUNTIME_OPERATION_TIMED_OUT,
747 RuntimeErrorKind::TimedOut,
748 operation,
749 "runtime",
750 )
751 }
752
753 #[must_use]
755 #[track_caller]
756 pub fn unsupported(operation: RuntimeOperation) -> Self {
757 Self::new(
758 &rocketmq_error::RUNTIME_OPERATION_UNSUPPORTED,
759 RuntimeErrorKind::Unsupported,
760 operation,
761 "runtime",
762 )
763 }
764
765 #[must_use]
767 #[track_caller]
768 pub fn join(operation: RuntimeOperation, source: tokio::task::JoinError) -> Self {
769 Self::caused_by(
770 &rocketmq_error::RUNTIME_TASK_JOIN_FAILED,
771 RuntimeErrorKind::Internal,
772 operation,
773 "task",
774 source,
775 )
776 }
777
778 #[must_use]
780 #[track_caller]
781 pub fn internal(operation: RuntimeOperation, source: impl Error + Send + Sync + 'static) -> Self {
782 Self::caused_by(
783 &rocketmq_error::RUNTIME_INTERNAL_FAILURE,
784 RuntimeErrorKind::Internal,
785 operation,
786 "runtime",
787 source,
788 )
789 }
790
791 #[must_use]
793 #[track_caller]
794 pub fn internal_failure(operation: RuntimeOperation) -> Self {
795 Self::new(
796 &rocketmq_error::RUNTIME_INTERNAL_FAILURE,
797 RuntimeErrorKind::Internal,
798 operation,
799 "runtime",
800 )
801 }
802}
803
804fn runtime_context(operation: RuntimeOperation, source_present: bool) -> ErrorContext {
805 let context = ErrorContext::new().with_text(
806 rocketmq_error::fields::OPERATION_DIAGNOSTIC,
807 operation.diagnostic_label(),
808 );
809 if source_present {
810 context.with_secret_presence(rocketmq_error::fields::SOURCE_PRESENT)
811 } else {
812 context
813 }
814}
815
816impl fmt::Display for RuntimeError {
817 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
818 fmt::Display::fmt(self.error.as_ref(), formatter)
819 }
820}
821
822impl fmt::Debug for RuntimeError {
823 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
824 formatter
825 .debug_struct("RuntimeError")
826 .field("code", &self.code())
827 .field("condition", &self.condition())
828 .field("kind", &self.kind)
829 .field("operation", &self.operation)
830 .field("component", &self.component)
831 .field("has_source", &self.error.source().is_some())
832 .finish()
833 }
834}
835
836impl Error for RuntimeError {
837 fn source(&self) -> Option<&(dyn Error + 'static)> {
838 self.error.source()
839 }
840}
841
842impl From<std::io::Error> for RuntimeError {
843 #[track_caller]
844 fn from(source: std::io::Error) -> Self {
845 Self::io(RuntimeOperation::ReadFile, source)
846 }
847}
848
849#[cfg(test)]
850mod tests {
851 use std::error::Error as _;
852 use std::io;
853
854 use rocketmq_error::CanonicalCondition;
855 use rocketmq_error::RecoveryHint;
856
857 use super::*;
858
859 const PERSIST_TEST_METADATA: RuntimeOperation = RuntimeOperation::external("persist-runtime-metadata");
860 const TEST_FAILURE: RuntimeOperation = RuntimeOperation::external("test-runtime-failure");
861
862 #[test]
863 fn operational_runtime_error_preserves_source_and_catalog_metadata_without_rendering_it() {
864 const SENTINEL: &str = "runtime-source-secret";
865 let error = RuntimeError::io(PERSIST_TEST_METADATA, io::Error::other(SENTINEL));
866
867 assert_eq!(error.code(), rocketmq_error::RUNTIME_IO_FAILED.code());
868 assert_eq!(error.condition(), CanonicalCondition::Internal);
869 assert_eq!(error.recovery_hint(), RecoveryHint::OperatorAction);
870 assert_eq!(error.operation(), PERSIST_TEST_METADATA);
871 assert_eq!(error.component(), "runtime");
872 assert!(error
873 .source()
874 .and_then(|source| source.downcast_ref::<io::Error>())
875 .is_some());
876 assert!(!error.to_string().contains(SENTINEL));
877 assert!(!format!("{error:?}").contains(SENTINEL));
878 assert!(error.public_view().is_ok());
879 assert!(error.diagnostic_view().is_ok());
880 }
881
882 #[test]
883 fn runtime_configuration_facade_uses_corrected_boundary_projection() {
884 let error = RuntimeError::configuration(RuntimeOperation::LoadConfigFile);
885 let public = error.public_view().expect("valid runtime public view");
886 let projection = public.projection();
887
888 assert_eq!("runtime.configuration.failed", public.code().as_str());
889 assert_eq!(rocketmq_error::HttpStatusCode::BAD_REQUEST, projection.http().status);
890 assert_eq!(rocketmq_error::CliExitCode::CONFIG, projection.cli().exit_code);
891 }
892
893 #[test]
894 fn runtime_clone_shares_source_location_and_backtrace() {
895 let caller_line = line!() + 1;
896 let error = RuntimeError::internal(PERSIST_TEST_METADATA, io::Error::other("typed leaf"));
897 let cloned = error.clone();
898
899 assert!(Arc::ptr_eq(&error.error, &cloned.error));
900 assert!(std::ptr::eq(
901 error.source().expect("runtime source"),
902 cloned.source().expect("cloned runtime source")
903 ));
904 assert!(error
905 .source()
906 .and_then(|source| source.downcast_ref::<io::Error>())
907 .is_some());
908 assert_eq!(error.location().file(), file!());
909 assert_eq!(error.location().line(), caller_line);
910
911 match (error.backtrace(), cloned.backtrace()) {
912 (Some(left), Some(right)) => assert!(std::ptr::eq(left, right)),
913 (None, None) => {}
914 _ => panic!("a clone must share the canonical backtrace state"),
915 }
916 }
917
918 #[test]
919 fn operation_debug_output_is_closed_for_sentinel_control_and_unbounded_text() {
920 let error = RuntimeError::internal_failure(TEST_FAILURE);
921 let debug = format!("{error:?}");
922 let control = "\u{0000}\u{001b}[31m";
923 let unbounded = "unbounded-operation-".repeat(4096);
924
925 assert!(debug.contains("test-runtime-failure"));
926 assert!(!debug.contains("runtime-source-secret"));
927 assert!(!debug.contains("operation_diagnostic"));
928 assert!(!debug.contains(control));
929 assert!(!debug.contains(&unbounded));
930
931 let constructor: fn(RuntimeOperation) -> RuntimeError = RuntimeError::internal_failure;
932 assert_eq!(constructor(TEST_FAILURE).operation(), TEST_FAILURE);
933 }
934
935 #[test]
936 fn an_external_operation_reports_its_own_label() {
937 const INITIALIZE_COMPONENT: RuntimeOperation = RuntimeOperation::external("initialize-component-2");
938
939 assert_eq!(INITIALIZE_COMPONENT.to_string(), "initialize-component-2");
940 assert_eq!(
941 INITIALIZE_COMPONENT,
942 RuntimeOperation::External(ExternalOperation::new("initialize-component-2"))
943 );
944 assert_ne!(INITIALIZE_COMPONENT, RuntimeOperation::external("start-component"));
945 let error = RuntimeError::closed(INITIALIZE_COMPONENT);
946 assert_eq!(error.operation(), INITIALIZE_COMPONENT);
947 assert_eq!(error.kind(), RuntimeErrorKind::Closed);
948 }
949
950 #[test]
951 fn external_operation_labels_accept_only_bounded_kebab_case() {
952 let longest = "a".repeat(ExternalOperation::MAX_LABEL_BYTES);
953 assert!(is_valid_external_label(&longest));
954 assert!(is_valid_external_label("admit-kv-mutation-bytes"));
955 assert!(is_valid_external_label("ha-runtime"));
956
957 let too_long = "a".repeat(ExternalOperation::MAX_LABEL_BYTES + 1);
958 for invalid in [
959 "",
960 too_long.as_str(),
961 "-leading",
962 "trailing-",
963 "Upper-case",
964 "under_score",
965 "white space",
966 "control\u{001b}",
967 "path/segment",
968 "non-ascii-é",
969 ] {
970 assert!(!is_valid_external_label(invalid), "{invalid:?} must be rejected");
971 }
972 }
973
974 #[test]
975 #[should_panic(expected = "external operation labels")]
976 fn a_runtime_constructed_invalid_label_is_rejected() {
977 let _ = ExternalOperation::new("Invalid Label");
978 }
979
980 #[test]
981 fn each_constructor_reports_a_matchable_kind() {
982 let cases = [
983 (
984 RuntimeError::configuration(RuntimeOperation::LoadConfigFile),
985 RuntimeErrorKind::Configuration,
986 ),
987 (
988 RuntimeError::build(RuntimeOperation::BuildTokioRuntime, io::Error::other("build")),
989 RuntimeErrorKind::Io,
990 ),
991 (
992 RuntimeError::io(RuntimeOperation::ReadFile, io::Error::other("read")),
993 RuntimeErrorKind::Io,
994 ),
995 (
996 RuntimeError::context_unavailable(RuntimeOperation::RuntimeContext),
997 RuntimeErrorKind::ContextUnavailable,
998 ),
999 (
1000 RuntimeError::closed(RuntimeOperation::SpawnTaskGroupTask),
1001 RuntimeErrorKind::Closed,
1002 ),
1003 (
1004 RuntimeError::poisoned(RuntimeOperation::SpawnTaskGroupTask),
1005 RuntimeErrorKind::Poisoned,
1006 ),
1007 (
1008 RuntimeError::capacity(RuntimeOperation::BlockingQueueAdmission),
1009 RuntimeErrorKind::CapacityExhausted,
1010 ),
1011 (
1012 RuntimeError::timed_out(RuntimeOperation::BlockingTask),
1013 RuntimeErrorKind::TimedOut,
1014 ),
1015 (
1016 RuntimeError::unsupported(RuntimeOperation::RegisterScheduledTask),
1017 RuntimeErrorKind::Unsupported,
1018 ),
1019 (RuntimeError::internal_failure(TEST_FAILURE), RuntimeErrorKind::Internal),
1020 ];
1021 for (error, kind) in cases {
1022 assert_eq!(error.kind(), kind, "{error:?}");
1023 }
1024 }
1025
1026 #[test]
1027 fn closed_and_poisoned_keep_the_unavailable_descriptor() {
1028 let unavailable = RuntimeError::context_unavailable(RuntimeOperation::SpawnTaskGroupTask);
1029 for error in [
1030 RuntimeError::closed(RuntimeOperation::SpawnTaskGroupTask),
1031 RuntimeError::poisoned(RuntimeOperation::SpawnTaskGroupTask),
1032 ] {
1033 assert_eq!(error.code(), unavailable.code());
1034 assert_eq!(error.condition(), unavailable.condition());
1035 assert_ne!(error.kind(), unavailable.kind());
1036 }
1037 }
1038
1039 #[test]
1040 fn a_relabelled_failure_keeps_its_kind_and_cause() {
1041 let closed = RuntimeError::closed(RuntimeOperation::SpawnTaskGroupTask);
1042 let error = RuntimeError::within(RuntimeOperation::SpawnServiceTask, closed.clone());
1043
1044 assert_eq!(error.kind(), RuntimeErrorKind::Closed);
1045 assert_eq!(error.operation(), RuntimeOperation::SpawnServiceTask);
1046 assert_eq!(error.code(), closed.code());
1047 assert_eq!(error.component(), closed.component());
1048 let source = error
1049 .source()
1050 .and_then(|source| source.downcast_ref::<RuntimeError>())
1051 .expect("the original failure is the typed cause");
1052 assert_eq!(source.operation(), RuntimeOperation::SpawnTaskGroupTask);
1053 }
1054
1055 #[test]
1056 fn contract_violations_stay_outside_the_operational_runtime_error_channel() {
1057 let violation = RuntimeContractViolation::PermitTargetInDifferentTree;
1058
1059 assert_eq!(violation.condition(), CanonicalCondition::InvalidArgument);
1060 assert_eq!(
1061 violation.to_string(),
1062 "resource permit target belongs to a different budget tree"
1063 );
1064
1065 let closed_policy = RuntimeContractViolation::InvalidConfiguration {
1066 policy: RuntimeContractPolicy::WorkerThreadsPositive,
1067 };
1068 assert_eq!(
1069 closed_policy.to_string(),
1070 "runtime configuration violates worker-threads-positive"
1071 );
1072 }
1073}