Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 10:47:24

0001 #!/usr/bin/env python3
0002 """segfault-dig.py — the trace of a crash signature from its payload log.
0003 
0004 Stage 3 of swf-epicprod docs/SEGFAULT_DIAGNOSIS.md. For a signature of
0005 the segfault catalog, this reads one representative crashed job's
0006 payload log and records the crash trace on the signature: the job is
0007 the one with the median time to death (or the one named), its log
0008 tarball is resolved from the PanDA record (the job's own file row while
0009 ``filestable4`` holds it, else the task's log dataset contents row) and
0010 fetched through the payload-log doer (``cache-payload-log.py``, the
0011 Rucio proxy and xrootd, cached under ``$SWF_TMP_DIR/panda-logs``), and
0012 ``trace_extract`` reads the cached ``payload.stdout`` and
0013 ``payload.stderr`` for the backtrace. Verified 2026-09-11: eicrecon
0014 prints JANA2's backtrace into payload.stdout (jobs 994773 and 2723039);
0015 the storm's tarballs (BNL_OSG_EPIC_PROD_1) have no replica, which the
0016 signature records as ``log_unavailable`` with the reason.
0017 
0018 Two record-level signatures with the same crashing frame merge into one
0019 trace-level entry; the record-level entries stay as its members.
0020 
0021 Django-bootstrap standalone script, run by the ops agent (the Dig
0022 action, and the nightly automatic dig) and usable by hand under the
0023 account that holds the Rucio proxy::
0024 
0025     cd /data/wenauseic/github/swf-monitor/src
0026     ../../swf-testbed/.venv/bin/python ../scripts/segfault-dig.py --key exit139:task39623
0027     ... --key K --pandaid P          # a chosen job instead of the median one,
0028                                      # or one of the signature's reproduction jobs
0029     ... --auto 10                    # the signatures never dug, largest first
0030 
0031 The last stdout line is a JSON summary; progress and errors go to stderr.
0032 """
0033 import argparse
0034 import json
0035 import logging
0036 import os
0037 import subprocess
0038 import sys
0039 import time
0040 
0041 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0042 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0043 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0044 
0045 import django  # noqa: E402
0046 django.setup()
0047 
0048 from monitor_app.epicprod_inventory import trace_extract  # noqa: E402
0049 from monitor_app.models import CrashSignature  # noqa: E402
0050 from monitor_app.segfaults import (dig_candidates, record_trace, representative,  # noqa: E402
0051                                    reproduction_entry, resolve_log_did, trace_from_report)
0052 
0053 DOER = os.path.join(THIS_DIR, 'cache-payload-log.py')
0054 SWF_TMP_DIR = os.environ.get('SWF_TMP_DIR', '/data/swf-tmp')
0055 DOER_TIMEOUT = int(os.environ.get('SEGFAULT_DIG_DOER_TIMEOUT', '300'))
0056 MEMBERS = ('payload.stdout', 'payload.stderr')
0057 
0058 log = logging.getLogger('segfault-dig')
0059 
0060 
0061 def fetch_log(scope, lfn, jeditaskid, pandaid):
0062     """The cached log directory after the doer ran, or (None, reason)."""
0063     jobdir = os.path.join(SWF_TMP_DIR, 'panda-logs', str(jeditaskid), str(pandaid))
0064     if os.path.exists(os.path.join(jobdir, '.done')):
0065         return jobdir, ''
0066     cmd = [sys.executable, DOER, '--scope', scope, '--lfn', lfn,
0067            '--jeditaskid', str(jeditaskid), '--pandaid', str(pandaid)]
0068     try:
0069         p = subprocess.run(cmd, capture_output=True, text=True, timeout=DOER_TIMEOUT)
0070     except subprocess.TimeoutExpired:
0071         return None, f'log fetch timed out after {DOER_TIMEOUT}s'
0072     for line in (p.stderr or '').splitlines():
0073         log.info('  cache-payload-log: %s', line[:200])
0074     if p.returncode != 0 or not os.path.exists(os.path.join(jobdir, '.done')):
0075         errs = [l for l in (p.stderr or '').splitlines() if l.startswith('ERROR')]
0076         return None, (errs[-1] if errs else f'doer exited {p.returncode}')[:300]
0077     return jobdir, ''
0078 
0079 
0080 def dig(sig, pandaid=None):
0081     """One dig; returns the summary dict for the signature."""
0082     if pandaid is None:
0083         rep = representative(sig)
0084         if rep is None:
0085             return {'key': sig.key, 'trace_status': 'log_unavailable',
0086                     'reason': 'no crashed job on record'}
0087         pandaid, jeditaskid = rep
0088     else:
0089         from monitor_app.models import EpicProdJob
0090         job = EpicProdJob.objects.filter(pandaid=int(pandaid)).only('jeditaskid').first()
0091         jeditaskid = job.jeditaskid if job else None
0092         if not jeditaskid:
0093             # A reproduction's canary job is not in the crash inventory; the
0094             # signature's own reproduction record names it.
0095             jeditaskid = (reproduction_entry(sig, pandaid) or {}).get('jedi_task_id')
0096         if not jeditaskid:
0097             return {'key': sig.key, 'trace_status': 'log_unavailable',
0098                     'reason': f'job {pandaid} is neither in the crash inventory '
0099                               f'nor a reproduction of {sig.key}'}
0100     sig.status = 'digging' if sig.status == 'new' else sig.status
0101     sig.save(update_fields=['status', 'updated_at'])
0102     log.info('%s: representative job %s (task %s)', sig.key, pandaid, jeditaskid)
0103     # Payload 0.12 and later send the crashing stage's log tail in the
0104     # report's note (SEGFAULT_DIAGNOSIS.md, Traces going forward), filed
0105     # by the sweep beside the job or lifted into the PanDA metatable for
0106     # a job that finished at the server; no tarball is fetched then.
0107     found = trace_from_report(sig, pandaid)
0108     if found:
0109         return found
0110     scope, lfn = resolve_log_did(pandaid, jeditaskid)
0111     if not lfn:
0112         reason = 'no log file on the PanDA record'
0113         record_trace(sig, {'trace_status': 'log_unavailable'}, pandaid, reason)
0114         return {'key': sig.key, 'pandaid': pandaid, 'trace_status': 'log_unavailable',
0115                 'reason': reason}
0116     jobdir, reason = fetch_log(scope, lfn, jeditaskid, pandaid)
0117     if jobdir is None:
0118         record_trace(sig, {'trace_status': 'log_unavailable'}, pandaid, reason)
0119         return {'key': sig.key, 'pandaid': pandaid, 'trace_status': 'log_unavailable',
0120                 'reason': reason}
0121     texts = []
0122     for member in MEMBERS:
0123         path = os.path.join(jobdir, member)
0124         if os.path.exists(path):
0125             with open(path, errors='replace') as fh:
0126                 texts.append(fh.read())
0127     result = trace_extract(texts)
0128     merged = record_trace(sig, result, pandaid,
0129                           '' if result['trace_status'] == 'found'
0130                           else 'no backtrace in the cached payload output')
0131     return {'key': sig.key, 'pandaid': pandaid, 'trace_status': result['trace_status'],
0132             'program': result.get('program'), 'stage': result.get('stage'),
0133             'frame': result.get('frame'), 'library': result.get('library'),
0134             'events_processed': result.get('events_processed'), 'merged_into': merged}
0135 
0136 
0137 def main():
0138     ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
0139     ap.add_argument('--key', help='the signature to dig')
0140     ap.add_argument('--pandaid', type=int, help='the job to read instead of the median one')
0141     ap.add_argument('--auto', type=int, metavar='N',
0142                     help='dig the N largest signatures never dug')
0143     args = ap.parse_args()
0144     logging.basicConfig(level=logging.INFO, stream=sys.stderr,
0145                         format='%(asctime)s %(name)s %(levelname)s %(message)s')
0146     t0 = time.monotonic()
0147     if args.key:
0148         sig = CrashSignature.objects.filter(key=args.key).first()
0149         if sig is None:
0150             print(json.dumps({'error': f'no crash signature {args.key}'}))
0151             return 2
0152         try:
0153             summary = dig(sig, args.pandaid)
0154         except Exception as e:                                # noqa: BLE001
0155             log.exception('dig failed')
0156             record_trace(sig, {'trace_status': 'log_unavailable'}, args.pandaid or 0,
0157                          f'dig failed: {str(e)[:200]}')
0158             print(json.dumps({'key': args.key, 'error': str(e)[:300]}))
0159             return 1
0160         summary['seconds'] = round(time.monotonic() - t0, 1)
0161         print(json.dumps(summary))
0162         return 0
0163     if args.auto:
0164         digs, found, unavailable = [], 0, 0
0165         for sig in dig_candidates(args.auto):
0166             try:
0167                 s = dig(sig)
0168             except Exception as e:                            # noqa: BLE001
0169                 log.exception('dig %s failed', sig.key)
0170                 record_trace(sig, {'trace_status': 'log_unavailable'}, 0,
0171                              f'dig failed: {str(e)[:200]}')
0172                 s = {'key': sig.key, 'trace_status': 'log_unavailable', 'error': str(e)[:200]}
0173             digs.append(s)
0174             found += s.get('trace_status') == 'found'
0175             unavailable += s.get('trace_status') == 'log_unavailable'
0176         print(json.dumps({'digs': len(digs), 'found': found, 'log_unavailable': unavailable,
0177                           'absent': len(digs) - found - unavailable, 'results': digs,
0178                           'seconds': round(time.monotonic() - t0, 1)}))
0179         return 0
0180     ap.error('one of --key or --auto is required')
0181 
0182 
0183 if __name__ == '__main__':
0184     sys.exit(main())