Skip to content

Commit ef9338c

Browse files
authored
Merge branch 'master' into 2024/fix/tracing
2 parents fc36beb + d8f1720 commit ef9338c

File tree

110 files changed

+855
-951
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+855
-951
lines changed

.codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ flag_management:
1818

1919
component_management:
2020
default_rules:
21+
carryforward: true
2122
statuses:
2223
- type: project
2324
target: auto

.github/workflows/ci-testing-deploy.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,51 @@ jobs:
450450
with:
451451
token: ${{ secrets.CODECOV_TOKEN }}
452452

453+
unit-test-webserver-04:
454+
needs: changes
455+
if: ${{ needs.changes.outputs.webserver == 'true' || github.event_name == 'push' }}
456+
timeout-minutes: 25 # if this timeout gets too small, then split the tests
457+
name: "[unit] webserver 04"
458+
runs-on: ${{ matrix.os }}
459+
strategy:
460+
matrix:
461+
python: ["3.11"]
462+
os: [ubuntu-22.04]
463+
fail-fast: false
464+
steps:
465+
- uses: actions/checkout@v4
466+
- name: setup docker buildx
467+
id: buildx
468+
uses: docker/setup-buildx-action@v3
469+
with:
470+
driver: docker-container
471+
- name: setup python environment
472+
uses: actions/setup-python@v5
473+
with:
474+
python-version: ${{ matrix.python }}
475+
- name: install uv
476+
uses: astral-sh/setup-uv@v3
477+
with:
478+
version: "0.4.x"
479+
enable-cache: false
480+
cache-dependency-glob: "**/web/server/requirements/ci.txt"
481+
- name: show system version
482+
run: ./ci/helpers/show_system_versions.bash
483+
- name: install webserver
484+
run: ./ci/github/unit-testing/webserver.bash install
485+
- name: test
486+
run: ./ci/github/unit-testing/webserver.bash test_with_db 04
487+
- uses: codecov/[email protected]
488+
env:
489+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
490+
with:
491+
flags: unittests #optional
492+
- name: Upload test results to Codecov
493+
if: ${{ !cancelled() }}
494+
uses: codecov/test-results-action@v1
495+
with:
496+
token: ${{ secrets.CODECOV_TOKEN }}
497+
453498
unit-test-storage:
454499
needs: changes
455500
if: ${{ needs.changes.outputs.storage == 'true' || github.event_name == 'push' }}
@@ -1875,6 +1920,7 @@ jobs:
18751920
unit-test-webserver-01,
18761921
unit-test-webserver-02,
18771922
unit-test-webserver-03,
1923+
unit-test-webserver-04,
18781924
]
18791925
runs-on: ubuntu-latest
18801926
steps:

packages/service-library/src/servicelib/background_task.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,36 @@ class PeriodicTaskCancellationError(PydanticErrorMixin, Exception):
2525
msg_template: str = "Could not cancel task '{task_name}'"
2626

2727

28+
class SleepUsingAsyncioEvent:
29+
"""Sleep strategy that waits on an event to be set."""
30+
31+
def __init__(self, event: "asyncio.Event") -> None:
32+
self.event = event
33+
34+
async def __call__(self, timeout: float | None) -> None:
35+
with contextlib.suppress(TimeoutError):
36+
await asyncio.wait_for(self.event.wait(), timeout=timeout)
37+
self.event.clear()
38+
39+
2840
async def _periodic_scheduled_task(
2941
task: Callable[..., Awaitable[None]],
3042
*,
3143
interval: datetime.timedelta,
3244
task_name: str,
45+
early_wake_up_event: asyncio.Event | None,
3346
**task_kwargs,
3447
) -> None:
3548
# NOTE: This retries forever unless cancelled
36-
async for attempt in AsyncRetrying(wait=wait_fixed(interval.total_seconds())):
49+
nap = (
50+
asyncio.sleep
51+
if early_wake_up_event is None
52+
else SleepUsingAsyncioEvent(early_wake_up_event)
53+
)
54+
async for attempt in AsyncRetrying(
55+
sleep=nap,
56+
wait=wait_fixed(interval.total_seconds()),
57+
):
3758
with attempt:
3859
with log_context(
3960
_logger,
@@ -51,6 +72,7 @@ def start_periodic_task(
5172
interval: datetime.timedelta,
5273
task_name: str,
5374
wait_before_running: datetime.timedelta = datetime.timedelta(0),
75+
early_wake_up_event: asyncio.Event | None = None,
5476
**kwargs,
5577
) -> asyncio.Task:
5678
with log_context(
@@ -64,6 +86,7 @@ def start_periodic_task(
6486
task,
6587
interval=interval,
6688
task_name=task_name,
89+
early_wake_up_event=early_wake_up_event,
6790
**kwargs,
6891
),
6992
name=task_name,

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/resource_usage_tracker/errors.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/service-library/tests/test_background_task.py

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import asyncio
88
import datetime
9-
from typing import AsyncIterator, Awaitable, Callable
9+
from collections.abc import AsyncIterator, Awaitable, Callable
10+
from typing import Final
1011
from unittest import mock
1112

1213
import pytest
@@ -18,38 +19,50 @@
1819
stop_periodic_task,
1920
)
2021

21-
_FAST_POLL_INTERVAL = 1
22+
_FAST_POLL_INTERVAL: Final[int] = 1
23+
_VERY_SLOW_POLL_INTERVAL: Final[int] = 100
2224

2325

2426
@pytest.fixture
2527
def mock_background_task(mocker: MockerFixture) -> mock.AsyncMock:
26-
mocked_task = mocker.AsyncMock(return_value=None)
27-
return mocked_task
28+
return mocker.AsyncMock(return_value=None)
2829

2930

3031
@pytest.fixture
3132
def task_interval() -> datetime.timedelta:
3233
return datetime.timedelta(seconds=_FAST_POLL_INTERVAL)
3334

3435

35-
@pytest.fixture(params=[None, 1])
36+
@pytest.fixture
37+
def very_long_task_interval() -> datetime.timedelta:
38+
return datetime.timedelta(seconds=_VERY_SLOW_POLL_INTERVAL)
39+
40+
41+
@pytest.fixture(params=[None, 1], ids=lambda x: f"stop-timeout={x}")
3642
def stop_task_timeout(request: pytest.FixtureRequest) -> float | None:
3743
return request.param
3844

3945

4046
@pytest.fixture
4147
async def create_background_task(
4248
faker: Faker, stop_task_timeout: float | None
43-
) -> AsyncIterator[Callable[[datetime.timedelta, Callable], Awaitable[asyncio.Task]]]:
49+
) -> AsyncIterator[
50+
Callable[
51+
[datetime.timedelta, Callable, asyncio.Event | None], Awaitable[asyncio.Task]
52+
]
53+
]:
4454
created_tasks = []
4555

