Skip to content

Commit 86503ca

Browse files
committed
feat(celery): add tasks table cleanup based on creation date
Cleans the tasks table by deleting the tasks older than a certain retention duration.
1 parent 5729501 commit 86503ca

14 files changed

Lines changed: 279 additions & 1 deletion

File tree

antarest/core/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ class StorageConfig:
191191
variable_view_gc_retention_days: int = 30
192192
watcher_scan_sleeping_time: int = 900
193193
watcher_scan_dry_run: bool = False
194+
tasks_gc_retention_days: int = 30
195+
tasks_gc_sleeping_time: int = 86400
196+
tasks_gc_dry_run: bool = False
194197
study_storage: StudyStorageConfig = StudyStorageConfig()
195198

196199
@classmethod
@@ -241,6 +244,7 @@ def from_dict(cls, data: JSON, desktop_mode: bool = False) -> "StorageConfig":
241244
),
242245
watcher_scan_sleeping_time=data.get("watcher_scan_sleeping_time", defaults.watcher_scan_sleeping_time),
243246
watcher_scan_dry_run=data.get("watcher_scan_dry_run", defaults.watcher_scan_dry_run),
247+
tasks_gc_retention_days=data.get("tasks_gc_retention_days", defaults.tasks_gc_retention_days),
244248
study_storage=StudyStorageConfig.from_dict(data.get("study", {}).get("storage", {})),
245249
)
246250

antarest/core/tasks/repository.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from antarest.core.tasks.model import TaskJob, TaskListFilter
2222
from antarest.core.utils.fastapi_sqlalchemy import db
23+
from antarest.core.utils.utils import current_time
2324

2425

2526
class TaskJobRepository:
@@ -96,3 +97,14 @@ def delete(self, tid: str) -> None:
9697
if task:
9798
session.delete(task)
9899
session.commit()
100+
101+
def delete_by_creation_date(self, task_retention_duration: int) -> int:
102+
session = self.session
103+
ctime = current_time()
104+
deleted_rows = (
105+
session.query(TaskJob)
106+
.filter(TaskJob.creation_date < ctime - datetime.timedelta(days=task_retention_duration))
107+
.delete()
108+
)
109+
session.commit()
110+
return deleted_rows

