Skip to content

Commit 5e0a865

Browse files
committed
Learn the file-IO graph without root, using strace
This makes the file-IO-graph learning pluggable and adds an strace backend, so a pilot run no longer needs a binary with CAP_SYS_ADMIN. The graph it produces is the one --remove-files-early already reads back. - The runner gains --filegraph-backends. Naming several at once runs them side by side, which is how they are compared. O2DPG_PRODUCE_FILEGRAPH still selects fanotify and names its monitor. - The strace backend wraps each task command, so a file access is attributed by the trace it lands in rather than by walking /proc after the event. --seccomp-bpf keeps the cost at about 59 us per traced open; the backend probes for it. - filegraph_report.py holds the exclusion rules, the ./tfN -> ./tfX templating, the JSON schema and the graphviz rendering, so the two analysers cannot drift. analyse_FileIO_v2.py reproduces its previous output byte for byte. - compare_reports.py grades one report against another. A missing edge deletes a file a later task still reads; an extra edge only delays the deletion. The verdicts EXACT, SAFE and UNSAFE follow that asymmetry. - tests/equivalence_test.py runs a workflow whose graph is known by construction and grades every backend against it, in seconds and with no ALICE software. strace comes out EXACT. - monitor_fileaccess_v2.cpp spun forever on a queue overflow, because the overflow branch skipped FAN_EVENT_NEXT and re-tested the same event. - 33 offline tests come with it, plain unittest so they also run on a worker node, and a CI job runs them. Part of the o2dpg_workflow_runner.py refactoring.
1 parent d4f14f0 commit 5e0a865

15 files changed

Lines changed: 1581 additions & 69 deletions

.github/workflows/syntax-checks.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ jobs:
120120
working-directory: MC/workflow_runner
121121
run: pytest o2dpg_runner/tests -q
122122

123+
filegraph-tests:
124+
name: File-IO-graph unit tests
125+
runs-on: ubuntu-latest
126+
127+
steps:
128+
- name: Checkout code
129+
uses: actions/checkout@v4
130+
131+
- name: Install prerequisites
132+
run: pip install psutil
133+
134+
- name: Run the FileIOGraph test suite
135+
run: python3 -m unittest discover -s UTILS/FileIOGraph/tests -t UTILS/FileIOGraph/tests
136+
123137
pylint:
124138
name: Pylint
125139
runs-on: ubuntu-latest

MC/workflow_runner/o2dpg_runner/cli.py

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88
from __future__ import annotations
99

1010
import argparse
11-
import json
1211
import logging
1312
import os
1413
import shutil
15-
import subprocess
1614
import sys
1715
from typing import Optional, Tuple
1816

1917
import psutil
2018

2119
from .config import RunnerConfig
20+
from .filegraph import BACKENDS as FILEGRAPH_BACKENDS, FileGraphManager
2221
from .workflow import build_workflow, load_json
2322
from .executor import WorkflowExecutor
2423

@@ -100,6 +99,11 @@ def build_parser() -> argparse.ArgumentParser:
10099
p.add_argument("--retry-on-failure", type=int, default=0)
101100
p.add_argument("--no-rootinit-speedup", action="store_true")
102101
p.add_argument("--remove-files-early", type=str, default="")
102+
p.add_argument("--filegraph-backends", type=str,
103+
default=os.getenv("O2DPG_FILEGRAPH_BACKENDS", ""),
104+
help="comma-separated file-IO-graph backends to learn the "
105+
"file dependencies with: "
106+
+ ", ".join(sorted(FILEGRAPH_BACKENDS)))
103107

104108
# Accept-and-ignore for backward compatibility of call sites
105109
# that still pass these flags. They have no effect.
@@ -154,6 +158,7 @@ def _args_to_config(ns: argparse.Namespace) -> RunnerConfig:
154158
retry_on_failure=ns.retry_on_failure,
155159
no_rootinit_speedup=ns.no_rootinit_speedup,
156160
remove_files_early=ns.remove_files_early,
161+
filegraph_backends=ns.filegraph_backends,
157162
stdout_on_failure=ns.stdout_on_failure,
158163
production_mode=ns.production_mode,
159164
action_logfile=ns.action_logfile,
@@ -341,22 +346,6 @@ def _maybe_draw_workflow(raw_spec):
341346
dot.render("workflow.gv")
342347

343348

344-
def _launch_fileaccess_sidecar(actionlogger_file: str):
345-
"""Start the fanotify-based file-IO graph sidecar if requested."""
346-
exe = os.getenv("O2DPG_PRODUCE_FILEGRAPH")
347-
if not exe:
348-
return None, None, None
349-
env = os.environ.copy()
350-
env["FILEACCESS_MON_ROOTPATH"] = os.getcwd()
351-
env["MAXMOTHERPID"] = f"{os.getpid()}"
352-
log_file = f"pipeline_fileaccess_{os.getpid()}.log"
353-
fh = open(log_file, "w")
354-
proc = subprocess.Popen(
355-
[exe], stdout=fh, stderr=subprocess.STDOUT, env=env,
356-
)
357-
return proc, fh, log_file
358-
359-
360349
def main(argv=None) -> int:
361350
ns = build_parser().parse_args(argv)
362351
_maybe_reexec_in_slice(ns) # may replace this process; returns only if not re-execing
@@ -409,6 +398,7 @@ def main(argv=None) -> int:
409398
"systemd_run_spec": cfg.systemd_run_spec,
410399
"in_systemd_slice": cfg.in_systemd_slice,
411400
"monitor_interval_cpu": cfg.monitor_interval_cpu,
401+
"filegraph_backends": cfg.filegraph_backends,
412402
})
413403
metric_logger.info(meta)
414404

