Skip to content

fix(memory): restart the cleanup task on a new event loop and add aclose() - #215

Merged
allen0099 merged 1 commit into
masterfrom
fix/memory-cleanup-loop
Sep 26, 2026
Merged

allen0099 merged 1 commit into
masterfrom
fix/memory-cleanup-loop

Conversation

@allen0099

Copy link
Copy Markdown
Owner

Closes #181

Problem

MemoryBackend started its cleanup task on the event loop of the first cache call and never looked at it again. If that loop was closed without cancelling the task (a manually managed loop, some test runners, a server restart inside one process), the task stayed "pending" forever. Every later call saw a task that was not done, so no cleanup ran on the new loop and expired entries piled up.

Change

  • _ensure_cleanup_started() checks that the existing task belongs to the running loop. If not, it cancels the old task and starts a new one on the current loop.
  • Cancelling goes through a small _cancel() helper: a task on a closed loop is left alone (cancelling it would raise), a task on the running loop is cancelled directly, and a task on another open loop is cancelled with call_soon_threadsafe.
  • New async def aclose(): cancels the task and waits until it has finished, for use in a lifespan shutdown. It only waits for a task on the running loop.
  • stop_cleanup() stays synchronous and unchanged in behaviour. It now uses the same helper, and its docstring points to aclose().
  • Docs (en and zh-TW BACKENDS.md): a paragraph on the loop restart and a lifespan example with aclose().
  • CHANGELOG: an Added entry for aclose() and a Fixed entry for the restart.

Tests

Four new tests in tests/backends/test_memory.py:

  • the task restarts on a new loop after the old loop was closed;
  • moving loops cancels the task on a loop that is still open;
  • aclose() waits for the task;
  • aclose() only cancels, and does not wait for, a task on another loop.

Each fix point was mutation-checked by reverting it: ignoring the loop, removing the closed-loop guard, not waiting in aclose(), and not cancelling across loops. Each revert fails the matching new tests.

Local checks: ruff, mypy --strict, the full suite against live Redis and Memcached (882 passed), and zensical build --strict for both languages.

…ose()

The cleanup task stayed bound to the loop of the first cache call. Once
that loop closed without cancelling it, the task looked pending forever
and was never started on the new loop. The backend now restarts the task
when a call runs on a different loop, cancelling the old one if its loop
is still open (thread-safely when it is not the running loop).

aclose() cancels the task and waits for it, for use in a lifespan
shutdown. stop_cleanup() keeps its synchronous behaviour.

Closes #181
@allen0099 allen0099 added this to the 0.3.8 milestone Sep 26, 2026
@allen0099 allen0099 added bug Something isn't working backends Cache backends and their atomic primitives labels Sep 26, 2026
@allen0099
allen0099 merged commit dea70ee into master Sep 26, 2026
11 checks passed
@allen0099
allen0099 deleted the fix/memory-cleanup-loop branch September 26, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backends Cache backends and their atomic primitives bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MemoryBackend: cleanup task lifecycle across event loops

1 participant