File indexing completed on 2026-11-09 10:47:24
0001
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
0046 django.setup()
0047
0048 from monitor_app.epicprod_inventory import trace_extract
0049 from monitor_app.models import CrashSignature
0050 from monitor_app.segfaults import (dig_candidates, record_trace, representative,
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
0094
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
0104
0105
0106
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:
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:
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())