Skip to content

Commit 373f657

Browse files
authored
Merge branch 'master' into enh/chat-bubble
2 parents 03c1a61 + 12fa12c commit 373f657

File tree

21 files changed

+412
-318
lines changed

21 files changed

+412
-318
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Update api-keys uniqueness constraint
2+
3+
Revision ID: 7e92447558e0
4+
Revises: 06eafd25d004
5+
Create Date: 2025-09-12 09:56:45.164921+00:00
6+
7+
"""
8+
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "7e92447558e0"
13+
down_revision = "06eafd25d004"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.drop_constraint("display_name_userid_uniqueness", "api_keys", type_="unique")
21+
op.create_unique_constraint(
22+
"display_name_userid_product_name_uniqueness",
23+
"api_keys",
24+
["display_name", "user_id", "product_name"],
25+
)
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
op.drop_constraint(
32+
"display_name_userid_product_name_uniqueness", "api_keys", type_="unique"
33+
)
34+
op.create_unique_constraint(
35+
"display_name_userid_uniqueness", "api_keys", ["display_name", "user_id"]
36+
)
37+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/api_keys.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@
7575
"If set to NULL then the key does not expire.",
7676
),
7777
sa.UniqueConstraint(
78-
"display_name", "user_id", name="display_name_userid_uniqueness"
78+
"display_name",
79+
"user_id",
80+
"product_name",
81+
name="display_name_userid_product_name_uniqueness",
7982
),
8083
)
8184

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# pylint: disable=protected-access
2+
3+
import pytest
4+
from fastapi import FastAPI
5+
from servicelib.long_running_tasks.errors import TaskNotFoundError
6+
from servicelib.long_running_tasks.manager import (
7+
LongRunningManager,
8+
)
9+
from servicelib.long_running_tasks.models import TaskContext
10+
from servicelib.long_running_tasks.task import TaskId
11+
from tenacity import (
12+
AsyncRetrying,
13+
retry_if_not_exception_type,
14+
stop_after_delay,
15+
wait_fixed,
16+
)
17+
18+
19+
def get_fastapi_long_running_manager(app: FastAPI) -> LongRunningManager:
20+
manager = app.state.long_running_manager
21+
assert isinstance(manager, LongRunningManager)
22+
return manager
23+
24+
25+
async def assert_task_is_no_longer_present(
26+
manager: LongRunningManager, task_id: TaskId, task_context: TaskContext
27+
) -> None:
28+
async for attempt in AsyncRetrying(
29+
reraise=True,
30+
wait=wait_fixed(0.1),
31+
stop=stop_after_delay(60),
32+
retry=retry_if_not_exception_type(TaskNotFoundError),
33+
):
34+
with attempt: # noqa: SIM117
35+
with pytest.raises(TaskNotFoundError):
36+
# use internals to detirmine when it's no longer here
37+
await manager._tasks_manager._get_tracked_task( # noqa: SLF001
38+
task_id, task_context
39+
)

packages/service-library/src/servicelib/aiohttp/long_running_tasks/_routes.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
from typing import Annotated, Any
1+
from typing import Any
22

33
from aiohttp import web
4-
from models_library.rest_base import RequestParameters
5-
from pydantic import BaseModel, Field
4+
from pydantic import BaseModel
65

76
from ...aiohttp import status
87
from ...long_running_tasks import lrt_api
98
from ...long_running_tasks.models import TaskGet, TaskId
109
from ..requests_validation import (
1110
parse_request_path_parameters_as,
12-
parse_request_query_parameters_as,
1311
)
1412
from ..rest_responses import create_data_response
1513
from ._manager import get_long_running_manager
@@ -69,29 +67,15 @@ async def get_task_result(request: web.Request) -> web.Response | Any:
6967
)
7068

7169

72-
class _RemoveTaskQueryParams(RequestParameters):
73-
wait_for_removal: Annotated[
74-
bool,
75-
Field(
76-
description=(
77-
"when True waits for the task to be removed "
78-
"completly instead of returning immediately"
79-
)
80-
),
81-
] = True
82-
83-
8470
@routes.delete("/{task_id}", name="remove_task")
8571
async def remove_task(request: web.Request) -> web.Response:
8672
path_params = parse_request_path_parameters_as(_PathParam, request)
87-
query_params = parse_request_query_parameters_as(_RemoveTaskQueryParams, request)
8873
long_running_manager = get_long_running_manager(request.app)
8974

9075
await lrt_api.remove_task(
9176
long_running_manager.rpc_client,
9277
long_running_manager.lrt_namespace,
9378
long_running_manager.get_task_context(request),
9479
path_params.task_id,
95-
wait_for_removal=query_params.wait_for_removal,
9680
)
9781
return web.json_response(status=status.HTTP_204_NO_CONTENT)

packages/service-library/src/servicelib/aiohttp/long_running_tasks/_server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ async def start_long_running_task(
108108
long_running_manager.lrt_namespace,
109109
task_context,
110110
task_id,
111-
wait_for_removal=True,
112111
)
113112
raise
114113

packages/service-library/src/servicelib/fastapi/long_running_tasks/_routes.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Annotated, Any
22

3-
from fastapi import APIRouter, Depends, Query, Request, status
3+
from fastapi import APIRouter, Depends, Request, status
44

55
from ...long_running_tasks import lrt_api
66
from ...long_running_tasks.models import TaskGet, TaskId, TaskResult, TaskStatus
@@ -101,22 +101,11 @@ async def remove_task(
101101
FastAPILongRunningManager, Depends(get_long_running_manager)
102102
],
103103
task_id: TaskId,
104-
*,
105-
wait_for_removal: Annotated[
106-
bool,
107-
Query(
108-
description=(
109-
"when True waits for the task to be removed "
110-
"completly instead of returning immediately"
111-
),
112-
),
113-
] = True,
114104
) -> None:
115105
assert request # nosec
116106
await lrt_api.remove_task(
117107
long_running_manager.rpc_client,
118108
long_running_manager.lrt_namespace,
119109
long_running_manager.get_task_context(request),
120110
task_id=task_id,
121-
wait_for_removal=wait_for_removal,
122111
)

packages/service-library/src/servicelib/long_running_tasks/_redis_store.py

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from ..redis._client import RedisClientSDK
99
from ..redis._utils import handle_redis_returns_union_types
1010
from ..utils import limited_gather
11-
from .models import LRTNamespace, TaskContext, TaskData, TaskId
11+
from .models import LRTNamespace, TaskData, TaskId
1212

1313
_STORE_TYPE_TASK_DATA: Final[str] = "TD"
14-
_STORE_TYPE_CANCELLED_TASKS: Final[str] = "CT"
15-
_LIST_CONCURRENCY: Final[int] = 2
14+
_LIST_CONCURRENCY: Final[int] = 3
15+
_MARKED_FOR_REMOVAL_FIELD: Final[str] = "marked_for_removal"
1616

1717

1818
def _to_redis_hash_mapping(data: dict[str, Any]) -> dict[str, str]:
@@ -52,11 +52,6 @@ def _get_redis_key_task_data_match(self) -> str:
5252
def _get_redis_task_data_key(self, task_id: TaskId) -> str:
5353
return f"{self.namespace}:{_STORE_TYPE_TASK_DATA}:{task_id}"
5454

55-
def _get_key_to_remove(self) -> str:
56-
return f"{self.namespace}:{_STORE_TYPE_CANCELLED_TASKS}"
57-
58-
# TaskData
59-
6055
async def get_task_data(self, task_id: TaskId) -> TaskData | None:
6156
result: dict[str, Any] = await handle_redis_returns_union_types(
6257
self._redis.hgetall(
@@ -115,24 +110,18 @@ async def delete_task_data(self, task_id: TaskId) -> None:
115110
self._redis.delete(self._get_redis_task_data_key(task_id))
116111
)
117112

118-
# to cancel
119-
120-
async def mark_task_for_removal(
121-
self, task_id: TaskId, with_task_context: TaskContext
122-
) -> None:
113+
async def mark_for_removal(self, task_id: TaskId) -> None:
123114
await handle_redis_returns_union_types(
124115
self._redis.hset(
125-
self._get_key_to_remove(), task_id, json_dumps(with_task_context)
116+
self._get_redis_task_data_key(task_id),
117+
mapping=_to_redis_hash_mapping({_MARKED_FOR_REMOVAL_FIELD: True}),
126118
)
127119
)
128120

129-
async def completed_task_removal(self, task_id: TaskId) -> None:
130-
await handle_redis_returns_union_types(
131-
self._redis.hdel(self._get_key_to_remove(), task_id)
132-
)
133-
134-
async def list_tasks_to_remove(self) -> dict[TaskId, TaskContext]:
135-
result: dict[str, str | None] = await handle_redis_returns_union_types(
136-
self._redis.hgetall(self._get_key_to_remove())
121+
async def is_marked_for_removal(self, task_id: TaskId) -> bool:
122+
result = await handle_redis_returns_union_types(
123+
self._redis.hget(
124+
self._get_redis_task_data_key(task_id), _MARKED_FOR_REMOVAL_FIELD
125+
)
137126
)
138-
return {task_id: json_loads(context) for task_id, context in result.items()}
127+
return False if result is None else json_loads(result)

packages/service-library/src/servicelib/long_running_tasks/_rpc_client.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,26 +118,13 @@ async def remove_task(
118118
*,
119119
task_context: TaskContext,
120120
task_id: TaskId,
121-
wait_for_removal: bool,
122-
cancellation_timeout: timedelta | None,
123121
) -> None:
124-
timeout_s = (
125-
None
126-
if cancellation_timeout is None
127-
else int(cancellation_timeout.total_seconds())
128-
)
129-
130-
# NOTE: task always gets cancelled even if not waiting for it
131-
# request will return immediatlye, no need to wait so much
132-
if wait_for_removal is False:
133-
timeout_s = _RPC_TIMEOUT_SHORT_REQUESTS
134122

135123
result = await rabbitmq_rpc_client.request(
136124
get_rabbit_namespace(namespace),
137125
TypeAdapter(RPCMethodName).validate_python("remove_task"),
138126
task_context=task_context,
139127
task_id=task_id,
140-
wait_for_removal=wait_for_removal,
141-
timeout_s=timeout_s,
128+
timeout_s=_RPC_TIMEOUT_SHORT_REQUESTS,
142129
)
143130
assert result is None # nosec

packages/service-library/src/servicelib/long_running_tasks/_rpc_server.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ async def get_task_result(
8888
await long_running_manager.tasks_manager.remove_task(
8989
task_id,
9090
with_task_context=task_context,
91-
wait_for_removal=True,
91+
wait_for_removal=False,
9292
)
9393

9494

@@ -98,10 +98,7 @@ async def remove_task(
9898
*,
9999
task_context: TaskContext,
100100
task_id: TaskId,
101-
wait_for_removal: bool,
102101
) -> None:
103102
await long_running_manager.tasks_manager.remove_task(
104-
task_id,
105-
with_task_context=task_context,
106-
wait_for_removal=wait_for_removal,
103+
task_id, with_task_context=task_context, wait_for_removal=False
107104
)

packages/service-library/src/servicelib/long_running_tasks/lrt_api.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from datetime import timedelta
21
from typing import Any
32

43
from ..rabbitmq._client_rpc import RabbitMQRPCClient
@@ -103,9 +102,6 @@ async def remove_task(
103102
lrt_namespace: LRTNamespace,
104103
task_context: TaskContext,
105104
task_id: TaskId,
106-
*,
107-
wait_for_removal: bool,
108-
cancellation_timeout: timedelta | None = None,
109105
) -> None:
110106
"""cancels and removes a task
111107
@@ -116,6 +112,4 @@ async def remove_task(
116112
lrt_namespace,
117113
task_id=task_id,
118114
task_context=task_context,
119-
wait_for_removal=wait_for_removal,
120-
cancellation_timeout=cancellation_timeout,
121115
)

0 commit comments

Comments
 (0)