From 8ddc53cdc6bfdf87fa3ec6be2c90272e9dd4bc9f Mon Sep 17 00:00:00 2001 From: lemke_h Date: Tue, 22 Sep 2026 13:01:29 +0200 Subject: [PATCH] simulate_scan_monitoring: add a --repeat-for-hours loop mode Repeats back-to-back start/stop recording cycles (one per simulated scan) for a total wall-clock budget instead of one long continuous recording, closer to what a real sequence of scans looks like. A single iteration's failure is logged and skipped rather than ending the whole soak test. Co-Authored-By: Claude Sonnet 5 --- eco/status_server/simulate_scan_monitoring.py | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/eco/status_server/simulate_scan_monitoring.py b/eco/status_server/simulate_scan_monitoring.py index 6f2d2b7..5c5c977 100644 --- a/eco/status_server/simulate_scan_monitoring.py +++ b/eco/status_server/simulate_scan_monitoring.py @@ -23,6 +23,10 @@ Usage: python -m eco.status_server.simulate_scan_monitoring \\ --url http://saresb-cons-04:8091 --duration 60 \\ --names bernina.mono.energy bernina.izero + + # repeat 10-minute "scans" back to back for 24 hours (soak test): + python -m eco.status_server.simulate_scan_monitoring \\ + --url http://saresb-cons-04:8091 --duration 600 --repeat-for-hours 24 """ from __future__ import annotations @@ -109,6 +113,78 @@ def simulate( return path +def simulate_loop( + url, + pgroup="p19641", + start_run_number=None, + scan_duration=600.0, + total_hours=24.0, + gap=5.0, + mode="throttle", + min_interval=0.1, + names=None, + poll_interval=30.0, + out_dir=None, + filename="namespace_monitor.ixp.h5", +): + """Repeat :func:`simulate` back to back - one recording per "scan" - + until `total_hours` have elapsed, the same start/stop cycle a real + sequence of scans produces rather than one long continuous recording. + + Each iteration gets its own recording_id (run_number incremented by one + each time, starting from `start_run_number`) and its own subdirectory + under `out_dir`, so nothing overwrites a previous iteration's file and a + later failure is easy to line up against which run it happened on. + + A single iteration failing (start_recording/stop_recording raising, a + transient server hiccup, ...) is logged and skipped rather than ending + the whole soak test - the point of a 24 h run is to find out whether + that happens at all, not to die the first time it does. + """ + run_number = int( + start_run_number if start_run_number is not None else time.time() % 10_000 + ) + base_out_dir = Path(out_dir) if out_dir else Path( + tempfile.mkdtemp(prefix="namespace_monitor_sim_loop_") + ) + base_out_dir.mkdir(parents=True, exist_ok=True) + print(f"looping {scan_duration:.0f} s scans for {total_hours:.2f} h into {base_out_dir}") + + t_start = time.time() + deadline = t_start + total_hours * 3600.0 + n_ok = n_failed = 0 + iteration = 0 + while time.time() < deadline: + iteration += 1 + print(f"\n=== scan {iteration} (run {run_number:04d}, " + f"t+{(time.time() - t_start) / 3600.0:.2f} h) ===") + try: + simulate( + url, pgroup=pgroup, run_number=run_number, + duration=min(scan_duration, max(0.0, deadline - time.time())), + mode=mode, min_interval=min_interval, names=names, + poll_interval=poll_interval, + out_dir=base_out_dir / f"run{run_number:04d}", + filename=filename, + ) + n_ok += 1 + except Exception as exc: + n_failed += 1 + print(f" !!! scan {iteration} (run {run_number:04d}) failed: " + f"{type(exc).__name__}: {exc}") + run_number += 1 + remaining = deadline - time.time() + if remaining <= 0: + break + if gap: + time.sleep(min(gap, remaining)) + print( + f"\ndone: {n_ok} scan(s) ok, {n_failed} failed, over " + f"{(time.time() - t_start) / 3600.0:.2f} h" + ) + return n_ok, n_failed + + def main(argv=None): parser = argparse.ArgumentParser( description="Simulate one scan's status-server monitoring traffic " @@ -158,13 +234,39 @@ def main(argv=None): ) parser.add_argument( "--out-dir", default=None, - help="where to write the resulting file (default: a fresh directory " - "under the system tmp dir)", + help="where to write the resulting file(s) (default: a fresh " + "directory under the system tmp dir)", ) parser.add_argument("--filename", default="namespace_monitor.ixp.h5") + parser.add_argument( + "--repeat-for-hours", type=float, default=None, + help="instead of one recording, repeat scans of --duration length " + "back to back until this many hours have elapsed in total (a " + "soak test of many start/stop cycles rather than one long " + "recording) - each scan gets its own subdirectory under " + "--out-dir", + ) + parser.add_argument( + "--gap", type=float, default=5.0, + help="idle seconds between consecutive scans in --repeat-for-hours " + "mode (default: %(default)s)", + ) args = parser.parse_args(argv) + if args.repeat_for_hours is not None and args.hours is not None: + parser.error("--hours is not meaningful together with --repeat-for-hours " + "(use --duration for one scan's length there)") duration = args.hours * 3600.0 if args.hours is not None else args.duration + if args.repeat_for_hours is not None: + simulate_loop( + args.url, pgroup=args.pgroup, start_run_number=args.run_number, + scan_duration=duration, total_hours=args.repeat_for_hours, + gap=args.gap, mode=args.mode, min_interval=args.min_interval, + names=args.names, poll_interval=args.poll_interval, + out_dir=args.out_dir, filename=args.filename, + ) + return + simulate( args.url, pgroup=args.pgroup, run_number=args.run_number, duration=duration, mode=args.mode, min_interval=args.min_interval,