use super::{
CONTENT_CONTROL_BUCKET, CONTENT_TOKEN_BUCKET, CONTENT_TOKEN_INDEX_BUCKET, ContentAttachment,
ContentAttachmentScope, ContentChangeId, ContentControlRecord, ContentQuarantineRecord,
ContentReclaimBlocker, ContentReclaimGraceRecord, ContentReclaimSweepRecord,
ContentReclaimSweepRecordState, Error, Result, Transaction, UploadToken, UploadTokenRecord,
content_control_key, content_quarantine_key, content_reclaim_grace_key,
content_reclaim_sweep_key, content_token_index_key, current_epoch_millis, upload_token_key,
};
impl Transaction {
pub fn consume_upload_token_sync(
&mut self,
token: UploadToken,
expected_scope: ContentAttachmentScope,
change_id: ContentChangeId,
) -> Result<ContentAttachment> {
let now_unix_ms = current_epoch_millis()?;
self.consume_upload_token_at_sync(token, expected_scope, change_id, now_unix_ms)
}
pub(crate) fn consume_upload_token_at_sync(
&mut self,
token: UploadToken,
expected_scope: ContentAttachmentScope,
change_id: ContentChangeId,
now_unix_ms: u64,
) -> Result<ContentAttachment> {
let key = upload_token_key(token);
self.require_consistent_staged_token(&key, change_id)?;
let bytes = match self.get_internal_bucket_sync(CONTENT_TOKEN_BUCKET, &key) {
Ok(Some(bytes)) => bytes,
Ok(None) | Err(Error::BucketMissing { .. }) => {
return Err(Error::UploadTokenInvalid);
}
Err(error) => return Err(error),
};
let consumed = UploadTokenRecord::decode(&bytes, token)?.consume(
expected_scope,
change_id,
now_unix_ms,
)?;
let attachment = consumed.attachment();
self.stage_content_activity_sync(
attachment.scope().storage_domain_id(),
attachment.content_id(),
)?;
self.delete_internal_bucket(
CONTENT_TOKEN_INDEX_BUCKET,
content_token_index_key(
attachment.scope().storage_domain_id(),
attachment.content_id(),
token,
),
)?;
self.put_internal_bucket(CONTENT_TOKEN_BUCKET, key.clone(), consumed.encode())?;
self.record_extension_claim(key, change_id.to_bytes());
Ok(attachment)
}
pub(crate) fn stage_content_activity_sync(
&mut self,
storage_domain_id: crate::StorageDomainId,
content_id: crate::ContentId,
) -> Result<()> {
let sweep_key = content_reclaim_sweep_key(storage_domain_id, content_id);
if let Some(bytes) = self.get_internal_bucket_sync(CONTENT_CONTROL_BUCKET, &sweep_key)? {
let sweep = ContentReclaimSweepRecord::decode(&bytes, storage_domain_id, content_id)?;
match sweep.state {
ContentReclaimSweepRecordState::Prepared => {
return Err(Error::ContentReclaimBlocked {
blocker: ContentReclaimBlocker::SweepPrepared {
prepared_at_commit_seq: sweep.prepared_at.as_u64(),
},
});
}
ContentReclaimSweepRecordState::Reclaimed => {
self.delete_internal_bucket(CONTENT_CONTROL_BUCKET, sweep_key)?;
}
}
}
let key = content_control_key(storage_domain_id, content_id);
if let Some(bytes) = self.get_internal_bucket_sync(CONTENT_CONTROL_BUCKET, &key)? {
ContentControlRecord::decode(&bytes, storage_domain_id, content_id)?;
}
let quarantine_key = content_quarantine_key(storage_domain_id, content_id);
let quarantine = self
.get_internal_bucket_sync(CONTENT_CONTROL_BUCKET, &quarantine_key)?
.map(|bytes| ContentQuarantineRecord::decode(&bytes, storage_domain_id, content_id))
.transpose()?;
let grace_key = content_reclaim_grace_key(storage_domain_id, content_id);
let grace = self
.get_internal_bucket_sync(CONTENT_CONTROL_BUCKET, &grace_key)?
.map(|bytes| ContentReclaimGraceRecord::decode(&bytes, storage_domain_id, content_id))
.transpose()?;
if let Some(grace) = grace
&& !quarantine.is_some_and(|record| grace.matches_quarantine(record))
{
return Err(Error::Corruption {
message: "content reclaim grace differs from its quarantine fence".to_owned(),
});
}
if quarantine.is_some() {
self.delete_internal_bucket(CONTENT_CONTROL_BUCKET, quarantine_key)?;
}
if grace.is_some() {
self.delete_internal_bucket(CONTENT_CONTROL_BUCKET, grace_key)?;
}
self.stage_active_content_control(storage_domain_id, content_id, key)
}
fn require_consistent_staged_token(
&self,
key: &[u8],
change_id: ContentChangeId,
) -> Result<()> {
if self
.extension_claim(key)
.is_some_and(|existing| existing != change_id.to_bytes())
{
return Err(Error::UploadTokenAlreadyConsumed);
}
Ok(())
}
pub async fn consume_upload_token(
&mut self,
token: UploadToken,
expected_scope: ContentAttachmentScope,
change_id: ContentChangeId,
) -> Result<ContentAttachment> {
let now_unix_ms = current_epoch_millis()?;
self.consume_upload_token_at(token, expected_scope, change_id, now_unix_ms)
.await
}
pub(crate) async fn consume_upload_token_at(
&mut self,
token: UploadToken,
expected_scope: ContentAttachmentScope,
change_id: ContentChangeId,
now_unix_ms: u64,
) -> Result<ContentAttachment> {
let key = upload_token_key(token);
self.require_consistent_staged_token(&key, change_id)?;
let bytes = match self.get_internal_bucket(CONTENT_TOKEN_BUCKET, &key).await {
Ok(Some(bytes)) => bytes,
Ok(None) | Err(Error::BucketMissing { .. }) => {
return Err(Error::UploadTokenInvalid);
}
Err(error) => return Err(error),
};
let consumed = UploadTokenRecord::decode(&bytes, token)?.consume(
expected_scope,
change_id,
now_unix_ms,
)?;
let attachment = consumed.attachment();
self.stage_content_activity(
attachment.scope().storage_domain_id(),
attachment.content_id(),
)
.await?;
self.delete_internal_bucket(
CONTENT_TOKEN_INDEX_BUCKET,
content_token_index_key(
attachment.scope().storage_domain_id(),
attachment.content_id(),
token,
),
)?;
self.put_internal_bucket(CONTENT_TOKEN_BUCKET, key.clone(), consumed.encode())?;
self.record_extension_claim(key, change_id.to_bytes());
Ok(attachment)
}
pub(crate) async fn stage_content_activity(
&mut self,
storage_domain_id: crate::StorageDomainId,
content_id: crate::ContentId,
) -> Result<()> {
let sweep_key = content_reclaim_sweep_key(storage_domain_id, content_id);
if let Some(bytes) = self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &sweep_key)
.await?
{
let sweep = ContentReclaimSweepRecord::decode(&bytes, storage_domain_id, content_id)?;
match sweep.state {
ContentReclaimSweepRecordState::Prepared => {
return Err(Error::ContentReclaimBlocked {
blocker: ContentReclaimBlocker::SweepPrepared {
prepared_at_commit_seq: sweep.prepared_at.as_u64(),
},
});
}
ContentReclaimSweepRecordState::Reclaimed => {
self.delete_internal_bucket(CONTENT_CONTROL_BUCKET, sweep_key)?;
}
}
}
let key = content_control_key(storage_domain_id, content_id);
if let Some(bytes) = self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &key)
.await?
{
ContentControlRecord::decode(&bytes, storage_domain_id, content_id)?;
}
let quarantine_key = content_quarantine_key(storage_domain_id, content_id);
let quarantine = self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &quarantine_key)
.await?
.map(|bytes| ContentQuarantineRecord::decode(&bytes, storage_domain_id, content_id))
.transpose()?;
let grace_key = content_reclaim_grace_key(storage_domain_id, content_id);
let grace = self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &grace_key)
.await?
.map(|bytes| ContentReclaimGraceRecord::decode(&bytes, storage_domain_id, content_id))
.transpose()?;
if let Some(grace) = grace
&& !quarantine.is_some_and(|record| grace.matches_quarantine(record))
{
return Err(Error::Corruption {
message: "content reclaim grace differs from its quarantine fence".to_owned(),
});
}
if quarantine.is_some() {
self.delete_internal_bucket(CONTENT_CONTROL_BUCKET, quarantine_key)?;
}
if grace.is_some() {
self.delete_internal_bucket(CONTENT_CONTROL_BUCKET, grace_key)?;
}
self.stage_active_content_control(storage_domain_id, content_id, key)
}
pub(crate) async fn stage_content_read_activity(
&mut self,
storage_domain_id: crate::StorageDomainId,
content_id: crate::ContentId,
) -> Result<()> {
let sweep_key = content_reclaim_sweep_key(storage_domain_id, content_id);
if let Some(bytes) = self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &sweep_key)
.await?
{
let sweep = ContentReclaimSweepRecord::decode(&bytes, storage_domain_id, content_id)?;
return match sweep.state {
ContentReclaimSweepRecordState::Prepared => Err(Error::ContentReclaimBlocked {
blocker: ContentReclaimBlocker::SweepPrepared {
prepared_at_commit_seq: sweep.prepared_at.as_u64(),
},
}),
ContentReclaimSweepRecordState::Reclaimed => Err(Error::ContentNotFound {
storage_domain_id: storage_domain_id.to_string(),
content_id: content_id.to_string(),
}),
};
}
let quarantine_key = content_quarantine_key(storage_domain_id, content_id);
if let Some(bytes) = self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &quarantine_key)
.await?
{
let quarantine =
ContentQuarantineRecord::decode(&bytes, storage_domain_id, content_id)?;
return Err(Error::ContentQuarantined {
quarantined_at: quarantine.quarantined_at,
});
}
let grace_key = content_reclaim_grace_key(storage_domain_id, content_id);
if self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &grace_key)
.await?
.is_some()
{
return Err(Error::Corruption {
message: "content reclaim grace exists without its quarantine fence".to_owned(),
});
}
let key = content_control_key(storage_domain_id, content_id);
if let Some(bytes) = self
.get_internal_bucket(CONTENT_CONTROL_BUCKET, &key)
.await?
{
ContentControlRecord::decode(&bytes, storage_domain_id, content_id)?;
}
self.stage_active_content_control(storage_domain_id, content_id, key)
}
fn stage_active_content_control(
&mut self,
storage_domain_id: crate::StorageDomainId,
content_id: crate::ContentId,
key: Vec<u8>,
) -> Result<()> {
let active = ContentControlRecord::active(storage_domain_id, content_id);
self.put_internal_bucket_with_commit_sequence(
CONTENT_CONTROL_BUCKET,
key,
&active.encode_prefix(),
&[],
)
}
}