Skip to content

Commit 7155550

Browse files
tarjan1XiaJunjie2020
authored andcommitted
test: cover startup distributed lock behavior
1 parent d21e7de commit 7155550

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

tests/test_distributed_lock.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""Focused tests for the startup embedding distributed lock."""
2+
3+
import sys
4+
import unittest
5+
from pathlib import Path
6+
from unittest.mock import MagicMock, patch
7+
8+
from sqlalchemy.engine import Connection
9+
from sqlalchemy.exc import SQLAlchemyError
10+
11+
12+
BACKEND_DIR = Path(__file__).resolve().parents[1] / "backend"
13+
sys.path.insert(0, str(BACKEND_DIR))
14+
15+
from common.utils import distributed_lock as lock_module # noqa: E402
16+
from common.utils.distributed_lock import ( # noqa: E402
17+
DistributedLock,
18+
LockStatus,
19+
SingleWorkerGuard,
20+
)
21+
22+
23+
class DistributedLockTestCase(unittest.TestCase):
24+
"""Verify PostgreSQL advisory-lock acquisition and cleanup behavior."""
25+
26+
@staticmethod
27+
def _connection(acquired: bool = True) -> MagicMock:
28+
connection = MagicMock(spec=Connection)
29+
connection.closed = False
30+
connection.execute.return_value.scalar_one.return_value = acquired
31+
return connection
32+
33+
def test_advisory_key_is_stable_across_processes(self) -> None:
34+
key = DistributedLock._postgres_advisory_key("sqlbot:startup:embedding")
35+
36+
self.assertEqual(6735965423478359195, key)
37+
self.assertGreaterEqual(key, -(2**63))
38+
self.assertLessEqual(key, 2**63 - 1)
39+
40+
def test_try_acquire_keeps_successful_connection_open(self) -> None:
41+
connection = self._connection(acquired=True)
42+
43+
with patch.object(lock_module, "engine") as engine:
44+
engine.connect.return_value = connection
45+
46+
acquired_lock = DistributedLock.try_acquire("startup-task")
47+
48+
self.assertIsInstance(acquired_lock, DistributedLock)
49+
connection.commit.assert_called_once_with()
50+
connection.close.assert_not_called()
51+
52+
def test_try_acquire_closes_connection_when_lock_is_busy(self) -> None:
53+
connection = self._connection(acquired=False)
54+
55+
with patch.object(lock_module, "engine") as engine:
56+
engine.connect.return_value = connection
57+
58+
acquired_lock = DistributedLock.try_acquire("startup-task")
59+
60+
self.assertIsNone(acquired_lock)
61+
connection.commit.assert_called_once_with()
62+
connection.close.assert_called_once_with()
63+
64+
def test_try_acquire_closes_connection_on_sqlalchemy_error(self) -> None:
65+
connection = self._connection()
66+
connection.execute.side_effect = SQLAlchemyError("database error")
67+
68+
with (
69+
patch.object(lock_module, "engine") as engine,
70+
patch.object(lock_module.SQLBotLogUtil, "exception") as log_error,
71+
):
72+
engine.connect.return_value = connection
73+
74+
acquired_lock = DistributedLock.try_acquire("startup-task")
75+
76+
self.assertIsNone(acquired_lock)
77+
connection.close.assert_called_once_with()
78+
log_error.assert_called_once()
79+
80+
def test_release_unlocks_commits_and_closes_connection(self) -> None:
81+
connection = self._connection()
82+
acquired_lock = DistributedLock(1234, connection)
83+
84+
acquired_lock.release()
85+
86+
connection.execute.assert_called_once()
87+
connection.commit.assert_called_once_with()
88+
connection.close.assert_called_once_with()
89+
90+
def test_release_still_closes_connection_when_unlock_fails(self) -> None:
91+
connection = self._connection()
92+
connection.execute.side_effect = SQLAlchemyError("unlock error")
93+
acquired_lock = DistributedLock(1234, connection)
94+
95+
with self.assertRaises(SQLAlchemyError):
96+
acquired_lock.release()
97+
98+
connection.commit.assert_not_called()
99+
connection.close.assert_called_once_with()
100+
101+
102+
class SingleWorkerGuardTestCase(unittest.TestCase):
103+
"""Verify that only the process holding the lock runs startup tasks."""
104+
105+
def setUp(self) -> None:
106+
SingleWorkerGuard._lock_status = LockStatus.NOT_ATTEMPTED
107+
SingleWorkerGuard._acquired_lock = None
108+
109+
def tearDown(self) -> None:
110+
SingleWorkerGuard._lock_status = LockStatus.NOT_ATTEMPTED
111+
SingleWorkerGuard._acquired_lock = None
112+
113+
def test_acquired_worker_runs_all_tasks_with_one_lock_attempt(self) -> None:
114+
acquired_lock = MagicMock(spec=DistributedLock)
115+
executed_tasks: list[str] = []
116+
117+
@SingleWorkerGuard.once
118+
def first_task() -> None:
119+
executed_tasks.append("first")
120+
121+
@SingleWorkerGuard.once
122+
def second_task() -> None:
123+
executed_tasks.append("second")
124+
125+
with patch.object(
126+
DistributedLock,
127+
"try_acquire",
128+
return_value=acquired_lock,
129+
) as try_acquire:
130+
first_task()
131+
second_task()
132+
133+
self.assertEqual(["first", "second"], executed_tasks)
134+
try_acquire.assert_called_once_with(SingleWorkerGuard._LOCK_KEY)
135+
136+
def test_non_acquired_worker_skips_tasks_without_retrying(self) -> None:
137+
task = MagicMock()
138+
guarded_task = SingleWorkerGuard.once(task)
139+
140+
with (
141+
patch.object(
142+
DistributedLock,
143+
"try_acquire",
144+
return_value=None,
145+
) as try_acquire,
146+
patch.object(lock_module.SQLBotLogUtil, "info"),
147+
):
148+
guarded_task()
149+
guarded_task()
150+
151+
task.assert_not_called()
152+
try_acquire.assert_called_once_with(SingleWorkerGuard._LOCK_KEY)
153+
154+
def test_release_releases_lock_and_resets_process_state(self) -> None:
155+
acquired_lock = MagicMock(spec=DistributedLock)
156+
SingleWorkerGuard._lock_status = LockStatus.ACQUIRED
157+
SingleWorkerGuard._acquired_lock = acquired_lock
158+
159+
SingleWorkerGuard.release()
160+
161+
acquired_lock.release.assert_called_once_with()
162+
self.assertIsNone(SingleWorkerGuard._acquired_lock)
163+
self.assertIs(
164+
LockStatus.NOT_ATTEMPTED,
165+
SingleWorkerGuard._lock_status,
166+
)
167+
168+
169+
if __name__ == "__main__":
170+
unittest.main()

0 commit comments

Comments
 (0)