Found while fixing #1904 in the same class. CronJob.batched_run(tasks) schedules the first task and then blocks forever, so the rest of the list is never scheduled and the method never returns.
def run(self, task: str, **kwargs):
try:
job = self._run(task, **kwargs)
while True: # <- blocks here for the life of the process
time.sleep(1)
return job # <- unreachable
...
def batched_run(self, tasks: List[str], **kwargs):
outputs = []
for task in tasks: # <- iteration 2 is never reached
output = self.run(task, **kwargs)
outputs.append(output)
return outputs
Verified with a recording callable and a 1-second interval:
job = CronJob(agent=rec, interval="1second")
threading.Thread(target=lambda: job.batched_run(["task-A", "task-B", "task-C"])).start()
time.sleep(5)
tasks actually scheduled: ['task-A']
batched_run never returned: True
Only task-A ever runs. task-B and task-C are silently dropped, and the return outputs is dead. The only way out of run() is KeyboardInterrupt, which calls self.stop() and returns None — so if a user does Ctrl-C, batched_run then proceeds to schedule task-B on a scheduler that has just been stopped.
Also worth noting: run() returns job after the infinite loop, so outputs would be a list of None even if the loop were escapable — _run() returns nothing.
The reason this needs a decision rather than a patch is what batched_run is supposed to mean:
- Schedule all, then block once —
for task in tasks: self._run(task, **kwargs) followed by a single blocking wait. This matches "run these tasks on a schedule" and is what I would expect the intent to be. It changes run()'s structure, since the blocking loop has to move out of it.
- Run each task to completion in turn — only coherent for a one-shot execution, which is not what a cron job does.
- Deprecate it — if the intended usage is one task per
CronJob, batched_run is a footgun and deleting it is smaller than fixing it.
I lean toward (1), with the blocking wait extracted into something like _block_forever() that both run and batched_run call after scheduling. Happy to send that as a PR if you agree on the semantics.
Found while fixing #1904 in the same class.
CronJob.batched_run(tasks)schedules the first task and then blocks forever, so the rest of the list is never scheduled and the method never returns.Verified with a recording callable and a 1-second interval:
Only
task-Aever runs.task-Bandtask-Care silently dropped, and thereturn outputsis dead. The only way out ofrun()isKeyboardInterrupt, which callsself.stop()and returnsNone— so if a user does Ctrl-C,batched_runthen proceeds to scheduletask-Bon a scheduler that has just been stopped.Also worth noting:
run()returnsjobafter the infinite loop, sooutputswould be a list ofNoneeven if the loop were escapable —_run()returns nothing.The reason this needs a decision rather than a patch is what
batched_runis supposed to mean:for task in tasks: self._run(task, **kwargs)followed by a single blocking wait. This matches "run these tasks on a schedule" and is what I would expect the intent to be. It changesrun()'s structure, since the blocking loop has to move out of it.CronJob,batched_runis a footgun and deleting it is smaller than fixing it.I lean toward (1), with the blocking wait extracted into something like
_block_forever()that bothrunandbatched_runcall after scheduling. Happy to send that as a PR if you agree on the semantics.