Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
Expand All @@ -45,9 +42,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
Expand All @@ -61,9 +55,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@1.85
- uses: Swatinem/rust-cache@v2
with:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
Expand Down
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = ["crates/aerro", "crates/aerro-macros"]
# (Reserved for workspace-wide clippy overrides; currently none.)

[workspace.package]
version = "0.8.0"
version = "0.9.0"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
Expand All @@ -15,9 +15,7 @@ homepage = "https://github.com/ae2rs/aerro"
documentation = "https://docs.rs/aerro"

[workspace.dependencies]
tonic = { version = "0.13", default-features = false, features = ["codegen", "prost", "transport"] }
prost = "0.13"
tonic-build = { version = "0.13", default-features = false, features = ["prost"] }
tonic = { version = "0.13", default-features = false, features = ["codegen", "transport"] }
bincode = { version = "2.0", default-features = false, features = ["std", "derive"] }
smallvec = { version = "1.13", features = ["const_generics", "union"] }
thiserror = "2"
Expand Down
8 changes: 2 additions & 6 deletions crates/aerro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "aerro"
description = "Cross-service gRPC error framework for Rust."
keywords = ["grpc", "tonic", "prost", "error", "trace"]
keywords = ["grpc", "tonic", "bincode", "error", "trace"]
categories = ["network-programming", "rust-patterns"]
readme = "../../README.md"
version.workspace = true
Expand All @@ -18,9 +18,8 @@ macro = ["dep:aerro-macros"]
tracing = ["dep:tracing", "dep:tracing-opentelemetry", "dep:opentelemetry"]

[dependencies]
aerro-macros = { path = "../aerro-macros", version = "0.8.0", optional = true }
aerro-macros = { path = "../aerro-macros", version = "0.9.0", optional = true }
tonic = { workspace = true }
prost = { workspace = true }
bytes = { workspace = true }
bincode = { workspace = true }
smallvec = { workspace = true }
Expand All @@ -32,9 +31,6 @@ opentelemetry = { workspace = true, optional = true }
[lints]
workspace = true

