297 lines
9.1 KiB
Rust
297 lines
9.1 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use super::{BlobOp, Operation, ValueClass, ValueOp, key::DeserializeBigEndian, now};
|
|
use crate::{
|
|
BlobStore, Deserialize, IterateParams, SerializeInfallible, Store, U16_LEN, U32_LEN, U64_LEN,
|
|
ValueKey,
|
|
write::{BatchBuilder, BlobLink, RegistryClass},
|
|
};
|
|
use registry::{
|
|
schema::prelude::Property,
|
|
types::{EnumImpl, id::ObjectId},
|
|
};
|
|
use std::time::Instant;
|
|
use trc::{AddContext, StoreEvent};
|
|
use types::{
|
|
blob::BlobClass,
|
|
blob_hash::{BLOB_HASH_LEN, BlobHash},
|
|
};
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct BlobQuota {
|
|
pub bytes: usize,
|
|
pub count: usize,
|
|
}
|
|
|
|
impl Store {
|
|
pub async fn blob_exists(&self, hash: impl AsRef<BlobHash> + Sync + Send) -> trc::Result<bool> {
|
|
self.key_exists(ValueKey {
|
|
account_id: 0,
|
|
collection: 0,
|
|
document_id: 0,
|
|
class: ValueClass::Blob(BlobOp::Commit {
|
|
hash: hash.as_ref().clone(),
|
|
}),
|
|
})
|
|
.await
|
|
.caused_by(trc::location!())
|
|
}
|
|
|
|
pub async fn blob_has_access(
|
|
&self,
|
|
hash: impl AsRef<BlobHash> + Sync + Send,
|
|
class: impl AsRef<BlobClass> + Sync + Send,
|
|
) -> trc::Result<bool> {
|
|
let key = match class.as_ref() {
|
|
BlobClass::Reserved {
|
|
account_id,
|
|
expires,
|
|
} if *expires > now() => ValueKey {
|
|
account_id: *account_id,
|
|
collection: 0,
|
|
document_id: 0,
|
|
class: ValueClass::Blob(BlobOp::Link {
|
|
hash: hash.as_ref().clone(),
|
|
to: BlobLink::Temporary { until: *expires },
|
|
}),
|
|
},
|
|
BlobClass::Linked {
|
|
account_id,
|
|
collection,
|
|
document_id,
|
|
} => ValueKey {
|
|
account_id: *account_id,
|
|
collection: *collection,
|
|
document_id: *document_id,
|
|
class: ValueClass::Blob(BlobOp::Link {
|
|
hash: hash.as_ref().clone(),
|
|
to: BlobLink::Document,
|
|
}),
|
|
},
|
|
_ => return Ok(false),
|
|
};
|
|
|
|
self.key_exists(key).await
|
|
}
|
|
|
|
pub async fn purge_blobs_all_shards(&self, blob_store: BlobStore) -> trc::Result<()> {
|
|
for shard_index in 0u8..=255 {
|
|
self.purge_blobs(blob_store.clone(), shard_index).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn purge_blobs(&self, blob_store: BlobStore, shard_index: u8) -> trc::Result<()> {
|
|
let mut total_active = 0;
|
|
let mut total_deleted = 0;
|
|
let started = Instant::now();
|
|
|
|
// Validate linked blobs
|
|
let mut from_hash = BlobHash::default();
|
|
let mut to_hash = BlobHash::new_max();
|
|
from_hash.0[0] = shard_index;
|
|
to_hash.0[0] = shard_index;
|
|
let from_key = ValueKey {
|
|
account_id: 0,
|
|
collection: 0,
|
|
document_id: 0,
|
|
class: ValueClass::Blob(BlobOp::Commit { hash: from_hash }),
|
|
};
|
|
let to_key = ValueKey {
|
|
account_id: u32::MAX,
|
|
collection: u8::MAX,
|
|
document_id: u32::MAX,
|
|
class: ValueClass::Blob(BlobOp::Link {
|
|
hash: to_hash,
|
|
to: BlobLink::Document,
|
|
}),
|
|
};
|
|
|
|
let mut state = BlobPurgeState::new();
|
|
self.iterate(
|
|
IterateParams::new(from_key, to_key).ascending(),
|
|
|key, value| {
|
|
let hash =
|
|
BlobHash::try_from_hash_slice(key.get(0..BLOB_HASH_LEN).ok_or_else(|| {
|
|
trc::Error::corrupted_key(key, value.into(), trc::location!())
|
|
})?)
|
|
.unwrap();
|
|
|
|
state.update_hash(hash);
|
|
state.process_key(key, value)?;
|
|
|
|
Ok(true)
|
|
},
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
state.finalize(BlobHash::default());
|
|
|
|
// Delete expired or unlinked blobs
|
|
for (_, op) in &state.delete_keys {
|
|
if let BlobOp::Commit { hash } = op {
|
|
blob_store
|
|
.delete_blob(hash.as_ref())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
}
|
|
|
|
// Delete hashes
|
|
let mut batch = BatchBuilder::new();
|
|
for (account_id, op) in state.delete_keys {
|
|
if batch.is_large_batch() {
|
|
self.write(batch.build_all())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
batch = BatchBuilder::new();
|
|
}
|
|
|
|
if let Some(account_id) = account_id {
|
|
batch.with_account_id(account_id);
|
|
}
|
|
|
|
batch.any_op(Operation::Value {
|
|
class: ValueClass::Blob(op),
|
|
op: ValueOp::Clear,
|
|
});
|
|
}
|
|
for (account_id, object_id) in state.delete_registry {
|
|
if batch.is_large_batch() {
|
|
self.write(batch.build_all())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
batch = BatchBuilder::new();
|
|
}
|
|
|
|
let item_id = object_id.id().id();
|
|
let object_id = object_id.object().to_id();
|
|
|
|
batch
|
|
.clear(ValueClass::Registry(RegistryClass::Index {
|
|
index_id: Property::AccountId.to_id(),
|
|
object_id,
|
|
item_id,
|
|
key: (account_id as u64).serialize(),
|
|
}))
|
|
.clear(ValueClass::Registry(RegistryClass::Item {
|
|
object_id,
|
|
item_id,
|
|
}));
|
|
}
|
|
if !batch.is_empty() {
|
|
self.write(batch.build_all())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
total_active += state.total_active - 1; // Exclude default hash
|
|
total_deleted += state.total_deleted;
|
|
|
|
trc::event!(
|
|
Store(StoreEvent::BlobStorePurged),
|
|
Id = shard_index as u16,
|
|
Expires = total_deleted,
|
|
Total = total_active,
|
|
Elapsed = started.elapsed()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct BlobPurgeState {
|
|
last_hash: BlobHash,
|
|
last_hash_is_linked: bool,
|
|
delete_keys: Vec<(Option<u32>, BlobOp)>,
|
|
delete_registry: Vec<(u32, ObjectId)>,
|
|
now: u64,
|
|
total_deleted: u64,
|
|
total_active: u64,
|
|
}
|
|
|
|
impl BlobPurgeState {
|
|
fn new() -> Self {
|
|
Self {
|
|
last_hash: BlobHash::default(),
|
|
last_hash_is_linked: true, // Avoid deleting non-existing last_hash on first iteration
|
|
delete_keys: Vec::new(),
|
|
delete_registry: Vec::new(),
|
|
now: now(),
|
|
total_deleted: 0,
|
|
total_active: 0,
|
|
}
|
|
}
|
|
|
|
pub fn update_hash(&mut self, hash: BlobHash) {
|
|
if self.last_hash != hash {
|
|
self.finalize(hash);
|
|
self.last_hash_is_linked = false;
|
|
}
|
|
}
|
|
|
|
pub fn finalize(&mut self, new_hash: BlobHash) {
|
|
if !self.last_hash_is_linked {
|
|
self.total_deleted += 1;
|
|
self.delete_keys.push((
|
|
None,
|
|
BlobOp::Commit {
|
|
hash: std::mem::replace(&mut self.last_hash, new_hash),
|
|
},
|
|
));
|
|
} else {
|
|
self.total_active += 1;
|
|
self.last_hash = new_hash;
|
|
}
|
|
}
|
|
|
|
pub fn process_key(&mut self, key: &[u8], value: &[u8]) -> trc::Result<()> {
|
|
const TEMP_LINK: usize = BLOB_HASH_LEN + U32_LEN + U64_LEN;
|
|
const DOC_LINK: usize = BLOB_HASH_LEN + U64_LEN + 1;
|
|
const ID_LINK: usize = BLOB_HASH_LEN + U64_LEN;
|
|
|
|
match key.len() {
|
|
BLOB_HASH_LEN => {
|
|
// Main blob entry
|
|
Ok(())
|
|
}
|
|
TEMP_LINK => {
|
|
// Temporary link
|
|
let until = key.deserialize_be_u64(BLOB_HASH_LEN + U32_LEN)?;
|
|
if until <= self.now {
|
|
let account_id = key.deserialize_be_u32(BLOB_HASH_LEN)?;
|
|
self.delete_keys.push((
|
|
Some(account_id),
|
|
BlobOp::Link {
|
|
hash: self.last_hash.clone(),
|
|
to: BlobLink::Temporary { until },
|
|
},
|
|
));
|
|
if value.len() == U16_LEN + U64_LEN {
|
|
self.delete_registry
|
|
.push((account_id, ObjectId::deserialize(value)?));
|
|
}
|
|
} else {
|
|
self.last_hash_is_linked = true;
|
|
}
|
|
Ok(())
|
|
}
|
|
DOC_LINK | ID_LINK => {
|
|
// Document/Id link
|
|
self.last_hash_is_linked = true;
|
|
Ok(())
|
|
}
|
|
_ => Err(trc::Error::corrupted_key(
|
|
key,
|
|
value.into(),
|
|
trc::location!(),
|
|
)),
|
|
}
|
|
}
|
|
}
|