refactor(errors): adopt reportable error terminology
This commit is contained in:
1 parent
21e9497915
commit
9fec2fa095
92 files changed
+270
-269
No files matched your search
@@ -0,0 +1,97 @@
|
||||
//! Fixed, allocation-free reportable-error ingress.
|
||||
|
||||
use core::{
|
||||
cell::UnsafeCell,
|
||||
mem::MaybeUninit,
|
||||
sync::atomic::{AtomicU8, Ordering},
|
||||
};
|
||||
|
||||
use crate::{ErrorId, ErrorRecord};
|
||||
|
||||
static GLOBAL_INGRESS: ErrorIngress = ErrorIngress::new();
|
||||
|
||||
const EMPTY: u8 = 0;
|
||||
const WRITING: u8 = 1;
|
||||
const READY: u8 = 2;
|
||||
const READING: u8 = 3;
|
||||
|
||||
struct ErrorIngress {
|
||||
state: AtomicU8,
|
||||
pending: UnsafeCell<MaybeUninit<ErrorRecord>>,
|
||||
}
|
||||
|
||||
// SAFETY: `state` gives exactly one producer or consumer exclusive access to
|
||||
// `pending`; neither side waits for a preempted owner.
|
||||
unsafe impl Sync for ErrorIngress {}
|
||||
|
||||
impl ErrorIngress {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
state: AtomicU8::new(EMPTY),
|
||||
pending: UnsafeCell::new(MaybeUninit::uninit()),
|
||||
}
|
||||
}
|
||||
|
||||
fn publish(&self, id: ErrorId, raised_at: &'static str) {
|
||||
if self
|
||||
.state
|
||||
.compare_exchange(EMPTY, WRITING, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
// SAFETY: the successful state transition grants this producer sole
|
||||
// access until it publishes READY with release ordering.
|
||||
unsafe {
|
||||
(*self.pending.get()).write(ErrorRecord::new(id, raised_at));
|
||||
}
|
||||
self.state.store(READY, Ordering::Release);
|
||||
}
|
||||
|
||||
fn take(&self) -> Option<ErrorRecord> {
|
||||
if self
|
||||
.state
|
||||
.compare_exchange(READY, READING, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// SAFETY: READY proves that the sole producer initialized the record;
|
||||
// READING grants this consumer exclusive access until EMPTY.
|
||||
let record = unsafe { (*self.pending.get()).assume_init_read() };
|
||||
self.state.store(EMPTY, Ordering::Release);
|
||||
Some(record)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn publish(id: ErrorId, raised_at: &'static str) {
|
||||
GLOBAL_INGRESS.publish(id, raised_at);
|
||||
}
|
||||
|
||||
/// Takes the pending reportable firmware error, if one exists.
|
||||
pub fn take() -> Option<ErrorRecord> {
|
||||
GLOBAL_INGRESS.take()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ErrorIngress;
|
||||
use crate::ErrorId;
|
||||
|
||||
#[test]
|
||||
fn retains_the_first_id_and_location_until_consumed() {
|
||||
let ingress = ErrorIngress::new();
|
||||
ingress.publish(ErrorId::new(65402), "first.rs:10");
|
||||
ingress.publish(ErrorId::new(65403), "second.rs:20");
|
||||
|
||||
let first = ingress.take().expect("first error retained");
|
||||
assert_eq!(first.id(), 65402);
|
||||
assert_eq!(first.raised_at(), "first.rs:10");
|
||||
assert!(ingress.take().is_none());
|
||||
|
||||
ingress.publish(ErrorId::new(65403), "second.rs:20");
|
||||
let second = ingress.take().expect("slot reusable after consumption");
|
||||
assert_eq!(second.id(), 65403);
|
||||
assert_eq!(second.raised_at(), "second.rs:20");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Stable reportable firmware errors with bounded diagnostic ingress.
|
||||
//!
|
||||
//! This crate owns error identity and ingress. It contains no logging levels,
|
||||
//! filtering, MAVLink, board, filesystem, or clock dependency.
|
||||
//!
|
||||
//! `#[propagate]` turns an annotated function into a reporting point. When a
|
||||
//! reportable error leaves that function, the macro makes one reporting
|
||||
//! attempt. Nested annotated callers would therefore attempt to report the
|
||||
//! same error again. The bounded ingress retaining only one pending error is
|
||||
//! backpressure, not semantic duplicate suppression.
|
||||
|
||||
#![no_std]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate std;
|
||||
|
||||
mod ingress;
|
||||
mod record;
|
||||
mod sink;
|
||||
|
||||
pub use ingress::take;
|
||||
pub use kaptan_err_reportable_macros::{KaptanError, propagate};
|
||||
pub use record::{ErrorId, ErrorRecord, ReportableError};
|
||||
pub use sink::ErrorSink;
|
||||
|
||||
/// Implementation details used only by macro expansions.
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
use crate::ReportableError;
|
||||
|
||||
/// Reports an error only when it is the final result leaving an annotated
|
||||
/// function. Borrowing preserves the exact result returned to the caller.
|
||||
#[inline]
|
||||
pub fn report_result<T, E: ReportableError>(
|
||||
result: &core::result::Result<T, E>,
|
||||
raised_at: &'static str,
|
||||
) {
|
||||
if let Err(error) = result {
|
||||
crate::ingress::publish(error.error_id(), raised_at);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Stable error IDs and bounded pending records.
|
||||
|
||||
/// Stable nonzero identity for one reportable firmware error variant.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[repr(transparent)]
|
||||
pub struct ErrorId(u16);
|
||||
|
||||
impl ErrorId {
|
||||
/// Creates one explicitly assigned error identity.
|
||||
pub const fn new(id: u16) -> Self {
|
||||
assert!(id != 0, "error ID zero is reserved");
|
||||
Self(id)
|
||||
}
|
||||
|
||||
/// Returns the compact wire representation.
|
||||
pub const fn get(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Implemented by [`crate::KaptanError`] derives.
|
||||
///
|
||||
/// Obtaining an ID never publishes, allocates, or changes safety state.
|
||||
pub trait ReportableError {
|
||||
/// Returns the stable ID assigned to this error variant.
|
||||
fn error_id(&self) -> ErrorId;
|
||||
}
|
||||
|
||||
impl ReportableError for ErrorId {
|
||||
fn error_id(&self) -> ErrorId {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
/// One reported firmware error and its reporting point.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ErrorRecord {
|
||||
id: ErrorId,
|
||||
raised_at: &'static str,
|
||||
}
|
||||
|
||||
impl ErrorRecord {
|
||||
pub(crate) const fn new(id: ErrorId, raised_at: &'static str) -> Self {
|
||||
Self { id, raised_at }
|
||||
}
|
||||
|
||||
/// Stable machine-readable identity for the error type and variant.
|
||||
pub const fn id(self) -> u16 {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
/// Compile-time `file:line` of the reporting point.
|
||||
pub const fn raised_at(self) -> &'static str {
|
||||
self.raised_at
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ErrorId, ErrorRecord};
|
||||
|
||||
#[test]
|
||||
fn record_keeps_only_the_explicit_id_and_reporting_point() {
|
||||
let record = ErrorRecord::new(ErrorId::new(65004), "module.rs:42");
|
||||
assert_eq!(record.id(), 65004);
|
||||
assert_eq!(record.raised_at(), "module.rs:42");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! One-slot retry ownership for a reportable-error sink.
|
||||
|
||||
use crate::ErrorRecord;
|
||||
|
||||
/// Retains one reportable error until a lower sink accepts it.
|
||||
///
|
||||
/// Reportable errors are never filtered. A composition may give them priority over
|
||||
/// best-effort logs without coupling the producer to its transport.
|
||||
pub struct ErrorSink {
|
||||
pending: Option<ErrorRecord>,
|
||||
}
|
||||
|
||||
impl ErrorSink {
|
||||
/// Creates an empty sink.
|
||||
pub const fn new() -> Self {
|
||||
Self { pending: None }
|
||||
}
|
||||
|
||||
/// Whether this sink can accept a record from composition-level fanout.
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.pending.is_none()
|
||||
}
|
||||
|
||||
/// Retains one record, returning it unchanged when this sink is occupied.
|
||||
pub fn publish_record(&mut self, record: ErrorRecord) -> Result<(), ErrorRecord> {
|
||||
if self.pending.is_some() {
|
||||
return Err(record);
|
||||
}
|
||||
self.pending = Some(record);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts the pending record and clears it only when `submit` accepts it.
|
||||
///
|
||||
/// Returns whether a record was attempted, independently of admission.
|
||||
pub fn submit_next(&mut self, submit: impl FnOnce(&ErrorRecord) -> bool) -> bool {
|
||||
let Some(record) = self.pending else {
|
||||
return false;
|
||||
};
|
||||
if submit(&record) {
|
||||
self.pending = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ErrorSink {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ErrorSink;
|
||||
use crate::{ErrorId, ErrorRecord};
|
||||
|
||||
#[test]
|
||||
fn retains_one_error_until_the_sink_accepts_it() {
|
||||
while crate::take().is_some() {}
|
||||
crate::ingress::publish(ErrorId::new(65401), "test.rs:10");
|
||||
|
||||
let mut sink = ErrorSink::new();
|
||||
let record = crate::take().expect("published error");
|
||||
sink.publish_record(record).unwrap();
|
||||
assert!(!sink.is_empty());
|
||||
assert!(sink.submit_next(|record| {
|
||||
assert_eq!(*record, ErrorRecord::new(ErrorId::new(65401), "test.rs:10"));
|
||||
false
|
||||
}));
|
||||
assert!(sink.submit_next(|record| {
|
||||
assert_eq!(record.id(), 65401);
|
||||
assert_eq!(record.raised_at(), "test.rs:10");
|
||||
true
|
||||
}));
|
||||
assert!(sink.is_empty());
|
||||
assert!(!sink.submit_next(|_| true));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user