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.
This commit is contained in:
Pratik Mankawde
2026-09-09 13:14:44 +01:00
parent d9427c2539
commit d2cefa05d9
3 changed files with 34 additions and 2 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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: