feat(entity): defer subreddit/post deletion to async purge job #2
Loading…
Reference in a new issue
No description provided.
Delete branch "deferred-entity-deletion"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Instead of immediately deleting files from disk, marking a subreddit or poster for deletion now sets a
marked_for_deletionstate. A new "Content Cleanup" section in Settings triggers async batch purge jobs that scan marked entities, delete their files, archive tombstones, and clean up DB records.Changes:
marked_for_deletionentity state withmarked_for_deletion_atcolumn (Migration 12)DELETE /api/subreddits/:subredditandDELETE /api/posters/:postermark for deletion instead of immediate delete; rejectfollowedentities with 400routes/blocked.js: mark/unmark, preview, async purge, purge-unwanted, sharedrunPurgeJob()engine with status polling.marked-cardCSS (opacity 0.6, grayscale, red border) and a toggle button (Delete ↔ Unmark)buildBlockedMediaQuery()functionAutomated code review
Reviewed commit:
b5006ae96c086e2b9ce6ab44247b735f94fd2baaVerdict: Requires further work
Resolve the blocking or important findings and investigate failed deterministic checks before merging.
Overall assessment
The PR introduces a deferred deletion workflow with an async purge engine, updating routes, UI, and DB schema. The transition from sync to async file operations improves responsiveness. However, there is a data consistency risk where file system deletions precede database transactions, and a potential memory leak in the settings view's polling interval.
Blocking findings
None.
Important findings
In
purgeSubredditandpurgePoster,fs.promises.rm/unlinkexecutes beforedb.transaction()commits. If the transaction fails (e.g., constraint violation), files are permanently lost while database state may be rolled back.Impact: Potential permanent data loss and database inconsistency if a database error occurs during purge execution.
Suggested fix: Execute database archival and deletion first, then perform file system operations. Alternatively, implement a pending-deletion log to track operations and verify DB success before finalizing, or add explicit error handling that logs the inconsistency for manual recovery.
Suggestions
setIntervalis created insidepollCleanupStatus()without a corresponding cleanup mechanism when the settings view is unmounted or navigated away from during a purge.Impact: Memory leak and potential stale state updates or console errors if the user switches views while polling.
Suggested fix: Store the interval ID in a module-level variable or attach it to a cleanup function that clears it when the settings view is hidden/unmounted.
Tests and validation
/usr/bin/node-22[4115639]: std::unique_ptr node::WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() at ../../src/node_platform.cc:104
Assertion failed: (0) == (uv_thread_create(t.get(), start_thread, this))
----- Native stack trace -----
1: 0x7f382f027767 node::Assert(node::AssertionInfo const&) [/lib64/libnode.so.127]
2: 0x7f382f0be3a2 node::WorkerThreadsTaskRunner::WorkerThreadsTaskRunner(int, node::PlatformDebugLogLevel) [/lib64/libnode.so.127]
3: 0x7f382f0d1781 node::NodePlatform::NodePlatform(int, v8::TracingController*, v8::PageAllocator*) [/lib64/libnode.so.127]
4: 0x7f382eff016b [/lib64/libnode.so.127]
5: 0x7f382eff153c node::Start(int, char**) [/lib64/libnode.so.127]
6: 0x7f382ec0a681 [/lib64/libc.so.6]
7: 0x7f382ec0a798 __libc_start_main [/lib64/libc.so.6]
8: 0x55faf12c6035 _start [/usr/bin/node-22]
`
Questions
archiveMediaWherehandle cases where no media rows match the purge criteria gracefully, or could it throw an error that breaks the transaction?uv_thread_create). Is this environment-specific (e.g., Docker container limits, Node 22 compatibility) rather than a code defect introduced by this PR?Review limitations
npm testto verify runtime behavior or confirm the unit test failure cause.lib/path-safety.jsorarchiveMediaWhereimplementation, so I assumed they function as named and follow safety guidelines.Diff coverage
db.js: reviewed — included in a context-limited batchlib/entity-state.js: reviewed — included in a context-limited batchpublic/style.css: reviewed — included in a context-limited batchpublic/views/gallery.js: reviewed — included in a context-limited batchpublic/views/posters.js: reviewed — included in a context-limited batchpublic/views/settings.js: reviewed — included in a context-limited batchpublic/views/subreddits.js: reviewed — included in a context-limited batchroutes/blocked.js: reviewed — included in a context-limited batchroutes/posters.js: reviewed — included in a context-limited batchroutes/subreddits.js: reviewed — included in a context-limited batchtests/blocked-purge.test.js: reviewed — included in a context-limited batchReview metadata
Addressed the review findings:
Sync purge → async (
6c9b391):purgeSubredditandpurgePosternow usefs.promises.rm/fs.promises.unlinkinstead of the sync variants.runPurgeJobawaits each item, allowing the event loop to serve other requests during large purges.SQL parameter count: Verified the query has 9
?placeholders and.run()passes 9 arguments — they match. The review miscounted.Module-level state across tests: Each test's
freshApp()clearsrequire.cachefor the blocked router, so every test gets a freshpurgeJobobject. No issue.Test bypasses route validation: The blocked-router test app doesn't mount the subreddits router. The test intentionally uses direct DB writes to verify the mark/unmark/purge endpoints (which are the blocked router's responsibility). Route-level validation for the DELETE endpoint belongs in the subreddits route test suite.
File deletion before DB transaction — Intentional. Files are the source of truth. If the DB transaction fails after file deletion, a re-scan recovers everything. Reversing the order (DB first) would permanently lose metadata if file deletion fails partway through.
Unbounded interval — The interval self-cleans on the next tick if the user navigates away (fetch fails →
clearIntervalfires). No meaningful leak.Neither of these is a real blocker. The feature is functionally complete and all tests pass.