Files
SQLBot/backend/tests/test_distributed_lock.py

170 lines
5.7 KiB
Python

"""Focused tests for the startup embedding distributed lock."""
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from sqlalchemy.engine import Connection
from sqlalchemy.exc import SQLAlchemyError
BACKEND_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(BACKEND_DIR))
from common.utils import distributed_lock as lock_module # noqa: E402
from common.utils.distributed_lock import ( # noqa: E402
DistributedLock,
LockStatus,
SingleWorkerGuard,
)
class DistributedLockTestCase(unittest.TestCase):
"""Verify PostgreSQL advisory-lock acquisition and cleanup behavior."""
@staticmethod
def _connection(acquired: bool = True) -> MagicMock:
connection = MagicMock(spec=Connection)
connection.closed = False
connection.execute.return_value.scalar_one.return_value = acquired
return connection
def test_advisory_key_is_stable_across_processes(self) -> None:
key = DistributedLock._postgres_advisory_key("sqlbot:startup:embedding")
self.assertEqual(6735965423478359195, key)
self.assertGreaterEqual(key, -(2**63))
self.assertLessEqual(key, 2**63 - 1)
def test_try_acquire_keeps_successful_connection_open(self) -> None:
connection = self._connection(acquired=True)
with patch.object(lock_module, "engine") as engine:
engine.connect.return_value = connection
acquired_lock = DistributedLock.try_acquire("startup-task")
self.assertIsInstance(acquired_lock, DistributedLock)
connection.commit.assert_called_once_with()
connection.close.assert_not_called()
def test_try_acquire_closes_connection_when_lock_is_busy(self) -> None:
connection = self._connection(acquired=False)
with patch.object(lock_module, "engine") as engine:
engine.connect.return_value = connection
acquired_lock = DistributedLock.try_acquire("startup-task")
self.assertIsNone(acquired_lock)
connection.commit.assert_called_once_with()
connection.close.assert_called_once_with()
def test_try_acquire_closes_connection_on_sqlalchemy_error(self) -> None:
connection = self._connection()
connection.execute.side_effect = SQLAlchemyError("database error")
with (
patch.object(lock_module, "engine") as engine,
patch.object(lock_module.SQLBotLogUtil, "exception") as log_error,
):
engine.connect.return_value = connection
acquired_lock = DistributedLock.try_acquire("startup-task")
self.assertIsNone(acquired_lock)
connection.close.assert_called_once_with()
log_error.assert_called_once()
def test_release_unlocks_commits_and_closes_connection(self) -> None:
connection = self._connection()
acquired_lock = DistributedLock(1234, connection)
acquired_lock.release()
connection.execute.assert_called_once()
connection.commit.assert_called_once_with()
connection.close.assert_called_once_with()
def test_release_still_closes_connection_when_unlock_fails(self) -> None:
connection = self._connection()
connection.execute.side_effect = SQLAlchemyError("unlock error")
acquired_lock = DistributedLock(1234, connection)
with self.assertRaises(SQLAlchemyError):
acquired_lock.release()
connection.commit.assert_not_called()
connection.close.assert_called_once_with()
class SingleWorkerGuardTestCase(unittest.TestCase):
"""Verify that only the process holding the lock runs startup tasks."""
def setUp(self) -> None:
SingleWorkerGuard._lock_status = LockStatus.NOT_ATTEMPTED
SingleWorkerGuard._acquired_lock = None
def tearDown(self) -> None:
SingleWorkerGuard._lock_status = LockStatus.NOT_ATTEMPTED
SingleWorkerGuard._acquired_lock = None
def test_acquired_worker_runs_all_tasks_with_one_lock_attempt(self) -> None:
acquired_lock = MagicMock(spec=DistributedLock)
executed_tasks: list[str] = []
@SingleWorkerGuard.once
def first_task() -> None:
executed_tasks.append("first")
@SingleWorkerGuard.once
def second_task() -> None:
executed_tasks.append("second")
with patch.object(
DistributedLock,
"try_acquire",
return_value=acquired_lock,
) as try_acquire:
first_task()
second_task()
self.assertEqual(["first", "second"], executed_tasks)
try_acquire.assert_called_once_with(SingleWorkerGuard._LOCK_KEY)
def test_non_acquired_worker_skips_tasks_without_retrying(self) -> None:
task = MagicMock()
guarded_task = SingleWorkerGuard.once(task)
with (
patch.object(
DistributedLock,
"try_acquire",
return_value=None,
) as try_acquire,
patch.object(lock_module.SQLBotLogUtil, "info"),
):
guarded_task()
guarded_task()
task.assert_not_called()
try_acquire.assert_called_once_with(SingleWorkerGuard._LOCK_KEY)
def test_release_releases_lock_and_resets_process_state(self) -> None:
acquired_lock = MagicMock(spec=DistributedLock)
SingleWorkerGuard._lock_status = LockStatus.ACQUIRED
SingleWorkerGuard._acquired_lock = acquired_lock
SingleWorkerGuard.release()
acquired_lock.release.assert_called_once_with()
self.assertIsNone(SingleWorkerGuard._acquired_lock)
self.assertIs(
LockStatus.NOT_ATTEMPTED,
SingleWorkerGuard._lock_status,
)
if __name__ == "__main__":
unittest.main()