Description of the Bug
cleanup_stale_documents, cleanup_old_deleted_documents, and cleanup_inactive_active_documents in backend/app/services/cleanup.py all page through results with LIMIT 100 OFFSET offset, incrementing offset by 100 each loop — while each iteration mutates (status = "failed") or deletes the very rows the WHERE clause filters on. Since the matching set shrinks by up to 100 rows every iteration but OFFSET keeps advancing as if it were stable, the loop skips whole batches of eligible documents.
Steps to Reproduce
- Seed 250 documents that are stuck in
status == "processing" with processing_started_at older than DOC_PROCESSING_TIMEOUT_MINUTES.
- Run
cleanup_stale_documents().
- First iteration:
OFFSET 0 returns rows 1-100, marks them "failed" (removing them from the matching set, now 150 rows).
- Second iteration:
OFFSET 100 is issued against a set that now only has 150 matching rows, skipping straight past what were originally rows 101-200.
- Loop exits after processing 150 of 250 eligible documents; 100 remain stuck/orphaned.
Expected Behavior
All 250 eligible documents should be processed (marked failed / hard-deleted) in a single run, regardless of how many pages of 100 are needed.
Screenshots / Logs
No response
Environment
Any deployment; reproducible in a single test process with >100 seeded rows matching any of the three cleanup predicates — no concurrency required, purely a pagination logic bug.
GSSoC '26
Description of the Bug
cleanup_stale_documents,cleanup_old_deleted_documents, andcleanup_inactive_active_documentsinbackend/app/services/cleanup.pyall page through results withLIMIT 100 OFFSET offset, incrementingoffsetby 100 each loop — while each iteration mutates (status = "failed") or deletes the very rows the WHERE clause filters on. Since the matching set shrinks by up to 100 rows every iteration butOFFSETkeeps advancing as if it were stable, the loop skips whole batches of eligible documents.Steps to Reproduce
status == "processing"withprocessing_started_atolder thanDOC_PROCESSING_TIMEOUT_MINUTES.cleanup_stale_documents().OFFSET 0returns rows 1-100, marks them"failed"(removing them from the matching set, now 150 rows).OFFSET 100is issued against a set that now only has 150 matching rows, skipping straight past what were originally rows 101-200.Expected Behavior
All 250 eligible documents should be processed (marked failed / hard-deleted) in a single run, regardless of how many pages of 100 are needed.
Screenshots / Logs
No response
Environment
Any deployment; reproducible in a single test process with >100 seeded rows matching any of the three cleanup predicates — no concurrency required, purely a pagination logic bug.
GSSoC '26