From d2cefa05d9a313c588adb3900253e2f4e3feebe1 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:14:44 +0100 Subject: [PATCH] fix(telemetry): make the load generators fail loudly instead of exiting 0 Three ways a run could produce no traffic and still report success: - tx_submitter logged a funding shortfall and returned an empty stats object; main() then printed the summary and exited 0, so the failure only surfaced later as "spans missing", which points nowhere. It now records setup_failed in the summary and exits 1 after the report is written. - --weights was checked for valid JSON but not for a positive sum. An all-zero mapping reached random.choices, which raises ValueError from inside the dispatch loop where only CancelledError is caught. Rejected at parse time now, in both generators. - a profile phase declaring neither rpc nor tx logged a warning and returned no error. Both error rates short-circuit to 0.0 when nothing was sent, so a mistyped key produced zero traffic and still passed the exit gate. That phase is now an error. --- .../telemetry/workload/rpc_load_generator.py | 6 ++++++ docker/telemetry/workload/tx_submitter.py | 19 +++++++++++++++++++ .../workload/workload_orchestrator.py | 11 +++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docker/telemetry/workload/rpc_load_generator.py b/docker/telemetry/workload/rpc_load_generator.py index 36834de9ef..cdd9156e0b 100644 --- a/docker/telemetry/workload/rpc_load_generator.py +++ b/docker/telemetry/workload/rpc_load_generator.py @@ -770,6 +770,12 @@ def main() -> None: try: custom = json.loads(args.weights) weights = {k: int(v) for k, v in custom.items()} + if not weights or sum(weights.values()) <= 0: + logger.error( + "Invalid --weights: the values must sum to more than 0, got %s", + weights, + ) + sys.exit(1) logger.info("Using custom weights: %s", weights) except (json.JSONDecodeError, ValueError) as exc: logger.error("Invalid --weights JSON: %s", exc) diff --git a/docker/telemetry/workload/tx_submitter.py b/docker/telemetry/workload/tx_submitter.py index 807c44a702..b05b567bbf 100644 --- a/docker/telemetry/workload/tx_submitter.py +++ b/docker/telemetry/workload/tx_submitter.py @@ -192,6 +192,8 @@ class TxStats: total_errors: Transactions that returned an error engine_result. by_type: Per-transaction-type count of submissions. errors_by_type: Per-transaction-type count of errors. + setup_failed: True if account setup never produced enough funded + accounts, so the timed loop never ran. """ total_submitted: int = 0 @@ -199,6 +201,7 @@ class TxStats: total_errors: int = 0 by_type: dict[str, int] = field(default_factory=dict) errors_by_type: dict[str, int] = field(default_factory=dict) + setup_failed: bool = False def record(self, tx_type: str, success: bool) -> None: """Record the result of a transaction submission.""" @@ -223,6 +226,7 @@ class TxStats: ), "by_type": self.by_type, "errors_by_type": self.errors_by_type, + "setup_failed": self.setup_failed, } @@ -978,6 +982,10 @@ async def run_submitter( len(accounts), len(created), ) + # The caller turns this into a non-zero exit. Without it a funding + # failure looks like a clean run of zero transactions, and the run + # only fails later as "spans missing", which points nowhere. + stats.setup_failed = True return stats logger.info( @@ -1078,6 +1086,12 @@ def main() -> None: try: custom = json.loads(args.weights) weights = {k: int(v) for k, v in custom.items()} + if not weights or sum(weights.values()) <= 0: + logger.error( + "Invalid --weights: the values must sum to more than 0, got %s", + weights, + ) + sys.exit(1) logger.info("Using custom weights: %s", weights) except (json.JSONDecodeError, ValueError) as exc: logger.error("Invalid --weights JSON: %s", exc) @@ -1101,6 +1115,11 @@ def main() -> None: json.dump(summary, f, indent=2) logger.info("Summary written to %s", args.output) + # After the report is written, so the failure is still diagnosable. + if stats.setup_failed: + logger.error("Account setup failed; no transactions were submitted.") + sys.exit(1) + if __name__ == "__main__": main() diff --git a/docker/telemetry/workload/workload_orchestrator.py b/docker/telemetry/workload/workload_orchestrator.py index 35ca4ded55..3df9d80ea4 100755 --- a/docker/telemetry/workload/workload_orchestrator.py +++ b/docker/telemetry/workload/workload_orchestrator.py @@ -436,9 +436,16 @@ async def run_phase( tasks = _launch_phase_tasks(phase, endpoints, report_dir, prefix) if not tasks: - logger.warning( - "Phase %d: %s — no workload configured, skipping", phase_idx + 1, name + # An error, not a warning. The exit gate is built from phase errors and + # from error RATES, and both rates short-circuit to 0.0 when nothing was + # sent -- so a profile with a mistyped key ("rpcs", "RPC") would produce + # no traffic at all and still exit 0. + message = ( + f"phase {phase_idx + 1} '{name}' configures no workload: " + "it declares neither 'rpc' nor 'tx'" ) + logger.error("%s", message) + result.errors.append(message) return result for label, report_path, task in tasks: