Skip to main content

rocketmq_runtime/
error.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
33/// Alias for an operational runtime result.
34pub type RuntimeResult<T> = Result<T, RuntimeError>;
35
36/// A closed, low-cardinality runtime operation identifier.
37///
38/// Runtime failures carry this value instead of caller-provided text so that
39/// diagnostics remain aggregatable and cannot disclose request, path, or
40/// configuration values. The named variants are operations this crate
41/// performs. A crate built on the runtime labels its own operations with
42/// [`Self::External`] instead of adding variants here.
43///
44/// New runtime operations may be added in a minor release, so a `match`
45/// outside this crate needs a wildcard arm.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47#[non_exhaustive]
48pub enum RuntimeOperation {
49    /// Builds an owned Tokio runtime.
50    BuildTokioRuntime,
51    /// Loads a runtime configuration file.
52    LoadConfigFile,
53    /// Deserializes a runtime configuration file.
54    DeserializeConfigFile,
55    /// Detects the process memory limit.
56    DetectProcessMemoryLimit,
57    /// Resolves the managed memory budget from the effective limit.
58    ResolveManagedMemoryBudget,
59    /// Reads a file.
60    ReadFile,
61    /// Reads a file before creating a backup.
62    ReadFileBackup,
63    /// Persists metadata through a compatibility filesystem path.
64    PersistMetadata,
65    /// Creates a metadata parent directory.
66    MetadataCreateParent,
67    /// Creates a metadata temporary file.
68    MetadataCreateTemporary,
69    /// Writes a metadata temporary file.
70    MetadataWriteTemporary,
71    /// Synchronizes a metadata temporary file.
72    MetadataSyncTemporary,
73    /// Replaces a metadata target file.
74    MetadataReplaceTarget,
75    /// Synchronizes a metadata parent directory.
76    MetadataSyncParent,
77    /// Removes a metadata temporary file.
78    MetadataRemoveTemporary,
79    /// Waits for metadata durability.
80    WaitForDurableMetadata,
81    /// Admits a metadata snapshot.
82    AdmitMetadataSnapshot,
83    /// Admits metadata work.
84    AdmitMetadataOperation,
85    /// Admits metadata bytes.
86    AdmitMetadataBytes,
87    /// Uses a stopped metadata worker.
88    MetadataWorkerStopped,
89    /// Uses a closed metadata actor.
90    MetadataIoClosed,
91    /// Registers the SIGTERM handler.
92    RegisterSigtermHandler,
93    /// Registers the SIGINT handler.
94    RegisterSigintHandler,
95    /// Waits for an operating-system signal.
96    WaitForSignal,
97    /// Creates a task-group child.
98    CreateTaskGroupChild,
99    /// Spawns a task-group task.
100    SpawnTaskGroupTask,
101    /// Spawns a service task.
102    SpawnServiceTask,
103    /// Spawns a scheduler task.
104    SpawnSchedulerTask,
105    /// Uses the ambient runtime context.
106    RuntimeContext,
107    /// Starts a service lifecycle.
108    StartServiceLifecycle,
109    /// Uses a service lifecycle task group.
110    ServiceLifecycleTaskGroup,
111    /// Binds a service health probe.
112    BindServiceHealthProbe,
113    /// Inspects a service health probe.
114    InspectServiceHealthProbe,
115    /// Marks a service ready.
116    MarkServiceReady,
117    /// Suspends service readiness.
118    SuspendServiceReadiness,
119    /// Restores service readiness.
120    RestoreServiceReadiness,
121    /// Shuts down an owned runtime from a blocking caller.
122    ShutdownRuntimeBlocking,
123    /// Performs blocking executor admission.
124    BlockingQueueAdmission,
125    /// Runs a blocking task.
126    RunBlockingTask,
127    /// Waits for a blocking task deadline.
128    BlockingTaskDeadline,
129    /// Waits for a blocking task.
130    BlockingTask,
131    /// Uses an unsupported blocking executor kind.
132    BlockingExecutorKind,
133    /// Waiting for an isolated blocking executor to release its work.
134    ShutdownBlockingExecutor,
135    /// Clears completed scheduled registrations.
136    ClearCompletedSchedules,
137    /// Registers a scheduled task.
138    RegisterScheduledTask,
139    /// Spawns an operation.
140    SpawnOperation,
141    /// Validates an operation owner.
142    OperationOwner,
143    /// Performs metadata I/O through an adapter.
144    MetadataIo,
145    /// Reads service lifecycle environment configuration.
146    ServiceLifecycleEnvironment,
147    /// Parses a service lifecycle probe address.
148    ServiceLifecycleProbeAddress,
149    /// Parses a service lifecycle duration.
150    ServiceLifecycleDuration,
151    /// Validates a service lifecycle duration range.
152    ServiceLifecycleDurationRange,
153    /// Parses the HTTP methods accepted by the drain probe.
154    ServiceLifecycleDrainMethods,
155    /// Validates metadata resource targeting.
156    MetadataResourceTarget,
157    /// An operation owned by a crate that builds on the runtime.
158    External(ExternalOperation),
159}
160
161impl RuntimeOperation {
162    /// Labels an operation owned outside this crate.
163    ///
164    /// Define the result as a constant so an invalid label fails compilation;
165    /// see [`ExternalOperation::new`] for the accepted form.
166    ///
167    /// # Panics
168    ///
169    /// Panics when `label` violates the [`ExternalOperation::new`] contract.
170    #[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/// A static, low-cardinality label for an operation owned outside this crate.
243///
244/// A crate built on the runtime reports failures of its own operations with
245/// [`RuntimeOperation::External`], so this crate does not need a variant for
246/// every consumer. A label is a string literal in lowercase kebab case: it stays
247/// aggregatable and cannot carry request, path, or configuration text.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub struct ExternalOperation(&'static str);
250
251impl ExternalOperation {
252    /// The longest accepted label, in bytes.
253    pub const MAX_LABEL_BYTES: usize = 64;
254
255    /// Creates a label for an operation owned outside this crate.
256    ///
257    /// Define the result as a constant so an invalid label fails compilation
258    /// instead of panicking at run time.
259    ///
260    /// # Panics
261    ///
262    /// Panics when `label` is empty, longer than [`Self::MAX_LABEL_BYTES`],
263    /// starts or ends with a hyphen, or contains anything other than lowercase
264    /// ASCII letters, digits, and hyphens. Labels are programmer-owned static
265    /// identifiers, not runtime input.
266    #[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    /// Returns the label reported in diagnostics.
276    #[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/// A closed identifier for a deterministic runtime contract rule.
302///
303/// New rules may be added in a minor release, so a `match` outside this crate
304/// needs a wildcard arm.
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306#[non_exhaustive]
307pub enum RuntimeContractPolicy {
308    /// The configured maximum blocking threads is outside its supported range.
309    MaxBlockingThreadsWithinSupportedRange,
310    /// The configured worker-thread count must be positive.
311    WorkerThreadsPositive,
312    /// The configured maximum blocking-thread count must be positive.
313    MaxBlockingThreadsPositive,
314    /// The configured runtime thread name must not be blank.
315    ThreadNameNotBlank,
316    /// The configured runtime thread-stack size must be positive.
317    ThreadStackSizePositive,
318    /// The blocking lane concurrency must be positive.
319    BlockingMaxConcurrencyPositive,
320    /// The blocking lane queue depth must be positive.
321    BlockingMaxQueueDepthPositive,
322    /// The combined blocking timeout must be representable.
323    BlockingTimeoutRepresentable,
324    /// The global blocking capacity must cover every lane.
325    BlockingGlobalCapacityCoversLanes,
326    /// A service-context scope must not be blank.
327    ServiceContextScopeNotBlank,
328    /// An explicitly configured process memory limit must be positive.
329    ConfiguredMemoryLimitPositive,
330    /// A process-memory fraction must be positive and bounded.
331    MemoryFractionPositiveAndBounded,
332    /// A managed-memory ratio must be positive and bounded.
333    ManagedMemoryRatioPositiveAndBounded,
334    /// Managed-memory headroom must leave a positive effective limit.
335    ManagedMemoryHeadroomBelowEffectiveLimit,
336    /// The managed memory budget must be positive.
337    ManagedMemoryBudgetPositive,
338    /// A critical failure subscription capacity must be positive.
339    CriticalFailureSubscriptionCapacityPositive,
340    /// The metadata I/O operation count must be positive.
341    MetadataMaxPendingOperationsPositive,
342    /// The metadata I/O byte capacity must be positive.
343    MetadataMaxPendingBytesPositive,
344    /// The metadata I/O blocking timeout must be positive.
345    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/// A deterministic caller configuration or invariant violation.
381///
382/// This type deliberately has no operational source or retry metadata. Its
383/// fields identify only bounded policy and invariant names; callers must not
384/// use it to carry configuration contents.
385#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
386pub enum RuntimeContractViolation {
387    /// A runtime configuration field violated its documented invariant.
388    #[error("runtime configuration violates {policy}")]
389    InvalidConfiguration {
390        /// The closed policy identifier.
391        policy: RuntimeContractPolicy,
392    },
393    /// A resource-budget name is blank.
394    #[error("resource budget name must not be blank")]
395    EmptyBudgetName,
396    /// A resource-budget name is structurally invalid.
397    #[error("resource budget name must not contain '/'")]
398    InvalidBudgetName,
399    /// A budget capacity is zero.
400    #[error("resource budget capacity is zero")]
401    ZeroBudgetCapacity {
402        /// The affected budget dimension.
403        dimension: BudgetDimension,
404    },
405    /// A configured rate has a zero limit or burst.
406    #[error("resource budget rate must be positive")]
407    ZeroBudgetRate,
408    /// A configured budget maximum age is zero.
409    #[error("resource budget maximum age must be positive")]
410    ZeroBudgetMaxAge,
411    /// A reserve exceeds its own capacity.
412    #[error("resource budget reserve exceeds capacity")]
413    ReserveExceedsBudgetCapacity {
414        /// The affected budget dimension.
415        dimension: BudgetDimension,
416    },
417    /// A reserve was configured without the corresponding capacity.
418    #[error("resource budget reserve requires capacity")]
419    ReserveWithoutBudgetCapacity {
420        /// The affected budget dimension.
421        dimension: BudgetDimension,
422    },
423    /// A child limit exceeds its parent limit.
424    #[error("resource budget child limit exceeds parent")]
425    ChildBudgetExceedsParent {
426        /// The affected budget dimension.
427        dimension: BudgetDimension,
428    },
429    /// A child maximum age exceeds its parent maximum age.
430    #[error("resource budget child maximum age exceeds parent")]
431    ChildBudgetMaxAgeExceedsParent,
432    /// A permit was asked to move across unrelated budget trees.
433    #[error("resource permit target belongs to a different budget tree")]
434    PermitTargetInDifferentTree,
435    /// A memory limit input is not a positive finite value.
436    #[error("process memory limit violates {policy}")]
437    InvalidMemoryLimit {
438        /// The closed memory-limit policy identifier.
439        policy: RuntimeContractPolicy,
440    },
441    /// A scheduler definition is invalid.
442    #[error("schedule definition violates {policy}")]
443    InvalidSchedule {
444        /// The closed schedule policy identifier.
445        policy: RuntimeContractPolicy,
446    },
447    /// Metadata actor configuration is invalid.
448    #[error("metadata I/O configuration violates {policy}")]
449    InvalidMetadataConfiguration {
450        /// The closed metadata policy identifier.
451        policy: RuntimeContractPolicy,
452    },
453}
454
455impl RuntimeContractViolation {
456    /// Returns a stable contract condition for this deterministic violation.
457    #[must_use]
458    pub const fn condition(&self) -> CanonicalCondition {
459        CanonicalCondition::InvalidArgument
460    }
461}
462
463/// Why a runtime operation failed, for callers that branch on the reason.
464///
465/// [`RuntimeError::operation`] names what was attempted; the kind says why it
466/// failed. Several kinds share one catalog descriptor: a closed or poisoned
467/// owner and a missing runtime context all report
468/// `runtime.context.unavailable`, so only the kind tells them apart.
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
470#[non_exhaustive]
471pub enum RuntimeErrorKind {
472    /// The owner, operation, lane or worker no longer admits work: it is
473    /// shutting down, closed its admission, was cancelled or passed its
474    /// deadline.
475    Closed,
476    /// The owning task group was poisoned by a task panic and rejects new work.
477    Poisoned,
478    /// A timeout or deadline elapsed while waiting.
479    TimedOut,
480    /// A bounded queue, budget or capacity is full.
481    CapacityExhausted,
482    /// The request is not supported, for example a schedule whose policy
483    /// contradicts its configuration.
484    Unsupported,
485    /// A required context is missing or in the wrong state, for example no
486    /// current Tokio runtime.
487    ContextUnavailable,
488    /// The configuration is invalid or could not be loaded.
489    Configuration,
490    /// An I/O operation failed, including building the Tokio runtime.
491    Io,
492    /// An unexpected internal failure, including a failed task join.
493    Internal,
494}
495
496/// Stable, catalog-backed operational runtime failure.
497///
498/// The facade exposes descriptor identity, a matchable [`RuntimeErrorKind`],
499/// closed operation/component labels, a bounded context and an optional
500/// typed source. It intentionally does not expose implementation variants.
501#[derive(Clone)]
502pub struct RuntimeError {
503    error: SharedError,
504    kind: RuntimeErrorKind,
505    operation: RuntimeOperation,
506    component: &'static str,
507}
508
509impl RuntimeError {
510    /// Creates a source-free operational failure.
511    #[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    /// Creates an operational failure while retaining its typed cause.
529    #[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    /// Reports `source` as a failure of `operation`.
548    ///
549    /// The result keeps the descriptor, kind and component of `source` and
550    /// retains it as the typed cause.
551    #[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    /// Returns the catalog descriptor.
558    #[must_use]
559    pub fn descriptor(&self) -> &'static ErrorDescriptor {
560        self.error.descriptor()
561    }
562
563    /// Returns the stable catalog code.
564    #[must_use]
565    pub fn code(&self) -> ErrorCode {
566        self.error.code()
567    }
568
569    /// Returns the descriptor-owned canonical condition.
570    #[must_use]
571    pub fn condition(&self) -> CanonicalCondition {
572        self.error.condition()
573    }
574
575    /// Returns the descriptor-owned recovery hint.
576    #[must_use]
577    pub fn recovery_hint(&self) -> RecoveryHint {
578        self.error.recovery_hint()
579    }
580
581    /// Returns why the operation failed.
582    #[must_use]
583    pub const fn kind(&self) -> RuntimeErrorKind {
584        self.kind
585    }
586
587    /// Returns the closed operation label.
588    #[must_use]
589    pub const fn operation(&self) -> RuntimeOperation {
590        self.operation
591    }
592
593    /// Returns the closed runtime component label.
594    #[must_use]
595    pub fn component(&self) -> &str {
596        self.component
597    }
598
599    /// Returns bounded descriptor context.
600    #[must_use]
601    pub fn context(&self) -> &ErrorContext {
602        self.error.context()
603    }
604
605    /// Returns the first-promotion caller location.
606    #[must_use]
607    pub fn location(&self) -> &'static Location<'static> {
608        self.error.location()
609    }
610
611    /// Returns the catalog-controlled captured backtrace, when enabled.
612    #[must_use]
613    pub fn backtrace(&self) -> Option<&Backtrace> {
614        self.error.backtrace()
615    }
616
617    /// Creates a safe public projection when the context matches the descriptor.
618    ///
619    /// # Errors
620    ///
621    /// Returns an error only if an internal descriptor/context invariant has
622    /// been violated.
623    pub fn public_view(&self) -> Result<PublicErrorView<'_>, rocketmq_error::ViewContextViolation> {
624        self.error.public_view()
625    }
626
627    /// Creates a controlled diagnostic projection when the context matches the descriptor.
628    ///
629    /// # Errors
630    ///
631    /// Returns an error only if an internal descriptor/context invariant has
632    /// been violated.
633    pub fn diagnostic_view(&self) -> Result<DiagnosticView<'_>, rocketmq_error::ViewContextViolation> {
634        self.error.diagnostic_view()
635    }
636
637    /// Creates an external configuration failure.
638    #[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    /// Creates a configuration-loading failure while retaining its typed source.
650    #[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    /// Creates a runtime-build failure while retaining the build source.
663    #[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    /// Creates an I/O failure while retaining the I/O source.
676    #[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    /// Creates a runtime-context-unavailable failure.
689    #[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    /// Creates a failure for an owner, operation or worker that no longer
701    /// admits work.
702    ///
703    /// It shares the `runtime.context.unavailable` descriptor with
704    /// [`Self::context_unavailable`]; [`Self::kind`] reports
705    /// [`RuntimeErrorKind::Closed`].
706    #[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    /// Creates a failure for a task group poisoned by a task panic.
718    #[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    /// Creates an operational capacity-exhausted failure.
730    #[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    /// Creates an operation timeout failure.
742    #[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    /// Creates an unsupported-operation failure.
754    #[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    /// Creates a task join failure while retaining the join source.
766    #[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    /// Creates an internal runtime failure while retaining a typed source.
779    #[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    /// Creates a source-free internal runtime failure.
792    #[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}