@@ -57,14 +57,13 @@ class TaskScheduler:
57
57
the code launching the task.
58
58
You can also specify the `result` (and/or an `error`) when returning from the function.
59
59
60
- The reconciliation loop runs every 5 mns, so this is not a precise scheduler. When wanting
61
- to launch now, the launch will still not happen before the next loop run.
62
-
63
- Tasks will be run on the worker specified with `run_background_tasks_on` config,
64
- or the main one by default.
60
+ The reconciliation loop runs every minute, so this is not a precise scheduler.
65
61
There is a limit of 10 concurrent tasks, so tasks may be delayed if the pool is already
66
62
full. In this regard, please take great care that scheduled tasks can actually finished.
67
63
For now there is no mechanism to stop a running task if it is stuck.
64
+
65
+ Tasks will be run on the worker specified with `run_background_tasks_on` config,
66
+ or the main one by default.
68
67
"""
69
68
70
69
# Precision of the scheduler, evaluation of tasks to run will only happen
@@ -85,7 +84,7 @@ def __init__(self, hs: "HomeServer"):
85
84
self ._actions : Dict [
86
85
str ,
87
86
Callable [
88
- [ScheduledTask , bool ],
87
+ [ScheduledTask ],
89
88
Awaitable [Tuple [TaskStatus , Optional [JsonMapping ], Optional [str ]]],
90
89
],
91
90
] = {}
@@ -98,11 +97,13 @@ def __init__(self, hs: "HomeServer"):
98
97
"handle_scheduled_tasks" ,
99
98
self ._handle_scheduled_tasks ,
100
99
)
100
+ else :
101
+ self .replication_client = hs .get_replication_command_handler ()
101
102
102
103
def register_action (
103
104
self ,
104
105
function : Callable [
105
- [ScheduledTask , bool ],
106
+ [ScheduledTask ],
106
107
Awaitable [Tuple [TaskStatus , Optional [JsonMapping ], Optional [str ]]],
107
108
],
108
109
action_name : str ,
@@ -115,10 +116,9 @@ def register_action(
115
116
calling `schedule_task` but rather in an `__init__` method.
116
117
117
118
Args:
118
- function: The function to be executed for this action. The parameters
119
- passed to the function when launched are the `ScheduledTask` being run,
120
- and a `first_launch` boolean to signal if it's a resumed task or the first
121
- launch of it. The function should return a tuple of new `status`, `result`
119
+ function: The function to be executed for this action. The parameter
120
+ passed to the function when launched is the `ScheduledTask` being run.
121
+ The function should return a tuple of new `status`, `result`
122
122
and `error` as specified in `ScheduledTask`.
123
123
action_name: The name of the action to be associated with the function
124
124
"""
@@ -171,6 +171,12 @@ async def schedule_task(
171
171
)
172
172
await self ._store .insert_scheduled_task (task )
173
173
174
+ if status == TaskStatus .ACTIVE :
175
+ if self ._run_background_tasks :
176
+ await self ._launch_task (task )
177
+ else :
178
+ self .replication_client .send_new_active_task (task .id )
179
+
174
180
return task .id
175
181
176
182
async def update_task (
@@ -265,21 +271,13 @@ async def delete_task(self, id: str) -> None:
265
271
Args:
266
272
id: id of the task to delete
267
273
"""
268
- if self .task_is_running (id ):
269
- raise Exception (f"Task { id } is currently running and can't be deleted" )
274
+ task = await self .get_task (id )
275
+ if task is None :
276
+ raise Exception (f"Task { id } does not exist" )
277
+ if task .status == TaskStatus .ACTIVE :
278
+ raise Exception (f"Task { id } is currently ACTIVE and can't be deleted" )
270
279
await self ._store .delete_scheduled_task (id )
271
280
272
- def task_is_running (self , id : str ) -> bool :
273
- """Check if a task is currently running.
274
-
275
- Can only be called from the worker handling the task scheduling.
276
-
277
- Args:
278
- id: id of the task to check
279
- """
280
- assert self ._run_background_tasks
281
- return id in self ._running_tasks
282
-
283
281
async def _handle_scheduled_tasks (self ) -> None :
284
282
"""Main loop taking care of launching tasks and cleaning up old ones."""
285
283
await self ._launch_scheduled_tasks ()
@@ -288,29 +286,11 @@ async def _handle_scheduled_tasks(self) -> None:
288
286
async def _launch_scheduled_tasks (self ) -> None :
289
287
"""Retrieve and launch scheduled tasks that should be running at that time."""
290
288
for task in await self .get_tasks (statuses = [TaskStatus .ACTIVE ]):
291
- if not self .task_is_running (task .id ):
292
- if (
293
- len (self ._running_tasks )
294
- < TaskScheduler .MAX_CONCURRENT_RUNNING_TASKS
295
- ):
296
- await self ._launch_task (task , first_launch = False )
297
- else :
298
- if (
299
- self ._clock .time_msec ()
300
- > task .timestamp + TaskScheduler .LAST_UPDATE_BEFORE_WARNING_MS
301
- ):
302
- logger .warn (
303
- f"Task { task .id } (action { task .action } ) has seen no update for more than 24h and may be stuck"
304
- )
289
+ await self ._launch_task (task )
305
290
for task in await self .get_tasks (
306
291
statuses = [TaskStatus .SCHEDULED ], max_timestamp = self ._clock .time_msec ()
307
292
):
308
- if (
309
- not self .task_is_running (task .id )
310
- and len (self ._running_tasks )
311
- < TaskScheduler .MAX_CONCURRENT_RUNNING_TASKS
312
- ):
313
- await self ._launch_task (task , first_launch = True )
293
+ await self ._launch_task (task )
314
294
315
295
running_tasks_gauge .set (len (self ._running_tasks ))
316
296
@@ -320,27 +300,27 @@ async def _clean_scheduled_tasks(self) -> None:
320
300
statuses = [TaskStatus .FAILED , TaskStatus .COMPLETE ]
321
301
):
322
302
# FAILED and COMPLETE tasks should never be running
323
- assert not self .task_is_running ( task . id )
303
+ assert task . id not in self ._running_tasks
324
304
if (
325
305
self ._clock .time_msec ()
326
306
> task .timestamp + TaskScheduler .KEEP_TASKS_FOR_MS
327
307
):
328
308
await self ._store .delete_scheduled_task (task .id )
329
309
330
- async def _launch_task (self , task : ScheduledTask , first_launch : bool ) -> None :
310
+ async def _launch_task (self , task : ScheduledTask ) -> None :
331
311
"""Launch a scheduled task now.
332
312
333
313
Args:
334
314
task: the task to launch
335
- first_launch: `True` if it's the first time is launched, `False` otherwise
336
315
"""
337
- assert task . action in self ._actions
316
+ assert self ._run_background_tasks
338
317
318
+ assert task .action in self ._actions
339
319
function = self ._actions [task .action ]
340
320
341
321
async def wrapper () -> None :
342
322
try :
343
- (status , result , error ) = await function (task , first_launch )
323
+ (status , result , error ) = await function (task )
344
324
except Exception :
345
325
f = Failure ()
346
326
logger .error (
@@ -360,6 +340,20 @@ async def wrapper() -> None:
360
340
)
361
341
self ._running_tasks .remove (task .id )
362
342
343
+ if len (self ._running_tasks ) >= TaskScheduler .MAX_CONCURRENT_RUNNING_TASKS :
344
+ return
345
+
346
+ if (
347
+ self ._clock .time_msec ()
348
+ > task .timestamp + TaskScheduler .LAST_UPDATE_BEFORE_WARNING_MS
349
+ ):
350
+ logger .warn (
351
+ f"Task { task .id } (action { task .action } ) has seen no update for more than 24h and may be stuck"
352
+ )
353
+
354
+ if task .id in self ._running_tasks :
355
+ return
356
+
363
357
self ._running_tasks .add (task .id )
364
358
await self .update_task (task .id , status = TaskStatus .ACTIVE )
365
359
description = f"{ task .id } -{ task .action } "
0 commit comments