@@ -429,37 +419,19 @@ def main(argv=None) -> int:
429419
for k, v in wf.global_env.items():
430420
os.environ.setdefault(k, str(v))
431421

432-
# Optional file-access sidecar
433-
fileaccess_proc, fileaccess_fh, fileaccess_log_file = _launch_fileaccess_sidecar(action_log)
422+
filegraph = FileGraphManager.from_config(
423+
cfg.filegraph_backends, os.getcwd(), os.getpid(), action_log, action_logger)
424+
filegraph.start()
434425

435426
rc = 0
436427
try:
437-
execer = WorkflowExecutor(cfg, wf, action_logger, metric_logger)
428+
execer = WorkflowExecutor(cfg, wf, action_logger, metric_logger,
429+
filegraph=filegraph)
438430
rc = int(execer.execute())
439431
finally:
440-
if fileaccess_proc is not None:
441-
fileaccess_proc.terminate()
442-
try:
443-
fileaccess_proc.wait(timeout=5)
444-
except subprocess.TimeoutExpired:
445-
fileaccess_proc.kill()
446-
if fileaccess_fh is not None:
447-
fileaccess_fh.close()
448-
o2dpg_root = os.getenv("O2DPG_ROOT")
449-
if o2dpg_root and fileaccess_log_file:
450-
analyse_cmd = [
451-
sys.executable,
452-
f"{o2dpg_root}/UTILS/FileIOGraph/analyse_FileIO_v2.py",
453-
"--actionFile", action_log,
454-
"--monitorFile", fileaccess_log_file,
455-
"-o", f"pipeline_fileaccess_report_{os.getpid()}.json",
456-
"--basedir", os.getcwd(),
457-
]
458-
print(f"Producing FileIOGraph with command {analyse_cmd}")
459-
try:
460-
subprocess.run(analyse_cmd, check=True)
461-
except subprocess.CalledProcessError as e:
462-
print(f"FileIOGraph analysis failed: {e}", file=sys.stderr)
432+
filegraph.stop()
433+
for backend, path in filegraph.analyse().items():
434+
print(f"FileIOGraph[{backend}] -> {path}")
463435

464436
return rc
465437

MC/workflow_runner/o2dpg_runner/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class RunnerConfig:
5454
retry_on_failure: int = 0
5555
no_rootinit_speedup: bool = False
5656
remove_files_early: str = ""
57+
filegraph_backends: str = ""
5758
stdout_on_failure: bool = False
5859
production_mode: bool = False
5960

MC/workflow_runner/o2dpg_runner/executor.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@
3434
from .graph import descendants, longest_path_length, kahn_topological_order
3535
from .resources import ResourceManager, ResourceLimitExceeded
3636
from .monitoring import MonitorThread, PsutilBackend, _read_cgroup_v2_dir
37+
from .filegraph import FileGraphManager
3738
from .scheduler import get_policy
3839
from .scheduler.base import SchedulerState
3940
from .scheduler.timeframe import TimeframeFirstPolicy
40-
from .cache import TaskCache, compute_fingerprint, remove_done_flag, done_path
41+
from .cache import TaskCache, compute_fingerprint, remove_done_flag
4142
from .alienv import get_alienv_software_environment
4243
from .cleanup import EarlyFileRemover, archive_task_logs
4344

@@ -90,8 +91,10 @@ def __init__(
9091
workflow: Workflow,
9192
action_logger: logging.Logger,
9293
metric_logger: logging.Logger,
94+
filegraph=None,
9395
):
9496
self.cfg = config
97+
self.filegraph = filegraph or FileGraphManager([], os.getpid(), action_logger)
9598
self.wf = workflow
9699
self.actionlog = action_logger
97100
self.metriclog = metric_logger
@@ -366,16 +369,23 @@ def submit(self, tid: int, nice: int) -> Optional[psutil.Popen]:
366369
slice_name if slice_name.endswith(".slice") else f"{slice_name}.slice"
367370
)
368371
unit = _unit_name(task["name"], tid)
369-
launch_argv = [
372+
prefix = [
370373
"systemd-run", "--user", "--scope", "--collect",
371374
"--expand-environment=no", # suppress the $VAR warning; bash handles expansion
372-
f"--unit={unit}", f"--slice={systemd_slice}",
373-
"--", "/bin/bash", "-c", cmd,
375+
f"--unit={unit}", f"--slice={systemd_slice}", "--",
374376
]
377+
else:
378+
prefix = []
379+
380+
# a tracer has to sit inside any systemd scope, or it would only ever
381+
# see systemd-run itself
382+
inner_argv = self.filegraph.wrap(["/bin/bash", "-c", cmd], task["name"], tid)
383+
launch_argv = prefix + inner_argv
384+
385+
if use_scope:
375386
p = psutil.Popen(launch_argv, cwd=workdir, env=env, stderr=subprocess.PIPE)
376387
_start_stderr_drainer(p.stderr, self.actionlog, task["name"])
377388
else:
378-
launch_argv = ["/bin/bash", "-c", cmd]
379389
p = psutil.Popen(launch_argv, cwd=workdir, env=env)
380390
try:
381391
p.nice(nice)

0 commit comments

Comments
 (0)