fix: leaner error handling by boxing large variants

This commit is contained in:
Per Stark
2026-06-06 07:59:57 +02:00
parent 4e20da538d
commit ac0d34bfbd
12 changed files with 238 additions and 83 deletions
+56 -6
View File
@@ -9,7 +9,7 @@ use crate::storage::types::file_info::FileError;
#[derive(Error, Debug)]
pub enum EmbeddingError {
#[error("openai error: {0}")]
OpenAI(#[from] OpenAIError),
OpenAI(Box<OpenAIError>),
#[error("fastembed error: {0}")]
FastEmbed(String),
#[error("task join error: {0}")]
@@ -24,6 +24,12 @@ pub enum EmbeddingError {
UnknownModel(String),
}
impl From<OpenAIError> for EmbeddingError {
fn from(err: OpenAIError) -> Self {
Self::OpenAI(Box::new(err))
}
}
impl EmbeddingError {
pub(crate) fn fastembed(err: impl std::fmt::Display) -> Self {
Self::FastEmbed(err.to_string())
@@ -39,9 +45,9 @@ impl EmbeddingError {
#[derive(Error, Debug)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] surrealdb::Error),
Database(Box<surrealdb::Error>),
#[error("openai error: {0}")]
OpenAI(#[from] OpenAIError),
OpenAI(Box<OpenAIError>),
#[error("embedding error: {0}")]
Embedding(#[from] EmbeddingError),
#[error("file error: {0}")]
@@ -61,17 +67,47 @@ pub enum AppError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
Reqwest(Box<reqwest::Error>),
#[error("storage error: {0}")]
Storage(#[from] object_store::Error),
Storage(Box<object_store::Error>),
#[error("ingestion processing error: {0}")]
Processing(String),
#[error("dom smoothie error: {0}")]
DomSmoothie(#[from] dom_smoothie::ReadabilityError),
DomSmoothie(Box<dom_smoothie::ReadabilityError>),
#[error("internal service error: {0}")]
InternalError(String),
}
impl From<surrealdb::Error> for AppError {
fn from(err: surrealdb::Error) -> Self {
Self::Database(Box::new(err))
}
}
impl From<OpenAIError> for AppError {
fn from(err: OpenAIError) -> Self {
Self::OpenAI(Box::new(err))
}
}
impl From<reqwest::Error> for AppError {
fn from(err: reqwest::Error) -> Self {
Self::Reqwest(Box::new(err))
}
}
impl From<object_store::Error> for AppError {
fn from(err: object_store::Error) -> Self {
Self::Storage(Box::new(err))
}
}
impl From<dom_smoothie::ReadabilityError> for AppError {
fn from(err: dom_smoothie::ReadabilityError) -> Self {
Self::DomSmoothie(Box::new(err))
}
}
impl AppError {
/// Builds an [`AppError::InternalError`] from a displayable message.
#[must_use]
@@ -79,3 +115,17 @@ impl AppError {
Self::InternalError(msg.to_string())
}
}
#[cfg(test)]
mod tests {
use super::AppError;
#[test]
fn app_error_is_reasonably_sized() {
assert!(
std::mem::size_of::<AppError>() <= 64,
"AppError is {} bytes",
std::mem::size_of::<AppError>()
);
}
}
+18 -6
View File
@@ -36,7 +36,7 @@ pub enum FileError {
/// Database operation on the file record failed.
#[error("surrealdb error: {0}")]
SurrealError(#[from] surrealdb::Error),
SurrealError(Box<surrealdb::Error>),
/// Failed to persist the temporary file to its final location.
#[error("failed to persist file: {0}")]
@@ -48,7 +48,19 @@ pub enum FileError {
/// The underlying object store operation failed.
#[error("object store error: {0}")]
ObjectStore(#[from] ObjectStoreError),
ObjectStore(Box<ObjectStoreError>),
}
impl From<surrealdb::Error> for FileError {
fn from(err: surrealdb::Error) -> Self {
Self::SurrealError(Box::new(err))
}
}
impl From<ObjectStoreError> for FileError {
fn from(err: ObjectStoreError) -> Self {
Self::ObjectStore(Box::new(err))
}
}
stored_object!(FileInfo, "file", {
@@ -163,7 +175,7 @@ impl FileInfo {
match db_client.get_item::<FileInfo>(id).await {
Ok(Some(file_info)) => Ok(file_info),
Ok(None) => Err(FileError::FileNotFound(id.to_string())),
Err(e) => Err(FileError::SurrealError(e)),
Err(e) => Err(FileError::from(e)),
}
}
@@ -233,7 +245,7 @@ impl FileInfo {
if let Ok(existing) = Self::get_by_sha(&sha256, user_id, db_client).await {
return Ok(existing);
}
Err(FileError::SurrealError(e))
Err(FileError::from(e))
}
}
}
@@ -263,7 +275,7 @@ impl FileInfo {
storage
.delete_prefix(&parent_prefix)
.await
.map_err(AppError::Storage)?;
.map_err(AppError::from)?;
info!(
"Removed object prefix {} and its contents via StorageManager",
parent_prefix
@@ -286,7 +298,7 @@ impl FileInfo {
&self,
storage: &StorageManager,
) -> Result<bytes::Bytes, AppError> {
storage.get(&self.path).await.map_err(AppError::Storage)
storage.get(&self.path).await.map_err(AppError::from)
}
/// Persist bytes to storage using StorageManager.
+18 -18
View File
@@ -183,9 +183,9 @@ impl KnowledgeEntity {
.bind(("sources", source_ids.to_vec()))
.bind(("user_id", user_id.to_owned()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.take(0)
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(entities)
}
@@ -203,9 +203,9 @@ impl KnowledgeEntity {
.bind(("table", Self::table_name()))
.bind(("source_id", source_id.to_owned()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -248,9 +248,9 @@ impl KnowledgeEntity {
.bind(("entity", entity))
.bind(("emb", emb))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -289,11 +289,11 @@ impl KnowledgeEntity {
.bind(("embedding", query_embedding))
.bind(("user_id", user_id.to_string()))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
response = response.check().map_err(AppError::Database)?;
response = response.check().map_err(AppError::from)?;
let rows: Vec<Row> = response.take::<Vec<Row>>(0).map_err(AppError::Database)?;
let rows: Vec<Row> = response.take::<Vec<Row>>(0).map_err(AppError::from)?;
Ok(rows
.into_iter()
@@ -321,7 +321,7 @@ impl KnowledgeEntity {
let entity: KnowledgeEntity = db_client
.get_item(id)
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.ok_or_else(|| AppError::NotFound(format!("entity {id} not found")))?;
let settings = SystemSettings::get_current(db_client).await?;
@@ -355,9 +355,9 @@ impl KnowledgeEntity {
.bind(("emb", emb))
.bind(("description", description.to_string()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -453,9 +453,9 @@ impl KnowledgeEntity {
KnowledgeEntityEmbedding::table_name()
))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
db.client
.query(format!(
@@ -463,9 +463,9 @@ impl KnowledgeEntity {
KnowledgeEntityEmbedding::table_name()
))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
// Perform DB updates in a single transaction
info!("Applying embedding updates in a transaction...");
@@ -504,9 +504,9 @@ impl KnowledgeEntity {
db.client
.query(transaction_query)
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
info!("Re-embedding process for knowledge entities completed successfully.");
Ok(())
@@ -29,8 +29,8 @@ impl KnowledgeEntityEmbedding {
dimension,
);
let res = db.client.query(query).await.map_err(AppError::Database)?;
res.check().map_err(AppError::Database)?;
let res = db.client.query(query).await.map_err(AppError::from)?;
res.check().map_err(AppError::from)?;
Ok(())
}
@@ -78,8 +78,8 @@ impl KnowledgeEntityEmbedding {
.query(query)
.bind(("entity_id", entity_id.clone()))
.await
.map_err(AppError::Database)?;
let embeddings: Vec<Self> = result.take(0).map_err(AppError::Database)?;
.map_err(AppError::from)?;
let embeddings: Vec<Self> = result.take(0).map_err(AppError::from)?;
Ok(embeddings.into_iter().next())
}
@@ -101,8 +101,8 @@ impl KnowledgeEntityEmbedding {
.query(query)
.bind(("entity_ids", entity_ids.to_vec()))
.await
.map_err(AppError::Database)?;
let embeddings: Vec<Self> = result.take(0).map_err(AppError::Database)?;
.map_err(AppError::from)?;
let embeddings: Vec<Self> = result.take(0).map_err(AppError::from)?;
Ok(embeddings
.into_iter()
@@ -123,9 +123,9 @@ impl KnowledgeEntityEmbedding {
.query(query)
.bind(("entity_id", entity_id.clone()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -142,9 +142,9 @@ impl KnowledgeEntityEmbedding {
.query(query)
.bind(("source_id", source_id.to_owned()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
}
@@ -69,9 +69,9 @@ impl KnowledgeRelationship {
.bind(("source_id", self.metadata.source_id.clone()))
.bind(("relationship_type", self.metadata.relationship_type.clone()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -89,9 +89,9 @@ impl KnowledgeRelationship {
.bind(("source_id", source_id.to_owned()))
.bind(("user_id", user_id.to_owned()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -109,9 +109,9 @@ impl KnowledgeRelationship {
.bind(("id", id.to_owned()))
.bind(("user_id", user_id.to_owned()))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
let deleted: Vec<KnowledgeRelationship> =
delete_result.take(0).map_err(AppError::Database)?;
delete_result.take(0).map_err(AppError::from)?;
if !deleted.is_empty() {
return Ok(());
@@ -122,9 +122,9 @@ impl KnowledgeRelationship {
.query("SELECT * FROM type::thing('relates_to', $id)")
.bind(("id", id.to_owned()))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
let existing: Option<KnowledgeRelationship> =
exists_result.take(0).map_err(AppError::Database)?;
exists_result.take(0).map_err(AppError::from)?;
if existing.is_some() {
Err(AppError::Auth(
+16 -16
View File
@@ -55,9 +55,9 @@ impl TextChunk {
.bind(("source_id", source_id.to_owned()))
.bind(("table", Self::table_name()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -97,9 +97,9 @@ impl TextChunk {
.bind(("chunk", chunk))
.bind(("emb", emb))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -140,11 +140,11 @@ impl TextChunk {
.bind(("embedding", query_embedding))
.bind(("user_id", user_id.to_string()))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
response = response.check().map_err(AppError::Database)?;
response = response.check().map_err(AppError::from)?;
let rows: Vec<Row> = response.take::<Vec<Row>>(0).map_err(AppError::Database)?;
let rows: Vec<Row> = response.take::<Vec<Row>>(0).map_err(AppError::from)?;
Ok(rows
.into_iter()
@@ -208,11 +208,11 @@ impl TextChunk {
.bind(("user_id", user_id.to_owned()))
.bind(("limit", limit))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
response = response.check().map_err(AppError::Database)?;
response = response.check().map_err(AppError::from)?;
let rows: Vec<Row> = response.take::<Vec<Row>>(0).map_err(AppError::Database)?;
let rows: Vec<Row> = response.take::<Vec<Row>>(0).map_err(AppError::from)?;
Ok(rows
.into_iter()
@@ -314,16 +314,16 @@ impl TextChunk {
TextChunkEmbedding::table_name()
))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
db.client
.query(format!("DELETE FROM {};", TextChunkEmbedding::table_name()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
// Perform DB updates in a single transaction against the embedding table
info!("Applying embedding updates in a transaction...");
@@ -366,9 +366,9 @@ impl TextChunk {
db.client
.query(transaction_query)
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
info!("Re-embedding process for text chunks completed successfully.");
Ok(())
@@ -33,8 +33,8 @@ impl TextChunkEmbedding {
dimension,
);
let res = db.client.query(query).await.map_err(AppError::Database)?;
res.check().map_err(AppError::Database)?;
let res = db.client.query(query).await.map_err(AppError::from)?;
res.check().map_err(AppError::from)?;
Ok(())
}
@@ -85,9 +85,9 @@ impl TextChunkEmbedding {
.query(query)
.bind(("chunk_id", chunk_id.clone()))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
let embeddings: Vec<Self> = result.take(0).map_err(AppError::Database)?;
let embeddings: Vec<Self> = result.take(0).map_err(AppError::from)?;
Ok(embeddings.into_iter().next())
}
@@ -106,9 +106,9 @@ impl TextChunkEmbedding {
.query(query)
.bind(("chunk_id", chunk_id.clone()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
@@ -129,9 +129,9 @@ impl TextChunkEmbedding {
.query(query)
.bind(("source_id", source_id.to_owned()))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.check()
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}
+7 -7
View File
@@ -115,7 +115,7 @@ impl TextContent {
surrealdb::Datetime::from(now),
))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
if updated.is_none() {
return Err(AppError::NotFound(format!("text content {id} not found")));
@@ -138,10 +138,10 @@ impl TextContent {
.bind(("file_id", file_id.to_owned()))
.bind(("exclude_id", exclude_id.to_owned()))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
let existing: Option<surrealdb::sql::Thing> =
response.take(0).map_err(AppError::Database)?;
response.take(0).map_err(AppError::from)?;
Ok(existing.is_some())
}
@@ -193,9 +193,9 @@ impl TextContent {
.bind(("user_id", user_id.to_owned()))
.bind(("limit", limit))
.await
.map_err(AppError::Database)?
.map_err(AppError::from)?
.take(0)
.map_err(AppError::Database)
.map_err(AppError::from)
}
/// Builds a fallback display label for a source id when no matching content row exists.
@@ -239,9 +239,9 @@ impl TextContent {
.bind(("user_id", user_id.to_owned()))
.bind(("record_ids", record_ids))
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
let contents: Vec<SourceLabelRow> = response.take(0).map_err(AppError::Database)?;
let contents: Vec<SourceLabelRow> = response.take(0).map_err(AppError::from)?;
tracing::debug!(
source_id_count = source_ids.len(),
+1 -1
View File
@@ -688,7 +688,7 @@ impl User {
db.delete_item::<IngestionTask>(id)
.await
.map_err(AppError::Database)?;
.map_err(AppError::from)?;
Ok(())
}