antarest/core/tasks/service.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ def await_task(self, task_id: str, timeout_sec: int = DEFAULT_AWAIT_MAX_TIMEOUT)
108108
"""
109109
raise NotImplementedError()
110110

111+
@abstractmethod
112+
def delete_task_by_creation_date(self, task_retention_duration: int) -> int:
113+
raise NotImplementedError()
114+
111115

112116
# noinspection PyUnusedLocal
113117
class NoopNotifier(ITaskNotifier):
@@ -513,3 +517,7 @@ def get_task_progress(self, task_id: str) -> Optional[int]:
513517
return task.progress
514518
else:
515519
raise UserHasNotPermissionError()
520+
521+
@override
522+
def delete_task_by_creation_date(self, task_retention_duration: int) -> int:
523+
return self.repo.delete_by_creation_date(task_retention_duration=task_retention_duration)

antarest/maintenance/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class TaskName(StrEnum):
5050
BLOBS_CLEANER = "blobs_cleaner"
5151
AUTO_ARCHIVER = "auto_archiver"
5252
VARIABLE_VIEW_CLEANER = "variable_view_cleaner"
53+
TASKS_CLEANER = "tasks_cleaner"
5354

5455

5556
def _mask_url_credentials(url: str) -> str:
@@ -103,6 +104,7 @@ def _setup_periodic_tasks(sender: Celery, **_: Any) -> None:
103104
from antarest.maintenance.tasks.auto_archive_task import setup_auto_archive_task
104105
from antarest.maintenance.tasks.gc_blob_task import clean_blobs_task
105106
from antarest.maintenance.tasks.gc_matrix_task import clean_matrices_task
107+
from antarest.maintenance.tasks.gc_tasks_task import gc_tasks_task
106108
from antarest.maintenance.tasks.gc_variable_view_task import clean_variable_views_task
107109
from antarest.maintenance.tasks.watcher_scan_task import watcher_scan_task
108110

@@ -115,6 +117,7 @@ def _setup_periodic_tasks(sender: Celery, **_: Any) -> None:
115117
sender.add_periodic_task(
116118
storage.variable_view_gc_sleeping_time, clean_variable_views_task.s(), name=TaskName.VARIABLE_VIEW_CLEANER
117119
)
120+
sender.add_periodic_task(storage.tasks_gc_sleeping_time, gc_tasks_task.s(), name=TaskName.TASKS_CLEANER)
118121

119122
logger.info(
120123
f"Periodic tasks registered: matrix_gc={storage.matrix_gc_sleeping_time}s, "

antarest/maintenance/context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import logging
2020
from typing import TYPE_CHECKING, cast
2121

22+
from antarest.core.tasks.service import ITaskService
2223
from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware
2324
from antarest.service_creator import SESSION_ARGS, create_core_services, init_db_engine
2425

@@ -66,3 +67,7 @@ def study_service(self) -> "StudyService":
6667
@property
6768
def output_service(self) -> "OutputService":
6869
return self.core_services.output_service
70+
71+
@property
72+
def task_service(self) -> "ITaskService":
73+
return self.core_services.task_service

antarest/maintenance/tasks/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class LockId(IntEnum):
9797
AUTO_ARCHIVE = 1003
9898
WATCHER_SCAN = 1004
9999
VARIABLE_VIEW_GC = 1005
100+
TASKS_GC = 1006
100101

101102

102103
class WatcherScanTaskResult(AntaresBaseModel):
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (c) 2026, RTE (https://www.rte-france.com)
2+
#
3+
# See AUTHORS.txt
4+
#
5+
# This Source Code Form is subject to the terms of the Mozilla Public
6+
# License, v. 2.0. If a copy of the MPL was not distributed with this
7+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
8+
#
9+
# SPDX-License-Identifier: MPL-2.0
10+
#
11+
# This file is part of the Antares project.
12+
import logging
13+
import time
14+
15+
from antarest.core.tasks.service import ITaskService
16+
from antarest.core.utils.fastapi_sqlalchemy import db
17+
from antarest.core.utils.lock import LockNotAcquired, create_lock
18+
from antarest.maintenance.tasks.common import BackGroundTaskStatus, GarbageCollectorTaskResult, LockId
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
def clean_tasks(task_service: ITaskService, dry_run: bool, task_retention_duration: int) -> GarbageCollectorTaskResult:
24+
start_time = time.time()
25+
deleted_count = 0
26+
try:
27+
with db():
28+
with create_lock(db.session, lock_id=LockId.TASKS_GC):
29+
logger.info(f"Deleting tasks older than {task_retention_duration} days from the database")
30+
if not dry_run:
31+
deleted_count = task_service.delete_task_by_creation_date(task_retention_duration)
32+
else:
33+
logger.info(f"[dry-run] Would delete tasks older than {task_retention_duration} days")
34+
deleted_count = 0
35+
36+
duration_seconds = time.time() - start_time
37+
38+
except LockNotAcquired:
39+
logger.warning(f"Could not acquire lock {LockId.TASKS_GC}, another GC is probably running")
40+
return GarbageCollectorTaskResult(
41+
status=BackGroundTaskStatus.SKIPPED,
42+
reason="lock_not_acquired",
43+
deleted_count=0,
44+
duration_seconds=time.time() - start_time,
45+
)
46+
47+
logger.info(f"Finished tasks GC in {duration_seconds}s (deleted {deleted_count})")
48+
49+
return GarbageCollectorTaskResult(
50+
status=BackGroundTaskStatus.SUCCESS,
51+
deleted_count=deleted_count,
52+
duration_seconds=duration_seconds,
53+
dry_run=dry_run,
54+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright (c) 2026, RTE (https://www.rte-france.com)
2+
#
3+
# See AUTHORS.txt
4+
#
5+
# This Source Code Form is subject to the terms of the Mozilla Public
6+
# License, v. 2.0. If a copy of the MPL was not distributed with this
7+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
8+
#
9+
# SPDX-License-Identifier: MPL-2.0
10+
#
11+
# This file is part of the Antares project.
12+
13+
"""Celery task for regular purge of task table."""
14+
15+
from antarest.maintenance.app import MaintenanceTask, TaskName, celery_app
16+
from antarest.maintenance.tasks.common import GarbageCollectorTaskResult
17+
from antarest.maintenance.tasks.gc_tasks import clean_tasks
18+
19+
20+
@celery_app.task(base=MaintenanceTask, bind=True, name=TaskName.TASKS_CLEANER, pydantic=True)
21+
def gc_tasks_task(self: MaintenanceTask) -> GarbageCollectorTaskResult:
22+
ctx = self.context
23+
24+
return clean_tasks(
25+
ctx.task_service, ctx.config.storage.tasks_gc_dry_run, ctx.config.storage.tasks_gc_retention_days
26+
)

tests/conftest_services.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ def list_tasks(self, task_filter: TaskListFilter) -> t.List[TaskDTO]:
113113
def await_task(self, task_id: str, timeout_sec: t.Optional[int] = None) -> None:
114114
pass
115115

116+
@override
117+
def delete_task_by_creation_date(self, task_retention_duration: int) -> int:
118+
pass
119+
116120

117121
@pytest.fixture(name="command_context")
118122
def command_context_fixture(matrix_service: MatrixService, blob_service: BlobService) -> CommandContext:

tests/integration/maintenance_blueprint/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
from antarest.blobstore.repository import BlobContentRepository
2020
from antarest.blobstore.service import BlobService
21+
from antarest.core.config import Config
22+
from antarest.core.interfaces.eventbus import IEventBus
23+
from antarest.core.tasks.repository import TaskJobRepository
24+
from antarest.core.tasks.service import ITaskService, TaskJobService
25+
from antarest.eventbus.business.local_eventbus import LocalEventBus
26+
from antarest.eventbus.service import EventBusService
2127
from tests.conftest_db import db_engine_fixture, db_middleware_fixture
2228
from tests.matrixstore.conftest import (
2329
content_repo_fixture,
@@ -45,3 +51,25 @@ def simple_blob_service_fixture(tmp_path: Path) -> BlobService:
4551
blob_dir.mkdir()
4652
blob_content_repository = BlobContentRepository(bucket_dir=blob_dir)
4753
return BlobService(blob_content_repository=blob_content_repository)
54+
55+
56+
@pytest.fixture(name="event_bus", scope="session")
57+
def event_bus_fixture() -> IEventBus:
58+
"""
59+
Fixture that creates a Mock instance of IEventBus with a session-level scope.
60+
61+
Returns:
62+
A Mock instance of the IEventBus class for event bus-related testing.
63+
"""
64+
return EventBusService(LocalEventBus())
65+
66+
67+
@pytest.fixture
68+
def task_service(task_job_repository: TaskJobRepository, event_bus: IEventBus) -> ITaskService:
69+
config = Config()
70+
return TaskJobService(config=config, repository=task_job_repository, event_bus=event_bus)
71+
72+
73+
@pytest.fixture
74+
def task_job_repository() -> TaskJobRepository:
75+
return TaskJobRepository()

0 commit comments

Comments
 (0)