Skip to content

Commit bd2e218

Browse files
refactoring project lock
1 parent 278fd85 commit bd2e218

File tree

4 files changed

+92
-73
lines changed

4 files changed

+92
-73
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import datetime
2+
import logging
3+
from asyncio.log import logger
4+
from collections.abc import AsyncIterator
5+
from contextlib import asynccontextmanager
6+
from typing import Final
7+
8+
import redis
9+
from models_library.projects import ProjectID
10+
from models_library.projects_access import Owner
11+
from models_library.projects_state import ProjectLocked, ProjectStatus
12+
from redis.asyncio.lock import Lock
13+
from servicelib.background_task import periodic_task
14+
from servicelib.logging_utils import log_context
15+
16+
_logger = logging.getLogger(__name__)
17+
18+
PROJECT_REDIS_LOCK_KEY: str = "project_lock:{}"
19+
PROJECT_LOCK_TIMEOUT: Final[datetime.timedelta] = datetime.timedelta(seconds=10)
20+
ProjectLock = Lock
21+
22+
ProjectLockError = redis.exceptions.LockError
23+
24+
25+
async def _auto_extend_project_lock(project_lock: Lock) -> None:
26+
# NOTE: the background task already catches anything that might raise here
27+
await project_lock.reacquire()
28+
29+
30+
@asynccontextmanager
31+
async def lock_project(
32+
redis_lock: Lock,
33+
project_uuid: str | ProjectID,
34+
status: ProjectStatus,
35+
owner: Owner | None = None,
36+
) -> AsyncIterator[None]:
37+
"""Context manager to lock and unlock a project by user_id
38+
39+
Raises:
40+
ProjectLockError: if project is already locked
41+
"""
42+
43+
try:
44+
if not await redis_lock.acquire(
45+
blocking=False,
46+
token=ProjectLocked(
47+
value=True,
48+
owner=owner,
49+
status=status,
50+
).json(),
51+
):
52+
msg = f"Lock for project {project_uuid!r} owner {owner!r} could not be acquired"
53+
raise ProjectLockError(msg)
54+
55+
with log_context(
56+
_logger,
57+
logging.DEBUG,
58+
msg=f"with lock for {owner=}:{project_uuid=}:{status=}",
59+
):
60+
async with periodic_task(
61+
_auto_extend_project_lock,
62+
interval=0.6 * PROJECT_LOCK_TIMEOUT,
63+
task_name=f"{PROJECT_REDIS_LOCK_KEY.format(project_uuid)}_lock_auto_extend",
64+
project_lock=redis_lock,
65+
):
66+
yield
67+
68+
finally:
69+
# let's ensure we release that stuff
70+
try:
71+
if await redis_lock.owned():
72+
await redis_lock.release()
73+
except (redis.exceptions.LockError, redis.exceptions.LockNotOwnedError) as exc:
74+
logger.warning(
75+
"releasing %s unexpectedly raised an exception: %s",
76+
f"{redis_lock=!r}",
77+
f"{exc}",
78+
)

services/director-v2/tests/unit/test_utils_distributed_identifier.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ async def _destroy(
163163
self.api.delete(identifier)
164164

165165

166-
# MD: here redis
167166
@pytest.fixture
168167
async def redis_client_sdk(
169168
redis_service: RedisSettings,
Lines changed: 14 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import logging
2-
from contextlib import asynccontextmanager
3-
from datetime import datetime, timedelta, timezone
4-
from typing import AsyncIterator, Final
2+
from datetime import datetime, timezone
53

6-
import redis
74
from fastapi import FastAPI
85
from models_library.projects import ProjectID
9-
from models_library.projects_state import ProjectLocked, ProjectStatus
10-
from redis.asyncio.lock import Lock
11-
from servicelib.background_task import periodic_task
12-
from servicelib.logging_utils import log_context
6+
from models_library.projects_state import ProjectStatus
7+
from servicelib.project_lock import (
8+
PROJECT_LOCK_TIMEOUT,
9+
PROJECT_REDIS_LOCK_KEY,
10+
lock_project,
11+
)
1312
from simcore_postgres_database.utils_projects import ProjectsRepo
1413

1514
from ..core.settings import ApplicationSettings
@@ -19,68 +18,6 @@
1918
_logger = logging.getLogger(__name__)
2019

2120

22-
PROJECT_REDIS_LOCK_KEY: str = "project_lock:{}"
23-
PROJECT_LOCK_TIMEOUT: Final[timedelta] = timedelta(seconds=10)
24-
25-
26-
async def _auto_extend_project_lock(project_lock: Lock) -> None:
27-
# NOTE: the background task already catches anything that might raise here
28-
await project_lock.reacquire()
29-
30-
31-
@asynccontextmanager
32-
async def lock_project(
33-
app: FastAPI,
34-
project_uuid: ProjectID,
35-
status: ProjectStatus = ProjectStatus.MAINTAINING,
36-
) -> AsyncIterator[None]:
37-
"""Context manager to lock and unlock a project by user_id
38-
39-
Raises:
40-
ProjectLockError: if project is already locked
41-
"""
42-
43-
redis_lock = get_redis_lock_client(app).redis.lock(
44-
PROJECT_REDIS_LOCK_KEY.format(project_uuid),
45-
timeout=PROJECT_LOCK_TIMEOUT.total_seconds(),
46-
)
47-
try:
48-
if not await redis_lock.acquire(
49-
blocking=False,
50-
token=ProjectLocked(
51-
value=True,
52-
owner=None,
53-
status=status,
54-
).json(),
55-
):
56-
msg = f"Lock for project {project_uuid!r} could not be acquired"
57-
raise ValueError(msg)
58-
59-
with log_context(
60-
_logger,
61-
logging.DEBUG,
62-
msg=f"with lock for {project_uuid=}:{status=}",
63-
):
64-
async with periodic_task(
65-
_auto_extend_project_lock,
66-
interval=0.6 * PROJECT_LOCK_TIMEOUT,
67-
task_name=f"{PROJECT_REDIS_LOCK_KEY.format(project_uuid)}_lock_auto_extend",
68-
project_lock=redis_lock,
69-
):
70-
yield
71-
72-
finally:
73-
try:
74-
if await redis_lock.owned():
75-
await redis_lock.release()
76-
except (redis.exceptions.LockError, redis.exceptions.LockNotOwnedError) as exc:
77-
_logger.warning(
78-
"releasing %s unexpectedly raised an exception: %s",
79-
f"{redis_lock=!r}",
80-
f"{exc}",
81-
)
82-
83-
8421
async def removal_policy_task(app: FastAPI) -> None:
8522
_logger.info("Removal policy task started")
8623

@@ -111,5 +48,11 @@ async def removal_policy_task(app: FastAPI) -> None:
11148
_project_last_change_date,
11249
app_settings.EFS_REMOVAL_POLICY_TASK_AGE_LIMIT_TIMEDELTA,
11350
)
114-
async with lock_project(app, project_uuid=project_id):
51+
redis_lock = get_redis_lock_client(app).redis.lock(
52+
PROJECT_REDIS_LOCK_KEY.format(project_id),
53+
timeout=PROJECT_LOCK_TIMEOUT.total_seconds(),
54+
)
55+
async with lock_project(
56+
redis_lock, project_uuid=project_id, status=ProjectStatus.MAINTAINING
57+
):
11558
await efs_manager.remove_project_efs_data(project_id)

services/efs-guardian/tests/unit/test_api_health.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ def app_environment(
2424
monkeypatch,
2525
{
2626
**app_environment,
27-
# **rabbit_env_vars_dict,
2827
},
2928
)
3029

0 commit comments

Comments
 (0)