Switches from custom process pool to ProcessPoolExecutor. - #124235
Switches from custom process pool to ProcessPoolExecutor.#124235ellisonmarks wants to merge 1 commit into
Conversation
Aside from using a stdlib component, this gives us access to the max_tasks_per_child parameter, so we can restart the workers after they process a defined number of tasks. The idea here is to control memory growth in the worker processes after handling many tasks. Starting at 100 as a sort of arbitrary number.
|
|
||
| # Ensure all tasks are completed before exiting | ||
| task_queue.join() | ||
| future_wait(futures) |
There was a problem hiding this comment.
Bug: The code uses future_wait but doesn't check for exceptions in completed futures. If a worker process crashes, associated cleanup tasks will silently fail, leading to incomplete data deletion.
Severity: HIGH
Suggested Fix
After future_wait(futures) returns, iterate through the futures list and call .result() or .exception() on each one to check for and handle any exceptions, particularly BrokenProcessPool. This will ensure that failures in worker processes are detected and logged, preventing silent data loss. Alternatively, consider replacing the executor.submit loop and wait call with executor.map, which propagates exceptions from workers.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/sentry/runner/commands/cleanup.py#L892
Potential issue: The cleanup process uses a `ProcessPoolExecutor` to run deletion tasks
in parallel and waits for them to complete using `future_wait` (an alias for
`concurrent.futures.wait`). If a worker process crashes due to an OOM error or segfault,
any futures assigned to it will fail with a `BrokenProcessPool` exception. However, the
`future_wait` function does not propagate exceptions from the futures. Since the code
never checks the result of individual futures via `.result()` or `.exception()`, these
failures are silently ignored. This results in cleanup tasks being skipped, leading to
incomplete data deletion and violation of data retention policies.
Also affects:
src/sentry/runner/commands/cleanup.py:953~953src/sentry/runner/commands/cleanup.py:1019~1019
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Neither did the original code, and I don't intend to change that in this PR.
Backend Test FailuresFailures on
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bf9014a. Configure here.
| for chunk in q.iterator(chunk_size=DELETES_BY_PROJECT_CHUNK_SIZE): | ||
| task_queue.put((imp, chunk, project_id)) | ||
| chunk_count += 1 | ||
| futures.append(executor.submit(task_execution, imp, chunk, project_id)) |
There was a problem hiding this comment.
Unbounded futures can exhaust memory
High Severity
Every chunk is now submitted up front and retained as a Future, with no replacement for the old Queue(1000) backpressure. run_bulk_deletes_by_project and run_bulk_deletes_in_deletes wait only after scheduling all models and projects, so a large cleanup can hold millions of chunk tuples in the parent and OOM it.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit bf9014a. Configure here.
|
|
||
| # Ensure all tasks are completed before exiting | ||
| task_queue.join() | ||
| future_wait(futures) |
There was a problem hiding this comment.
Worker failures are silently ignored
Medium Severity
future_wait only waits for completion and never inspects Future results. Process crashes, pickling errors, and initializer failures (BrokenProcessPool) therefore finish as success, so chunks can be skipped while cleanup still reports that it completed.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit bf9014a. Configure here.


Aside from using a stdlib component, this gives us access to the max_tasks_per_child parameter, so we can restart the workers after they process a defined number of tasks. The idea here is to control memory growth in the worker processes after handling many tasks. Starting at 100 as a sort of arbitrary number.