4656
async def _creator(
47-
interval: datetime.timedelta, task: Callable[..., Awaitable]
57+
interval: datetime.timedelta,
58+
task: Callable[..., Awaitable],
59+
early_wake_up_event: asyncio.Event | None,
4860
) -> asyncio.Task:
4961
background_task = start_periodic_task(
5062
task,
5163
interval=interval,
5264
task_name=faker.pystr(),
65+
early_wake_up_event=early_wake_up_event,
5366
)
5467
assert background_task
5568
created_tasks.append(background_task)
@@ -62,33 +75,69 @@ async def _creator(
6275
)
6376

6477

78+
@pytest.mark.parametrize(
79+
"wake_up_event", [None, asyncio.Event], ids=lambda x: f"wake-up-event: {x}"
80+
)
6581
async def test_background_task_created_and_deleted(
6682
mock_background_task: mock.AsyncMock,
6783
task_interval: datetime.timedelta,
6884
create_background_task: Callable[
69-
[datetime.timedelta, Callable], Awaitable[asyncio.Task]
85+
[datetime.timedelta, Callable, asyncio.Event | None], Awaitable[asyncio.Task]
7086
],
87+
wake_up_event: Callable | None,
7188
):
72-
task = await create_background_task(
89+
event = wake_up_event() if wake_up_event else None
90+
_task = await create_background_task(
7391
task_interval,
7492
mock_background_task,
93+
event,
7594
)
7695
await asyncio.sleep(5 * task_interval.total_seconds())
7796
mock_background_task.assert_called()
78-
assert mock_background_task.call_count > 1
97+
assert mock_background_task.call_count > 2
98+
99+
100+
async def test_background_task_wakes_up_early(
101+
mock_background_task: mock.AsyncMock,
102+
very_long_task_interval: datetime.timedelta,
103+
create_background_task: Callable[
104+
[datetime.timedelta, Callable, asyncio.Event | None], Awaitable[asyncio.Task]
105+
],
106+
):
107+
wake_up_event = asyncio.Event()
108+
_task = await create_background_task(
109+
very_long_task_interval,
110+
mock_background_task,
111+
wake_up_event,
112+
)
113+
await asyncio.sleep(5 * _FAST_POLL_INTERVAL)
114+
# now the task should have run only once
115+
mock_background_task.assert_called_once()
116+
await asyncio.sleep(5 * _FAST_POLL_INTERVAL)
117+
mock_background_task.assert_called_once()
118+
# this should wake up the task
119+
wake_up_event.set()
120+
await asyncio.sleep(5 * _FAST_POLL_INTERVAL)
121+
mock_background_task.assert_called()
122+
assert mock_background_task.call_count == 2
123+
# no change this now waits again a very long time
124+
await asyncio.sleep(5 * _FAST_POLL_INTERVAL)
125+
mock_background_task.assert_called()
126+
assert mock_background_task.call_count == 2
79127

80128

81129
async def test_background_task_raises_restarts(
82130
mock_background_task: mock.AsyncMock,
83131
task_interval: datetime.timedelta,
84132
create_background_task: Callable[
85-
[datetime.timedelta, Callable], Awaitable[asyncio.Task]
133+
[datetime.timedelta, Callable, asyncio.Event | None], Awaitable[asyncio.Task]
86134
],
87135
):
88136
mock_background_task.side_effect = RuntimeError("pytest faked runtime error")
89-
task = await create_background_task(
137+
_task = await create_background_task(
90138
task_interval,
91139
mock_background_task,
140+
None,
92141
)
93142
await asyncio.sleep(5 * task_interval.total_seconds())
94143
mock_background_task.assert_called()
@@ -99,13 +148,14 @@ async def test_background_task_correctly_cancels(
99148
mock_background_task: mock.AsyncMock,
100149
task_interval: datetime.timedelta,
101150
create_background_task: Callable[
102-
[datetime.timedelta, Callable], Awaitable[asyncio.Task]
151+
[datetime.timedelta, Callable, asyncio.Event | None], Awaitable[asyncio.Task]
103152
],
104153
):
105154
mock_background_task.side_effect = asyncio.CancelledError
106-
task = await create_background_task(
155+
_task = await create_background_task(
107156
task_interval,
108157
mock_background_task,
158+
None,
109159
)
110160
await asyncio.sleep(5 * task_interval.total_seconds())
111161
# the task will be called once, and then stop

scripts/maintenance/migrate_project/src/db.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def _get_project(connection: Connection, project_uuid: UUID) -> ResultProxy:
4848
def _get_hidden_project(connection: Connection, prj_owner: int) -> ResultProxy:
4949
return connection.execute(
5050
select(projects).where(
51-
and_(projects.c.prj_owner == prj_owner, projects.c.hidden == True)
51+
and_(projects.c.prj_owner == prj_owner, projects.c.hidden.is_(True))
5252
)
5353
)
5454

@@ -61,7 +61,7 @@ def _get_file_meta_data_without_soft_links(
6161
and_(
6262
file_meta_data.c.node_id == f"{node_uuid}",
6363
file_meta_data.c.project_id == f"{project_id}",
64-
file_meta_data.c.is_soft_link != True,
64+
file_meta_data.c.is_soft_link.is_not(True),
6565
)
6666
)
6767
)

services/director-v2/src/simcore_service_director_v2/api/dependencies/scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import Depends, FastAPI, Request
22

33
from ...core.settings import ComputationalBackendSettings
4-
from ...modules.comp_scheduler.base_scheduler import BaseCompScheduler
4+
from ...modules.comp_scheduler import BaseCompScheduler
55
from . import get_app
66

77

services/director-v2/src/simcore_service_director_v2/api/routes/computations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
from ...models.comp_runs import CompRunsAtDB, ProjectMetadataDict, RunMetadataDict
6464
from ...models.comp_tasks import CompTaskAtDB
6565
from ...modules.catalog import CatalogClient
66-
from ...modules.comp_scheduler.base_scheduler import BaseCompScheduler
66+
from ...modules.comp_scheduler import BaseCompScheduler
6767
from ...modules.db.repositories.clusters import ClustersRepository
6868
from ...modules.db.repositories.comp_pipelines import CompPipelinesRepository
6969
from ...modules.db.repositories.comp_runs import CompRunsRepository
@@ -288,7 +288,7 @@ async def _try_start_pipeline(
288288
)
289289
# NOTE: in case of a burst of calls to that endpoint, we might end up in a weird state.
290290
@run_sequentially_in_context(target_args=["computation.project_id"])
291-
async def create_computation( # noqa: PLR0913 # pylint:disable=too-many-positional-arguments
291+
async def create_computation( # noqa: PLR0913 # pylint: disable=too-many-positional-arguments
292292
computation: ComputationCreate,
293293
request: Request,
294294
project_repo: Annotated[

services/director-v2/src/simcore_service_director_v2/core/application.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ def init_app(settings: AppSettings | None = None) -> FastAPI:
173173
)
174174
if dynamic_scheduler_enabled or computational_backend_enabled:
175175
rabbitmq.setup(app)
176+
redis.setup(app)
176177

177178
if dynamic_scheduler_enabled:
178-
redis.setup(app)
179179
dynamic_sidecar.setup(app)
180180
socketio.setup(app)
181181
notifier.setup(app)
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1-
from .background_task import setup
1+
from fastapi import FastAPI
22

3-
__all__: tuple[str, ...] = ("setup",)
3+
from ._base_scheduler import BaseCompScheduler
4+
from ._task import on_app_shutdown, on_app_startup
5+
6+
7+
def setup(app: FastAPI):
8+
app.add_event_handler("startup", on_app_startup(app))
9+
app.add_event_handler("shutdown", on_app_shutdown(app))
10+
11+
12+
__all__: tuple[str, ...] = (
13+
"setup",
14+
"BaseCompScheduler",
15+
)

0 commit comments

Comments
 (0)