Skip to content

Commit e02ad2b

Browse files
authored
Avoid out-of-order task execution (#164)
* attempt to enforce order in task execution * sad oink * one more test fix * make test more robust 2 -> 3 * restrict queue to 1 task. * MAX_TASK_WAIT * bugfixes * snapshot_interval & docs
1 parent 7bfa775 commit e02ad2b

7 files changed

Lines changed: 107 additions & 41 deletions

File tree

docs/source/version.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
Version history
22
===============
3+
**tomato**-v2.2
4+
---------------
5+
..
6+
.. image:: https://img.shields.io/static/v1?label=tomato&message=v2.1&color=blue&logo=github
7+
:target: https://github.com/dgbowl/tomato/tree/2.1
8+
.. image:: https://img.shields.io/static/v1?label=tomato&message=v2.1&color=blue&logo=pypi
9+
:target: https://pypi.org/project/tomato/2.1/
10+
.. image:: https://img.shields.io/static/v1?label=release%20date&message=2025-06-07&color=red&logo=pypi
11+
12+
.. sectionauthor::
13+
Peter Kraus
14+
15+
Developed at the ConCat lab at TU Berlin.
16+
17+
Changes from ``tomato-2.1`` include:
18+
19+
- Fixes many bugs due to the :func:`cmp_measure` function race condition with running tasks.
20+
- Introduces the "lazy pirate" pattern in ``tomato-job`` processes, which should make jobs more reliable.
21+
22+
- A new ``Payload-2.2``, where the ``settings.snapshot.snapshot_interval`` replaces ``settings.snapshot.frequency``. As a consequence, the ``snapshot_interval`` can be provided as :class:`str`, which will be converted to the number of seconds using :mod:`pint`.
23+
24+
.. codeauthor::
25+
Peter Kraus
26+
327
**tomato**-v2.1
428
---------------
529
.. image:: https://img.shields.io/static/v1?label=tomato&message=v2.1&color=blue&logo=github

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dependencies = [
3030
"pyyaml >= 6.0",
3131
"psutil >= 5.9",
3232
# "dgbowl_schemas >= 124",
33-
"dgbowl_schemas @ git+https://github.com/dgbowl/dgbowl-schemas.git#egg=payload_2.2",
33+
"dgbowl_schemas @ git+https://github.com/dgbowl/dgbowl-schemas.git@payload_2.2",
3434
"pyzmq >= 25.1",
3535
"netcdf4 >= 1.7",
3636
"xarray >= 2024.10.0",

src/tomato/daemon/job.py

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
logger = logging.getLogger(__name__)
3737

3838
MAX_JOB_NOPID = timedelta(seconds=10)
39+
MAX_TASK_WAIT = 10
3940

4041

4142
def method_validate(
@@ -479,7 +480,7 @@ def tomato_job() -> None:
479480

480481

481482
def job_thread(
482-
tasks: list,
483+
tasks: list[Task],
483484
component: Component,
484485
device: Device,
485486
driver: Driver,
@@ -518,23 +519,26 @@ def job_thread(
518519
taskid += f":{task.task_name!r}"
519520
thread.current_task = task
520521
logger.info("%s: processing task", taskid)
522+
523+
# Hold while start contidions are not met
521524
while True:
522-
time.sleep(1e-1)
523525
if task.start_with_task_name is None:
524-
pass
526+
break
525527
elif task.start_with_task_name in thread.started_task_names:
526-
pass
528+
break
527529
else:
528530
logger.debug(
529531
"%s: waiting for task_name '%s'", taskid, task.start_with_task_name
530532
)
531-
continue
533+
time.sleep(0.1)
534+
535+
# Hold while component task_list is not ready
536+
while True:
532537
logger.debug(
533538
"%s: polling component %s for task readiness", taskid, component.name
534539
)
535-
ret, req = lpp.comm(
536-
req, dict(cmd="task_status", params={**kwargs}), **lppargs
537-
)
540+
msg = dict(cmd="task_status", params={**kwargs})
541+
ret, req = lpp.comm(req, msg, **lppargs)
538542
if ret.success and ret.data["can_submit"]:
539543
break
540544
elif req.closed:
@@ -543,18 +547,56 @@ def job_thread(
543547
logger.warning(
544548
"%s: cannot submit onto component %s, waiting", taskid, component.name
545549
)
550+
time.sleep(0.1)
546551

552+
# Send task to component
547553
logger.info("%s: sending task to component %s", taskid, component.name)
554+
t0 = time.perf_counter()
548555
msg = dict(cmd="task_start", params={"task": task, **kwargs})
549556
ret, req = lpp.comm(req, msg, **lppargs)
550557
if req.closed:
551558
thread.crashed = True
552559
sys.exit()
553560

554-
t0 = time.perf_counter()
561+
# Wait until the correct task is running, or MAX_TASK_WAIT
562+
while True:
563+
dt = time.perf_counter() - t0
564+
msg = dict(cmd="task_status", params={**kwargs})
565+
ret, req = lpp.comm(req, msg, **lppargs)
566+
if req.closed:
567+
thread.crashed = True
568+
sys.exit()
569+
elif ret.success and ret.data["running"] is False:
570+
logger.warning(
571+
"%s: task submitted %f s ago but not yet running", taskid, dt
572+
)
573+
pass
574+
elif ret.success and "task" in ret.data and ret.data["task"] != task:
575+
logger.warning(
576+
"%s: task submitted %f s ago but other task running: %s",
577+
taskid,
578+
dt,
579+
ret.data["task"],
580+
)
581+
pass
582+
elif ret.success and "task" in ret.data and ret.data["task"] == task:
583+
break
584+
elif ret.success and "task" not in ret.data:
585+
break
586+
if dt > MAX_TASK_WAIT:
587+
logger.critical("%s: task was submitted, but is not executed, aborting")
588+
thread.crashed = True
589+
sys.exit()
590+
time.sleep(0.1)
591+
logger.info("%s: correct task running on component %s", taskid, component.role)
592+
593+
# Main task loop
594+
tP = time.perf_counter()
555595
while True:
556596
tN = time.perf_counter()
557-
if tN - t0 > device.pollrate:
597+
598+
# Poll for data every device.pollrate, save to pickle
599+
if tN - tP > device.pollrate:
558600
logger.debug("%s: polling task for data", taskid)
559601
msg = dict(cmd="task_data", params={**kwargs})
560602
ret, req = lpp.comm(req, msg, **lppargs, timeout=5000)
@@ -566,8 +608,9 @@ def job_thread(
566608
ds: xr.Dataset = ret.data
567609
ds.attrs["tomato_Component"] = component.model_dump_json()
568610
data_to_pickle(ds, datapath, role=component.role)
569-
t0 += device.pollrate
611+
tP += device.pollrate
570612

613+
# Poll for completion and correct task status
571614
logger.debug("%s: polling task for completion", taskid)
572615
msg = dict(cmd="task_status", params={**kwargs})
573616
ret, req = lpp.comm(req, msg, **lppargs)
@@ -577,10 +620,16 @@ def job_thread(
577620
elif ret.success and not ret.data["running"]:
578621
logger.info("%s: task no longer running, break", taskid)
579622
break
623+
elif ret.success and "task" in ret.data and ret.data["task"] != task:
624+
logger.critical("%s: wront task running, break", taskid)
625+
logger.debug("%s: expected task: %s", taskid, task)
626+
logger.debug("%s: executed task: %s", taskid, ret.data["task"])
627+
break
580628
elif ret.success is False:
581629
logger.critical(f"{ret=}")
582630
break
583631

632+
# Stop task if stop trigger condition met, save to pickle
584633
if (
585634
task.stop_with_task_name is not None
586635
and task.stop_with_task_name in thread.started_task_names
@@ -598,7 +647,9 @@ def job_thread(
598647
data_to_pickle(ds, datapath, role=component.role)
599648
break
600649

601-
time.sleep(max(1e-1, (device.pollrate - (tN - t0)) / 2))
650+
time.sleep(max(1e-1, (device.pollrate - (tN - tP)) / 2))
651+
652+
# Store final task data, housekeeping.
602653
logger.info("%s: task fetching final data", taskid)
603654
msg = dict(cmd="task_data", params={**kwargs})
604655
ret, req = lpp.comm(req, msg, **lppargs, timeout=5000)
@@ -613,6 +664,7 @@ def job_thread(
613664
thread.completed_tasks.append(task)
614665
thread.current_task = None
615666

667+
# Reset component at the end of the job
616668
logger.info(
617669
"%s: all tasks done on component %s, resetting", component.role, component.name
618670
)
@@ -705,10 +757,10 @@ def job_main_loop(
705757
started_task_names = set()
706758
while True:
707759
tN = time.perf_counter()
708-
if snapshot is not None and tN - t0 > snapshot.frequency:
760+
if snapshot is not None and tN - t0 > snapshot.snapshot_interval:
709761
logger.debug("creating snapshot")
710762
merge_netcdfs(job, snapshot=True)
711-
t0 += snapshot.frequency
763+
t0 += snapshot.snapshot_interval
712764

713765
# Collect and push task names
714766
for t in threads.values():

src/tomato/driverinterface_2_1/__init__.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -379,11 +379,11 @@ def task_status(self, key: Key, **kwargs: dict) -> tuple[bool, str, dict]:
379379
"""
380380
running = self.devmap[key].running
381381
can_submit = not self.devmap[key].task_list.full()
382-
data = dict(running=running, can_submit=can_submit)
383-
if running:
384-
return (True, "component has a running task", data)
385-
else:
382+
data = dict(running=bool(running), can_submit=can_submit, task=running)
383+
if running is False:
386384
return (True, "component is idle", data)
385+
else:
386+
return (True, "component has a running task", data)
387387

388388
@log_errors
389389
@to_reply
@@ -589,9 +589,9 @@ def task_runner(self) -> None:
589589
thread.do_run = False
590590
break
591591

592-
self.running = True
593-
if isinstance(task, Task):
594-
try:
592+
self.running = task
593+
try:
594+
if isinstance(task, Task):
595595
thread.do_run_task = True
596596
self.prepare_task(task=task)
597597
t_0 = time.perf_counter()
@@ -614,28 +614,18 @@ def task_runner(self) -> None:
614614
task.technique_name,
615615
self.key,
616616
)
617-
except Exception as e:
618-
logger.critical(e, exc_info=True)
619-
thread.do_run = False
620-
break
621-
elif task == "measure":
622-
try:
617+
elif task == "measure":
623618
self.do_measure()
624619
logger.debug("measurement on component %s is done", self.key)
625-
except Exception as e:
626-
logger.critical(e, exc_info=True)
620+
else:
621+
logger.critical("Unknown task received: '%s'", task)
627622
thread.do_run = False
628623
break
629-
else:
630-
logger.critical("Unknown task received: '%s'", task)
631-
thread.do_run = False
632-
break
633-
634-
try:
635624
self.task_list.task_done()
636-
except ValueError as e:
625+
except Exception as e:
637626
logger.critical(e, exc_info=True)
638-
logger.critical("above error raised on task '%s'", task)
627+
thread.do_run = False
628+
break
639629
self.running = False
640630
logger.warning("task runner is quitting")
641631
self.running = False

tests/common/psutil_counter_with_task_stop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ method:
1414
- component_role: "counter"
1515
technique_name: "random"
1616
max_duration: 20.0
17-
sampling_interval: 1.5
17+
sampling_interval: 1.0
1818
task_params:
1919
min: 50.0
2020
max: 100.0

tests/test_05_passata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def test_passata_api_reset_force(datadir, start_tomato_daemon, stop_tomato_daemo
9696
)
9797
print(f"{ret=}")
9898
assert ret.success
99-
assert ret.data["running"] is True
99+
assert ret.data["running"]
100100

101101
ret = tomato.passata.reset(
102102
name="example_counter:(example-addr,1)",

tests/test_99_psutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
("psutil_1_0.1", {"psutil": 10}),
2020
("psutil_counter", {"psutil": 12, "counter": 10}),
2121
("psutil_counter_with_task_start", {"psutil": 12, "counter": 10}),
22-
("psutil_counter_with_task_stop", {"psutil": 10, "counter": 2}),
22+
("psutil_counter_with_task_stop", {"psutil": 10, "counter": 3}),
2323
],
2424
)
2525
def test_psutil_multidev(casename, npoints, datadir, stop_tomato_daemon):

0 commit comments

Comments
 (0)