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}