fix(ugc): purging and the storage cap delete folders off the main thread

Purge all ran on the UGC server's main thread inside the dashboard's
request: it measured and deleted every stored folder, then updated rows one
by one, and the main thread (downloads, the master link, the database)
waited minutes for it. Now a purge answers at once and a file thread lists,
measures and deletes the folders; the main thread applies what it removed
and updates the rows a few hundred a tick. No new item is started while a
purge runs, and items being made when it starts are left alone, so no worker
writes into a folder being deleted. One purge at a time. The storage cap's
clean-up lists and deletes on the same thread. The UGC page shows the
purge's progress (/status purge).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 19:19:07 -05:00
parent 3c004fd36d
commit bdb4c39db3
4 changed files with 245 additions and 84 deletions

View File

@@ -495,9 +495,10 @@ namespace {
const auto after = body->value("after", std::string("on_demand"));
request.after = after == "now" ? UgcProcessor::eAfterDelete::NOW : after == "gone" ? UgcProcessor::eAfterDelete::GONE : UgcProcessor::eAfterDelete::ON_DEMAND;
const auto result = g_Processor->Delete(request);
LOG("Deleted the files of %zu item(s) (%llu bytes) for the dashboard", result.deleted, static_cast<unsigned long long>(result.bytes));
LOG(result.started ? "Purge started for the dashboard (%zu item(s) to look at, 0: all)" : "Purge not started: %zu", result.started ? result.queued : result.notes.size());
reply.status = eHTTPStatusCode::OK;
reply.message = nlohmann::json{ { "success", true }, { "deleted", result.deleted }, { "bytes", result.bytes }, { "busy", result.busy }, { "notes", result.notes } }.dump();
reply.message = nlohmann::json{ { "success", true }, { "deleted", result.deleted }, { "bytes", result.bytes }, { "busy", result.busy }, { "notes", result.notes },
{ "started", result.started }, { "queued", result.queued } }.dump();
} });
Game::web.RegisterHTTPRoute({ .path = "/status", .method = eHTTPMethod::GET, .middleware = {},