[build-dependencies]
tonic-build = { workspace = true }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
dhat = { workspace = true }
Expand Down
12 changes: 6 additions & 6 deletions crates/aerro/benches/error_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn bench_encode(c: &mut Criterion) {
let mut group = c.benchmark_group("encode");
let opts = EncodeOptions::default();

group.bench_function("aerro_prost", |b| {
group.bench_function("aerro_bincode", |b| {
b.iter(|| {
let v = Bench::Item {
x: 42,
Expand All @@ -43,18 +43,18 @@ fn bench_encode(c: &mut Criterion) {
fn bench_decode(c: &mut Criterion) {
let mut group = c.benchmark_group("decode");
let opts = EncodeOptions::default();
let prost_status = Bench::Item {
let encoded_status = Bench::Item {
x: 42,
y: "hello-world".into(),
}
.into_status(&opts);

group.bench_function("aerro_prost", |b| {
group.bench_function("aerro_bincode", |b| {
b.iter(|| {
let st = tonic::Status::with_details(
prost_status.code(),
prost_status.message(),
bytes::Bytes::copy_from_slice(prost_status.details()),
encoded_status.code(),
encoded_status.message(),
bytes::Bytes::copy_from_slice(encoded_status.details()),
);
black_box(st.into_aerro::<Bench>().unwrap());
});
Expand Down
8 changes: 0 additions & 8 deletions crates/aerro/build.rs

This file was deleted.

29 changes: 0 additions & 29 deletions crates/aerro/proto/aerro.v1.proto

This file was deleted.

4 changes: 2 additions & 2 deletions crates/aerro/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ pub enum DecodeError {
UnknownTypeId(String),
#[error("bincode decode: {0}")]
Payload(String),
#[error("prost decode: {0}")]
Prost(String),
#[error("wire decode: {0}")]
Wire(String),
}

#[derive(Debug, thiserror::Error)]
Expand Down
39 changes: 18 additions & 21 deletions crates/aerro/src/wire/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
//! [`encode`](crate::wire::encode::encode).

use bytes::Bytes;
use prost::Message;
use smallvec::SmallVec;
use tonic::{Code, Status};

use crate::{Aerro, Category, Frame, RemoteError, ServiceFailure, trace::TraceContext};

use super::envelope::from_proto;
use super::raw;
use super::envelope::{ENVELOPE_VERSION, WireEnvelope, WireFrame};

/// Decode a `tonic::Status` into a [`ServiceFailure<E>`](crate::ServiceFailure).
///
Expand All @@ -21,10 +19,15 @@ pub fn decode<E: Aerro>(status: Status) -> Result<ServiceFailure<E>, RemoteError
if details.is_empty() {
return Err(transport_remote_error(&status));
}
let env = match raw::Envelope::decode(details) {
Ok(e) => e,
Err(_) => return Err(transport_remote_error(&status)),
};
let (env, _): (WireEnvelope, _) =
match bincode::decode_from_slice(details, bincode::config::standard()) {
Ok(pair) => pair,
Err(_) => return Err(transport_remote_error(&status)),
};

if env.version != ENVELOPE_VERSION {
return Err(transport_remote_error(&status));
}

if !E::TYPE_IDS.contains(&env.type_id.as_str()) {
return Err(into_remote_error(env, &status));
Expand All @@ -39,9 +42,8 @@ pub fn decode<E: Aerro>(status: Status) -> Result<ServiceFailure<E>, RemoteError
Ok(ServiceFailure::from_parts(inner, frames, trace))
}

fn into_remote_error(env: raw::Envelope, status: &Status) -> RemoteError {
let category =
from_proto(raw::Category::try_from(env.category).unwrap_or(raw::Category::System));
fn into_remote_error(env: WireEnvelope, status: &Status) -> RemoteError {
let category = Category::try_from(env.category).unwrap_or(Category::System);
let trace = decode_trace(&env.trace_id, &env.span_id);
let frames = decode_frames(&env.frames);
RemoteError::from_parts(crate::remote::RemoteErrorInner {
Expand All @@ -51,7 +53,7 @@ fn into_remote_error(env: raw::Envelope, status: &Status) -> RemoteError {
trace,
outer_code: status.code(),
outer_message: status.message().to_string(),
payload_bytes: Bytes::from(env.payload.to_vec()),
payload_bytes: Bytes::from(env.payload),
})
}

Expand All @@ -67,10 +69,10 @@ fn transport_remote_error(status: &Status) -> RemoteError {
})
}

fn decode_frames(frames: &[raw::Frame]) -> SmallVec<[Frame; 4]> {
fn decode_frames(frames: &[WireFrame]) -> SmallVec<[Frame; 4]> {
let mut out = SmallVec::new();
for f in frames {
let cat = from_proto(raw::Category::try_from(f.category).unwrap_or(raw::Category::System));
let cat = Category::try_from(f.category).unwrap_or(Category::System);
let code = code_from_u32(f.code);
let loc = if f.location.is_empty() {
None
Expand All @@ -90,18 +92,13 @@ fn decode_frames(frames: &[raw::Frame]) -> SmallVec<[Frame; 4]> {
}

fn code_from_u32(c: u32) -> Code {
// tonic::Code is `#[repr(i32)]` over the gRPC code numbers 0..=16.
Code::from(c as i32)
}

fn decode_trace(trace_id: &[u8], span_id: &[u8]) -> TraceContext {
fn decode_trace(trace_id: &[u8; 16], span_id: &[u8; 8]) -> TraceContext {
let mut t = TraceContext::default();
if trace_id.len() == 16 {
t.trace_id.copy_from_slice(trace_id);
}
if span_id.len() == 8 {
t.span_id.copy_from_slice(span_id);
}
t.trace_id.copy_from_slice(trace_id);
t.span_id.copy_from_slice(span_id);
t
}

Expand Down
41 changes: 20 additions & 21 deletions crates/aerro/src/wire/encode.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
//! `ServiceFailure<E>` → `tonic::Status` encoding — see spec §6, §9.

use prost::Message;
use tonic::{Code, Status};

use crate::{Aerro, Category, Exposure, Frame, ServiceFailure};

use super::envelope::{ENVELOPE_VERSION, to_proto};
use super::raw;
use super::envelope::{ENVELOPE_VERSION, WireEnvelope, WireFrame};

/// Encoder configuration.
#[derive(Debug, Copy, Clone)]
Expand All @@ -29,11 +27,8 @@ impl Default for EncodeOptions {
}

/// Encode a typed failure into a `tonic::Status` whose `details()` carries
/// the prost-encoded aerro envelope.
/// the bincode-encoded aerro envelope.
pub fn encode<E: Aerro>(sf: &ServiceFailure<E>, opts: &EncodeOptions) -> Status {
// Stripping is keyed on the *route* exposure (the operator's contract with
// its callers). The variant's declared exposure is informational in v1 — the
// route is what governs what leaves the process.
let route = opts.exposure;

let outer_code = sf.inner().code();
Expand All @@ -50,16 +45,17 @@ pub fn encode<E: Aerro>(sf: &ServiceFailure<E>, opts: &EncodeOptions) -> Status
elide_to_cap(sf.frames(), opts.max_frames)
};

let env = raw::Envelope {
category: to_proto(sf.inner().category()) as i32,
let trace = sf.trace();
let env = WireEnvelope {
version: ENVELOPE_VERSION,
category: u8::from(sf.inner().category()),
type_id: Aerro::type_id(sf.inner()).to_string(),
trace_id: sf.trace().trace_id.to_vec(),
span_id: sf.trace().span_id.to_vec(),
trace_id: trace.trace_id,
span_id: trace.span_id,
frames: wire_frames,
payload,
version: ENVELOPE_VERSION,
};
let bytes = env.encode_to_vec();
let bytes = bincode::encode_to_vec(&env, bincode::config::standard()).expect("bincode encode");

Status::with_details(outer_code, outer_msg, bytes.into())
}
Expand All @@ -72,7 +68,7 @@ fn redact_message<E: Aerro>(inner: &E, route: Exposure) -> String {
}
}

fn elide_to_cap(frames: &[Frame], cap: u8) -> Vec<raw::Frame> {
fn elide_to_cap(frames: &[Frame], cap: u8) -> Vec<WireFrame> {
let cap = cap.max(1) as usize;
if frames.len() <= cap {
return frames.iter().map(to_wire_frame).collect();
Expand All @@ -82,22 +78,22 @@ fn elide_to_cap(frames: &[Frame], cap: u8) -> Vec<raw::Frame> {
let n_elided = frames.len() - keep_front - keep_back;
let mut out = Vec::with_capacity(cap);
out.extend(frames[..keep_front].iter().map(to_wire_frame));
out.push(raw::Frame {
out.push(WireFrame {
service: "...".into(),
rpc: "elided".into(),
code: 0,
message: format!("{} frames elided", n_elided),
location: String::new(),
category: to_proto(frames[keep_front].category) as i32,
category: u8::from(frames[keep_front].category),
});
if keep_back > 0 {
out.extend(frames[frames.len() - keep_back..].iter().map(to_wire_frame));
}
out
}

fn to_wire_frame(f: &Frame) -> raw::Frame {
raw::Frame {
fn to_wire_frame(f: &Frame) -> WireFrame {
WireFrame {
service: f.service.to_string(),
rpc: f.rpc.to_string(),
code: f.code as i32 as u32,
Expand All @@ -106,14 +102,15 @@ fn to_wire_frame(f: &Frame) -> raw::Frame {
.location
.map(|l| format!("{}:{}", l.file(), l.line()))
.unwrap_or_default(),
category: to_proto(f.category) as i32,
category: u8::from(f.category),
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::Boom;
use crate::wire::envelope::WireEnvelope;
use tonic::Code;

#[test]
Expand Down Expand Up @@ -157,7 +154,8 @@ mod tests {
max_frames: 8,
},
);
let env = raw::Envelope::decode(st.details()).unwrap();
let (env, _): (WireEnvelope, _) =
bincode::decode_from_slice(st.details(), bincode::config::standard()).unwrap();
assert_eq!(env.frames.len(), 8);
assert!(env.frames.iter().any(|f| f.rpc == "elided"));
}
Expand All @@ -179,7 +177,8 @@ mod tests {
max_frames: 16,
},
);
let env = raw::Envelope::decode(st.details()).unwrap();
let (env, _): (WireEnvelope, _) =
bincode::decode_from_slice(st.details(), bincode::config::standard()).unwrap();
assert!(env.frames.is_empty());
}
}
Loading
Loading