Files: wake the task manager when a deletion queues its undelete task #234

Merged
jcoffey-dev merged 1 commits from fix/file-deletes-wake-tasks into main 2026-10-29 16:34:19 +00:00
7 changed files with 153 additions and 0 deletions

No files matched your search

+5
View File
@@ -653,6 +653,8 @@ async fn overwrite_and_delete_item(
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
// inbuxa: the deleted source queued its undelete task
server.notify_task_queue();
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
@@ -807,6 +809,9 @@ async fn move_item(
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
// inbuxa: a move to another account deletes the source, which queued
// its undelete task
server.notify_task_queue();
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
+4
View File
@@ -129,6 +129,10 @@ impl DestroyArchive<Vec<u32>> {
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
// inbuxa: deleted files queue the task that notes them for
// undelete; wake the task manager rather than leave the task to
// its next periodic look at the queue
server.notify_task_queue();
}
Ok(())
+2
View File
@@ -495,6 +495,8 @@ impl FileNodeCopy for Server {
.await
.and_then(|ids| ids.last_change_id(account_id))
.caused_by(trc::location!())?;
// inbuxa: replaced files queued their undelete tasks
self.notify_task_queue();
response.new_state = State::Exact(change_id);
}
+2
View File
@@ -764,6 +764,8 @@ impl FileNodeSet for Server {
.await
.and_then(|ids| ids.last_change_id(account_id))
.caused_by(trc::location!())?;
// inbuxa: destroyed files queued their undelete tasks
self.notify_task_queue();
response.new_state = State::Exact(change_id).into();
}
+33
View File
@@ -341,6 +341,39 @@ pub async fn wait_for_tasks(server: &Server, skip_not_due: bool, skip_permanent_
}
}
/// inbuxa: waits for the task queue to empty, giving up after `within`, and
/// returns the task still queued then. The task manager only looks at the
/// queue again when woken or every few minutes, so a write that queues a
/// task without waking it shows up here as a task that sits.
pub async fn task_left_after(server: &Server, within: std::time::Duration) -> Option<Task> {
let give_up_at = std::time::Instant::now() + within;
loop {
let mut pending = None;
server
.core
.storage
.data
.iterate(
IterateParams::new(
ValueKey::from(ValueClass::TaskQueue(TaskQueueClass::Task { id: 0 })),
ValueKey::from(ValueClass::TaskQueue(TaskQueueClass::Task { id: u64::MAX })),
)
.ascending(),
|_, value| {
pending = Some(Task::deserialize(value)?);
Ok(false)
},
)
.await
.unwrap();
if pending.is_none() || std::time::Instant::now() > give_up_at {
return pending;
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
}
pub async fn assert_is_empty(server: &Server, include_registry: bool) {
// Wait for pending index tasks
wait_for_tasks(server, false, false).await;
+103
View File
@@ -0,0 +1,103 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
// inbuxa: deleting a file queues the task that archives it for undelete
// (UD-1). Each way of deleting one must wake the task manager, or that task
// sits until its next periodic look at the queue, minutes later.
use crate::utils::{server::TestServer, storage::task_left_after};
use hyper::StatusCode;
use serde_json::json;
use std::time::Duration;
const BASE: &str = "/dav/file/john%40example.com";
pub async fn test(test: &TestServer) {
println!("Running file deletion task tests...");
let account = test.account("[email protected]");
let client = account.webdav_client();
test.wait_for_tasks().await;
// WebDAV DELETE
let path = format!("{BASE}/tasks-1.txt");
put(test, &path).await;
client
.request("DELETE", &path, "")
.await
.with_status(StatusCode::NO_CONTENT);
assert_tasks_run(test, "WebDAV DELETE").await;
// WebDAV COPY over an existing file deletes the file it replaces
let (from, to) = (format!("{BASE}/tasks-2.txt"), format!("{BASE}/tasks-3.txt"));
put(test, &from).await;
put(test, &to).await;
client
.request_with_headers("COPY", &from, [("destination", to.as_str())], "")
.await
.with_status(StatusCode::NO_CONTENT);
assert_tasks_run(test, "WebDAV COPY over a file").await;
// WebDAV MOVE over an existing file deletes the source
client
.request_with_headers("MOVE", &from, [("destination", to.as_str())], "")
.await
.with_status(StatusCode::NO_CONTENT);
assert_tasks_run(test, "WebDAV MOVE over a file").await;
client
.request("DELETE", &to, "")
.await
.with_status(StatusCode::NO_CONTENT);
assert_tasks_run(test, "WebDAV DELETE").await;
// JMAP FileNode/set destroy
let response = account
.jmap_method_calls(json!([
["Blob/upload", {
"accountId": account.id_string(),
"create": { "file": { "data": [{ "data:asText": "tasks" }] } }
}, "0"],
["FileNode/set", {
"accountId": account.id_string(),
"create": { "i0": { "name": "tasks-4.txt", "parentId": null, "blobId": "#file" } }
}, "1"]
]))
.await
.into_inner();
let file_id = response
.pointer("/methodResponses/1/1/created/i0/id")
.and_then(|id| id.as_str())
.unwrap_or_else(|| panic!("{response}"))
.to_string();
let response = account
.jmap_method_calls(json!([
["FileNode/set", {
"accountId": account.id_string(),
"destroy": [&file_id]
}, "0"]
]))
.await
.into_inner();
assert_eq!(
response.pointer("/methodResponses/0/1/destroyed/0"),
Some(&json!(file_id)),
"{response}"
);
assert_tasks_run(test, "JMAP FileNode/set destroy").await;
}
async fn put(test: &TestServer, path: &str) {
test.account("[email protected]")
.webdav_client()
.request_with_headers("PUT", path, [("content-type", "text/plain")], "tasks")
.await
.with_status(StatusCode::CREATED);
}
async fn assert_tasks_run(test: &TestServer, what: &str) {
if let Some(task) = task_left_after(&test.server, Duration::from_secs(10)).await {
panic!("{what}: a task was still queued after 10 s: {task:?}");
}
}
+4
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use crate::utils::server::TestServerBuilder;
@@ -32,6 +34,7 @@ pub mod cal_query;
pub mod cal_scheduling;
pub mod card_query;
pub mod copy_move;
pub mod file_tasks; // inbuxa: file deletes wake the task manager
pub mod lock;
pub mod mkcol;
pub mod multiget;
@@ -183,6 +186,7 @@ pub async fn webdav_tests() {
}
basic::test(&test).await;
put_get::test(&test).await;
file_tasks::test(&test).await;
mkcol::test(&test).await;
copy_move::test(&test, assisted_discovery).await;
prop::test(&test, assisted_discovery).await;