feat(co-gahusb): 어드바이저리 락 (acquire/release/heartbeat/list, TDD)

This commit is contained in:
2026-06-12 07:20:30 +09:00
parent 0d466b235c
commit 8212a51f90
4 changed files with 131 additions and 0 deletions

66
co-gahusb/app/locks.py Normal file
View File

@@ -0,0 +1,66 @@
# co-gahusb/app/locks.py
from redis.exceptions import WatchError
LOCK_PREFIX = "co:lock:"
async def acquire_lock(r, resource, role, ttl_sec=300):
key = LOCK_PREFIX + resource
ok = await r.set(key, role, nx=True, ex=ttl_sec)
if ok:
return {"acquired": True}
held_by = await r.get(key)
ttl = await r.ttl(key)
return {"acquired": False, "held_by": held_by, "ttl_remaining": max(ttl, 0)}
async def release_lock(r, resource, role):
key = LOCK_PREFIX + resource
async with r.pipeline() as pipe:
while True:
try:
await pipe.watch(key)
owner = await pipe.get(key)
if owner != role:
await pipe.unwatch()
return {"released": False, "held_by": owner}
pipe.multi()
pipe.delete(key)
await pipe.execute()
return {"released": True}
except WatchError:
continue
async def heartbeat_lock(r, resource, role, ttl_sec=300):
key = LOCK_PREFIX + resource
async with r.pipeline() as pipe:
while True:
try:
await pipe.watch(key)
owner = await pipe.get(key)
if owner != role:
await pipe.unwatch()
return {"renewed": False, "held_by": owner}
pipe.multi()
pipe.expire(key, ttl_sec)
await pipe.execute()
return {"renewed": True}
except WatchError:
continue
async def list_locks(r):
keys = await r.keys(LOCK_PREFIX + "*")
out = []
for key in keys:
held_by = await r.get(key)
if held_by is None:
continue
ttl = await r.ttl(key)
out.append({
"resource": key[len(LOCK_PREFIX):],
"held_by": held_by,
"ttl_remaining": max(ttl, 0),
})
return {"locks": out}