blitz_api/tests/test_app_manage_lock.py
fusion44 a9bbc9a27c
Some checks failed
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
fix(api): release Redis connections in channel listeners/notifiers
BaseChannelListener.listen() only called pubsub.unsubscribe() in its
finally block: the pubsub connection and the listener's own Redis client
were never released, leaking a connection for every install/uninstall
(_watcher) and every recreation of the app-status listener. The Celery
task notifiers were likewise never closed, and used the deprecated
Redis.close() instead of aclose().

- add aclose() to BaseChannelNotifier and BaseChannelListener
- listen() now closes the pubsub and its Redis client in finally
- close the notifiers in the app_manage / app_status_update tasks and
  switch to aclose()
- back off in the app-status watch loop so a Redis outage no longer
  spins, and drop a leftover debug print

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 22:40:14 +02:00

110 lines
3.5 KiB
Python

"""
Regression test: a duplicate app-manage request must not touch the lock or
lifecycle of the install that is already running.
app_manage_task_impl released APP_MANAGE_LOCK_KEY and broadcast a FINISHED
message from its `finally` block even on the early-return path where the lock
was already held by another task. That deleted the running install's lock
(allowing concurrent installs) and made its listener stop early.
"""
import json
from app.apps.models import AppId, InstallMode
from app.external.result_type.src.result.result import Ok
from app.apps.tasks_impl import app_manage
async def test_lock_already_held_does_not_release_or_finish(monkeypatch):
sent = []
release_calls = []
class FakeNotifier:
def __init__(self, *args, **kwargs):
pass
async def connect(self):
return Ok(None)
async def send_message(self, key, contents):
sent.append(contents)
return Ok(None)
async def aclose(self):
pass
async def fake_acquire(key, lock_ttl, redis):
return Ok(False) # lock already held by the running install
async def fake_release(key, redis):
release_calls.append(key)
return Ok(None)
monkeypatch.setattr(app_manage, "BaseChannelNotifier", FakeNotifier)
monkeypatch.setattr(app_manage, "acquire_lock", fake_acquire)
monkeypatch.setattr(app_manage, "release_update_lock", fake_release)
await app_manage.app_manage_task_impl(
"redis://127.0.0.1:6379/0", AppId.MEMPOOL, InstallMode.ON, True
)
states = [json.loads(c)["state"] for c in sent]
assert release_calls == [], (
"must not release the lock it never acquired (belongs to the running install)"
)
assert "finished" not in states, (
"must not broadcast FINISHED and stop the running install's listener"
)
# the client should still be told the task is already running
assert "failure" in states
async def test_acquired_lock_is_released_and_finished(monkeypatch):
"""The task that actually holds the lock must always release it and send
FINISHED, otherwise the lock stays stuck until its TTL expires."""
sent = []
release_calls = []
class FakeNotifier:
def __init__(self, *args, **kwargs):
pass
async def connect(self):
return Ok(None)
async def send_message(self, key, contents):
sent.append(contents)
return Ok(None)
async def aclose(self):
pass
async def fake_acquire(key, lock_ttl, redis):
return Ok(True) # lock acquired by this task
async def fake_release(key, redis):
release_calls.append(key)
return Ok(None)
async def empty_install(app_id):
# an async generator that yields nothing (install does nothing here)
return
yield # pragma: no cover
class FakeApps:
def install_app(self, app_id):
return empty_install(app_id)
monkeypatch.setattr(app_manage, "BaseChannelNotifier", FakeNotifier)
monkeypatch.setattr(app_manage, "acquire_lock", fake_acquire)
monkeypatch.setattr(app_manage, "release_update_lock", fake_release)
monkeypatch.setattr(app_manage, "Apps", FakeApps)
await app_manage.app_manage_task_impl(
"redis://127.0.0.1:6379/0", AppId.MEMPOOL, InstallMode.ON, True
)
states = [json.loads(c)["state"] for c in sent]
assert release_calls == [app_manage.AppsServiceKeys.APP_MANAGE_LOCK_KEY]
assert "finished" in states