Skip to content

Commit 8062e4a

Browse files
authored
Added additional test for hierarchical lock (#768)
1 parent bde6df7 commit 8062e4a

File tree

2 files changed

+34
-0
lines changed

2 files changed

+34
-0
lines changed

packages/opal-common/opal_common/synchronization/hierarchical_lock.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from contextlib import asynccontextmanager
33
from typing import Set
44

5+
from loguru import logger
6+
57

68
class HierarchicalLock:
79
"""A hierarchical lock for asyncio.
@@ -43,13 +45,18 @@ async def acquire(self, path: str):
4345

4446
# Wait until there is no conflict with existing locked paths
4547
while any(self._is_conflicting(path, lp) for lp in self._locked_paths):
48+
logger.debug(
49+
f"Found conflicting path with {path!r}, waiting for release to check again..."
50+
)
51+
# Condition.wait() releases the lock and waits for notify_all()
4652
await self._cond.wait()
4753

4854
# Acquire the path
4955
self._locked_paths.add(path)
5056
if task not in self._task_locks:
5157
self._task_locks[task] = set()
5258
self._task_locks[task].add(path)
59+
logger.debug("Acquired lock for path: {}", path)
5360

5461
async def release(self, path: str):
5562
"""Release the lock for the given path and notify waiting tasks."""
@@ -74,6 +81,7 @@ async def release(self, path: str):
7481

7582
# Notify all tasks that something was released
7683
self._cond.notify_all()
84+
logger.debug("Released lock for path: {}", path)
7785

7886
@asynccontextmanager
7987
async def lock(self, path: str) -> "HierarchicalLock":

packages/opal-common/opal_common/tests/hierarchical_lock_test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,32 @@ async def lock_sibling(path):
5757
assert duration < 0.2, "Both siblings should acquire lock concurrently"
5858

5959

60+
@pytest.mark.asyncio
61+
async def test_conflict_do_not_block_unrelated():
62+
lock = HierarchicalLock()
63+
64+
# Acquire two sibling paths concurrently
65+
# They should not block each other
66+
async def lock_sibling(path: str, delay: float = 0.1):
67+
async with lock.lock(path):
68+
await asyncio.sleep(delay)
69+
return path
70+
71+
parent = asyncio.create_task(lock_sibling("parent", 0.2))
72+
child = lock_sibling("parent.child", 0.1)
73+
unrelated = lock_sibling("unrelated", 0.1)
74+
75+
# Wait for all tasks to complete, in the order they complete
76+
order = []
77+
for coro in asyncio.as_completed([child, parent, unrelated], timeout=10):
78+
order.append(await coro)
79+
assert order == [
80+
"unrelated",
81+
"parent",
82+
"parent.child",
83+
], "Unrelated paths should not block"
84+
85+
6086
@pytest.mark.asyncio
6187
async def test_parent_blocks_child():
6288
lock = HierarchicalLock()

0 commit comments

Comments
 (0)