File indexing completed on 2026-11-09 10:47:25
0001
0002 """segfault-repro-package.py — a crashed job's reproduction for a software expert.
0003
0004 Stage 4 of swf-epicprod docs/SEGFAULT_DIAGNOSIS.md. For one crashed job
0005 of the segfault catalog this writes a directory, and a tarball of it,
0006 holding everything an expert needs to run the crash again outside PanDA
0007 and nothing they have to assemble: a README stating the crash, the
0008 signature and the reproduction outcomes; ``run-repro.sh``, which runs
0009 the campaign image through apptainer (or from inside eic-shell) with
0010 the payload from this release, the job's own manifest row, its
0011 environment and its seed, streaming the input from the JLab door by
0012 path as the job did; the trace as found; and the job records of the
0013 original and the reproductions. The package carries no credential: the
0014 input path is public read, and the run registers nothing
0015 (``CANARY_OUTPUT_DATASET`` unset, ``USERUCIO`` and the copies off).
0016
0017 Written under ``$SWF_TMP_DIR/segfault-packages/<key>/<pandaid>/``, the
0018 tarball beside it, and the path recorded on the signature (``package``).
0019
0020 Django-bootstrap standalone script, run by hand or by the ops agent::
0021
0022 cd /data/wenauseic/github/swf-monitor/src
0023 ../../swf-testbed/.venv/bin/python ../scripts/segfault-repro-package.py 2723039
0024
0025 The last stdout line is a JSON summary; progress and errors go to stderr.
0026 """
0027 import argparse
0028 import json
0029 import logging
0030 import os
0031 import shutil
0032 import sys
0033 import tarfile
0034 import time
0035 from datetime import datetime, timezone as dt_timezone
0036
0037 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0038 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0039 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0040
0041 import django
0042 django.setup()
0043
0044 from monitor_app.models import CrashSignature, EpicProdJob
0045 from monitor_app.segfaults import (MEMORY_MECHANISMS, SIGNAL_NAMES,
0046 repro_environment, signature_for_job)
0047
0048 SWF_TMP_DIR = os.environ.get('SWF_TMP_DIR', '/data/swf-tmp')
0049 PACKAGE_ROOT = os.path.join(SWF_TMP_DIR, 'segfault-packages')
0050 log = logging.getLogger('segfault-repro-package')
0051
0052
0053 def _payload_dir():
0054 """The payload as this release ships it (the submit doer's rule)."""
0055 import swf_epicprod
0056 return os.path.join(os.path.dirname(swf_epicprod.__file__), 'payload')
0057
0058
0059 def _container(prod_task):
0060 cfg = prod_task.prod_config
0061 return getattr(cfg, 'container_image', '') if cfg else ''
0062
0063
0064 def build(pandaid):
0065 job = EpicProdJob.objects.filter(
0066 pandaid=pandaid, phase__in=('payload_crash', 'payload_memory')).select_related(
0067 'prod_task', 'prod_task__prod_config', 'prod_task__dataset').first()
0068 if job is None:
0069 raise ValueError(f'job {pandaid} is not in the crash inventory '
0070 f'nor a memory signature representative')
0071 crash = (job.data or {}).get('crash') or {}
0072
0073 if job.phase == 'payload_memory':
0074 mechanism = MEMORY_MECHANISMS.get(crash.get('mechanism') or '', {})
0075 what = (f"the job was killed for memory ({mechanism.get('label', 'removed')}) at "
0076 f"{crash.get('maxrss_mb')} MB peak RSS against a {crash.get('request_mb')} MB request")
0077 else:
0078 what = (f"the payload died on {SIGNAL_NAMES.get(crash.get('signal'), crash.get('signal'))} "
0079 f"(exit {crash.get('exit_code')})")
0080 row = crash.get('row')
0081 if not row:
0082 raise ValueError(f"job {pandaid}'s manifest row is unresolved")
0083 if job.prod_task is None:
0084 raise ValueError(f'job {pandaid} has no PCS task')
0085 sig_summary = signature_for_job(pandaid)
0086 sig = CrashSignature.objects.filter(key=sig_summary['key']).first() if sig_summary else None
0087 key = sig.key if sig else f"exit{crash.get('exit_code')}:task{job.jeditaskid}"
0088 outdir = os.path.join(PACKAGE_ROOT, key.replace(':', '_'), str(pandaid))
0089 if os.path.isdir(outdir):
0090 shutil.rmtree(outdir)
0091 os.makedirs(outdir)
0092
0093 env = repro_environment(job.prod_task)
0094 container = _container(job.prod_task)
0095 row_text = f"{row['file']},{row['ext']},{row['nevents']},{int(row['ichunk']):04d}"
0096 with open(os.path.join(outdir, 'manifest.csv'), 'w') as fh:
0097 fh.write(row_text + '\n')
0098 with open(os.path.join(outdir, 'environment-manifest.sh'), 'w') as fh:
0099 for k, v in env.items():
0100 fh.write(f'export {k}={v}\n')
0101 shutil.copytree(_payload_dir(), os.path.join(outdir, 'payload'),
0102 ignore=shutil.ignore_patterns('__pycache__', '*.pyc'))
0103 shutil.copy(os.path.join(THIS_DIR, 'evgen_job_dispatcher.py'),
0104 os.path.join(outdir, 'evgen_job_dispatcher.py'))
0105 with open(os.path.join(outdir, 'job-record.json'), 'w') as fh:
0106 json.dump({'pandaid': pandaid, 'jeditaskid': job.jeditaskid,
0107 'seq_number': job.seq_number, 'prod_task': job.prod_task.name,
0108 'crash': crash}, fh, indent=1, default=str)
0109 trace = (sig.trace if sig else {}) or {}
0110 with open(os.path.join(outdir, 'trace.txt'), 'w') as fh:
0111 fh.write(f"trace_status: {trace.get('trace_status', 'unknown')}\n")
0112 fh.write(f"program: {trace.get('program', '')} stage: {trace.get('stage', '')}\n")
0113 fh.write(f"frame: {trace.get('frame', '')} library: {trace.get('library', '')}\n")
0114 fh.write(f"source job: {trace.get('source_pandaid', '')}\n\n")
0115 for line in trace.get('frames') or []:
0116 fh.write(line + '\n')
0117 if trace.get('context'):
0118 fh.write('\n--- the lines before the crash ---\n')
0119 for line in trace['context']:
0120 fh.write(line + '\n')
0121 repro = (sig.reproduction if sig else []) or []
0122 with open(os.path.join(outdir, 'reproductions.json'), 'w') as fh:
0123 json.dump(repro, fh, indent=1, default=str)
0124
0125 seed = int(row['ichunk']) + 1
0126 with open(os.path.join(outdir, 'run-repro.sh'), 'w') as fh:
0127 fh.write(f'''#!/bin/bash
0128 # Reproduce PanDA job {pandaid} (task {job.jeditaskid}, {job.prod_task.name}):
0129 # {what}
0130 # after {crash.get('minutes')} min at {crash.get('computingsite')}.
0131 #
0132 # Runs the campaign image with the payload this package carries, on the
0133 # job's own manifest row, with its seed ({seed} = chunk {int(row['ichunk'])} + 1),
0134 # streaming the input from the JLab door as the job did. Outputs stay in
0135 # the working directory; nothing is registered or uploaded.
0136 #
0137 # Usage: ./run-repro.sh (apptainer on the host)
0138 # inside eic-shell or the image: ./run-repro.sh --inside
0139 set -euo pipefail
0140 HERE="$(cd "$(dirname "$0")" && pwd)"
0141 IMAGE="{container}"
0142 if [ "${{1:-}}" != "--inside" ]; then
0143 if ! command -v apptainer >/dev/null 2>&1; then
0144 echo "apptainer not found; run inside the image with --inside" >&2; exit 2
0145 fi
0146 exec apptainer exec --bind "$HERE:$HERE" "$IMAGE" bash "$HERE/run-repro.sh" --inside
0147 fi
0148 cd "$HERE"
0149 rm -rf work && mkdir work && cd work
0150 cp ../manifest.csv manifest.csv
0151 cp ../environment-manifest.sh environment-manifest.sh
0152 cp -r ../payload payload
0153 cp ../evgen_job_dispatcher.py .
0154 export PAYLOAD_STAGES_LOG="$PWD/payload-stages.log"
0155 echo "row: $(cat manifest.csv)"
0156 python3 evgen_job_dispatcher.py 1 manifest || rc=$?
0157 echo "payload exit: ${{rc:-0}}"
0158 echo "stage log:"; cat payload-stages.log 2>/dev/null || true
0159 exit ${{rc:-0}}
0160 ''')
0161 os.chmod(os.path.join(outdir, 'run-repro.sh'), 0o755)
0162
0163 with open(os.path.join(outdir, 'README.md'), 'w') as fh:
0164 fh.write(f'''# Reproduction package: job {pandaid}
0165
0166 Signature {key}{' (' + sig_summary['class_label'] + ')' if sig_summary else ''}:
0167 {what}
0168 after {crash.get('minutes')} minutes at {crash.get('computingsite')} on
0169 {crash.get('modificationhost')}, peak resident memory {crash.get('maxrss_mb')} MB.
0170 {'Stage ' + crash['stage'] + '. ' if crash.get('stage') else ''}The signature holds
0171 {sig.crashes if sig else '?'} crashed jobs across {len(sig.tasks) if sig else '?'} task(s).
0172
0173 ## The run
0174
0175 - PCS task: {job.prod_task.name}
0176 - Container image: {container}
0177 - Manifest row: {row_text}
0178 - Seed: {seed} (chunk {int(row['ichunk'])} + 1; the payload enables the per-event seed)
0179 - Payload: the epicprod payload in `payload/` (version file inside)
0180 - Command: `./run-repro.sh` with apptainer on the host, or `./run-repro.sh --inside`
0181 from a shell inside the image or eic-shell. The input streams from the JLab
0182 door by path (public read); outputs stay in `work/`; nothing is registered.
0183
0184 ## The trace
0185
0186 {('Crashing frame: ' + trace.get('frame', '') + (' in ' + trace['library'] if trace.get('library') else '') + ', read from job ' + str(trace.get('source_pandaid', ''))) if trace.get('trace_status') == 'found' else 'No trace on the record (' + str(trace.get('trace_status', 'unknown')) + '); the reproduction supplies it.'}
0187
0188 See `trace.txt`.
0189
0190 ## Reproductions so far
0191
0192 {chr(10).join('- ' + str(r.get('queue')) + ': ' + str(r.get('outcome')) + (' (exit ' + str(r.get('exit_code')) + ')' if r.get('exit_code') is not None else '') for r in repro) or '- none yet'}
0193
0194 ## Files
0195
0196 - `run-repro.sh`, `manifest.csv`, `environment-manifest.sh`, `payload/`, `evgen_job_dispatcher.py`: the run
0197 - `trace.txt`: the crash trace as found
0198 - `job-record.json`: the crashed job's record
0199 - `reproductions.json`: the reproduction runs and their outcomes
0200 ''')
0201 tarball = outdir + '.tar.gz'
0202 with tarfile.open(tarball, 'w:gz') as tf:
0203 tf.add(outdir, arcname=f'segfault-repro-{pandaid}')
0204 if sig:
0205 sig.package = {'path': outdir, 'tarball': tarball, 'pandaid': pandaid,
0206 'built_at': datetime.now(dt_timezone.utc).isoformat(timespec='seconds')}
0207 sig.save(update_fields=['package', 'updated_at'])
0208 return {'pandaid': pandaid, 'key': key, 'path': outdir, 'tarball': tarball,
0209 'bytes': os.path.getsize(tarball)}
0210
0211
0212 def main():
0213 ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
0214 ap.add_argument('pandaid', type=int, help='the crashed job')
0215 args = ap.parse_args()
0216 logging.basicConfig(level=logging.INFO, stream=sys.stderr,
0217 format='%(asctime)s %(name)s %(levelname)s %(message)s')
0218 t0 = time.monotonic()
0219 try:
0220 summary = build(args.pandaid)
0221 except Exception as e:
0222 log.exception('package failed')
0223 print(json.dumps({'error': str(e)[:300]}))
0224 return 1
0225 summary['seconds'] = round(time.monotonic() - t0, 1)
0226 print(json.dumps(summary))
0227 return 0
0228
0229
0230 if __name__ == '__main__':
0231 sys.exit(main())