|
33 | 33 |
|
34 | 34 | from conductor.client.automator.task_handler import TaskHandler, get_registered_workers |
35 | 35 | from conductor.client.configuration.configuration import Configuration |
| 36 | +from conductor.client.context.task_context import LEAVE_TASK_UNRESOLVED |
36 | 37 | from conductor.client.worker.worker_task import worker_task |
37 | 38 | from conductor.client.http.models.workflow_def import WorkflowDef |
38 | 39 | from conductor.client.http.models.task_def import TaskDef |
@@ -74,9 +75,10 @@ def _retry_on_transient(func, *args, retries=4, base_delay=1, **kwargs): |
74 | 75 | # Short response timeout — task must heartbeat to stay alive |
75 | 76 | RESPONSE_TIMEOUT_SECONDS = 10 |
76 | 77 |
|
77 | | -# Task sleeps longer than the response timeout to prove heartbeat works. |
78 | | -# Must be long enough that the server's workflow sweeper catches the expired |
79 | | -# task BEFORE the worker completes. |
| 78 | +# The heartbeat worker sleeps this long (well beyond responseTimeout) to prove |
| 79 | +# that lease extension keeps a genuinely long-running task alive. The |
| 80 | +# no-heartbeat worker no longer sleeps — it returns LEAVE_TASK_UNRESOLVED so the |
| 81 | +# server times the task out deterministically (see async_lease_no_heartbeat_task). |
80 | 82 | TASK_SLEEP_SECONDS = 50 |
81 | 83 |
|
82 | 84 | # Short task duration for performance test — well within timeout |
@@ -131,13 +133,20 @@ async def async_lease_heartbeat_task(job_id: str) -> dict: |
131 | 133 | ), |
132 | 134 | overwrite_task_def=True, |
133 | 135 | ) |
134 | | -async def async_lease_no_heartbeat_task(job_id: str) -> dict: |
135 | | - """Async long-running task without heartbeat — should time out.""" |
136 | | - logger.info("[async_no_heartbeat] Starting job %s, sleeping %ss (timeout=%ss)", |
137 | | - job_id, TASK_SLEEP_SECONDS, RESPONSE_TIMEOUT_SECONDS) |
138 | | - await asyncio.sleep(TASK_SLEEP_SECONDS) |
139 | | - logger.info("[async_no_heartbeat] Completed job %s", job_id) |
140 | | - return {'job_id': job_id, 'status': 'completed', 'slept': TASK_SLEEP_SECONDS} |
| 136 | +async def async_lease_no_heartbeat_task(job_id: str): |
| 137 | + """Async task without heartbeat that deliberately never resolves itself. |
| 138 | +
|
| 139 | + Returning LEAVE_TASK_UNRESOLVED makes the SDK lease the task but report no |
| 140 | + result and stop extending the lease. With no heartbeat, the lease expires |
| 141 | + after responseTimeoutSeconds and the server TIMES_OUT the task — with no |
| 142 | + race against a worker completion. (The old version slept 50s then completed, |
| 143 | + so a slow server sweeper could let the completion win, producing the flaky |
| 144 | + "expected TIMED_OUT but got COMPLETED" failures.) |
| 145 | + """ |
| 146 | + logger.info("[async_no_heartbeat] Picked up job %s; leaving it unresolved so " |
| 147 | + "the server times it out (responseTimeout=%ss)", |
| 148 | + job_id, RESPONSE_TIMEOUT_SECONDS) |
| 149 | + return LEAVE_TASK_UNRESOLVED |
141 | 150 |
|
142 | 151 |
|
143 | 152 | @worker_task( |
@@ -357,10 +366,11 @@ def test_01_async_with_heartbeat_completes(self): |
357 | 366 | print("\n PASS: Async task completed with heartbeat keeping lease alive") |
358 | 367 |
|
359 | 368 | def test_02_async_without_heartbeat_times_out(self): |
360 | | - """Async task WITHOUT lease_extend_enabled times out when sleep > responseTimeout.""" |
| 369 | + """Async task WITHOUT lease_extend_enabled times out when it isn't resolved.""" |
361 | 370 | print("\n" + "=" * 80) |
362 | 371 | print("TEST: Async without heartbeat — task should TIME OUT") |
363 | | - print(f" responseTimeoutSeconds={RESPONSE_TIMEOUT_SECONDS}s, task sleeps {TASK_SLEEP_SECONDS}s") |
| 372 | + print(f" responseTimeoutSeconds={RESPONSE_TIMEOUT_SECONDS}s, " |
| 373 | + f"worker leaves task unresolved (server must time it out)") |
364 | 374 | print("=" * 80) |
365 | 375 |
|
366 | 376 | wf_name = f'test_async_lease_no_heartbeat_{RUN_ID}' |
|
0 commit comments