perf(blobs): batch chunk fsyncs into one durability sweep per upload
Storing a new file through CDC dedup issued sync_all + a parent-dir fsync for every ~256 KB chunk (~8,200 fsyncs for a 1 GB upload), plus one PG INSERT round-trip per chunk. The actual durability boundary is the manifest INSERT: chunks only need to be durable before any PG row references them, not one by one. - BlobStorageBackend grows put_blob_from_bytes_unsynced + sync_blobs with conservative defaults (unsynced delegates to the synced write, sync_blobs is a no-op) so backends that don't opt in keep the per-write durability semantics. Remote stores are durable on PUT. - LocalBlobBackend writes chunks without fsync and implements sync_blobs as a parallel sweep: every listed blob file (hard requirement) plus each distinct prefix directory exactly once (best-effort, same tier as fsync_parent_dir). - DedupService::store_chunks writes new chunks unsynced, runs one sync_blobs sweep, then registers all new chunks in ONE batched UNNEST INSERT - durability before visibility, and the per-chunk PG round-trips collapse into one. - Encrypted/Migration decorators forward both methods so the optimization survives encrypted-local and live-migration stacks. https://claude.ai/code/session_013Bk4BMQEvR9QxCU7QXLRwv
This commit is contained in:
@@ -53,6 +53,19 @@ impl EncryptedBlobBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt `data` into the on-disk layout: `[12-byte nonce][ciphertext + tag]`.
|
||||
fn encrypt_bytes(cipher: &Aes256Gcm, data: &[u8]) -> Result<Bytes, DomainError> {
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
let ciphertext = cipher
|
||||
.encrypt(&nonce, data)
|
||||
.map_err(|e| DomainError::internal_error("Encryption", format!("encrypt failed: {e}")))?;
|
||||
|
||||
let mut encrypted = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
|
||||
encrypted.extend_from_slice(nonce.as_slice());
|
||||
encrypted.extend_from_slice(&ciphertext);
|
||||
Ok(Bytes::from(encrypted))
|
||||
}
|
||||
|
||||
impl BlobStorageBackend for EncryptedBlobBackend {
|
||||
fn initialize(
|
||||
&self,
|
||||
@@ -113,22 +126,34 @@ impl BlobStorageBackend for EncryptedBlobBackend {
|
||||
let hash = hash.to_string();
|
||||
let cipher = self.cipher.clone();
|
||||
Box::pin(async move {
|
||||
// Encrypt in memory: nonce || ciphertext (includes GCM tag)
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
let ciphertext = cipher.encrypt(&nonce, data.as_ref()).map_err(|e| {
|
||||
DomainError::internal_error("Encryption", format!("encrypt failed: {e}"))
|
||||
})?;
|
||||
|
||||
let mut encrypted = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
|
||||
encrypted.extend_from_slice(nonce.as_slice());
|
||||
encrypted.extend_from_slice(&ciphertext);
|
||||
|
||||
inner
|
||||
.put_blob_from_bytes(&hash, Bytes::from(encrypted))
|
||||
.await
|
||||
let encrypted = encrypt_bytes(&cipher, data.as_ref())?;
|
||||
inner.put_blob_from_bytes(&hash, encrypted).await
|
||||
})
|
||||
}
|
||||
|
||||
fn put_blob_from_bytes_unsynced(
|
||||
&self,
|
||||
hash: &str,
|
||||
data: Bytes,
|
||||
) -> Pin<Box<dyn std::future::Future<Output = Result<u64, DomainError>> + Send + '_>> {
|
||||
let inner = self.inner.clone();
|
||||
let hash = hash.to_string();
|
||||
let cipher = self.cipher.clone();
|
||||
Box::pin(async move {
|
||||
let encrypted = encrypt_bytes(&cipher, data.as_ref())?;
|
||||
inner.put_blob_from_bytes_unsynced(&hash, encrypted).await
|
||||
})
|
||||
}
|
||||
|
||||
fn sync_blobs(
|
||||
&self,
|
||||
hashes: &[String],
|
||||
) -> Pin<Box<dyn std::future::Future<Output = Result<(), DomainError>> + Send + '_>> {
|
||||
// Hashes key the *plaintext* content but address the same inner
|
||||
// blobs, so the durability sweep forwards untouched.
|
||||
self.inner.sync_blobs(hashes)
|
||||
}
|
||||
|
||||
fn get_blob_stream(
|
||||
&self,
|
||||
hash: &str,
|
||||
|
||||
Reference in New Issue
Block a user