refactor(backend): normalize naming convention to backend rather storage

no ambiguity with the backend rather storage
This commit is contained in:
Edouard Vanbelle
2026-08-02 14:49:35 +02:00
parent a10d3254bf
commit 015f2da0f7
30 changed files with 185 additions and 185 deletions
+2 -2
View File
@@ -124,7 +124,7 @@ pub enum RunOutcome {
/// `extra_stats` is merged into the run row's `stats` JSONB
/// alongside the engine-owned `scanned_count` + `finding_count`
/// / `severity_counts`. Handlers use it to surface per-run
/// summary counters (e.g. `storage_rotate` reports
/// summary counters (e.g. `backend_rotate` reports
/// `{"rewritten": N, "skipped": M, "failed": K}`) — the outcome
/// message in `JobOutcome.extra` and every downstream reader
/// of `RunSummary.stats` see the merged fields.
@@ -304,7 +304,7 @@ pub trait JobStore: Send + Sync {
/// Set an arbitrary string field on `params` (JSONB). Used by
/// handlers on a Fresh run to persist per-run configuration that
/// must survive a mid-run restart — e.g. `storage_migration`
/// must survive a mid-run restart — e.g. `backend_migration`
/// stamping `params.target_name` at run start so a resume can
/// pick up the same target without the admin re-specifying it.
///
+1 -1
View File
@@ -37,7 +37,7 @@ use serde::{Deserialize, Serialize};
///
/// Semantics of `storage`, per job (added for the multi-entry storage
/// design — see `docs/plan/storage-multi-entry.md`):
/// - `storage_migration` — the NAME of the target storage entry to
/// - `backend_migration` — the NAME of the target storage entry to
/// copy blobs INTO. Required on a Fresh run (handler refuses
/// without it); ignored on a Resumed run (target read from the
/// persisted `params.target_name`).
@@ -173,8 +173,8 @@ impl BlobStorageBackend for AzureBlobBackend {
})
}
/// Atomic overwrite path used by `storage_rotate` and
/// `storage_migration` when re-writing an already-present blob
/// Atomic overwrite path used by `backend_rotate` and
/// `backend_migration` when re-writing an already-present blob
/// under a new head key/format. Trait default delegates to
/// `put_blob_from_bytes` which `get_properties`-probes and
/// silently skips — exactly wrong for the rotate/migrate use
@@ -67,7 +67,7 @@ use crate::infrastructure::services::entry_backend::{
build_entry_backend_typed, persist_active_backend_name, persist_migration_readonly,
};
pub const STORAGE_MIGRATION_JOB_NAME: &str = "storage_migration";
pub const BACKEND_MIGRATION_JOB_NAME: &str = "backend_migration";
/// The `params` JSONB key under which the run's target entry name is
/// stashed at Fresh-open time via `JobStore::set_string_param`.
@@ -93,7 +93,7 @@ pub const SOURCE_NAME_PARAM: &str = "source_name";
/// every 100 rows too. Match `blobs_consistency` for consistency.
const BATCH_SIZE: i64 = 100;
pub struct StorageMigrationService {
pub struct BackendMigrationService {
pool: Arc<PgPool>,
/// Backend the running app is bound to at handler-construction
/// time. Refers to the hot-swap wrapper when multi-entry is
@@ -140,7 +140,7 @@ pub struct StorageMigrationService {
Arc<std::sync::RwLock<Option<crate::common::migration_progress::MigrationProgress>>>,
}
impl StorageMigrationService {
impl BackendMigrationService {
#[allow(clippy::too_many_arguments)]
pub fn new(
pool: Arc<PgPool>,
@@ -183,9 +183,9 @@ impl StorageMigrationService {
}
#[async_trait]
impl RecoverableJobHandler for StorageMigrationService {
impl RecoverableJobHandler for BackendMigrationService {
fn name(&self) -> &str {
STORAGE_MIGRATION_JOB_NAME
BACKEND_MIGRATION_JOB_NAME
}
/// Definitive count — one row per blob. `SELECT COUNT(*) FROM
@@ -200,7 +200,7 @@ impl RecoverableJobHandler for StorageMigrationService {
Err(e) => {
tracing::debug!(
target: "oxicloud::migration",
event = "storage_migration.count_total_failed",
event = "backend_migration.count_total_failed",
error = %e,
"count_total failed — run will not surface a progress bar"
);
@@ -234,7 +234,7 @@ impl RecoverableJobHandler for StorageMigrationService {
let Some(name) = args.storage.clone() else {
return RunOutcome::Failed {
message:
"storage_migration requires `target_name` on a fresh run — trigger via \
"backend_migration requires `target_name` on a fresh run — trigger via \
POST /api/admin/storage/migration/start with `{\"target_name\": \"<entry>\"}`."
.to_string(),
};
@@ -305,7 +305,7 @@ impl RecoverableJobHandler for StorageMigrationService {
.clone();
tracing::warn!(
target: "oxicloud::migration",
event = "storage_migration.legacy_paused_row_source_defaulted",
event = "backend_migration.legacy_paused_row_source_defaulted",
run_id = %store.run_id(),
fallback_source = %fallback,
"resumed run has no source_name in params (pre-K3.8 row) — defaulting \
@@ -330,11 +330,11 @@ impl RecoverableJobHandler for StorageMigrationService {
if target_name == active_backend_name {
tracing::warn!(
target: "audit",
event = "storage_migration.refused_noop",
event = "backend_migration.refused_noop",
run_id = %store.run_id(),
target_name = %target_name,
active = %active_backend_name,
"storage_migration refused: target equals the currently-active entry"
"backend_migration refused: target equals the currently-active entry"
);
return RunOutcome::Failed {
message: format!(
@@ -397,12 +397,12 @@ impl RecoverableJobHandler for StorageMigrationService {
};
tracing::warn!(
target: "audit",
event = "storage_migration.refused_same_physical_storage",
event = "backend_migration.refused_same_physical_storage",
run_id = %store.run_id(),
target_name = %target_name,
source_name = %active_backend_name,
encryption_differs = key_differs,
"storage_migration refused: named target differs from source but physical storage matches"
"backend_migration refused: named target differs from source but physical storage matches"
);
return RunOutcome::Failed {
message: format!(
@@ -484,14 +484,14 @@ impl RecoverableJobHandler for StorageMigrationService {
let target_kind = target.backend_type();
tracing::info!(
target: "audit",
event = "storage_migration.run_started",
event = "backend_migration.run_started",
run_id = %store.run_id(),
source_name = %active_backend_name,
target_name = %target_name,
source_kind = source_kind,
target_kind = target_kind,
resuming = !is_fresh,
"storage_migration starting {active_backend_name} ({source_kind}) → {target_name} ({target_kind})"
"backend_migration starting {active_backend_name} ({source_kind}) → {target_name} ({target_kind})"
);
// Cursor = the last-visited blob hash, UTF-8-encoded. On resume
@@ -527,13 +527,13 @@ impl RecoverableJobHandler for StorageMigrationService {
Ok(RunStatus::CancelRequested) => {
tracing::info!(
target: "oxicloud::migration",
event = "storage_migration.cancelled",
event = "backend_migration.cancelled",
run_id = %store.run_id(),
copied = copied_count,
skipped = skipped_count,
failed = failed_count,
source_missing = source_missing_count,
"storage_migration cancelled cooperatively, pausing"
"backend_migration cancelled cooperatively, pausing"
);
return RunOutcome::Paused {
cursor: cursor
@@ -604,7 +604,7 @@ impl RecoverableJobHandler for StorageMigrationService {
source_missing_count += 1;
tracing::warn!(
target: "oxicloud::migration",
event = "storage_migration.source_missing",
event = "backend_migration.source_missing",
run_id = %store.run_id(),
hash = %hash,
source = source_kind,
@@ -612,7 +612,7 @@ impl RecoverableJobHandler for StorageMigrationService {
);
record_or_log(
store,
STORAGE_MIGRATION_JOB_NAME,
BACKEND_MIGRATION_JOB_NAME,
"source_missing",
"data_loss",
None,
@@ -635,7 +635,7 @@ impl RecoverableJobHandler for StorageMigrationService {
// it.
tracing::warn!(
target: "oxicloud::migration",
event = "storage_migration.source_probe_error",
event = "backend_migration.source_probe_error",
run_id = %store.run_id(),
hash = %hash,
error = %e,
@@ -670,7 +670,7 @@ impl RecoverableJobHandler for StorageMigrationService {
skipped_count += 1;
tracing::debug!(
target: "oxicloud::migration",
event = "storage_migration.blob_skipped_head_match",
event = "backend_migration.blob_skipped_head_match",
run_id = %store.run_id(),
hash = %hash,
head_format = %target.head_format(),
@@ -687,7 +687,7 @@ impl RecoverableJobHandler for StorageMigrationService {
failed_count += 1;
tracing::warn!(
target: "oxicloud::migration",
event = "storage_migration.blob_failed",
event = "backend_migration.blob_failed",
run_id = %store.run_id(),
hash = %hash,
error = %e,
@@ -698,7 +698,7 @@ impl RecoverableJobHandler for StorageMigrationService {
// where the admin UI reads it.
record_or_log(
store,
STORAGE_MIGRATION_JOB_NAME,
BACKEND_MIGRATION_JOB_NAME,
"migration_failed",
"data_loss",
None,
@@ -760,7 +760,7 @@ impl RecoverableJobHandler for StorageMigrationService {
}
}
impl StorageMigrationService {
impl BackendMigrationService {
/// Terminal successful path — reached from both Completed sites
/// in the batch loop (empty-first-batch and short-batch).
///
@@ -831,7 +831,7 @@ impl StorageMigrationService {
if !readonly_persisted {
tracing::warn!(
target: "oxicloud::migration",
event = "storage_migration.readonly_clear_persist_failed",
event = "backend_migration.readonly_clear_persist_failed",
run_id = %store.run_id(),
"cleared migration_readonly in memory (writes allowed against source) but the \
DB persist failed. Boot-clear rule will fix on next restart."
@@ -839,7 +839,7 @@ impl StorageMigrationService {
}
tracing::info!(
target: "audit",
event = "storage_migration.aborted",
event = "backend_migration.aborted",
reason = "blobs_failed",
run_id = %store.run_id(),
active_backend_name = previous_active,
@@ -848,7 +848,7 @@ impl StorageMigrationService {
skipped = skipped,
failed = failed,
source_missing = source_missing,
"🛑 storage_migration aborted — {failed} blob(s) failed, active backend left at \
"🛑 backend_migration aborted — {failed} blob(s) failed, active backend left at \
`{previous_active}`, readonly cleared. Inspect findings and retry, or accept \
the partial migration via `oxicloud --select-storage {target_name}`."
);
@@ -907,7 +907,7 @@ impl StorageMigrationService {
if !readonly_persisted {
tracing::warn!(
target: "oxicloud::migration",
event = "storage_migration.readonly_clear_persist_failed",
event = "backend_migration.readonly_clear_persist_failed",
run_id = %store.run_id(),
"cleared migration_readonly in memory (writes allowed) but the DB persist \
failed. If the server crashes before next boot, boot will re-seed the flag \
@@ -917,7 +917,7 @@ impl StorageMigrationService {
tracing::info!(
target: "audit",
event = "storage_migration.completed",
event = "backend_migration.completed",
run_id = %store.run_id(),
active_backend_name = target_name,
previous_active = previous_active,
@@ -925,11 +925,11 @@ impl StorageMigrationService {
skipped = skipped,
failed = failed,
source_missing = source_missing,
"✅ storage_migration completed — hot-swapped runtime backend to `{target_name}`, \
"✅ backend_migration completed — hot-swapped runtime backend to `{target_name}`, \
writes resumed. No restart required."
);
// Per-run summary counters merged into `stats` for the admin
// UI drawer. Same shape as `storage_rotate`'s extras + one
// UI drawer. Same shape as `backend_rotate`'s extras + one
// extra `source_missing` counter unique to migration.
RunOutcome::completed_with(serde_json::json!({
"copied": copied,
@@ -1022,7 +1022,7 @@ async fn collect_stream_bytes(
let mut stream = std::pin::pin!(stream);
while let Some(chunk) = stream.next().await {
let bytes = chunk.map_err(|e| {
DomainError::internal_error("StorageMigration", format!("source stream read: {e}"))
DomainError::internal_error("BackendMigration", format!("source stream read: {e}"))
})?;
buf.extend_from_slice(&bytes);
}
@@ -15,13 +15,13 @@
//!
//! ### No readonly, no cutover
//!
//! `storage_rotate` is per-blob idempotent — repeat rewrites are
//! `backend_rotate` is per-blob idempotent — repeat rewrites are
//! byte-safe (content-addressability holds; the wrapper always
//! produces the head format). Concurrent user writes coexist: they
//! land as head-format themselves, so when the walk reaches that
//! hash the classifier reports "already at head format" and the
//! decision tree collapses to `skip`. No app-wide read-only gate is
//! ever engaged — a critical improvement over `storage_migration`,
//! ever engaged — a critical improvement over `backend_migration`,
//! whose target-different-from-source cutover forces one.
//!
//! ### Restart survival
@@ -36,13 +36,13 @@
//! ### Design notes
//!
//! * **Cursor** — UTF-8 hex of the last-processed blob hash (64
//! chars). Same encoding as `storage_migration` and
//! chars). Same encoding as `backend_migration` and
//! `blobs_consistency`.
//! * **Target lookup** — the entry NAME is stashed in `params` at
//! Fresh-open time and re-read on Resume. The wrapper for that
//! entry is rebuilt at the top of every run via
//! `build_entry_backend_typed`; mid-run config changes are
//! ignored until the next run (mirrors `storage_migration`).
//! ignored until the next run (mirrors `backend_migration`).
//! * **Per-blob failures don't fail the run** — each failure records
//! a `rotation_failed` finding (severity `data_loss` — the bytes
//! didn't get rewritten) and the walk continues. A run that
@@ -68,20 +68,20 @@ use crate::infrastructure::scheduler::{
use crate::infrastructure::services::encrypted_blob_backend::BlobFormat;
use crate::infrastructure::services::entry_backend::build_entry_backend_typed;
pub const STORAGE_ROTATE_JOB_NAME: &str = "storage_rotate";
pub const BACKEND_ROTATE_JOB_NAME: &str = "backend_rotate";
/// The `params` JSONB key under which the run's target entry name is
/// stashed at Fresh-open time via `JobStore::set_string_param`.
/// Kept identical to `storage_migration`'s TARGET_NAME_PARAM so
/// Kept identical to `backend_migration`'s TARGET_NAME_PARAM so
/// operators grepping run rows see the same convention across both
/// storage-touching tenants.
pub const TARGET_NAME_PARAM: &str = "target_name";
/// Rows per batch. Matches `storage_migration` / `blobs_consistency`
/// Rows per batch. Matches `backend_migration` / `blobs_consistency`
/// so the checkpoint + cancel-poll cadence is uniform across tenants.
const BATCH_SIZE: i64 = 100;
pub struct StorageRotateService {
pub struct BackendRotateService {
pool: Arc<PgPool>,
/// Immutable per-deploy snapshot; used to look up the target
/// entry by name at run start. Matches `AppConfig.storage_entries`.
@@ -99,7 +99,7 @@ pub struct StorageRotateService {
rotation_progress: Arc<std::sync::RwLock<Option<MigrationProgress>>>,
}
impl StorageRotateService {
impl BackendRotateService {
pub fn new(
pool: Arc<PgPool>,
storage_entries: Vec<NamedStorageEntry>,
@@ -115,7 +115,7 @@ impl StorageRotateService {
}
/// Chainable self-registration — mirrors the `*_consistency`
/// tenants and `storage_migration`. On-demand only (no periodic
/// tenants and `backend_migration`. On-demand only (no periodic
/// tick).
pub async fn register_recoverable_job(
self: Arc<Self>,
@@ -130,13 +130,13 @@ impl StorageRotateService {
}
#[async_trait]
impl RecoverableJobHandler for StorageRotateService {
impl RecoverableJobHandler for BackendRotateService {
fn name(&self) -> &str {
STORAGE_ROTATE_JOB_NAME
BACKEND_ROTATE_JOB_NAME
}
/// Definitive count — one row per blob. Same query as
/// `storage_migration::count_total`; the two walk the same rows.
/// `backend_migration::count_total`; the two walk the same rows.
async fn count_total(&self) -> Option<u64> {
let row: Result<(i64,), sqlx::Error> = sqlx::query_as("SELECT COUNT(*) FROM storage.blobs")
.fetch_one(self.pool.as_ref())
@@ -146,7 +146,7 @@ impl RecoverableJobHandler for StorageRotateService {
Err(e) => {
tracing::debug!(
target: "oxicloud::rotate",
event = "storage_rotate.count_total_failed",
event = "backend_rotate.count_total_failed",
error = %e,
"count_total failed — run will not surface a progress bar"
);
@@ -161,12 +161,12 @@ impl RecoverableJobHandler for StorageRotateService {
args: &JobRunArgs,
resume_cursor: Option<Vec<u8>>,
) -> RunOutcome {
// Resolve target entry name — same shape as `storage_migration`.
// Resolve target entry name — same shape as `backend_migration`.
let is_fresh = resume_cursor.is_none();
let target_name = if is_fresh {
let Some(name) = args.storage.clone() else {
return RunOutcome::Failed {
message: "storage_rotate requires `target_name` on a fresh run — trigger via \
message: "backend_rotate requires `target_name` on a fresh run — trigger via \
POST /api/admin/storage/entries/{name}/rotate"
.to_string(),
};
@@ -230,7 +230,7 @@ impl RecoverableJobHandler for StorageRotateService {
tracing::info!(
target: "audit",
event = "storage_rotate.run_started",
event = "backend_rotate.run_started",
run_id = %store.run_id(),
target_name = %target_name,
// `%` (Display) → SSH-style `encrypted-v1 key_fp=83:96:...`
@@ -239,7 +239,7 @@ impl RecoverableJobHandler for StorageRotateService {
// header bytes on disk.
head_format = %head_format,
resuming = !is_fresh,
"storage_rotate started on `{target_name}` (head_format = {head_format})"
"backend_rotate started on `{target_name}` (head_format = {head_format})"
);
// Seed the progress snapshot. Total = count_total's estimate;
@@ -280,12 +280,12 @@ impl RecoverableJobHandler for StorageRotateService {
self.clear_progress();
tracing::info!(
target: "oxicloud::rotate",
event = "storage_rotate.cancelled",
event = "backend_rotate.cancelled",
run_id = %store.run_id(),
rewritten = rewritten_count,
skipped = skipped_count,
failed = failed_count,
"storage_rotate cancelled cooperatively, pausing"
"backend_rotate cancelled cooperatively, pausing"
);
return RunOutcome::Paused {
cursor: cursor
@@ -304,7 +304,7 @@ impl RecoverableJobHandler for StorageRotateService {
}
// Fetch the next batch. Same keyset pagination shape as
// `storage_migration` — `hash > $1` on the PK, index-only.
// `backend_migration` — `hash > $1` on the PK, index-only.
let rows: Vec<(String,)> = match sqlx::query_as(
r#"
SELECT hash
@@ -351,7 +351,7 @@ impl RecoverableJobHandler for StorageRotateService {
failed_count += 1;
tracing::warn!(
target: "oxicloud::rotate",
event = "storage_rotate.read_failed",
event = "backend_rotate.read_failed",
run_id = %store.run_id(),
hash = %hash,
error = %e,
@@ -359,7 +359,7 @@ impl RecoverableJobHandler for StorageRotateService {
);
record_or_log(
store,
STORAGE_ROTATE_JOB_NAME,
BACKEND_ROTATE_JOB_NAME,
"rotation_failed",
"data_loss",
None,
@@ -402,7 +402,7 @@ impl RecoverableJobHandler for StorageRotateService {
failed_count += 1;
tracing::warn!(
target: "oxicloud::rotate",
event = "storage_rotate.write_failed",
event = "backend_rotate.write_failed",
run_id = %store.run_id(),
hash = %hash,
error = %e,
@@ -410,7 +410,7 @@ impl RecoverableJobHandler for StorageRotateService {
);
record_or_log(
store,
STORAGE_ROTATE_JOB_NAME,
BACKEND_ROTATE_JOB_NAME,
"rotation_failed",
"data_loss",
None,
@@ -467,9 +467,9 @@ impl RecoverableJobHandler for StorageRotateService {
}
}
impl StorageRotateService {
impl BackendRotateService {
/// Terminal successful path — clear the header snapshot and log a
/// final audit line. Unlike `storage_migration::finish_completed`
/// final audit line. Unlike `backend_migration::finish_completed`
/// there's no cutover / hot-swap step: rotation writes in place
/// on the entry that's already there.
///
@@ -508,14 +508,14 @@ impl StorageRotateService {
tracing::info!(
target: "audit",
event = "storage_rotate.run_completed",
event = "backend_rotate.run_completed",
run_id = %store.run_id(),
target_name = %target_name,
rewritten = rewritten,
skipped = skipped,
failed = failed,
head_format = %head_display,
"storage_rotate completed on `{target_name}` — {rewritten} rewritten, {skipped} skipped, {failed} failed; head = {head_display}"
"backend_rotate completed on `{target_name}` — {rewritten} rewritten, {skipped} skipped, {failed} failed; head = {head_display}"
);
// Surface the per-run summary counters as extras merged into
@@ -75,7 +75,7 @@ pub const BLOBS_CONSISTENCY_JOB_NAME: &str = "blobs_consistency";
/// `params` JSONB key under which the entry name being probed is
/// stashed on a Fresh run (matches `TARGET_NAME_PARAM` on
/// `storage_migration`). Resumed runs re-read it so a paused audit
/// `backend_migration`). Resumed runs re-read it so a paused audit
/// survives restart without the admin re-specifying the target.
pub const PROBED_STORAGE_PARAM: &str = "probed_storage";
@@ -189,7 +189,7 @@ impl RecoverableJobHandler for BlobsConsistencyCheck {
resume_cursor: Option<Vec<u8>>,
) -> RunOutcome {
// Resolve the backend to probe. Two paths, mirroring the
// Fresh/Resumed split the storage_migration handler uses:
// Fresh/Resumed split the backend_migration handler uses:
//
// * Fresh + args.storage=Some — probe that named entry
// instead of the live backend. Stamp probed_storage in
@@ -142,7 +142,7 @@ pub struct EncryptedBlobBackend {
/// * `read_dispatch` legacy-fallback path — iterates in order
/// (oldest → newest) to try every real-cipher pair when a
/// legacy blob's head-key decrypt fails.
/// * K3 `storage_rotate` — needs to walk pair indices.
/// * K3 `backend_rotate` — needs to walk pair indices.
pairs: Vec<KeyPair>,
/// `<key_fp>` → per-pair cipher, for O(1) read dispatch on v1
/// blobs. Excludes any `none:` pair (nothing to build). Cloned
@@ -218,7 +218,7 @@ impl EncryptedBlobBackend {
key
}
/// The format `storage_rotate` should normalise every blob TO —
/// The format `backend_rotate` should normalise every blob TO —
/// derived from the wrapper's head pair. When
/// `head_cipher.is_some()` we're writing encrypted-v1 with the
/// head pair's `key_fp`; when it's `None` we're writing
@@ -237,8 +237,8 @@ impl EncryptedBlobBackend {
}
}
/// Smart-skip probe used by `storage_migration` (and potentially
/// `storage_rotate` if it ever gains a fast-path). Reads the
/// Smart-skip probe used by `backend_migration` (and potentially
/// `backend_rotate` if it ever gains a fast-path). Reads the
/// first [`HEADER_SIZE`] bytes of the on-disk blob and returns
/// `true` iff:
///
@@ -280,7 +280,7 @@ impl EncryptedBlobBackend {
}
/// Fetch, classify, and decrypt a blob in one round-trip. Used by
/// K3's `storage_rotate` per-blob step: it needs both the
/// K3's `backend_rotate` per-blob step: it needs both the
/// plaintext (to re-encrypt under the head pair) AND the current
/// on-disk format (to decide whether a rewrite is needed at all).
///
@@ -317,7 +317,7 @@ impl EncryptedBlobBackend {
}
/// Classification of a raw blob's on-disk format. Exposed for K3's
/// `storage_rotate` decision tree; not used on the hot request path.
/// `backend_rotate` decision tree; not used on the hot request path.
///
/// PartialEq is derived so `current == head_format` collapses the
/// plan's six-case decision tree into a single equality check:
@@ -541,7 +541,7 @@ fn read_dispatch(
hash = %expected_hash,
size = encrypted.len(),
"🩹 legacy plaintext blob served via BLAKE3 rescue — no configured key \
decrypted it, but content hash matched. Run storage_rotate to re-write \
decrypted it, but content hash matched. Run backend_rotate to re-write \
under the current head."
);
return Ok(Bytes::from(encrypted));
@@ -737,7 +737,7 @@ impl BlobStorageBackend for EncryptedBlobBackend {
/// Frame the plaintext with the head pair's format (encrypted-v1
/// or plaintext-v1), then delegate the atomic replace to the
/// inner backend. Used by `storage_rotate` to actually change the
/// inner backend. Used by `backend_rotate` to actually change the
/// on-disk bytes — `put_blob_from_bytes` would silently no-op on
/// `LocalBlobBackend` when the object key already exists.
fn put_blob_from_bytes_replace(
@@ -1373,7 +1373,7 @@ mod tests {
// ─────────────────────────────────────────────────────────────
// K3 tests — BlobFormat classifier + head_format + read_and_classify.
//
// These pin the format-inspection contract that `storage_rotate`
// These pin the format-inspection contract that `backend_rotate`
// depends on. The rotate job's per-blob decision tree collapses
// to `current != head_format ? rewrite : skip`, so any drift in
// either helper would silently change rotation semantics.
+1 -1
View File
@@ -201,7 +201,7 @@ pub async fn resolve_active_entry<'a>(
///
/// Same construction path as `build_entry_backend`; the trait-object
/// version delegates through this. Preferred for job handlers
/// (`storage_rotate`) that need typed access. The trait-object
/// (`backend_rotate`) that need typed access. The trait-object
/// version stays for the DI hot-path where the caller only needs
/// the generic `BlobStorageBackend` contract.
pub fn build_entry_backend_typed(
@@ -470,7 +470,7 @@ impl BlobStorageBackend for LocalBlobBackend {
/// then `rename(2)` over the target. `write_blob_bytes`'s
/// `O_CREAT|O_EXCL` idempotent-skip (the right choice for uploads)
/// silently no-ops when the target already exists — wrong for
/// callers like `storage_rotate` that need the bytes to change.
/// callers like `backend_rotate` that need the bytes to change.
/// See the trait doc for the full picture.
///
/// Tempfile lives beside the target under the same shard directory
+2 -2
View File
@@ -1,6 +1,8 @@
pub mod audio_metadata_service;
pub mod azure_blob_backend;
pub mod backend_consistency_service;
pub mod backend_migration_service;
pub mod backend_rotate_service;
pub mod blobs_consistency_service;
pub mod cached_blob_backend;
pub mod chunked_upload_service;
@@ -45,8 +47,6 @@ pub mod s3_blob_backend;
pub mod search_index;
pub mod share_unlock_cookie;
pub mod smtp_email_sender;
pub mod storage_migration_service;
pub mod storage_rotate_service;
pub mod swappable_blob_backend;
pub mod thumbnail_service;
#[cfg(test)]
@@ -237,8 +237,8 @@ impl BlobStorageBackend for S3BlobBackend {
})
}
/// Atomic overwrite path used by `storage_rotate` and
/// `storage_migration` when re-writing an already-present blob
/// Atomic overwrite path used by `backend_rotate` and
/// `backend_migration` when re-writing an already-present blob
/// under a new head key/format. Trait default delegates to
/// `put_blob_from_bytes` which HEAD-probes and silently skips —
/// exactly wrong for the rotate/migrate use case (the whole