ATLAS Offline Software
Loading...
Searching...
No Matches
athenaEF_tdaq_infra.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
3#
4# Start a private TDAQ online infrastructure for testing the athenaEF OH
5# publication path (athenaEF -M) in an offline job.
6#
7# athenaEF decides the online environment: partition name, webis host/port and OH server name.
8# TDAQ_WEBDAQ_BASE has to be available.
9# This script only reports readiness.
10#
11# The script first sources the TDAQ release via cm_setup.sh and re-execs itself.
12# exec preserves the PID, so the PR_SET_PDEATHSIG that athenaEF set on this process stays valid across the environment setup.
13#
14# Started applications:
15# ipc_server initial partition
16# ipc_server -p <part> job partition
17# is_server DF, RunParams and the OH server
18# webproxy Used by WebdaqHistSvc on webdaq-port
19#
20# NB: webproxy MUST be used, NOT webis_server: webproxy deserialises OH POSTs as
21# binary TBufferFile (matching webdaq::oh::put), while webis_server expects
22# JSON - it answers 201 anyway and then silently drops every publication.
23#
24# Once all servers are up the script prints ATHENAEF_INFRA_READY on stdout, then
25# stays alive polling them and exits non-zero if any of them dies.
26# Server log files are written to --log-dir (default: cwd).
27#
28# All child processes are spawned with PR_SET_PDEATHSIG=SIGKILL so they die with this script.
29# On SIGTERM/SIGINT the histograms are dumped with oh_cp and the partitions are removed cleanly with ipc_rm.
30#
31
32import argparse
33import os
34import signal
35import subprocess
36import sys
37import time
38import socket
39
40import logging
41logging.basicConfig(stream=sys.stdout, level=logging.INFO,
42 format='%(asctime)s %(name)s %(levelname)-8s %(message)s')
43log = logging.getLogger('athenaEF_infra')
44
45# TDAQ release setup (hardcoded for now, see --tdaq-release)
46CM_SETUP = '/cvmfs/atlas.cern.ch/repo/sw/tdaq/tools/cmake_tdaq/bin/cm_setup.sh'
47DEFAULT_TDAQ_RELEASE = 'tdaq-14-00-00'
48
49# Environment marker distinguishing stage 2 from stage 1
50STAGE2_ENV = 'ATHENAEF_INFRA_STAGE2'
51
52# Printed on stdout once the infrastructure is up
53READY_MARKER = 'ATHENAEF_INFRA_READY'
54
55
57 parser = argparse.ArgumentParser(
58 description='Start a private TDAQ infrastructure (IPC/IS/webproxy) for athenaEF online-monitoring tests')
59 parser.add_argument('--partition', metavar='NAME', required=True,
60 help='partition name (TDAQ_PARTITION)')
61 parser.add_argument('--webdaq-port', metavar='PORT', type=int, required=True,
62 help='port the webproxy listens on')
63 parser.add_argument('--oh-server', metavar='NAME', required=True,
64 help='name of the OH IS server (TDAQ_OH_SERVER)')
65 parser.add_argument('--run-number', metavar='N', type=int, required=True,
66 help='run number, used to name the output file')
67 parser.add_argument('--log-dir', metavar='DIR', default='.',
68 help='directory for infrastructure log files (default: cwd)')
69 parser.add_argument('--tdaq-release', metavar='REL', default=DEFAULT_TDAQ_RELEASE,
70 help='TDAQ release to source via cm_setup.sh (default: %(default)s)')
71 return parser.parse_args()
72
73
75 """re-exec this script through bash after sourcing the TDAQ release."""
76 if not os.path.exists(CM_SETUP):
77 log.error('TDAQ release setup not found at %s', CM_SETUP)
78 sys.exit(1)
79
80 log.info('Sourcing TDAQ release %s via %s', args.tdaq_release, CM_SETUP)
81 os.environ[STAGE2_ENV] = '1'
82 script = os.path.abspath(__file__)
83 cmd = (f'source {CM_SETUP} {args.tdaq_release} || exit 66; '
84 f'exec python3 "{script}" "$@"')
85 os.execv('/bin/bash', ['/bin/bash', '-c', cmd, script] + sys.argv[1:])
86
87
89 """Manage the private TDAQ infrastructure (adapted from HLTMPPy.runner.Infrastructure)"""
90
91 sigs = [signal.SIGFPE, signal.SIGHUP, signal.SIGQUIT, signal.SIGSEGV,
92 signal.SIGTERM, signal.SIGINT]
93
94 def __init__(self, args):
95 self.args = args
96 self.processes = [] # (name, subprocess.Popen)
97 self.pid = os.getpid() # Distinguish mother from children after forking
98 self.ready = False # True once all servers are up
100
101 def __del__(self):
102 """Stop infrastructure in the mother process, in case program exits before stop()"""
103 if os.getpid() == self.pid:
104 self.stop()
105
107 self.prehandlers = {}
108 for s in self.sigs:
109 self.prehandlers[s] = signal.getsignal(s)
110 signal.signal(s, self._handle_quit)
111
112 def _handle_quit(self, signum, frame):
113 log.info('Caught signal %d. Cleaning up the infrastructure and exiting', signum)
114 self.stop()
115 prehandler = self.prehandlers.pop(signum, signal.SIG_DFL)
116 signal.signal(signum, prehandler)
117 sys.exit(0)
118
119 def _implant_bomb(self):
120 """preexec_fn ensuring infrastructure processes exit when this process dies"""
121 from ctypes import cdll
122 PR_SET_PDEATHSIG = 1
123 try:
124 return lambda: cdll['libc.so.6'].prctl(PR_SET_PDEATHSIG, signal.SIGKILL)
125 except Exception:
126 log.error('Error setting PR_SET_PDEATHSIG for infrastructure processes. '
127 'Using a dummy function instead')
128 return lambda: 1
129
130 def _launch(self, name, cmd):
131 logbase = os.path.join(self.args.log_dir, f'{name}_{self.args.partition}')
132 proc = subprocess.Popen(cmd, preexec_fn=self._implant_bomb(),
133 stdout=open(logbase + '.out', 'w'),
134 stderr=open(logbase + '.err', 'w'),
135 close_fds=True)
136 log.info('Started %s (pid %d): %s', name, proc.pid, ' '.join(cmd))
137 self.processes.append((name, proc))
138 return proc
139
140 def start(self):
141 from ispy import IPCPartition
142
143 partition = self.args.partition
144
145 # Private IPC domain: reference file local to this job's directory
146 ipc_ref = 'file:' + os.path.join(os.getcwd(), 'ipc_init.ref')
147 log.info('Setting TDAQ_IPC_INIT_REF: %s', ipc_ref)
148 os.environ['TDAQ_IPC_INIT_REF'] = ipc_ref
149
150 log.info('Initializing OH monitoring infrastructure for partition %s', partition)
151
152 self._launch('ipc_initial', ['ipc_server'])
153 while not IPCPartition('initial').isValid():
154 log.info('Waiting until initial partition is available...')
155 time.sleep(1)
156
157 self._launch('ipc_partition', ['ipc_server', '-p', partition])
158 while not IPCPartition(partition).isValid():
159 log.info('Waiting until partition %s is available...', partition)
160 time.sleep(1)
161
162 for server in ['DF', 'RunParams', self.args.oh_server]:
163 self._launch(f'is_{server}', ['is_server', '-p', partition, '-n', server])
164
165 self.start_webproxy()
166 self.ready = True
167
168 def start_webproxy(self):
169 """Start the webproxy REST server"""
170 port = self.args.webdaq_port
171 webproxy = self._launch('webproxy', ['webproxy', '-p', str(port)])
172
173 timeout = 60
174 for _ in range(timeout):
175 if webproxy.poll() is not None:
176 log.error('webproxy exited early (code %s); see webproxy_%s.err',
177 webproxy.returncode, self.args.partition)
178 self.stop()
179 sys.exit(1)
180 try:
181 with socket.create_connection(('localhost', port), timeout=1):
182 log.info('webproxy listening on http://localhost:%d', port)
183 return
184 except OSError:
185 time.sleep(1)
186
187 log.error('webproxy is not listening on localhost:%d after %d s', port, timeout)
188 self.stop()
189 sys.exit(1)
190
191 def check_alive(self):
192 """Return False if any infrastructure process has exited"""
193 for name, proc in self.processes:
194 ret = proc.poll()
195 if ret is not None:
196 log.error('Infrastructure process %s (pid %d) exited with code %s',
197 name, proc.pid, ret)
198 return False
199 return True
200
201 def copy_histograms(self):
202 fname = f'r{self.args.run_number:010d}_{self.args.partition}_{self.args.oh_server}.root'
203 log.info('Copying histograms into %s (oh_cp)', fname)
204 subprocess.call(['oh_cp', '-p', self.args.partition, '-s', self.args.oh_server,
205 '-n', '.*', '-o', '.*', '-O',
206 '-r', str(self.args.run_number), '-f', fname])
207
208 def stop(self):
209 if not self.processes:
210 return
211
212 if self.ready: # Nothing was ever published if we did not fully start
213 self.copy_histograms()
214
215 log.info('Finalizing OH monitoring infrastructure')
216 for part in [self.args.partition, 'initial']:
217 log.info('Destroying partition: %s', part)
218 subprocess.call(['ipc_rm', '-f', '-p', part, '-i', '".*"', '-n', '".*"'],
219 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
220
221 for name, proc in self.processes:
222 while proc.poll() is None:
223 proc.kill()
224 time.sleep(0.1)
225 self.processes = []
226 log.info('Terminated all infrastructure processes')
227
228
229def main():
230 args = parse_args()
231
232 if STAGE2_ENV not in os.environ:
233 setup_tdaq_and_reexec(args) # does not return
234
235 os.makedirs(args.log_dir, exist_ok=True)
236
237 infra = Infrastructure(args)
238 infra.start()
239 print(READY_MARKER, flush=True)
240
241 while infra.check_alive():
242 time.sleep(2)
243
244 infra.stop()
245 return 1
246
247
248if __name__ == '__main__':
249 sys.exit(main())