Back to home page

EIC code displayed by LXR

 
 

    


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

0001 #!/usr/bin/env python3
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  # noqa: E402
0042 django.setup()
0043 
0044 from monitor_app.models import CrashSignature, EpicProdJob  # noqa: E402
0045 from monitor_app.segfaults import (MEMORY_MECHANISMS, SIGNAL_NAMES,  # noqa: E402
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     # What happened to the job, in the README's and the script's words.
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:                                    # noqa: BLE001
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())