ATLAS Offline Software
Loading...
Searching...
No Matches
athenaEF.py
Go to the documentation of this file.
1#!/bin/sh
2# -*- mode: python -*-
3#
4# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
5#
6# athenaEF.py - executable to run the EF online and offline.
7#
8"""date"
9
10# defaults
11export USETCMALLOC=1
12export USEIMF=1
13
14# parse command line arguments
15for a in ${@}
16do
17 case "$a" in
18 --stdcmalloc) USETCMALLOC=0;;
19 --tcmalloc) USETCMALLOC=1;;
20 --stdcmath) USEIMF=0;;
21 --imf) USEIMF=1;;
22 --preloadlib*) export ATHENA_ADD_PRELOAD=${a#*=};;
23 --no-ers-signal-handlers) export TDAQ_ERS_NO_SIGNAL_HANDLERS=1;;
24 esac
25done
26
27# Do the actual preloading via LD_PRELOAD
28source `which athena_preload.sh `
29
30# Now resurrect ourselves as python script
31python_path=`which python`
32"exec" "$python_path" "-tt" "$0" "$@";
33
34"""
35
36import sys
37import os
38import argparse
39import json
40import traceback
41from datetime import datetime as dt
42
43# Use single-threaded oracle client library to avoid extra
44# threads when forking (see ATR-21890, ATDBOPS-115)
45os.environ["CORAL_ORA_NO_OCI_THREADED"] = "1"
46
47from TrigCommon import AthHLT
48from AthenaCommon.Logging import logging
49log = logging.getLogger('athenaEF')
50
51# Fraction of the hard timeout to be used for soft timeout. NB: athenaEF only enforces the soft
52# timeout: HltEventLoopMgr never acts on HardTimeout itself, it only uses it to compute the soft timeout
53SOFT_TIMEOUT_FRACTION = 0.95
54
55# =============================================================================
56# Run Parameters Configuration
57# =============================================================================
58# Default values for run parameters used by prepareForStart.
59# These can be overridden by command-line arguments or fetched from IS.
60
62 """
63 Container for run parameters needed by HltEventLoopMgr::prepareForStart().
64
65 This class centralizes all run parameter defaults or their retrieval from IS.
66 """
67
68 # Default values
69 DEFAULT_RUN_NUMBER = 0
70 DEFAULT_LB_NUMBER = 0
71 DEFAULT_DETECTOR_MASK = 'f' * 32 # All detectors enabled
72 DEFAULT_SOR_TIME = None # Will use 'now' if not set
73 DEFAULT_SOLENOID_CURRENT = 7730.0 # (nominal)
74 DEFAULT_TOROIDS_CURRENT = 20400.0 # (nominal)
75 DEFAULT_BEAM_TYPE = 0
76 DEFAULT_BEAM_ENERGY = 0
77 DEFAULT_RUN_TYPE = "Physics"
78 DEFAULT_TRIGGER_TYPE = 0
79 DEFAULT_RECORDING_ENABLED = False
80
81 def __init__(self,
82 run_number=None,
83 lb_number=None,
84 detector_mask=None,
85 sor_time=None,
86 solenoid_current=None,
87 toroids_current=None,
88 beam_type=None,
89 beam_energy=None,
90 run_type=None,
91 trigger_type=None,
92 recording_enabled=None,
93 T0_project_tag='',
94 stream='',
95 lumiblock=0):
96 """Initialize run parameters with defaults for any unspecified values."""
97 self.run_number = run_number if run_number is not None else self.DEFAULT_RUN_NUMBER
98 self.lb_number = lb_number if lb_number is not None else self.DEFAULT_LB_NUMBER
99 self.detector_mask = detector_mask if detector_mask is not None else self.DEFAULT_DETECTOR_MASK
100 self.sor_time = sor_time if sor_time is not None else dt.now().strftime('%d/%m/%y %H:%M:%S.%f')
101 self.solenoid_current = solenoid_current
102 self.toroids_current = toroids_current
103 self.beam_type = beam_type if beam_type is not None else self.DEFAULT_BEAM_TYPE
104 self.beam_energy = beam_energy if beam_energy is not None else self.DEFAULT_BEAM_ENERGY
105 self.run_type = run_type if run_type is not None else self.DEFAULT_RUN_TYPE
106 self.trigger_type = trigger_type if trigger_type is not None else self.DEFAULT_TRIGGER_TYPE
107 self.recording_enabled = recording_enabled if recording_enabled is not None else self.DEFAULT_RECORDING_ENABLED
108 self.T0_project_tag = T0_project_tag
109 self.stream = stream
110 self.lumiblock = lumiblock
111
112 def to_dict(self):
113 """Return run parameters as a dictionary for prepareForStart."""
114 return {
115 'run_number': self.run_number,
116 'lb_number': self.lb_number,
117 'detector_mask': self.detector_mask,
118 'sor_time': self.sor_time,
119 'solenoid_current': self.solenoid_current,
120 'toroids_current': self.toroids_current,
121 'beam_type': self.beam_type,
122 'beam_energy': self.beam_energy,
123 'run_type': self.run_type,
124 'trigger_type': self.trigger_type,
125 'recording_enabled': self.recording_enabled,
126 'T0_project_tag': self.T0_project_tag,
127 'stream': self.stream,
128 'lumiblock': self.lumiblock,
129 }
130
131 @classmethod
132 def from_args(cls, args):
133 """Create RunParams from argparse args, using defaults for unset values."""
134 return cls(
135 run_number=args.run_number,
136 lb_number=args.lb_number,
137 detector_mask=args.detector_mask,
138 sor_time=args.sor_time,
139 solenoid_current=getattr(args, 'solenoid_current', None),
140 toroids_current=getattr(args, 'toroids_current', None),
141 beam_type=getattr(args, 'beam_type', None),
142 beam_energy=getattr(args, 'beam_energy', None),
143 T0_project_tag=getattr(args, 'T0_project_tag', ''),
144 stream=getattr(args, 'stream', ''),
145 lumiblock=getattr(args, 'lumiblock', 0),
146 )
147
148 @classmethod
149 def from_is(cls, partition=None, webdaq_base=None, strict=False,
150 solenoid_current_override=None, toroids_current_override=None):
151 """
152 Create RunParams by reading from IS via the WEBDAQ REST API.
153
154 This uses the webis_server REST API to fetch run parameters, avoiding
155 direct dependencies on TDAQ libraries. The API endpoint is determined by:
156 1. The webdaq_base parameter if provided
157 2. The TDAQ_WEBDAQ_BASE environment variable
158
159 The IS objects accessed are:
160 - RunParams.RunParams: run_number, det_mask, timeSOR, trigger_type, etc.
161 - Magnets.Magnets: SolenoidCurrent, ToroidsCurrent
162
163 Args:
164 partition: The partition name (default: from TDAQ_PARTITION env var)
165 webdaq_base: Base URL for webis_server (default: from TDAQ_WEBDAQ_BASE env var)
166 strict: If True, raise an exception if IS read fails (for --online-environment)
167 solenoid_current_override: If provided, skip IS fetch for solenoid (command-line override)
168 toroids_current_override: If provided, skip IS fetch for toroids (command-line override)
169
170 Returns:
171 RunParams instance with values from IS, or defaults if unavailable
172
173 Raises:
174 RuntimeError: If strict=True and IS read fails
175 """
176 import requests
177
178 # Determine the base URL
179 if webdaq_base is None:
180 webdaq_base = os.environ.get('TDAQ_WEBDAQ_BASE')
181
182 if not webdaq_base:
183 msg = "TDAQ_WEBDAQ_BASE not set, cannot read from IS"
184 if strict:
185 raise RuntimeError(msg + " (required for --online-environment)")
186 log.warning(msg + ". Using defaults.")
187 return cls()
188
189 # Determine partition
190 if partition is None:
191 partition = os.environ.get('TDAQ_PARTITION', 'ATLAS')
192
193 log.info("Reading run parameters from IS via WEBDAQ: %s (partition=%s)", webdaq_base, partition)
194
195 params = {}
196
197 # Fetch RunParams from IS
198 # API: GET /info/current/{partition}/is/{server}/{server}.{name}?format=compact
199 # Response format: [name, type, timestamp, data] - we need element [3]
200 try:
201 url = f"{webdaq_base}/info/current/{partition}/is/RunParams/RunParams.RunParams?format=compact"
202 log.debug("Fetching RunParams from: %s", url)
203
204 response = requests.get(url, timeout=10)
205 if response.status_code == 200:
206 response_data = response.json()
207 log.debug("RunParams response from IS: %s", response_data)
208
209 # Response is [name, type, timestamp, data]
210 if isinstance(response_data, list) and len(response_data) >= 4:
211 runparams = response_data[3]
212 else:
213 runparams = response_data
214
215 log.debug("RunParams data: %s", runparams)
216
217 # Map IS fields to our RunParams fields
218 if 'run_number' in runparams:
219 params['run_number'] = int(runparams['run_number'])
220 if 'lumiblock' in runparams:
221 params['lb_number'] = int(runparams['lumiblock'])
222 if 'det_mask' in runparams:
223 params['detector_mask'] = runparams['det_mask']
224 if 'timeSOR' in runparams:
225 sor_time = runparams['timeSOR']
226 # Ensure microseconds are present (TrigSORFromPtreeHelper expects format with .%f)
227 if '.' not in sor_time:
228 sor_time += '.000000'
229 params['sor_time'] = sor_time
230 if 'beam_type' in runparams:
231 params['beam_type'] = int(runparams['beam_type'])
232 if 'beam_energy' in runparams:
233 params['beam_energy'] = int(runparams['beam_energy'])
234 if 'run_type' in runparams:
235 params['run_type'] = runparams['run_type']
236 if 'trigger_type' in runparams:
237 params['trigger_type'] = int(runparams['trigger_type'])
238 if 'recording_enabled' in runparams:
239 params['recording_enabled'] = runparams['recording_enabled'] in ('1', 'true', 'True', True, 1)
240
241 log.info("Got run parameters from IS: run=%s, lb=%s",
242 params.get('run_number'), params.get('lb_number'))
243 else:
244 msg = f"Failed to fetch RunParams from IS: HTTP {response.status_code}"
245 if strict:
246 raise RuntimeError(msg + " (required for --online-environment)")
247 log.warning(msg)
248
249 except requests.exceptions.RequestException as e:
250 msg = f"Error fetching RunParams from IS: {e}"
251 if strict:
252 raise RuntimeError(msg + " (required for --online-environment)")
253 log.warning(msg)
254 except (ValueError, KeyError) as e:
255 msg = f"Error parsing RunParams from IS: {e}"
256 if strict:
257 raise RuntimeError(msg + " (required for --online-environment)")
258 log.warning(msg)
259
260 # Fetch Magnets from IS
261 # In strict mode (online environment), magnets are required unless provided via command line
262 # If command-line overrides are provided, use those instead of fetching from IS
263 have_solenoid_override = solenoid_current_override is not None
264 have_toroids_override = toroids_current_override is not None
265
266 if have_solenoid_override:
267 params['solenoid_current'] = solenoid_current_override
268 log.info("Using solenoid_current=%.1f from command line override", solenoid_current_override)
269 if have_toroids_override:
270 params['toroids_current'] = toroids_current_override
271 log.info("Using toroids_current=%.1f from command line override", toroids_current_override)
272
273 # Only fetch from IS if we need at least one value
274 if not (have_solenoid_override and have_toroids_override):
275 try:
276 url = f"{webdaq_base}/info/current/{partition}/is/Magnets/Magnets.Magnets?format=compact"
277 log.debug("Fetching Magnets from: %s", url)
278
279 response = requests.get(url, timeout=10)
280 if response.status_code == 200:
281 response_data = response.json()
282 log.debug("Magnets response from IS: %s", response_data)
283
284 magnets = response_data[3] if isinstance(response_data, list) and len(response_data) >= 4 else response_data
285 log.debug("Magnets data: %s", magnets)
286
287 # Magnets structure: { "SolenoidCurrent": {"value": ..., "ts": ...},
288 # "ToroidsCurrent": {"value": ..., "ts": ...} }
289 if not have_solenoid_override:
290 params['solenoid_current'] = float(magnets['SolenoidCurrent']['value'])
291 if not have_toroids_override:
292 params['toroids_current'] = float(magnets['ToroidsCurrent']['value'])
293
294 log.info("Got magnet currents from IS: solenoid=%s, toroids=%s",
295 params.get('solenoid_current'), params.get('toroids_current'))
296 elif strict:
297 raise RuntimeError(f"Magnets not available from IS: HTTP {response.status_code} "
298 "(required for --online-environment, use --solenoid-current and --toroids-current to override)")
299 else:
300 log.debug("Magnets not available from IS: HTTP %d", response.status_code)
301
302 except requests.exceptions.RequestException as e:
303 if strict:
304 raise RuntimeError(f"Error fetching Magnets from IS: {e} "
305 "(required for --online-environment, use --solenoid-current and --toroids-current to override)")
306 log.debug("Error fetching Magnets from IS: %s", e)
307 except (ValueError, KeyError, TypeError) as e:
308 if strict:
309 raise RuntimeError(f"Error parsing Magnets from IS: {e} "
310 "(required for --online-environment, use --solenoid-current and --toroids-current to override)")
311 log.debug("Error parsing Magnets from IS: %s", e)
312
313 # In strict mode, verify we got at least run_number from IS
314 if strict and 'run_number' not in params:
315 raise RuntimeError("Failed to get run_number from IS (required for --online-environment)")
316
317 return cls(**params)
318
319
320def get_trigconf_keys_from_oks(partition=None, webdaq_base=None, strict=False):
321 """
322 Read trigger configuration keys (SMK, L1PSK, HLTPSK) and DB info from OKS via WEBDAQ REST API.
323
324 This reads the keys from the partition's TriggerConfiguration object and its
325 related L1TriggerConfiguration and TriggerDBConnection objects.
326
327 OKS Structure:
328 - Partition -> TriggerConfiguration -> L1TriggerConfiguration (Lvl1PrescaleKey)
329 - Partition -> TriggerConfiguration -> TriggerDBConnection (SuperMasterKey)
330 - Partition -> TriggerConfiguration -> HLTImplementationDB (hltPrescaleKey)
331
332 Args:
333 partition: The partition name (default: from TDAQ_PARTITION env var)
334 webdaq_base: Base URL for webis_server (default: from TDAQ_WEBDAQ_BASE env var)
335 strict: If True, raise an exception if OKS read fails (for --online-environment)
336
337 Returns:
338 dict with keys: SMK, L1PSK, HLTPSK, db_alias (values may be None if not found)
339
340 Raises:
341 RuntimeError: If strict=True and OKS read fails
342 """
343 import requests
344
345 # Determine the base URL
346 if webdaq_base is None:
347 webdaq_base = os.environ.get('TDAQ_WEBDAQ_BASE')
348
349 if not webdaq_base:
350 msg = "TDAQ_WEBDAQ_BASE not set, cannot read from OKS"
351 if strict:
352 raise RuntimeError(msg + " (required for --online-environment)")
353 log.warning(msg)
354 return {'SMK': None, 'L1PSK': None, 'HLTPSK': None, 'db_alias': None}
355
356 # Determine partition
357 if partition is None:
358 partition = os.environ.get('TDAQ_PARTITION', 'ATLAS')
359
360 log.info("Reading trigger configuration keys from OKS via WEBDAQ: %s (partition=%s)",
361 webdaq_base, partition)
362
363 result = {'SMK': None, 'L1PSK': None, 'HLTPSK': None, 'db_alias': None}
364
365 def extract_oks_data(response_json):
366 """
367 Extract data from OKS compact format: [name, type, attributes, relationships]
368 Returns tuple (attributes_dict, relationships_dict)
369 """
370 if isinstance(response_json, list) and len(response_json) >= 4:
371 return response_json[2], response_json[3] # attributes, relationships
372 elif isinstance(response_json, list) and len(response_json) >= 3:
373 return response_json[2], {} # attributes only
374 return response_json, {} # fallback
375
376 def get_ref_id(ref):
377 """Extract object ID from a relationship reference."""
378 if isinstance(ref, list) and len(ref) >= 2:
379 return ref[0] # [id, class] format
380 elif isinstance(ref, dict) and 'id' in ref:
381 return ref['id']
382 elif isinstance(ref, str):
383 return ref
384 return None
385
386 # OKS API: GET /info/current/{partition}/oks/{class}/{name}?format=compact
387 # Response format: [name, type, attributes, relationships]
388 # - attributes: dict of simple values (strings, ints, etc.)
389 # - relationships: dict of references to other objects
390 try:
391 url = f"{webdaq_base}/info/current/{partition}/oks/Partition/{partition}?format=compact"
392 log.debug("Fetching Partition from OKS: %s", url)
393
394 response = requests.get(url, timeout=10)
395 if response.status_code == 200:
396 part_attrs, part_rels = extract_oks_data(response.json())
397 log.debug("Partition attributes: %s", part_attrs)
398 log.debug("Partition relationships: %s", part_rels)
399
400 # Get TriggerConfiguration reference from relationships
401 trig_conf_id = None
402 if 'TriggerConfiguration' in part_rels:
403 trig_conf_id = get_ref_id(part_rels['TriggerConfiguration'])
404
405 if trig_conf_id:
406 log.debug("TriggerConfiguration ID: %s", trig_conf_id)
407
408 # Get TriggerConfiguration object
409 url = f"{webdaq_base}/info/current/{partition}/oks/TriggerConfiguration/{trig_conf_id}?format=compact"
410 response = requests.get(url, timeout=10)
411 if response.status_code == 200:
412 trig_attrs, trig_rels = extract_oks_data(response.json())
413 log.debug("TriggerConfiguration attributes: %s", trig_attrs)
414 log.debug("TriggerConfiguration relationships: %s", trig_rels)
415
416 # Get L1TriggerConfiguration for L1PSK (relationship 'l1')
417 if 'l1' in trig_rels:
418 l1_id = get_ref_id(trig_rels['l1'])
419 if l1_id:
420 url = f"{webdaq_base}/info/current/{partition}/oks/L1TriggerConfiguration/{l1_id}?format=compact"
421 resp = requests.get(url, timeout=10)
422 if resp.status_code == 200:
423 l1_attrs, _ = extract_oks_data(resp.json())
424 log.debug("L1TriggerConfiguration attributes: %s", l1_attrs)
425 if 'Lvl1PrescaleKey' in l1_attrs:
426 result['L1PSK'] = int(l1_attrs['Lvl1PrescaleKey'])
427 log.info("Got L1PSK=%d from OKS", result['L1PSK'])
428
429 # Get TriggerDBConnection for SMK and db_alias (relationship 'TriggerDBConnection')
430 if 'TriggerDBConnection' in trig_rels:
431 db_id = get_ref_id(trig_rels['TriggerDBConnection'])
432 if db_id:
433 url = f"{webdaq_base}/info/current/{partition}/oks/TriggerDBConnection/{db_id}?format=compact"
434 resp = requests.get(url, timeout=10)
435 if resp.status_code == 200:
436 db_attrs, _ = extract_oks_data(resp.json())
437 log.debug("TriggerDBConnection attributes: %s", db_attrs)
438 if 'SuperMasterKey' in db_attrs:
439 result['SMK'] = int(db_attrs['SuperMasterKey'])
440 log.info("Got SMK=%d from OKS", result['SMK'])
441 if 'Alias' in db_attrs:
442 result['db_alias'] = db_attrs['Alias']
443 log.info("Got db_alias=%s from OKS", result['db_alias'])
444
445 # Get HLTImplementationDB for HLTPSK (relationship 'hlt')
446 if 'hlt' in trig_rels:
447 hlt_id = get_ref_id(trig_rels['hlt'])
448 if hlt_id:
449 url = f"{webdaq_base}/info/current/{partition}/oks/HLTImplementationDB/{hlt_id}?format=compact"
450 resp = requests.get(url, timeout=10)
451 if resp.status_code == 200:
452 hlt_attrs, _ = extract_oks_data(resp.json())
453 log.debug("HLTImplementationDB attributes: %s", hlt_attrs)
454 if 'hltPrescaleKey' in hlt_attrs:
455 result['HLTPSK'] = int(hlt_attrs['hltPrescaleKey'])
456 log.info("Got HLTPSK=%d from OKS", result['HLTPSK'])
457 else:
458 msg = f"Failed to fetch Partition from OKS: HTTP {response.status_code}"
459 if strict:
460 raise RuntimeError(msg + " (required for --online-environment)")
461 log.warning(msg)
462
463 except requests.exceptions.RequestException as e:
464 msg = f"Error fetching trigger keys from OKS: {e}"
465 if strict:
466 raise RuntimeError(msg + " (required for --online-environment)")
467 log.warning(msg)
468 except (ValueError, KeyError, TypeError) as e:
469 msg = f"Error parsing trigger keys from OKS: {e}"
470 if strict:
471 raise RuntimeError(msg + " (required for --online-environment)")
472 log.warning(msg)
473
474 # In strict mode, verify we got the required keys from OKS
475 if strict:
476 missing = [k for k in ['SMK', 'L1PSK', 'HLTPSK'] if result.get(k) is None]
477 if missing:
478 raise RuntimeError(f"Failed to get {', '.join(missing)} from OKS (required for --online-environment)")
479
480 return result
481
482
483def get_run_params(args=None, from_is=False, partition=None, webdaq_base=None, strict=False,
484 solenoid_current_override=None, toroids_current_override=None):
485 """
486 Get run parameters from the appropriate source.
487
488 This is the main entry point for obtaining run parameters. It provides
489 a single place to modify when adding new sources (like WEBDAQ).
490
491 Args:
492 args: argparse Namespace with command-line arguments (optional)
493 from_is: If True, try to read from WEBDAQ first
494 partition: Partition name for IS access (defaults to TDAQ_PARTITION env var)
495 webdaq_base: WEBDAQ base URL (defaults to TDAQ_WEBDAQ_BASE env var)
496 strict: If True, raise an exception if IS read fails (for --online-environment)
497 solenoid_current_override: Command-line override for solenoid current
498 toroids_current_override: Command-line override for toroids current
499
500 Returns:
501 RunParams instance
502
503 Raises:
504 RuntimeError: If strict=True and IS read fails
505 """
506 if from_is:
507 return RunParams.from_is(partition=partition, webdaq_base=webdaq_base, strict=strict,
508 solenoid_current_override=solenoid_current_override,
509 toroids_current_override=toroids_current_override)
510 elif args is not None:
511 return RunParams.from_args(args)
512 else:
513 return RunParams()
514
515
517 """
518 Changes applied to the configuration after ApplicationMgr::configure() and before initialize().
519 Built entirely in main() from the command line and applied once in ConfigRunner.run(), adding a new override is a single line.
520 NB: we bypass the ComponentAccumulator. They must not end up in the generated JobOptions.
521 """
522 def __init__(self):
523 self.service_types = {} # service name -> type
524 self.create_services = [] # (name, type) to create
525 self.drop_services = [] # service names to remove
526 self.properties = {} # "Component.Property" -> value
527 self.commands = [] # Post commands to execute (--postcommand)
528
529 def declare_type(self, name, type_):
530 """Schedule the service registered as 'name' to be of type 'type_'"""
531 self.service_types[name] = type_
532
533 def drop_service(self, name):
534 """Schedule the removal of a service already created by configure()"""
535 self.drop_services.append(name)
536
537 def create_service(self, name, type_=None):
538 """Schedule the creation of a service the configuration does not list"""
539 self.create_services.append((name, type_ or name))
540
541 def set(self, key, value):
542 """Schedule 'Component.Property' = value"""
543 self.properties[key] = value
544
545 def add_command(self, cmd):
546 """Schedule a Python command to be executed after configure()"""
547 self.commands.append(cmd)
548
549 def apply(self):
550 """Apply all overrides."""
551 from GaudiPython import InterfaceCast, gbl
552 from GaudiPython.Bindings import iProperty
553
554 if self.service_types or self.create_services or self.drop_services:
555 svcMgr = InterfaceCast(gbl.ISvcManager)(gbl.Gaudi.svcLocator())
556 for name, type_ in self.service_types.items():
557 log.info("Configuring %s under the name %s", type_, name)
558 svcMgr.declareSvcType(name, type_)
559 for name, type_ in self.create_services:
560 # For services not listed in the configuration.
561 if svcMgr.addService(gbl.Gaudi.Utils.TypeNameString(f"{type_}/{name}")).isSuccess():
562 log.info("Created service %s/%s", type_, name)
563 else:
564 log.error("Failed to create service %s/%s", type_, name)
565 for name in self.drop_services:
566 # Services are instantiated by configure(), they have to be taken out before initialize().
567 if svcMgr.removeService(name).isSuccess():
568 log.info("Removed service %s", name)
569 else:
570 log.debug("Service %s not present, nothing to remove", name)
571
572 for key, value in self.properties.items():
573 component, _, prop = key.rpartition('.')
574 log.info("Overriding %s.%s = %s (from command line)", component, prop, value)
575 setattr(iProperty(component), prop, value)
576
577
579 """
580 Runner class that executes Gaudi configuration from JSON file or database.
581 Uses TrigConf::JobOptionsSvc with TYPE="FILE" or TYPE="DB" to load configuration.
582 It sets JobOptionsType and JobOptionsPath on the ApplicationMgr, and TrigConf::JobOptionsSvc
583 handles both FILE and DB modes transparently.
584 """
585 def __init__(self, job_options_type, job_options_path, run_params=None,
586 properties=None, db_server=None, smk=None, overrides=None):
587 """
588 Args:
589 job_options_type: "FILE" or "DB"
590 job_options_path: JSON file path (for FILE) or DB connection string (for DB)
591 run_params: Run parameters dict for prepareForStart
592 properties: Pre-loaded properties dict (optional, for FILE mode)
593 db_server: DB server alias (for store() in DB mode)
594 smk: Super Master Key (for store() in DB mode)
595 overrides: RuntimeOverrides applied between configure() and initialize()
596 """
597 self.job_options_type = job_options_type
598 self.job_options_path = job_options_path
599 self.run_params = run_params or {}
600 self.properties = properties
601 self.db_server = db_server # For store() in DB mode
602 self.smk = smk # For store() in DB mode
603 self.overrides = overrides or RuntimeOverrides()
604 self._app = None
605
606 @classmethod
607 def from_json(cls, json_file, run_params=None, properties=None, overrides=None):
608 """Create runner for JSON file (TYPE=FILE)"""
609 return cls("FILE", os.path.abspath(json_file), run_params, properties, overrides=overrides)
610
611 @classmethod
612 def from_database(cls, db_server, smk, l1psk=None, hltpsk=None, run_params=None, overrides=None):
613 """Create runner for database (TYPE=DB)"""
614 # Build the DB connection string: server=X;smkey=Y;lvl1key=Z;hltkey=W
615 db_path = f"server={db_server};smkey={smk}"
616 if l1psk is not None:
617 db_path += f";lvl1key={l1psk}"
618 if hltpsk is not None:
619 db_path += f";hltkey={hltpsk}"
620 return cls("DB", db_path, run_params, db_server=db_server, smk=smk, overrides=overrides)
621
622 def run(self, maxEvents=None):
623 """
624 1. Create ApplicationMgr via BootstrapHelper
625 2. Set JobOptionsSvcType, JobOptionsType, JobOptionsPath
626 3. configure() -> initialize() -> prepareForStart() -> start() ->
627 hltUpdateAfterFork() -> run() -> stop() -> finalize() -> terminate()
628 """
629 from Gaudi.Main import BootstrapHelper
630
631 # For FILE mode, load properties from JSON if not already provided
632 if self.job_options_type == "FILE" and self.properties is None:
633 with open(self.job_options_path, 'r') as f:
634 jocat = json.load(f)
635 self.properties = jocat.get('properties', {})
636
637 bsh = BootstrapHelper()
638 app = bsh.createApplicationMgr()
639 self._app = app
640 #Set trigger defaults here, see ATR-32996
641 app.setProperty("MessageSvcType", "TrigMessageSvc")
642
643 # For FILE mode, set ApplicationMgr properties from JSON before configure
644 if self.job_options_type == "FILE" and self.properties:
645 app_props = self.properties.get('ApplicationMgr', {})
646 for k, v in app_props.items():
647 if k not in ('JobOptionsSvcType', 'JobOptionsType', 'JobOptionsPath'):
648 log.debug("Setting ApplicationMgr.%s = %s", k, v)
649 app.setProperty(k, str(v) if not isinstance(v, str) else v)
650
651 # Set JobOptionsSvc properties
652 log.info("Configuring TrigConf::JobOptionsSvc with TYPE=%s, PATH=%s",
654 app.setProperty("JobOptionsSvcType", "TrigConf::JobOptionsSvc")
655 app.setProperty("JobOptionsType", self.job_options_type)
656 app.setProperty("JobOptionsPath", self.job_options_path)
657
658 # Configure the application - TrigConf::JobOptionsSvc will load from FILE or DB
659 app.configure()
660
661 # Override EvtMax AFTER configure() only if explicitly specified by user
662 # Otherwise use whatever value is in the DB
663 if maxEvents is not None:
664 log.info("Setting EvtMax=%d (overriding DB value)", maxEvents)
665 app.setProperty('EvtMax', str(maxEvents))
666
667 # Overrides must be applied after configure() but before initialize().
668 self.overrides.apply()
669
670 # THistSvc.Output cannot be scheduled in main(): setTHistSvcOutput() has to run
671 # against the service that configure() actually created.
672 if self.overrides.service_types.get('THistSvc') == 'THistSvc':
673 from GaudiPython.Bindings import iProperty
674 from TriggerJobOpts.TriggerHistSvcConfig import setTHistSvcOutput
675 output = []
676 setTHistSvcOutput(output)
677 iProperty("THistSvc").Output = output
678
679 # Postcommands run last.
680 if self.overrides.commands:
681 log.info("Executing postcommand(s)")
682 for cmd in self.overrides.commands:
683 log.info(" %s", cmd)
684 exec(cmd, globals(), {'app': app})
685
686 # Initialize
687 sc = app.initialize()
688 if not sc.isSuccess():
689 log.error("Failed to initialize AppMgr")
690 return sc
691
692 # Initialize TrigServicesHelper for lifecycle calls (prepareForStart, prepareForRun, hltUpdateAfterFork)
693 try:
694 from TrigServices.TrigServicesHelper import TrigServicesHelper
695 helper = TrigServicesHelper()
696 except ImportError as e:
697 log.error("TrigServicesHelper not available: %s", e)
698 log.error("Cannot proceed without TrigServicesHelper - required for HLTEventLoopMgr lifecycle")
699 raise RuntimeError("TrigServicesHelper not available") from e
700
701 # Call prepareForStart to set up ByteStreamMetadata
702 try:
703 run_number = self.run_params['run_number']
704 det_mask = self.run_params['detector_mask']
705 sor_time = self.run_params['sor_time']
706 solenoid_current = self.run_params['solenoid_current']
707 toroids_current = self.run_params['toroids_current']
708 beam_type = self.run_params['beam_type']
709 beam_energy = self.run_params['beam_energy']
710 lb_number = self.run_params['lb_number']
711
712 log.info("Calling prepareForStart with run=%d, det_mask=0x%s, sor_time=%s",
713 run_number, det_mask, sor_time)
714
715 success = helper.prepareForStart(
716 run_number=run_number,
717 det_mask=det_mask,
718 sor_time=sor_time,
719 lb_number=lb_number,
720 beam_type=beam_type,
721 beam_energy=beam_energy,
722 solenoid_current=solenoid_current,
723 toroids_current=toroids_current
724 )
725 if not success:
726 log.error("prepareForStart failed")
727 raise RuntimeError("prepareForStart failed")
728 log.info("prepareForStart completed successfully")
729 except Exception as e:
730 log.error("Error calling prepareForStart: %s", e)
731 traceback.print_exc()
732 raise
733
734 # Start
735 sc = app.start()
736 if not sc.isSuccess():
737 log.error("Failed to start AppMgr")
738 return sc
739
740 # prepareForRun initializes COOL folder helper - must be called after start()
741 # which fires the start incident
742 try:
743 log.info("Calling prepareForRun to initialize COOL folder helper")
744 success = helper.prepareForRun()
745 if not success:
746 log.error("prepareForRun failed")
747 raise RuntimeError("prepareForRun failed")
748 log.info("prepareForRun completed successfully")
749 except Exception as e:
750 log.error("Error calling prepareForRun: %s", e)
751 traceback.print_exc()
752 raise
753
754 # hltUpdateAfterFork initializes the scheduler
755 # worker_id=1 for single-worker, non-forked mode
756 try:
757 log.info("Calling hltUpdateAfterFork to initialize scheduler (worker_id=1)")
758 success = helper.hltUpdateAfterFork(worker_id=1)
759 if not success:
760 log.error("hltUpdateAfterFork failed")
761 raise RuntimeError("hltUpdateAfterFork failed")
762 log.info("hltUpdateAfterFork completed successfully")
763 except Exception as e:
764 log.error("Error calling hltUpdateAfterFork: %s", e)
765 traceback.print_exc()
766 raise
767
768 # Run the event loop
769 # Note: Python signal handlers won't work during C++ execution.
770 nevt = maxEvents if maxEvents is not None else -1
771 sc = app.run(nevt)
772
773 if not sc.isSuccess():
774 log.error("Failure running application")
775 return sc
776
777 # Stop
778 sc = app.stop()
779 if not sc.isSuccess():
780 log.error("Failed to stop AppMgr")
781 return sc
782
783 # Finalize
784 sc = app.finalize()
785 if not sc.isSuccess():
786 log.error("Failed to finalize AppMgr")
787 return sc
788
789 # Terminate
790 sc = app.terminate()
791 return sc
792
793
794def load_from_json(json_file, run_params=None, overrides=None):
795 """
796 Load configuration from a Gaudi joboptions JSON file.
797
798 Returns a ConfigRunner with a run() method that executes the configuration
799 using TrigConf::JobOptionsSvc with TYPE="FILE".
800 """
801 with open(json_file, 'r') as f:
802 jocat = json.load(f)
803
804 if jocat.get('filetype') != 'joboptions':
805 raise ValueError(f"Invalid JSON file type: {jocat.get('filetype')}, expected 'joboptions'")
806
807 properties = jocat.get('properties', {})
808 return ConfigRunner.from_json(json_file, run_params, properties, overrides=overrides)
809
810
811def load_from_database(db_server, smk, l1psk=None, hltpsk=None, run_params=None, overrides=None):
812 """
813 Load configuration from trigger database using the Super Master Key (SMK).
814
815 Returns a ConfigRunner that uses TrigConf::JobOptionsSvc with TYPE="DB"
816 to load configuration directly from the database.
817 """
818 log.info("Loading job options from database %s with SMK %d", db_server, smk)
819 return ConfigRunner.from_database(db_server, smk, l1psk, hltpsk, run_params, overrides=overrides)
820
821
824def arg_sor_time(s) -> str:
825 """Convert possible SOR time arguments to an OWLTime compatible string"""
826 fmt = '%d/%m/%y %H:%M:%S.%f'
827 if s=='now': return dt.now().strftime(fmt)
828 elif s.isdigit(): return dt.fromtimestamp(float(s)/1e9).strftime(fmt)
829 else: return s
830
831
833 """Convert detector mask to format expected by eformat"""
834 if s=='all':
835 return RunParams.DEFAULT_DETECTOR_MASK
836 dmask = hex(int(s,16)) # Normalize input to hex-string
837 dmask = dmask.lower().replace('0x', '').replace('l', '') # remove markers
838 return '0' * (32 - len(dmask)) + dmask # (pad with 0s)
839
840
841def check_args(parser, args):
842 """Consistency check of command line arguments"""
843
844 if not args.jobOptions and not args.use_database:
845 parser.error("No job options file specified")
846
847 if args.jobOptions and args.jobOptions.endswith('.pkl'):
848 parser.error("Running from a pickle file is not supported in athenaEF.")
849
850 if (not args.file and not args.dump_config_exit
851 and (args.efdf_interface_library or 'TrigDFEmulator') == 'TrigDFEmulator'):
852 parser.error("--file is required unless using --dump-config-exit or online efdf-interface-library")
853
854 if args.use_crest and not args.use_database:
855 parser.error("--use-crest requires --use-database")
856
857 if args.oh_monitoring and args.online_environment:
858 parser.error("--oh-monitoring (-M) and --online-environment are mutually exclusive.")
859
860 if args.timeout is not None and args.timeout <= 0:
861 parser.error("--timeout must be a positive number of milliseconds")
862
864 """Update run parameters from IS, file, or conditions DB"""
865
866 # If --online-environment is specified, try to read from Information Service first
867 if getattr(args, 'online_environment', False):
868 log.info("Reading run parameters from Information Service via WEBDAQ")
869 # Pass command-line magnet values as overrides (if provided)
870 # strict=True ensures we fail if IS read fails, rather than falling back to defaults
871 # But if user provided magnet values on command line, those take precedence over IS
872 solenoid_override = getattr(args, 'solenoid_current', None)
873 toroids_override = getattr(args, 'toroids_current', None)
874
875 run_params = get_run_params(from_is=True,
876 partition=getattr(args, 'partition', None),
877 webdaq_base=getattr(args, 'webdaq_base', None),
878 strict=True,
879 solenoid_current_override=solenoid_override,
880 toroids_current_override=toroids_override)
881 # Update args with values from IS (if not already set on command line)
882 if args.run_number is None and run_params.run_number is not None:
883 args.run_number = run_params.run_number
884 log.info("Using run_number=%d from IS", args.run_number)
885 if args.lb_number is None and run_params.lb_number is not None:
886 args.lb_number = run_params.lb_number
887 log.info("Using lb_number=%d from IS", args.lb_number)
888 if args.sor_time is None and run_params.sor_time is not None:
889 args.sor_time = run_params.sor_time
890 log.info("Using sor_time=%s from IS", args.sor_time)
891 if args.detector_mask is None and run_params.detector_mask is not None:
892 args.detector_mask = run_params.detector_mask
893 log.info("Using detector_mask=%s from IS", args.detector_mask)
894 # Update magnet currents from IS (run_params already has command-line overrides if provided)
895 args.solenoid_current = run_params.solenoid_current
896 args.toroids_current = run_params.toroids_current
897 args.beam_type = run_params.beam_type
898 args.beam_energy = run_params.beam_energy
899
900 if (args.run_number is not None and args.lb_number is None) or (args.run_number is None and args.lb_number is not None):
901 log.error("Both or neither of the options -R (--run-number) and -L (--lb-number) have to be specified")
902
903 # Read metadata from input file (like HLTMPPy/runner.py getRunParamsFromFile)
904 if args.file:
905 from eformat import EventStorage
906 dr = EventStorage.pickDataReader(args.file[0])
907 if args.run_number is None:
908 args.run_number = dr.runNumber()
909 args.lb_number = dr.lumiblockNumber()
910 args.T0_project_tag = dr.projectTag()
911 args.beam_type = dr.beamType()
912 args.beam_energy = dr.beamEnergy()
913 args.trigger_type = dr.triggerType()
914 args.stream = dr.stream()
915 args.lumiblock = dr.lumiblockNumber()
916 args.file_detector_mask = "{:032x}".format(dr.detectorMask())
917 else:
918 args.T0_project_tag = getattr(args, 'T0_project_tag', '')
919 args.beam_type = getattr(args, 'beam_type', 0)
920 args.beam_energy = getattr(args, 'beam_energy', 0)
921 args.trigger_type = getattr(args, 'trigger_type', 0)
922 args.stream = getattr(args, 'stream', '')
923 args.lumiblock = getattr(args, 'lumiblock', 0)
924 args.file_detector_mask = getattr(args, 'file_detector_mask', '00000000000000000000000000000000')
925
926 sor_params = None
927 if (args.sor_time is None or args.detector_mask is None) and args.run_number is not None:
928 sor_params = AthHLT.get_sor_params(args.run_number)
929 log.debug('SOR parameters: %s', sor_params)
930 if sor_params is None:
931 log.error("Run %d does not exist. If you want to use this run-number specify "
932 "remaining run parameters, e.g.: --sor-time=now --detector-mask=all", args.run_number)
933 sys.exit(1)
934
935 if args.sor_time is None and sor_params is not None:
936 args.sor_time = arg_sor_time(str(sor_params['SORTime']))
937
938 if args.detector_mask is None and sor_params is not None:
939 dmask = sor_params['DetectorMask']
940 if args.run_number < AthHLT.CondDB._run2:
941 dmask = hex(dmask)
942 args.detector_mask = arg_detector_mask(dmask)
943
944 if args.dump_config_exit and not args.run_number:
945 args.run_number = 0
946
947 # Apply defaults for magnet currents if not set (offline mode only)
948 # In online mode, magnets must come from IS or command line (handled above)
949 if getattr(args, 'solenoid_current', None) is None:
950 args.solenoid_current = RunParams.DEFAULT_SOLENOID_CURRENT
951 log.debug("Using default solenoid_current=%.1f", args.solenoid_current)
952 if getattr(args, 'toroids_current', None) is None:
953 args.toroids_current = RunParams.DEFAULT_TOROIDS_CURRENT
954 log.debug("Using default toroids_current=%.1f", args.toroids_current)
955
956
958 """Update trigger configuration keys from OKS, COOL, or CREST.
959
960 Priority order:
961 1. Command-line arguments (always take precedence)
962 2. OKS via WEBDAQ (if --online-environment is set)
963 3. CREST (if --use-crest is set)
964 4. COOL (default)
965 """
966
967 if args.smk is None or args.l1psk is None or args.hltpsk is None:
968 trigconf = None
969
970 # Try OKS first if --online-environment is set
971 if getattr(args, 'online_environment', False):
972 log.info("Reading trigger configuration keys from OKS (online environment)")
973 # strict=True ensures we fail if OKS read fails, rather than falling back to COOL
975 partition=getattr(args, 'partition', None),
976 webdaq_base=getattr(args, 'webdaq_base', None),
977 strict=True
978 )
979 log.info("Retrieved trigger keys from OKS: %s", oks_keys)
980
981 # With strict=True, we're guaranteed to have all keys or an exception was raised
982 trigconf = {
983 'SMK': oks_keys.get('SMK'),
984 'LVL1PSK': oks_keys.get('L1PSK'),
985 'HLTPSK': oks_keys.get('HLTPSK')
986 }
987 # Also update db_server if provided by OKS and not set on command line
988 if oks_keys.get('db_alias') and args.db_server == 'TRIGGERDB_RUN3':
989 args.db_server = oks_keys['db_alias']
990 log.info("Using db_server=%s from OKS", args.db_server)
991
992 # Fall back to CREST or COOL only if NOT in online-environment mode
993 if trigconf is None:
994 if args.use_crest:
995 log.info("Reading trigger configuration keys from CREST for run %s", args.run_number)
996 trigconf = AthHLT.get_trigconf_keys_crest(args.run_number, args.lb_number, args.crest_server)
997 log.info("Retrieved trigger keys from CREST: %s", trigconf)
998 else:
999 log.info("Reading trigger configuration keys from COOL for run %s", args.run_number)
1000 trigconf = AthHLT.get_trigconf_keys(args.run_number, args.lb_number)
1001 log.info("Retrieved trigger keys from COOL: %s", trigconf)
1002
1003 try:
1004 if args.smk is None:
1005 args.smk = trigconf['SMK']
1006 log.debug("Using SMK=%d from conditions DB/OKS", args.smk)
1007 else:
1008 log.debug("Using SMK=%d from command line (ignoring DB/OKS value %s)", args.smk, trigconf.get('SMK'))
1009 if args.l1psk is None:
1010 args.l1psk = trigconf['LVL1PSK']
1011 log.debug("Using L1PSK=%d from conditions DB/OKS", args.l1psk)
1012 else:
1013 log.debug("Using L1PSK=%d from command line (ignoring DB/OKS value %s)", args.l1psk, trigconf.get('LVL1PSK'))
1014 if args.hltpsk is None:
1015 args.hltpsk = trigconf['HLTPSK']
1016 log.debug("Using HLTPSK=%d from conditions DB/OKS", args.hltpsk)
1017 else:
1018 log.debug("Using HLTPSK=%d from command line (ignoring DB/OKS value %s)", args.hltpsk, trigconf.get('HLTPSK'))
1019 except KeyError:
1020 log.error("Cannot read trigger configuration keys from the conditions database for run %d", args.run_number)
1021 sys.exit(1)
1022 else:
1023 log.info("Using trigger configuration keys from command line: SMK=%d, L1PSK=%d, HLTPSK=%d",
1024 args.smk, args.l1psk, args.hltpsk)
1025
1026# IS schema files, installed under <...>/share/schema (e.g. see TrigCaloHypo/CMakeLists.txt).
1027# Add an entry here for every new IS type
1028IS_SCHEMA_FILES = ['schema/Larg.LArNoiseBurstCandidates.is.schema.xml']
1029
1031 """
1032 Resolve IS_SCHEMA_FILES to absolute paths using DATAPATH.
1033 rdb_server must be given the schema of every IS type we publish.
1034 """
1035 from AthenaCommon.Utils.unixtools import find_datafile
1036
1037 found = []
1038 for fname in IS_SCHEMA_FILES:
1039 path = find_datafile(fname)
1040 if path:
1041 found.append(os.path.abspath(path))
1042 else:
1043 log.error("IS schema file %s not found on DATAPATH: IS publication will fail with HTTP 400", fname)
1044 return found
1045
1046
1048 """Start a private TDAQ infrastructure (offline test of OH publication)."""
1049 import shutil, socket, signal, subprocess, time
1050
1051 infra_script = shutil.which('athenaEF_tdaq_infra.py')
1052 if infra_script is None:
1053 log.error("athenaEF_tdaq_infra.py not found on PATH (required for -M)")
1054 sys.exit(1)
1055
1056 partition = args.partition or 'athenaEF'
1057 host = 'localhost'
1058 # Get a free port for webis
1059 s = socket.socket()
1060 s.bind((host, 0))
1061 port = s.getsockname()[1]
1062 s.close()
1063 oh_server = 'Histogramming' # WebdaqHistSvc.OHServerName default
1064 run_number = args.run_number if args.run_number is not None else 0
1065
1066 # Export variables for the -M case
1067 os.environ['TDAQ_PARTITION'] = partition
1068 os.environ['TDAQ_WEBDAQ_BASE'] = f'http://{host}:{port}'
1069 os.environ['TDAQ_OH_SERVER'] = oh_server
1070
1071 log.info("Starting private OH infrastructure: partition=%s, webdaq=%s, oh_server=%s",
1072 partition, os.environ['TDAQ_WEBDAQ_BASE'], oh_server)
1073
1074 logfile = open('athenaEF_oh_infra.log', 'w')
1075
1076 # PR_SET_PDEATHSIG so the infrastructure is torn down (SIGTERM -> oh_cp +
1077 # ipc_rm) if athenaEF dies unexpectedly, e.g. segfaults mid-run.
1078 from ctypes import cdll
1079 PR_SET_PDEATHSIG = 1
1080 def _pdeathsig():
1081 cdll['libc.so.6'].prctl(PR_SET_PDEATHSIG, signal.SIGTERM)
1082
1083 schemas = find_is_schema_files()
1084 log.info("IS schema files: %s", ', '.join(schemas) or '(none)')
1085
1086 proc = subprocess.Popen(
1087 [infra_script,
1088 '--partition', partition,
1089 '--webdaq-port', str(port),
1090 '--oh-server', oh_server,
1091 '--run-number', str(run_number),
1092 *(arg for f in schemas for arg in ('--schema', f))],
1093 stdout=logfile, stderr=subprocess.STDOUT,
1094 preexec_fn=_pdeathsig, close_fds=True)
1095
1096 # Wait for the readiness marker (or early failure / timeout)
1097 timeout = 120
1098 deadline = time.time() + timeout
1099 while time.time() < deadline:
1100 if proc.poll() is not None:
1101 log.error("OH infrastructure exited early (code %s); see %s", proc.returncode, logfile.name)
1102 sys.exit(1)
1103 with open(logfile.name) as f:
1104 if 'ATHENAEF_INFRA_READY' in f.read():
1105 log.info("OH infrastructure is ready")
1106 return proc
1107 time.sleep(1)
1108
1109 log.error("OH infrastructure did not become ready within %d s; see %s", timeout, logfile.name)
1110 proc.terminate()
1111 sys.exit(1)
1112
1113
1115 """Terminate the private TDAQ infrastructure (SIGTERM triggers oh_cp + ipc_rm)."""
1116 if proc is None or proc.poll() is not None:
1117 return
1118 import signal
1119 log.info("Stopping OH infrastructure")
1120 proc.send_signal(signal.SIGTERM)
1121 try:
1122 proc.wait(timeout=60)
1123 except Exception:
1124 proc.kill()
1125
1126
1127class MyHelp(argparse.Action):
1128 """Custom help to hide/show expert groups"""
1129 def __call__(self, parser, namespace, values, option_string=None):
1130
1131 for g in parser.expert_groups:
1132 for a in g._group_actions:
1133 if values!='all':
1134 a.help = argparse.SUPPRESS
1135
1136 parser.print_help()
1137 if values!='all':
1138 print('\nUse --help=all to show all (expert) options')
1139 sys.exit(0)
1140
1141
1142def configure_from_ca(args, unparsed_args):
1143 """Configure the job from a CA module and re-execute athenaEF from the resulting JSON.
1144
1145 This function never returns: athenaEF either exits (--dump-config-exit) or replaces
1146 itself with a new athenaEF running from the JSON file it just created.
1147 """
1148 from AthenaCommon import Constants
1149 from AthenaConfiguration.AllConfigFlags import initConfigFlags
1150 from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
1151 from AthenaConfiguration.ComponentFactory import CompFactory
1152 from AthenaConfiguration.MainServicesConfig import addMainSequences
1153 from TrigServices.TrigServicesConfig import commonServicesCfg, setDefaultOnlineFlags
1154
1155 # Create flags with online defaults
1156 flags = initConfigFlags()
1157 setDefaultOnlineFlags(flags)
1158
1159 # set MessageSvc OutputLevel
1160 flags.Exec.OutputLevel = getattr(Constants, args.log_level)
1161
1162 # Enable WebdaqHistSvc for online histogram publishing if requested
1163 if args.oh_monitoring:
1164 flags.Trigger.Online.useOnlineWebdaqHistSvc = True
1165 log.info("Enabled WebdaqHistSvc for online histogram publishing")
1166
1167 # Fill flags from the command line.
1168 AthHLT.unparsedArguments = unparsed_args
1169 AthHLT.fillFromUnparsedArgs(flags)
1170
1171 # NOTE: Do NOT set flags.Input.Files here!
1172 # We keep Input.Files=[] during configuration to ensure the configuration
1173 # is portable and doesn't depend on specific input file metadata.
1174 # Input files are passed to EFInterface for runtime use only.
1175
1176 # Set conditions run number override (for test partitions with fake run numbers)
1177 if args.conditions_run is not None:
1178 log.info("Using conditions from reference run %d (overriding run %s for IOV lookup)",
1179 args.conditions_run, args.run_number)
1180 flags.Input.ConditionsRunNumber = args.conditions_run
1181
1182 # Set number of events
1183 if args.number_of_events is not None and args.number_of_events > 0:
1184 flags.Exec.MaxEvents = args.number_of_events
1185
1186 # Set skip events
1187 if args.skip_events is not None and args.skip_events > 0:
1188 flags.Exec.SkipEvents = args.skip_events
1189
1190 # NOTE: Do NOT set flags.Concurrency.NumThreads or NumConcurrentEvents here.
1191 # Threading is set at runtime via iProperty after configure() - see ConfigRunner.run()
1192
1193 # Enable PerfMon if requested
1194 flags.PerfMon.doFastMonMT = args.perfmon
1195
1196 # Execute precommands
1197 if args.precommand:
1198 log.info("Executing precommand(s)")
1199 for cmd in args.precommand:
1200 log.info(" %s", cmd)
1201 exec(cmd, globals(), {'flags': flags})
1202
1203 # Load from CA module:
1204 # 1. Build the full configuration with services
1205 # 2. Dump to JSON file
1206 # 3. Use AthHLT.reload_from_json to re-exec and reload from JSON
1207 log.info("Loading CA configuration from: %s", args.jobOptions)
1208
1209 # Clone and lock flags for services configuration
1210 locked_flags = flags.clone()
1211 locked_flags.lock()
1212
1213 # Create base CA with framework services
1214 cfg = ComponentAccumulator(CompFactory.AthSequencer("AthMasterSeq", Sequential=True))
1215 cfg.setAppProperty('ExtSvcCreates', False)
1216 cfg.setAppProperty("MessageSvcType", "TrigMessageSvc")
1217 cfg.setAppProperty("JobOptionsSvcType", "TrigConf::JobOptionsSvc")
1218
1219 # Add main sequences and common services (includes TrigServicesCfg)
1220 addMainSequences(locked_flags, cfg)
1221 cfg.merge(commonServicesCfg(locked_flags))
1222
1223 # Now merge user CA config (with unlocked flags)
1224 cfg_func = AthHLT.getCACfg(args.jobOptions)
1225 cfg.merge(cfg_func(flags))
1226
1227 # Execute postcommands before dumping
1228 if args.postcommand:
1229 log.info("Executing postcommand(s)")
1230 for cmd in args.postcommand:
1231 log.info(" %s", cmd)
1232 exec(cmd, globals(), {'flags': flags, 'cfg': cfg})
1233
1234 # Dump configuration to JSON
1235 fname = "HLTJobOptions"
1236 log.info("Dumping configuration to %s.pkl and %s.json", fname, fname)
1237 with open(f"{fname}.pkl", "wb") as f:
1238 cfg.store(f)
1239
1240 from TrigConfIO.JsonUtils import create_joboptions_json
1241 create_joboptions_json(f"{fname}.pkl", f"{fname}.json")
1242
1243 # Check for dump-and-exit
1244 if args.dump_config_exit:
1245 log.info("Configuration dumped to %s.json. Exiting...", fname)
1246 sys.exit(0)
1247
1248 # Re-exec from the JSON. Replaces the process image freeing up the configuration heap.
1249 log.info("Configuration dumped to %s.json. Re-exec...", fname)
1250 AthHLT.reload_from_json(f"{fname}.json", suppress_args=unparsed_args + ['--dump-config'], jobOptions=args.jobOptions)
1251
1252
1253def main():
1254 parser = argparse.ArgumentParser(prog='athenaEF.py', formatter_class=
1255 lambda prog : argparse.ArgumentDefaultsHelpFormatter(prog, max_help_position=32, width=100),
1256 usage = '%(prog)s [OPTION]... -f FILE jobOptions',
1257 add_help=False)
1258 parser.expert_groups = [] # Keep list of expert option groups
1259
1260
1261 g = parser.add_argument_group('Options')
1262 g.add_argument('jobOptions', nargs='?', help='job options: CA module (package.module:function) or JSON file (.json)')
1263 g.add_argument('--threads', metavar='N', type=int, default=1, help='number of threads')
1264 g.add_argument('--concurrent-events', metavar='N', type=int, help='number of concurrent events if different from --threads')
1265 g.add_argument('--log-level', '-l', metavar='LVL', default='INFO', help='OutputLevel of athena')
1266 g.add_argument('--precommand', '-c', metavar='CMD', action='append', default=[],
1267 help='Python commands executed before job options')
1268 g.add_argument('--postcommand', '-C', metavar='CMD', action='append', default=[],
1269 help='Python commands executed after job options')
1270 g.add_argument('--interactive', '-i', action='store_true', help='interactive mode')
1271 g.add_argument('--help', '-h', nargs='?', choices=['all'], action=MyHelp, help='show help')
1272
1273 g = parser.add_argument_group('Input/Output')
1274 g.add_argument('--file', '--filesInput', '-f', action='append', help='input RAW file')
1275 g.add_argument('--save-output', '-o', metavar='FILE', help='output file name')
1276 g.add_argument('--number-of-events', '--evtMax', '-n', metavar='N', type=int, default=None,
1277 help='processes N events (default: from DB/config, -1 means all)')
1278 g.add_argument('--skip-events', '--skipEvents', '-k', metavar='N', type=int, default=None,
1279 help='skip N first events')
1280 g.add_argument('--loop-files', action=argparse.BooleanOptionalAction, default=None,
1281 help='loop over input files if no more events')
1282 g.add_argument('--efdf-interface-library', metavar='LIB', default=None,
1283 help='name of the EFDF interface shared library to load (default: TrigDFEmulator)')
1284
1285
1286 g = parser.add_argument_group('Performance and debugging')
1287 g.add_argument('--perfmon', action='store_true', help='enable PerfMon')
1288 g.add_argument('--tcmalloc', action='store_true', default=True, help='use tcmalloc')
1289 g.add_argument('--stdcmalloc', action='store_true', help='use stdcmalloc')
1290 g.add_argument('--stdcmath', action='store_true', help='use stdcmath library')
1291 g.add_argument('--imf', action='store_true', default=True, help='use Intel math library')
1292 g.add_argument('--timeout', metavar='MSEC', type=int, default=None,
1293 help='event processing timeout (HardTimeout) in milliseconds. '
1294 f'NB: only the soft timeout ({SOFT_TIMEOUT_FRACTION*100:.0f}%% of it) is enforced')
1295
1296
1297 g = parser.add_argument_group('Conditions')
1298 g.add_argument('--run-number', '-R', metavar='RUN', type=int,
1299 help='run number (if None, read from first event)')
1300 g.add_argument('--lb-number', '-L', metavar='LBN', type=int,
1301 help='lumiblock number (if None, read from first event)')
1302 g.add_argument('--conditions-run', metavar='RUN', type=int, default=None,
1303 help='reference run number for conditions lookup (use when IS run number has no COOL data)')
1304 g.add_argument('--sor-time', type=arg_sor_time,
1305 help='The Start Of Run time. Three formats are accepted: '
1306 '1) the string "now", for current time; '
1307 '2) the number of nanoseconds since epoch (e.g. 1386355338658000000 or int(time.time() * 1e9)); '
1308 '3) human-readable "20/11/18 17:40:42.3043". If not specified the sor-time is read from the conditions DB')
1309 g.add_argument('--detector-mask', metavar='MASK', type=arg_detector_mask,
1310 help='detector mask (if None, read from the conditions DB), use string "all" to enable all detectors')
1311
1312
1313 g = parser.add_argument_group('Database')
1314 g.add_argument('--use-database', '-b', action='store_true',
1315 help='configure from trigger database using SMK')
1316 g.add_argument('--db-server', metavar='DB', default='TRIGGERDB_RUN3', help='DB server name (alias)')
1317 g.add_argument('--smk', type=int, default=None, help='Super Master Key')
1318 g.add_argument('--l1psk', type=int, default=None, help='L1 prescale key')
1319 g.add_argument('--hltpsk', type=int, default=None, help='HLT prescale key')
1320 g.add_argument('--use-crest', action='store_true', default=False,
1321 help='Use CREST for trigger configuration')
1322 g.add_argument('--crest-server', metavar='URL', default=None,
1323 help='CREST server URL (default: $CREST_SERVER or crest.cern.ch)')
1324 g.add_argument('--dump-config', action='store_true', help='Dump joboptions JSON file')
1325 g.add_argument('--dump-config-exit', action='store_true', help='Dump joboptions JSON file and exit')
1326
1327
1328 g = parser.add_argument_group('Magnets')
1329 g.add_argument('--solenoid-current', type=float, default=None,
1330 help='Solenoid current in Amperes (default: nominal current for offline running, required from IS online)')
1331 g.add_argument('--toroids-current', type=float, default=None,
1332 help='Toroids current in Amperes (default: nominal current for offline running, required from IS online)')
1333
1334
1335 g = parser.add_argument_group('Online')
1336 g.add_argument('--online-environment', action='store_true',
1337 help='Enable online environment: read run parameters from IS and trigger '
1338 'configuration keys (SMK, L1PSK, HLTPSK) from OKS via WEBDAQ REST API')
1339 g.add_argument('--partition', metavar='NAME', default=None,
1340 help='TDAQ partition name (defaults to TDAQ_PARTITION environment variable)')
1341 g.add_argument('--webdaq-base', metavar='URL', default=None,
1342 help='WEBDAQ base URL (defaults to TDAQ_WEBDAQ_BASE environment variable)')
1343
1344
1345 g = parser.add_argument_group('Online Histogramming')
1346 g.add_argument('--oh-monitoring', '-M', action='store_true', default=False,
1347 help='enable online histogram publishing via WebdaqHistSvc')
1348
1349
1350 g = parser.add_argument_group('Expert')
1351 parser.expert_groups.append(g)
1352 (args, unparsed_args) = parser.parse_known_args()
1353 check_args(parser, args)
1354
1355 # set ROOT to batch mode (ATR-21890)
1356 from PyUtils.Helpers import ROOTSetup
1357 ROOTSetup(batch=True)
1358
1359 # Enable ROOT thread safety
1360 import ROOT
1361 ROOT.ROOT.EnableThreadSafety()
1362
1363 # set default Python OutputLevel and file inclusion
1364 import AthenaCommon.Logging
1365 AthenaCommon.Logging.log.setLevel(getattr(logging, args.log_level))
1366 AthenaCommon.Logging.log.setFormat("%(asctime)s Py:%(name)-31s %(levelname)7s %(message)s")
1367
1368 # consistency checks for arguments
1369 if not args.concurrent_events:
1370 args.concurrent_events = args.threads
1371
1372 # Determine the source of the configuration.
1373 is_database = args.use_database
1374 is_json = bool(args.jobOptions) and not is_database and args.jobOptions.endswith('.json')
1375 is_ca = not (is_database or is_json)
1376
1377 # CREST configuration (only used with --use-database, see check_args)
1378 log.info("Using CREST for trigger configuration: %s", args.use_crest)
1379 if args.use_crest and args.crest_server is None:
1380 from IOVDbSvc.IOVDbAutoCfgFlags import getCrestConnection
1381 args.crest_server = getCrestConnection()
1382 log.info("Using default CREST server: %s", args.crest_server)
1383
1384 update_run_params(args)
1385
1386 # If the HLT PSK was given on the command line OR from OKS (--online-environment), ignore what is
1387 # stored in COOL and read that key directly from the DB (ATR-25974).
1388 # This is needed because COOL may point to a different HLTPSK for the forced run number.
1389 # NB: must be evaluated before update_trigconf_keys, which fills args.hltpsk from COOL/OKS.
1390 force_psk = args.use_database and ((args.hltpsk is not None) or args.online_environment)
1391
1392 if args.use_database:
1393 # Read trigger config keys from COOL/OKS if not specified
1395
1396 # Configure from a CA module: athenaEF is re-executed from the JSON (or exits for --dump-config-exit).
1397 if is_ca:
1398 configure_from_ca(args, unparsed_args)
1399
1400
1404 config_source = "the trigger database" if is_database else "a JSON file"
1405
1406 if unparsed_args:
1407 log.warning("Ignoring flag(s) given on the command line, the configuration is read from %s: %s",
1408 config_source, ' '.join(unparsed_args))
1409
1410 # Overrides applied to the configuration at runtime.
1411 # Only options explicitly given on the command line are collected, anything else keeps its DB/jobOptions value.
1412 # NB: Do NOT set the corresponding flags here, that would put them in the SMK.
1413 overrides = RuntimeOverrides()
1414
1415 overrides.set('AvalancheSchedulerSvc.ThreadPoolSize', args.threads)
1416 overrides.set('EventDataSvc.NSlots', args.concurrent_events)
1417
1418 ef_files = args.file if args.file else []
1419 if ef_files:
1420 overrides.set('EFInterfaceSvc.Files', ef_files)
1421 overrides.set('EFInterfaceSvc.T0ProjectTag', args.T0_project_tag)
1422 overrides.set('EFInterfaceSvc.BeamType', args.beam_type)
1423 overrides.set('EFInterfaceSvc.BeamEnergy', args.beam_energy)
1424 overrides.set('EFInterfaceSvc.TriggerType', args.trigger_type)
1425 overrides.set('EFInterfaceSvc.Stream', args.stream)
1426 overrides.set('EFInterfaceSvc.Lumiblock', args.lumiblock)
1427 overrides.set('EFInterfaceSvc.DetMask', args.file_detector_mask)
1428 if args.run_number is not None: # from -R, IS, or the input file
1429 overrides.set('EFInterfaceSvc.RunNumber', args.run_number)
1430 if args.save_output is not None:
1431 overrides.set('EFInterfaceSvc.OutputFileName', args.save_output)
1432 if args.loop_files is not None:
1433 overrides.set('EFInterfaceSvc.LoopOverFiles', args.loop_files)
1434 if args.number_of_events is not None:
1435 overrides.set('EFInterfaceSvc.NumEvents', args.number_of_events)
1436 if args.skip_events is not None:
1437 overrides.set('EFInterfaceSvc.SkipEvents', args.skip_events)
1438 if args.efdf_interface_library is not None:
1439 overrides.set('EFInterfaceSvc.EFDFInterfaceLibraryName', args.efdf_interface_library)
1440
1441 if args.timeout is not None:
1442 overrides.set('HltEventLoopMgr.HardTimeout', float(args.timeout))
1443 overrides.set('HltEventLoopMgr.SoftTimeoutFraction', SOFT_TIMEOUT_FRACTION)
1444 if args.conditions_run is not None:
1445 # Run number used for the conditions IOV lookup
1446 overrides.set('HltEventLoopMgr.forceRunNumber', args.conditions_run)
1447
1448 if force_psk:
1449 overrides.set('HLTPrescaleCondAlg.Source', 'DB')
1450
1451 # Histogram service:
1452 # Offline the command line decides and overrides the SMK/JSON conf (offline behaviour never depends on the SMK).
1453 # Online (--online-environment) we leave the configuration exactly as it is.
1454 if not args.online_environment:
1455 if args.oh_monitoring:
1456 overrides.declare_type('THistSvc', 'WebdaqHistSvc')
1457 overrides.create_service('WebdaqInfoSvc')
1458 else:
1459 overrides.declare_type('THistSvc', 'THistSvc')
1460 overrides.drop_service('WebdaqInfoSvc')
1461
1462 # Postcommands are applied by ConfigRunner.run() after configure(). NB: do not run for --dump-config-exit.
1463 for cmd in args.postcommand:
1464 overrides.add_command(cmd)
1465
1466 # Execute precommands
1467 if args.precommand:
1468 log.info("Executing precommand(s)")
1469 for cmd in args.precommand:
1470 log.info(" %s", cmd)
1471 exec(cmd, globals(), {})
1472
1473 if is_database:
1474 # Load configuration from trigger database
1475 # Handle CREST vs standard DB access
1476 if args.use_crest:
1477 from TrigConfStorage.TriggerCrestUtil import TriggerCrestUtil
1478 crestconn = TriggerCrestUtil.getCrestConnection(args.db_server)
1479 db_alias = f"{args.crest_server}/{crestconn}"
1480 log.info("Loading configuration via CREST from %s with SMK %d", db_alias, args.smk)
1481 else:
1482 db_alias = args.db_server
1483 log.info("Loading configuration from database %s with SMK %d", db_alias, args.smk)
1484
1485 # Get run parameters for prepareForStart
1486 run_params = get_run_params(args).to_dict()
1487 acc = load_from_database(db_alias, args.smk, args.l1psk, args.hltpsk, run_params, overrides=overrides)
1488 log.info("Configuration loaded from database")
1489
1490 else: # is_json
1491 # Load configuration from JSON file
1492 log.info("Loading configuration from JSON file: %s", args.jobOptions)
1493 # Get run parameters for prepareForStart
1494 run_params = get_run_params(args).to_dict()
1495 acc = load_from_json(args.jobOptions, run_params, overrides=overrides)
1496 log.info("Configuration loaded from JSON")
1497
1498 # Dump configuration if requested
1499 if args.dump_config or args.dump_config_exit:
1500 fname = "HLTJobOptions"
1501
1502 if is_database:
1503 # For DB mode, fetch properties via Python API
1504 from TrigConfIO.HLTTriggerConfigAccess import HLTJobOptionsAccess
1505 log.info("Fetching configuration from database for dump...")
1506 jo_access = HLTJobOptionsAccess(dbalias=acc.db_server, smkey=acc.smk)
1507 props = jo_access.algorithms()
1508
1509 log.info("Dumping configuration to %s.json", fname)
1510 hlt_json = {'filetype': 'joboptions', 'properties': props}
1511 with open(f"{fname}.json", "w") as f:
1512 json.dump(hlt_json, f, indent=4, sort_keys=True, ensure_ascii=True)
1513
1514 elif is_json:
1515 # For JSON mode, properties were already loaded
1516 props = acc.properties
1517 if props:
1518 log.info("Dumping configuration to %s.json", fname)
1519 hlt_json = {'filetype': 'joboptions', 'properties': props}
1520 with open(f"{fname}.json", "w") as f:
1521 json.dump(hlt_json, f, indent=4, sort_keys=True, ensure_ascii=True)
1522 else:
1523 log.warning("No properties available to dump")
1524
1525 if args.dump_config_exit:
1526 log.info("Configuration dumped. Exiting...")
1527 sys.exit(0)
1528
1529 # Run the application directly (like athena.py does)
1530 log.info("Starting Athena execution...")
1531
1532 # Create worker directory structure that HLT services expect
1533 # (normally created by HLTMPPU/PSC). Worker ID 1 means single-worker, non-forked mode
1534 # and must match what we pass to hltUpdateAfterFork(worker_id=1) in ConfigRunner.run()
1535 worker_dir = os.path.join(os.getcwd(), "athenaHLT_workers", "athenaHLT-01")
1536 if not os.path.exists(worker_dir):
1537 log.info("Creating worker directory: %s", worker_dir)
1538 os.makedirs(worker_dir, exist_ok=True)
1539
1540 # Start the private TDAQ infrastructure for -M
1541 oh_infra = start_oh_infrastructure(args) if args.oh_monitoring else None
1542
1543 if args.interactive:
1544 log.info("Interactive mode - call acc.run() to execute")
1545 import code
1546 code.interact(local={'acc': acc})
1547 else:
1548 # Run the application
1549 from AthenaCommon import ExitCodes
1550 exitcode = 0
1551 try:
1552 # Pass maxEvents if explicitly set (including -1 for all events)
1553 sc = acc.run(args.number_of_events)
1554 if sc.isFailure():
1555 exitcode = ExitCodes.EXE_ALG_FAILURE
1556 except SystemExit as e:
1557 exitcode = ExitCodes.EXE_ALG_FAILURE if e.code == 1 else e.code
1558 except Exception:
1559 traceback.print_exc()
1560 exitcode = ExitCodes.UNKNOWN_EXCEPTION
1561 finally:
1562 stop_oh_infrastructure(oh_infra)
1563
1564 log.info('Leaving with code %d: "%s"', exitcode, ExitCodes.what(exitcode))
1565 sys.exit(exitcode)
1566
1567
1568if "__main__" in __name__:
1569 sys.exit(main())
void print(char *figname, TCanvas *c1)
Helper class to call ITrigEventLoopMgr methods from Python.
from_json(cls, json_file, run_params=None, properties=None, overrides=None)
Definition athenaEF.py:607
__init__(self, job_options_type, job_options_path, run_params=None, properties=None, db_server=None, smk=None, overrides=None)
Definition athenaEF.py:586
run(self, maxEvents=None)
Definition athenaEF.py:622
from_database(cls, db_server, smk, l1psk=None, hltpsk=None, run_params=None, overrides=None)
Definition athenaEF.py:612
__call__(self, parser, namespace, values, option_string=None)
Definition athenaEF.py:1129
from_is(cls, partition=None, webdaq_base=None, strict=False, solenoid_current_override=None, toroids_current_override=None)
Definition athenaEF.py:150
__init__(self, run_number=None, lb_number=None, detector_mask=None, sor_time=None, solenoid_current=None, toroids_current=None, beam_type=None, beam_energy=None, run_type=None, trigger_type=None, recording_enabled=None, T0_project_tag='', stream='', lumiblock=0)
Definition athenaEF.py:95
from_args(cls, args)
Definition athenaEF.py:132
bool DEFAULT_RECORDING_ENABLED
Definition athenaEF.py:79
str DEFAULT_DETECTOR_MASK
Definition athenaEF.py:71
set(self, key, value)
Definition athenaEF.py:541
declare_type(self, name, type_)
Definition athenaEF.py:529
create_service(self, name, type_=None)
Definition athenaEF.py:537
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:312
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:132
get_run_params(args=None, from_is=False, partition=None, webdaq_base=None, strict=False, solenoid_current_override=None, toroids_current_override=None)
Definition athenaEF.py:484
str arg_sor_time(s)
The following arg_* methods are used as custom types in argparse.
Definition athenaEF.py:824
check_args(parser, args)
Definition athenaEF.py:841
arg_detector_mask(s)
Definition athenaEF.py:832
load_from_json(json_file, run_params=None, overrides=None)
Definition athenaEF.py:794
start_oh_infrastructure(args)
Definition athenaEF.py:1047
load_from_database(db_server, smk, l1psk=None, hltpsk=None, run_params=None, overrides=None)
Definition athenaEF.py:811
find_is_schema_files()
Definition athenaEF.py:1030
get_trigconf_keys_from_oks(partition=None, webdaq_base=None, strict=False)
Definition athenaEF.py:320
update_trigconf_keys(args)
Definition athenaEF.py:957
configure_from_ca(args, unparsed_args)
Definition athenaEF.py:1142
update_run_params(args)
Definition athenaEF.py:863
stop_oh_infrastructure(proc)
Definition athenaEF.py:1114