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
8 changes: 4 additions & 4 deletions codegen/src/generators/persist/table/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,10 @@ impl PersistGenerator {
if !std::path::Path::new(&table_path).exists() {
return Self::new(engine).await;
};
let space = #space_ident::parse_file(&table_path)
.await
.map_err(|error| PersistenceLoadError::corrupt(&table_path, error))?;
let table = space.into_worktable(engine, &table_path).await?;
let table = load_persisted_state(&table_path, async {
let space = #space_ident::parse_file(&table_path).await?;
Ok::<_, eyre::Report>(space.into_worktable(engine, &table_path).await?)
}).await?;
Ok(table)
}
}
Expand Down
8 changes: 4 additions & 4 deletions codegen/src/generators/read_only/table/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,10 @@ impl ReadOnlyGenerator {
if !std::path::Path::new(&table_path).exists() {
return Self::new(engine).await;
};
let space = #space_ident::parse_file(&table_path)
.await
.map_err(|error| PersistenceLoadError::corrupt(&table_path, error))?;
let table = space.into_worktable(&table_path)?;
let table = load_persisted_state(&table_path, async {
let space = #space_ident::parse_file(&table_path).await?;
Ok::<_, eyre::Report>(space.into_worktable(&table_path)?)
}).await?;
Ok(table)
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub mod prelude {
IndexTableOfContents, InsertOperation, Operation, OperationId, PersistedWorkTable, PersistenceConfig,
PersistenceEngine, PersistenceError, PersistenceLoadError, PersistenceResult, PersistenceState,
PersistenceTask, ReadOnlyPersistenceEngine, SpaceArcticIndex, SpaceCongeeIndex, SpaceData, SpaceDataOps,
SpaceIndex, SpaceIndexOps, SpaceIndexUnsized, SpaceSecondaryIndexOps, UpdateOperation,
SpaceIndex, SpaceIndexOps, SpaceIndexUnsized, SpaceSecondaryIndexOps, UpdateOperation, load_persisted_state,
map_index_pages_to_toc_and_general, map_unsized_index_pages_to_toc_and_general, reconstruct_multi_index_nodes,
validate_events,
};
Expand Down
63 changes: 63 additions & 0 deletions src/persistence/error.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use futures::FutureExt;

/// A persisted table could not be loaded without risking invalid data.
///
/// WorkTable persistence is best-effort rather than crash-atomic. Abrupt
Expand Down Expand Up @@ -47,6 +51,34 @@ impl Display for PersistenceLoadError {

impl Error for PersistenceLoadError {}

/// Contains dependency panics while decoding an existing persisted store.
///
/// Some lower-level page decoders still panic when torn bytes violate their
/// internal invariants. A persisted table is not exposed until the entire
/// decode and validation pipeline succeeds, so the future and all partially
/// decoded state are discarded on unwind. `AssertUnwindSafe` is used only to
/// establish that containment boundary; no value from the failed future is
/// reused.
#[doc(hidden)]
pub async fn load_persisted_state<T, F>(path: impl AsRef<Path>, future: F) -> Result<T, PersistenceLoadError>
where
F: Future<Output = eyre::Result<T>>,
{
let path = path.as_ref();
match AssertUnwindSafe(future).catch_unwind().await {
Ok(Ok(value)) => Ok(value),
Ok(Err(error)) => Err(PersistenceLoadError::corrupt(path, format!("{error:#}"))),
Err(payload) => {
let reason = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("persisted-state loader panicked");
Err(PersistenceLoadError::corrupt(path, reason))
}
}
}

/// Terminal and lifecycle errors reported by a persistence task.
#[derive(Debug)]
pub enum PersistenceError {
Expand Down Expand Up @@ -80,3 +112,34 @@ pub enum PersistenceState {
Failed(Arc<PersistenceError>),
Closed,
}

#[cfg(test)]
mod tests {
use super::{PersistenceLoadError, load_persisted_state};

#[tokio::test]
async fn persisted_state_panics_are_typed_load_errors() {
let error = load_persisted_state("table/path", async {
panic!("dependency decoder rejected torn bytes");
#[allow(unreachable_code)]
Ok::<(), eyre::Report>(())
})
.await
.unwrap_err();

assert_eq!(error.path(), std::path::Path::new("table/path"));
assert_eq!(error.reason(), "dependency decoder rejected torn bytes");
}

#[tokio::test]
async fn persisted_state_errors_are_typed_load_errors() {
let error: PersistenceLoadError = load_persisted_state("table/path", async {
Err::<(), _>(eyre::eyre!("invalid persisted page"))
})
.await
.unwrap_err();

assert_eq!(error.path(), std::path::Path::new("table/path"));
assert_eq!(error.reason(), "invalid persisted page");
}
}
2 changes: 1 addition & 1 deletion src/persistence/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::persistence::operation::BatchOperation;

pub use engine::DiskConfig;
pub use engine::DiskPersistenceEngine;
pub use error::{PersistenceError, PersistenceLoadError, PersistenceResult, PersistenceState};
pub use error::{PersistenceError, PersistenceLoadError, PersistenceResult, PersistenceState, load_persisted_state};
pub use operation::{
AcknowledgeOperation, DeleteOperation, InsertOperation, Operation, OperationId, OperationType, UpdateOperation,
validate_events,
Expand Down
Loading