Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rustmail-api/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ pub async fn delete_all_messages(
) -> Result<impl IntoResponse, AppError> {
let count = state.repo.delete_all().await?;
state.broadcast(WsEvent::MessagesClear);
state.repo.reclaim_after_delete_all().await;
Ok(Json(serde_json::json!({ "deleted": count })))
}

Expand Down
13 changes: 13 additions & 0 deletions crates/rustmail-storage/src/reclaim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,17 @@ mod tests {
assert!(stats(file_pages, 257 * MIB / PAGE).warrants_retention_reclaim());
}

#[tokio::test]
async fn delete_all_returns_before_it_reclaims_so_the_caller_can_announce_it_first() {
let file = file_repo("delete-all-then-reclaim").await;
fill(&file.repo, (0..SMALL_MESSAGES).map(small_message).collect()).await;

file.repo.delete_all().await.unwrap();

assert_eq!(file.repo.count().await.unwrap(), 0);
assert!(page_stats(&file.writer).await.unwrap().freelist_count > 0);
}

#[tokio::test]
async fn delete_all_leaves_no_free_pages_and_a_small_file() {
let file = file_repo("delete-all").await;
Expand All @@ -339,6 +350,7 @@ mod tests {
);

let deleted = file.repo.delete_all().await.unwrap();
file.repo.reclaim_after_delete_all().await;

assert_eq!(deleted, SMALL_MESSAGES as u64);
assert_eq!(page_stats(&file.writer).await.unwrap().freelist_count, 0);
Expand Down Expand Up @@ -435,6 +447,7 @@ mod tests {
let populated = page_stats(&pool).await.unwrap();

repo.delete_all().await.unwrap();
repo.reclaim_after_delete_all().await;

let emptied = page_stats(&pool).await.unwrap();
assert_eq!(emptied.freelist_count, 0);
Expand Down
23 changes: 15 additions & 8 deletions crates/rustmail-storage/src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,8 @@ impl MessageRepository {
Ok(())
}

/// Deletes all messages and clears the FTS5 index atomically, then gives
/// the freed pages back to the filesystem. Returns the count of deleted
/// messages.
/// Deletes all messages and clears the FTS5 index atomically. Returns the
/// count of deleted messages.
///
/// Uses FTS5's `delete-all` command rather than `DELETE FROM messages_fts`.
/// An external-content index reads the content row to work out which tokens
Expand All @@ -446,11 +445,20 @@ impl MessageRepository {
/// dropping its pages whole, so removing `messages` last has no cascade left
/// to walk.
///
/// The reclaim runs after the commit and before this returns. The messages
/// are gone once the commit lands, so a failed reclaim is logged rather than
/// reported: the pages stay on the freelist for new mail to reuse.
/// The freed pages stay on the freelist until
/// [`reclaim_after_delete_all`](Self::reclaim_after_delete_all) gives them
/// back to the filesystem.
pub async fn delete_all(&self) -> Result<u64, StorageError> {
let deleted = retry_on_lock(|| self.delete_all_once()).await?;
retry_on_lock(|| self.delete_all_once()).await
}

/// Gives the pages a [`delete_all`](Self::delete_all) freed back to the
/// filesystem, within a time budget.
///
/// Inserts keep committing between its steps. The messages are already gone,
/// so a failed reclaim is logged rather than reported: the pages stay on the
/// freelist for new mail to reuse.
pub async fn reclaim_after_delete_all(&self) {
if let Err(failure) = reclaim(
&self.writer,
ReclaimTrigger::DeleteAll,
Expand All @@ -466,7 +474,6 @@ impl MessageRepository {
"failed to reclaim disk after deleting every message"
);
}
Ok(deleted)
}

async fn delete_all_once(&self) -> Result<u64, StorageError> {
Expand Down
Loading