Implement recoverable exact result deletion

This commit is contained in:
ms2k 2026-07-17 19:28:28 +09:00
commit 791e00f4c0
6 changed files with 2521 additions and 1 deletions

View file

@ -0,0 +1,25 @@
use remora_durable::DurableError;
#[derive(Debug, thiserror::Error)]
pub enum DeletionError {
#[error("resource not found")]
NotFound,
#[error("job is not terminal")]
NotTerminal,
#[error("hard deletion has already started")]
HardDeleteStarted,
#[error("committed result inventory is corrupt")]
CorruptInventory,
#[error("deletion claim is no longer authoritative")]
ClaimLost,
#[error("invalid deletion configuration: {0}")]
InvalidConfig(String),
#[error("durable deletion operation failed: {0}")]
Durable(#[from] DurableError),
}
impl From<sqlx::Error> for DeletionError {
fn from(error: sqlx::Error) -> Self {
Self::Durable(DurableError::Database(error))
}
}

View file

@ -0,0 +1,16 @@
//! Durable soft-delete visibility and recoverable exact hard deletion.
mod error;
mod migrate;
mod registry;
mod service;
mod types;
pub use error::DeletionError;
pub use migrate::{migrate, migrator};
pub use registry::TargetRegistry;
pub use service::DeletionService;
pub use types::{
DeleteClaim, DeleteIdentity, DeletionConfig, DeletionOperation, DeletionProblem,
DeletionProblemClass, DeletionState, DrainOutcome, HardDeleteOutcome, LifecycleMutation,
};

View file

@ -0,0 +1,17 @@
use remora_durable::DurableError;
use sqlx::PgPool;
const MIGRATION_TABLE: &str = "_remora_deletion_migrations";
pub async fn migrate(pool: &PgPool) -> Result<(), DurableError> {
let mut migrator = sqlx::migrate!("./migrations");
migrator.dangerous_set_table_name(MIGRATION_TABLE);
migrator.run(pool).await?;
Ok(())
}
pub fn migrator() -> sqlx::migrate::Migrator {
let mut migrator = sqlx::migrate!("./migrations");
migrator.dangerous_set_table_name(MIGRATION_TABLE);
migrator
}

View file

@ -0,0 +1,46 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use remora_target::TargetStore;
#[derive(Clone, Default)]
pub struct TargetRegistry {
stores: Arc<RwLock<HashMap<(String, String), TargetStore>>>,
}
impl TargetRegistry {
#[must_use]
pub fn new(stores: impl IntoIterator<Item = TargetStore>) -> Self {
let registry = Self::default();
for store in stores {
registry.register(store);
}
registry
}
pub fn register(&self, store: TargetStore) {
let key = (
store.config().target_store_id.clone(),
store.config().bucket.clone(),
);
self.stores
.write()
.expect("target registry poisoned")
.insert(key, store);
}
pub fn remove(&self, target_store: &str, bucket: &str) -> Option<TargetStore> {
self.stores
.write()
.expect("target registry poisoned")
.remove(&(target_store.to_owned(), bucket.to_owned()))
}
pub(crate) fn resolve(&self, target_store: &str, bucket: &str) -> Option<TargetStore> {
self.stores
.read()
.expect("target registry poisoned")
.get(&(target_store.to_owned(), bucket.to_owned()))
.cloned()
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,88 @@
use std::time::Duration;
use remora_domain::OpaqueId;
use remora_target::{MultipartIdentity, ObjectIdentity};
use time::OffsetDateTime;
#[derive(Clone, Debug)]
pub struct DeletionConfig {
pub claim_ttl: Duration,
}
impl Default for DeletionConfig {
fn default() -> Self {
Self {
claim_ttl: Duration::from_secs(30),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LifecycleMutation {
Changed,
Unchanged,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DeletionState {
Queued,
Running,
RetryWait,
Blocked,
Completed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DeletionProblemClass {
Authorization,
TargetUnavailable,
Configuration,
Internal,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeletionProblem {
pub class: DeletionProblemClass,
pub message: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeletionOperation {
pub id: OpaqueId,
pub job_id: OpaqueId,
pub state: DeletionState,
pub problem: Option<DeletionProblem>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
pub completed_at: Option<OffsetDateTime>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HardDeleteOutcome {
pub mutation: LifecycleMutation,
pub operation: DeletionOperation,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DeleteIdentity {
WriteIntent(ObjectIdentity),
Object(ObjectIdentity),
Manifest(ObjectIdentity),
Multipart(MultipartIdentity),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeleteClaim {
pub item_id: u64,
pub operation_id: OpaqueId,
pub claim_token: OpaqueId,
pub identity: DeleteIdentity,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DrainOutcome {
Idle,
ItemCompleted,
Blocked,
Completed,
}