Skip to content

Fix memory leak in _tasks_by_key cleanup#309

Merged
chrisguidry merged 1 commit intomainfrom
fix-tasks-by-key-leak
Jan 27, 2026
Merged

Fix memory leak in _tasks_by_key cleanup#309
chrisguidry merged 1 commit intomainfrom
fix-tasks-by-key-leak

Conversation

@chrisguidry
Copy link
Owner

The worker was storing tasks in _tasks_by_key[execution.key] but then trying to remove them with _tasks_by_key.pop(task.get_name()). Since task names are formatted as "docket-name - task:key" while the dict key is just "key", the pop never found a match and entries accumulated forever.

Each leaked entry held a reference to the asyncio.Task and its associated Execution, so memory grew linearly with task count.

🤖 Generated with Claude Code

The worker was storing tasks in `_tasks_by_key[execution.key]` but then
trying to remove them with `_tasks_by_key.pop(task.get_name())`. Since
task names are formatted as "docket-name - task:key" while the dict key
is just "key", the pop never found a match and entries accumulated
forever.

Each leaked entry held a reference to the asyncio.Task and its
associated Execution, so memory grew linearly with task count.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@github-actions
Copy link

📚 Documentation has been built for this PR!

You can download the documentation directly here:
https://github.com/chrisguidry/docket/actions/runs/21379980295/artifacts/5265472654

@codecov-commenter
Copy link

codecov-commenter commented Jan 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.61%. Comparing base (8e06cdb) to head (362d7c2).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #309   +/-   ##
=======================================
  Coverage   98.61%   98.61%           
=======================================
  Files          99       99           
  Lines        9906     9906           
  Branches      483      483           
=======================================
  Hits         9769     9769           
  Misses        121      121           
  Partials       16       16           
Flag Coverage Δ
python-3.10 98.61% <100.00%> (ø)
python-3.11 97.34% <100.00%> (ø)
python-3.12 98.61% <100.00%> (ø)
python-3.13 98.61% <100.00%> (ø)
python-3.14 98.60% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/docket/worker.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chrisguidry chrisguidry merged commit c5a2b6d into main Jan 27, 2026
40 checks passed
@chrisguidry chrisguidry deleted the fix-tasks-by-key-leak branch January 27, 2026 00:52
chrisguidry added a commit that referenced this pull request Jan 27, 2026
Adds tests that verify Worker and StrikeList internal data structures are
properly cleaned up after task completion and context exit. These tests
would have caught the memory leak fixed in #309 where `_tasks_by_key`
entries accumulated because we were using `task.get_name()` instead of
`execution.key` for cleanup.

Worker invariant tests check:
- `_tasks_by_key` is empty after `run_until_finished()`
- `_tasks_by_key` doesn't grow across multiple batches
- `_execution_counts` is cleared after `run_at_most()`
- Internal attributes are deleted after `__aexit__`
- Cleanup works with failing tasks and varied task types
- SharedContext ContextVars are properly reset

StrikeList invariant tests check:
- `_conditions` only contains default after removing temp conditions
- No empty dicts remain in `task_strikes`/`parameter_strikes` after restore

Also reorganizes tests into subdirectories:
- `tests/worker/` for worker-related tests
- `tests/instrumentation/` for metrics and tracing tests
- Renames `test_synced_strikelist.py` to `test_strikelist.py`

Adds `--import-mode=importlib` to pytest config to support same-named
test files in different directories.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
chrisguidry added a commit that referenced this pull request Jan 27, 2026
)

Adds tests that verify Worker and StrikeList internal data structures
are
properly cleaned up after task completion and context exit. These tests
would have caught the memory leak fixed in #309 where `_tasks_by_key`
entries accumulated because we were using `task.get_name()` instead of
`execution.key` for cleanup.

Worker invariant tests check:
- `_tasks_by_key` is empty after `run_until_finished()`
- `_tasks_by_key` doesn't grow across multiple batches
- `_execution_counts` is cleared after `run_at_most()`
- Internal attributes are deleted after `__aexit__`
- Cleanup works with failing tasks and varied task types
- SharedContext ContextVars are properly reset

StrikeList invariant tests check:
- `_conditions` only contains default after removing temp conditions
- No empty dicts remain in `task_strikes`/`parameter_strikes` after
restore

Also reorganizes tests into subdirectories:
- `tests/worker/` for worker-related tests
- `tests/instrumentation/` for metrics and tracing tests
- Renames `test_synced_strikelist.py` to `test_strikelist.py`

Adds `--import-mode=importlib` to pytest config to support same-named
test files in different directories.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants