mirror of
https://github.com/fusion44/blitz_api.git
synced 2026-08-13 11:52:45 +02:00
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>
110 lines
3.5 KiB
Python
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
|