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