Skip to content

Commit 9cf9bea

Browse files
goynamhoophalabjunhaoliao
authored
fix(job-orchestration)!: Add defensive timeouts to prevent scheduler hang (fixes y-scope#2252). (y-scope#2271)
Co-authored-by: hoophalab <200652805+hoophalab@users.noreply.github.com> Co-authored-by: Junhao Liao <junhao.liao@yscope.com>
1 parent 5d58479 commit 9cf9bea

14 files changed

Lines changed: 146 additions & 20 deletions

File tree

components/clp-py-utils/clp_py_utils/clp_config.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,12 +445,18 @@ def transform_for_container(self):
445445
self.port = self.DEFAULT_PORT
446446

447447

448-
class CompressionWorker(BaseModel):
448+
class WorkerConfigBase(BaseModel):
449449
logging_level: LoggingLevel = "INFO"
450+
task_soft_time_limit: NonNegativeInt = 600
451+
task_time_limit: NonNegativeInt = 1200
450452

451453

452-
class QueryWorker(BaseModel):
453-
logging_level: LoggingLevel = "INFO"
454+
class CompressionWorker(WorkerConfigBase):
455+
pass
456+
457+
458+
class QueryWorker(WorkerConfigBase):
459+
pass
454460

455461

456462
class Redis(BaseModel):

components/job-orchestration/job_orchestration/executor/compress/celery_compress.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from celery import signals
22
from celery.app.task import Task
3+
from celery.exceptions import SoftTimeLimitExceeded
34
from celery.utils.log import get_task_logger
45

56
from job_orchestration.executor.compress.celery import app
@@ -23,11 +24,17 @@ def compress(
2324
paths_to_compress_json: str,
2425
clp_metadata_db_connection_config,
2526
):
26-
return compression_entry_point(
27-
job_id,
28-
task_id,
29-
clp_io_config_json,
30-
paths_to_compress_json,
31-
clp_metadata_db_connection_config,
32-
logger,
33-
)
27+
try:
28+
return compression_entry_point(
29+
job_id,
30+
task_id,
31+
clp_io_config_json,
32+
paths_to_compress_json,
33+
clp_metadata_db_connection_config,
34+
logger,
35+
)
36+
except SoftTimeLimitExceeded:
37+
logger.exception(
38+
f"Compression task job_id={job_id} task_id={task_id} exceeded soft time limit."
39+
)
40+
raise

components/job-orchestration/job_orchestration/executor/compress/celeryconfig.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22

3+
from job_orchestration.executor.utils import load_clp_config_from_config_path_env_var
34
from job_orchestration.scheduler.constants import SchedulerType, TASK_QUEUE_HIGHEST_PRIORITY
45

56
# Worker settings
@@ -18,6 +19,11 @@
1819

1920
# Results backend settings
2021
result_persistent = True
22+
result_expires = 7200
2123

2224
broker_url = os.getenv("BROKER_URL")
2325
result_backend = os.getenv("RESULT_BACKEND")
26+
27+
_clp_config = load_clp_config_from_config_path_env_var()
28+
task_soft_time_limit = _clp_config.compression_worker.task_soft_time_limit
29+
task_time_limit = _clp_config.compression_worker.task_time_limit

components/job-orchestration/job_orchestration/executor/query/celeryconfig.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22

3+
from job_orchestration.executor.utils import load_clp_config_from_config_path_env_var
34
from job_orchestration.scheduler.constants import SchedulerType
45

56
imports = (
@@ -17,6 +18,11 @@
1718
result_backend = os.getenv("RESULT_BACKEND")
1819

1920
result_persistent = True
21+
result_expires = 7200
22+
23+
_clp_config = load_clp_config_from_config_path_env_var()
24+
task_soft_time_limit = _clp_config.query_worker.task_soft_time_limit
25+
task_time_limit = _clp_config.query_worker.task_time_limit
2026

2127
# Differentiate between tasks that have started v.s. tasks still in queue
2228
task_track_started = True

components/job-orchestration/job_orchestration/executor/query/extract_stream_task.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any
66

77
from celery.app.task import Task
8+
from celery.exceptions import SoftTimeLimitExceeded
89
from celery.utils.log import get_task_logger
910
from clp_py_utils.clp_config import (
1011
Database,
@@ -181,9 +182,7 @@ def _make_command_and_env_vars(
181182
return command, env_vars
182183

183184

184-
@app.task(bind=True)
185-
def extract_stream(
186-
self: Task,
185+
def extract_stream_entry_point(
187186
job_id: str,
188187
task_id: int,
189188
job_config: dict,
@@ -298,3 +297,31 @@ def extract_stream(
298297
logger.info("Finished uploading streams.")
299298

300299
return task_results.model_dump()
300+
301+
302+
@app.task(bind=True)
303+
def extract_stream(
304+
self: Task,
305+
job_id: str,
306+
task_id: int,
307+
job_config: dict,
308+
archive_id: str,
309+
clp_metadata_db_conn_params: dict,
310+
results_cache_uri: str,
311+
dataset: str | None = None,
312+
) -> dict[str, Any]:
313+
try:
314+
return extract_stream_entry_point(
315+
job_id,
316+
task_id,
317+
job_config,
318+
archive_id,
319+
clp_metadata_db_conn_params,
320+
results_cache_uri,
321+
dataset,
322+
)
323+
except SoftTimeLimitExceeded:
324+
logger.exception(
325+
f"Stream extraction task job_id={job_id} task_id={task_id} exceeded soft time limit."
326+
)
327+
raise

components/job-orchestration/job_orchestration/executor/query/fs_search_task.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import msgpack
77
from celery.app.task import Task
8+
from celery.exceptions import SoftTimeLimitExceeded
89
from celery.utils.log import get_task_logger
910
from clp_py_utils.clp_config import (
1011
Database,
@@ -208,9 +209,7 @@ def upload_results_to_s3(
208209
return
209210

210211

211-
@app.task(bind=True)
212-
def search(
213-
self: Task,
212+
def search_entry_point(
214213
job_id: str,
215214
task_id: int,
216215
job_config_blob: bytes,
@@ -286,3 +285,29 @@ def search(
286285
upload_results_to_s3(task_results, s3_config, src_file, dest_path)
287286

288287
return task_results.model_dump()
288+
289+
290+
@app.task(bind=True)
291+
def search(
292+
self: Task,
293+
job_id: str,
294+
task_id: int,
295+
job_config_blob: bytes,
296+
archive_id: str,
297+
clp_metadata_db_conn_params: dict,
298+
results_cache_uri: str,
299+
dataset: str | None = None,
300+
) -> dict[str, Any]:
301+
try:
302+
return search_entry_point(
303+
job_id,
304+
task_id,
305+
job_config_blob,
306+
archive_id,
307+
clp_metadata_db_conn_params,
308+
results_cache_uri,
309+
dataset,
310+
)
311+
except SoftTimeLimitExceeded:
312+
logger.exception(f"Search task job_id={job_id} task_id={task_id} exceeded soft time limit.")
313+
raise

components/job-orchestration/job_orchestration/executor/utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
1+
"""Utilities shared by job-orchestration executors."""
2+
3+
import os
14
from logging import Logger
25
from pathlib import Path
36

4-
from clp_py_utils.clp_config import WorkerConfig
7+
from clp_py_utils.clp_config import ClpConfig, WorkerConfig
58
from clp_py_utils.core import read_yaml_config_file
69

710

11+
def load_clp_config_from_config_path_env_var() -> ClpConfig:
12+
"""
13+
Loads the CLP config from `CLP_CONFIG_PATH`.
14+
15+
:return: The loaded `ClpConfig` object.
16+
"""
17+
config_path = os.getenv("CLP_CONFIG_PATH")
18+
if config_path is None or "" == config_path:
19+
return ClpConfig()
20+
config = read_yaml_config_file(Path(config_path))
21+
if config is None:
22+
return ClpConfig()
23+
return ClpConfig.model_validate(config)
24+
25+
826
def load_worker_config(
927
config_path: Path,
1028
logger: Logger,

components/job-orchestration/job_orchestration/scheduler/compress/task_manager/celery_task_manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
from typing import Any
45

56
import celery
@@ -8,6 +9,8 @@
89
from job_orchestration.scheduler.compress.task_manager.task_manager import TaskManager
910
from job_orchestration.scheduler.task_result import CompressionTaskResult
1011

12+
logger = logging.getLogger(__name__)
13+
1114

1215
class CeleryTaskManager(TaskManager):
1316
class ResultHandle(TaskManager.ResultHandle):
@@ -20,6 +23,9 @@ def get_result(self, timeout: float = 0.1) -> list[CompressionTaskResult] | None
2023
return [CompressionTaskResult.model_validate(res) for res in results]
2124
except celery.exceptions.TimeoutError:
2225
return None
26+
except celery.exceptions.SoftTimeLimitExceeded:
27+
logger.exception("Compression task exceeded soft time limit.")
28+
raise
2329

2430
def submit(self, task_params: list[dict[str, Any]]) -> TaskManager.ResultHandle:
2531
task_instances = [compress.s(**params) for params in task_params]

components/job-orchestration/job_orchestration/scheduler/query/query_scheduler.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
)
7878
from job_orchestration.scheduler.utils import kill_hanging_jobs
7979

80+
TASK_RESULT_GET_TIMEOUT = 10
81+
TASK_RESULT_GET_INTERVAL = 0.005
82+
8083
# Setup logging
8184
logger = get_logger("search-job-handler")
8285

@@ -837,7 +840,13 @@ def handle_pending_query_jobs(
837840
def try_getting_task_result(async_task_result):
838841
if not async_task_result.ready():
839842
return None
840-
return async_task_result.get(interval=0.005)
843+
try:
844+
return async_task_result.get(
845+
timeout=TASK_RESULT_GET_TIMEOUT, interval=TASK_RESULT_GET_INTERVAL
846+
)
847+
except celery.exceptions.TimeoutError:
848+
logger.exception("Timed out waiting for task result.")
849+
raise
841850

842851

843852
def found_max_num_latest_results(

components/package-template/src/etc/clp-config.template.json.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,13 @@ telemetry:
104104
#
105105
#compression_worker:
106106
# logging_level: "INFO"
107+
# task_soft_time_limit: 600 # A value of 0 disables the limit
108+
# task_time_limit: 1200 # A value of 0 disables the limit
107109
#
108110
#query_worker:
109111
# logging_level: "INFO"
112+
# task_soft_time_limit: 600 # A value of 0 disables the limit
113+
# task_time_limit: 1200 # A value of 0 disables the limit
110114
#
111115
#webui:
112116
# host: "localhost"

0 commit comments

Comments
 (0)