Skip to main content

frame_core/
error.rs

1//! Typed registration and lifecycle failures.
2
3use beamr::scheduler::MailboxSendError;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::capability::{Capability, CapabilityRequest, ScopeValidationError};
8use crate::component::ComponentId;
9use crate::event::LifecycleState;
10
11/// Stable classification of a beamr process tombstone.
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
13pub enum TombstoneKind {
14    /// Process completed normally.
15    Normal,
16    /// Host requested an untrappable kill.
17    Kill,
18    /// Process observed a kill and terminated.
19    Killed,
20    /// Runtime execution failed.
21    Error,
22    /// A linked distributed node disconnected.
23    NoConnection,
24    /// A link targeted a process that was already absent.
25    NoProcess,
26}
27
28/// Observable reason a component entered [`LifecycleState::Failed`].
29#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
30pub struct FailureReason {
31    /// Child associated with the failure, if a child had been identified.
32    pub child: Option<String>,
33    /// Tombstone observed for the failing process, when one existed.
34    pub tombstone: Option<TombstoneKind>,
35    /// Runtime detail; rich exception information is intentionally optional.
36    pub detail: String,
37}
38
39/// A typed refusal or runtime failure from the component registry.
40#[derive(Debug, Error)]
41pub enum RegistryError {
42    /// Registration attempted to reuse a stable identity.
43    #[error("component {id} is already registered")]
44    DuplicateComponent {
45        /// Conflicting stable identity.
46        id: ComponentId,
47    },
48    /// A declared dependency was not in the registration table.
49    #[error("component {component} requires missing component {required}")]
50    MissingDependency {
51        /// Component being registered.
52        component: ComponentId,
53        /// Missing edge target.
54        required: ComponentId,
55    },
56    /// A dependency edge would make the graph cyclic.
57    #[error("dependency edge {component} -> {required} creates a cycle")]
58    CyclicDependency {
59        /// Source of the cyclic edge.
60        component: ComponentId,
61        /// Target of the cyclic edge.
62        required: ComponentId,
63    },
64    /// Two registered components claimed the same capability identifier.
65    #[error("capability {capability} is already provided by component {provider}")]
66    CapabilityConflict {
67        /// Colliding service capability identifier.
68        capability: String,
69        /// Existing provider.
70        provider: ComponentId,
71    },
72    /// A host-authority declaration used a malformed typed scope.
73    #[error("component {component} declares invalid capability need {request:?}: {reason}")]
74    InvalidCapabilityScope {
75        /// Component being registered.
76        component: ComponentId,
77        /// Offending declaration.
78        request: CapabilityRequest,
79        /// Exact scope validation refusal.
80        reason: ScopeValidationError,
81    },
82    /// An action declaration failed field validation (empty id, description,
83    /// or schema source).
84    #[error("component {component} action '{action}' declaration is invalid: {detail}")]
85    InvalidActionDeclaration {
86        /// Component being registered.
87        component: ComponentId,
88        /// Offending action id text (possibly empty — the offense itself).
89        action: String,
90        /// Exact field refusal.
91        detail: String,
92    },
93    /// A component declared the same local action id twice.
94    #[error("component {component} declares action id '{action}' more than once")]
95    DuplicateActionId {
96        /// Component being registered.
97        component: ComponentId,
98        /// Duplicate local id.
99        action: String,
100    },
101    /// One action listed an exact capability requirement twice.
102    #[error(
103        "component {component} action '{action}' lists requirement {requirement:?} more than once"
104    )]
105    DuplicateActionRequirement {
106        /// Component being registered.
107        component: ComponentId,
108        /// Action with the duplicate requirement.
109        action: String,
110        /// Duplicate requirement.
111        requirement: Capability,
112    },
113    /// An action requires a capability its component never declared as a
114    /// need — a permanently dead tool, refused at registration (F-6a R1).
115    #[error(
116        "component {component} action '{action}' requires {requirement:?} which is not among its declared needs"
117    )]
118    ActionRequirementUndeclared {
119        /// Component being registered.
120        component: ComponentId,
121        /// Action naming the undeclared requirement.
122        action: String,
123        /// The requirement no declared need covers.
124        requirement: Capability,
125    },
126    /// A fragment declaration failed field validation (empty id or kind).
127    #[error("component {component} fragment '{fragment}' declaration is invalid: {detail}")]
128    InvalidFragmentDeclaration {
129        /// Component being registered.
130        component: ComponentId,
131        /// Offending fragment id text (possibly empty — the offense itself).
132        fragment: String,
133        /// Exact field refusal.
134        detail: String,
135    },
136    /// A component declared the same local fragment id twice (F-5a R1's
137    /// typed refusal; the whole batch is refused atomically).
138    #[error("component {component} declares fragment id '{fragment}' more than once")]
139    DuplicateFragmentId {
140        /// Component being registered.
141        component: ComponentId,
142        /// Duplicate local id.
143        fragment: String,
144    },
145    /// Fragment content exceeded the registry's caller-supplied byte limit
146    /// (registration and content update validate identically — F-5a R2).
147    #[error(
148        "component {component} fragment '{fragment}' content is {size} bytes, over the configured max_fragment_bytes limit of {limit}"
149    )]
150    FragmentContentOversize {
151        /// Declaring or updating component.
152        component: ComponentId,
153        /// Offending fragment id.
154        fragment: String,
155        /// Offered content size in bytes.
156        size: usize,
157        /// The caller-supplied limit in force.
158        limit: usize,
159    },
160    /// Fragments were declared but the embedding host supplied no
161    /// `max_fragment_bytes` — there is no hidden default limit (F-5a R1);
162    /// the host states one in its lifecycle configuration or fragments are
163    /// refused loudly.
164    #[error(
165        "component {component} declares fragments but the registry's lifecycle configuration carries no max_fragment_bytes limit"
166    )]
167    FragmentLimitUnconfigured {
168        /// Component being registered.
169        component: ComponentId,
170    },
171    /// A fragment act named a fragment id with no live entry in the
172    /// authority — an undeclared local id (the ID set is static per
173    /// incarnation, F-5a R2), or an internal invariant breach surfaced
174    /// loudly rather than papered over.
175    #[error("component {component} carries no live fragment '{fragment}'")]
176    UnknownFragment {
177        /// Component named by the act.
178        component: ComponentId,
179        /// The fragment id with no live entry.
180        fragment: String,
181    },
182    /// A fragment act carried an incarnation that is not the live Running
183    /// incarnation — the typed held-vs-current refusal (F-2a's rule applied
184    /// to fragments, F-5a R2). `current` is absent when no incarnation is
185    /// live (the component is not Running).
186    #[error(
187        "component {component} fragment '{fragment}' act holds incarnation {held:?} but the live incarnation is {current:?}"
188    )]
189    FragmentIncarnationStale {
190        /// Component named by the act.
191        component: ComponentId,
192        /// Fragment named by the act.
193        fragment: String,
194        /// The incarnation the caller holds.
195        held: crate::fragment::ComponentIncarnation,
196        /// The live Running incarnation, absent when not Running.
197        current: Option<crate::fragment::ComponentIncarnation>,
198    },
199    /// A manifest repeated an exact kind-and-scope pair.
200    #[error("component {component} declares capability need {request:?} more than once")]
201    DuplicateCapabilityNeed {
202        /// Component being registered.
203        component: ComponentId,
204        /// Duplicate declaration.
205        request: CapabilityRequest,
206    },
207    /// Registration omitted the mandatory restart intensity declaration.
208    #[error("component {id} has no declared supervision intensity")]
209    UndeclaredSupervision {
210        /// Component missing the declaration.
211        id: ComponentId,
212    },
213    /// A restart window of zero cannot define an intensity interval.
214    #[error("component {id} declares a zero-length supervision window")]
215    InvalidSupervisionWindow {
216        /// Component with the invalid policy.
217        id: ComponentId,
218    },
219    /// Child names are component-local unique keys.
220    #[error("component {id} declares child name {child} more than once")]
221    DuplicateChild {
222        /// Component with the duplicate declaration.
223        id: ComponentId,
224        /// Duplicate child key.
225        child: String,
226    },
227    /// The requested component is not registered.
228    #[error("component {id} is not registered")]
229    NotRegistered {
230        /// Requested identity.
231        id: ComponentId,
232    },
233    /// A start is already in flight for this identity.
234    #[error("component {id} is already starting")]
235    AlreadyStarting {
236        /// Busy identity.
237        id: ComponentId,
238    },
239    /// The component is already running.
240    #[error("component {id} is already running")]
241    AlreadyRunning {
242        /// Running identity.
243        id: ComponentId,
244    },
245    /// A stop is already in flight for this identity.
246    #[error("component {id} is already stopping")]
247    AlreadyStopping {
248        /// Busy identity.
249        id: ComponentId,
250    },
251    /// A remove is already in flight for this identity.
252    #[error("component {id} is already being removed")]
253    AlreadyRemoving {
254        /// Busy identity.
255        id: ComponentId,
256    },
257    /// A bytecode upgrade is already in flight for this identity.
258    #[error("component {id} is already upgrading")]
259    AlreadyUpgrading {
260        /// Busy identity.
261        id: ComponentId,
262    },
263    /// A bytecode upgrade was requested outside the stopped states.
264    #[error(
265        "component {id} bytecode upgrade requires Registered or Stopped, found {state:?}: \
266         the upgrade barrier drains and stops the old incarnation before new bytes stage"
267    )]
268    UpgradeRequiresStopped {
269        /// Requested identity.
270        id: ComponentId,
271        /// The state that refused the upgrade.
272        state: LifecycleState,
273    },
274    /// A dependency exists but has not reached Running.
275    #[error("component {component} requires {required} to be Running, found {state:?}")]
276    DependencyNotRunning {
277        /// Component whose start was refused.
278        component: ComponentId,
279        /// Required component.
280        required: ComponentId,
281        /// Observed dependency state.
282        state: LifecycleState,
283    },
284    /// A running component still depends on the removal target.
285    #[error("running component {dependent} depends on {required}")]
286    RunningDependent {
287        /// Running dependent preventing removal.
288        dependent: ComponentId,
289        /// Requested removal target.
290        required: ComponentId,
291    },
292    /// Hot-loaded bytecode carries `erlang:*` imports the scheduler composition
293    /// left Deferred — dispatch would kill the process at first use.
294    #[error(
295        "component {id} module {module} defers built-in imports [{imports}]: the scheduler was \
296         composed without a populated BIF registry; compose through \
297         frame_core::composition::compose_scheduler, never bare Scheduler::with_services"
298    )]
299    DeferredBifImports {
300        /// Component whose start was refused.
301        id: ComponentId,
302        /// Module whose committed import table carries the deferrals.
303        module: String,
304        /// Deferred imports in `erlang:name/arity` form.
305        imports: String,
306    },
307    /// beamr refused or could not parse component bytecode.
308    #[error("failed to load bytecode for component {id}: {detail}")]
309    ModuleLoad {
310        /// Component being started.
311        id: ComponentId,
312        /// beamr loader detail.
313        detail: String,
314    },
315    /// A native supervisor or linked child could not be spawned.
316    #[error("failed to spawn {process} for component {id}: {detail}")]
317    Spawn {
318        /// Component being started or restarted.
319        id: ComponentId,
320        /// Supervisor or child name.
321        process: String,
322        /// beamr spawn detail.
323        detail: String,
324    },
325    /// A child did not answer its declared mailbox probe in time.
326    #[error("component {id} child {child} liveness probe timed out")]
327    LivenessTimeout {
328        /// Component being started or probed.
329        id: ComponentId,
330        /// Child that did not answer.
331        child: String,
332    },
333    /// Start failed after entering Starting; status remains Failed.
334    #[error("component {id} failed during start: {reason:?}")]
335    StartFailed {
336        /// Failed identity.
337        id: ComponentId,
338        /// Observable reason retained in status.
339        reason: FailureReason,
340    },
341    /// Ordered stop observed a non-normal or missing tombstone.
342    #[error("component {id} failed during ordered stop: {reason:?}")]
343    StopFailed {
344        /// Component being stopped.
345        id: ComponentId,
346        /// Exact observed failure.
347        reason: FailureReason,
348    },
349    /// A retained old module generation could not be safely purged.
350    #[error("failed to purge module {module} for component {id}: {detail}")]
351    ModulePurge {
352        /// Component being removed.
353        id: ComponentId,
354        /// Module display name.
355        module: String,
356        /// beamr purge detail.
357        detail: String,
358    },
359    /// The current module generation was absent when removal expected it.
360    #[error("module {module} for component {id} was not present during delete")]
361    ModuleDelete {
362        /// Component being removed.
363        id: ComponentId,
364        /// Module display name.
365        module: String,
366    },
367    /// Lookup still found code after deletion.
368    #[error("module {module} for component {id} remained visible after delete")]
369    ModuleStillLoaded {
370        /// Component being removed.
371        id: ComponentId,
372        /// Module display name.
373        module: String,
374    },
375    /// beamr refused a typed host command at supervisor mailbox admission.
376    #[error("component {id} supervisor {command} command delivery failed: {source}")]
377    CommandDelivery {
378        /// Affected component.
379        id: ComponentId,
380        /// Stable command kind whose delivery was attempted.
381        command: &'static str,
382        /// Exact beamr mailbox admission refusal.
383        #[source]
384        source: MailboxSendError,
385    },
386    /// Host/supervisor mailbox protocol was violated.
387    #[error("component {id} supervisor protocol failed: {detail}")]
388    SupervisorProtocol {
389        /// Affected component.
390        id: ComponentId,
391        /// Protocol detail.
392        detail: String,
393    },
394    /// Internal synchronization was poisoned by a panic.
395    #[error("component registry synchronization is poisoned")]
396    SynchronizationPoisoned,
397    /// A component monitor thread terminated without returning its result.
398    #[error("component {id} monitor thread terminated unexpectedly")]
399    MonitorTerminated {
400        /// Component whose monitor was lost.
401        id: ComponentId,
402    },
403}