From 13c7382bad99f5efeb2c90db727d87b62b7329ee Mon Sep 17 00:00:00 2001 From: Celestial Date: Fri, 13 Feb 2026 14:36:14 +0100 Subject: [PATCH 1/5] chore: add phase0 architecture guardrails and docs taxonomy --- .github/workflows/pr.yml | 3 + .github/workflows/release.yml | 3 + AGENTS.md | 138 +++ CHANGELOG.md | 4 +- CONTRIBUTING.md | 3 + Makefile.toml | 6 + README.md | 9 +- docs/README.md | 21 + docs/architecture/README.md | 25 + .../adr/ADR-0001-hexagonal-vertical-slices.md | 53 + docs/architecture/adr/README.md | 9 + .../ard/ARCHITECTURE_BASELINE_METRICS.md | 30 + .../architecture/ard/ARCHITECTURE_OVERVIEW.md | 957 ++++++++++++++++++ .../ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md | 342 +++++++ docs/architecture/ard/README.md | 11 + docs/architecture/patterns/README.md | 7 + .../patterns/vertical-slices-hexagonal.md | 25 + docs/architecture/rfc/README.md | 15 + docs/architecture/srs/README.md | 13 + .../charts}/average_response_time.png | Bin .../charts}/cumulative_error_rate.png | Bin .../cumulative_successful_requests.png | Bin .../charts}/cumulative_total_requests.png | Bin .../charts}/error_rate_breakdown.png | Bin .../{ => assets/charts}/inflight_requests.png | Bin .../charts}/latency_percentiles_P50.png | Bin .../charts}/latency_percentiles_P50_all.png | Bin .../charts}/latency_percentiles_P90.png | Bin .../charts}/latency_percentiles_P90_all.png | Bin .../charts}/latency_percentiles_P99.png | Bin .../charts}/latency_percentiles_P99_all.png | Bin .../charts}/requests_per_second.png | Bin .../charts}/status_code_distribution.png | Bin .../charts}/timeouts_per_second.png | Bin docs/{ => assets/images}/screenshot.png | Bin docs/{ => guides}/ADVANCED.md | 0 docs/{ => guides}/USAGE.md | 48 +- scripts/check_architecture.sh | 113 +++ src/protocol/runtime/grpc.rs | 3 +- src/protocol/runtime/resolve.rs | 30 +- src/protocol/runtime/spawner.rs | 16 +- src/protocol/runtime/tests/mod.rs | 18 +- .../runtime/tests/transport_http_grpc.rs | 2 +- 43 files changed, 1853 insertions(+), 51 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/README.md create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/adr/ADR-0001-hexagonal-vertical-slices.md create mode 100644 docs/architecture/adr/README.md create mode 100644 docs/architecture/ard/ARCHITECTURE_BASELINE_METRICS.md create mode 100644 docs/architecture/ard/ARCHITECTURE_OVERVIEW.md create mode 100644 docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md create mode 100644 docs/architecture/ard/README.md create mode 100644 docs/architecture/patterns/README.md create mode 100644 docs/architecture/patterns/vertical-slices-hexagonal.md create mode 100644 docs/architecture/rfc/README.md create mode 100644 docs/architecture/srs/README.md rename docs/{ => assets/charts}/average_response_time.png (100%) rename docs/{ => assets/charts}/cumulative_error_rate.png (100%) rename docs/{ => assets/charts}/cumulative_successful_requests.png (100%) rename docs/{ => assets/charts}/cumulative_total_requests.png (100%) rename docs/{ => assets/charts}/error_rate_breakdown.png (100%) rename docs/{ => assets/charts}/inflight_requests.png (100%) rename docs/{ => assets/charts}/latency_percentiles_P50.png (100%) rename docs/{ => assets/charts}/latency_percentiles_P50_all.png (100%) rename docs/{ => assets/charts}/latency_percentiles_P90.png (100%) rename docs/{ => assets/charts}/latency_percentiles_P90_all.png (100%) rename docs/{ => assets/charts}/latency_percentiles_P99.png (100%) rename docs/{ => assets/charts}/latency_percentiles_P99_all.png (100%) rename docs/{ => assets/charts}/requests_per_second.png (100%) rename docs/{ => assets/charts}/status_code_distribution.png (100%) rename docs/{ => assets/charts}/timeouts_per_second.png (100%) rename docs/{ => assets/images}/screenshot.png (100%) rename docs/{ => guides}/ADVANCED.md (100%) rename docs/{ => guides}/USAGE.md (88%) create mode 100755 scripts/check_architecture.sh diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 63a6c73..8f4905e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -50,5 +50,8 @@ jobs: - name: Format Check run: cargo make format-check + - name: Architecture Guardrails + run: cargo make architecture-check + - name: Verify Publish Readiness run: cargo publish --dry-run --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2688c95..da36ca4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,6 +110,9 @@ jobs: - name: Format Check run: cargo make format-check + - name: Architecture Guardrails + run: cargo make architecture-check + - name: Verify Publish Readiness run: cargo publish --dry-run --locked diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a21f595 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,138 @@ +# AGENTS.md + +## Purpose +This file defines how contributors and coding agents should work in this repository. +It combines contribution rules, engineering best practices, and the target architecture direction. + +## Scope and Priority +When making changes, follow this order: +1. Correctness and user safety. +2. Architectural direction (vertical slices + hexagonal boundaries). +3. Existing contribution and lint rules. +4. Minimal, reviewable diffs. + +## Project Context +- `strest` is a high-performance load testing CLI. +- Use is only valid for infrastructure you own or are explicitly authorized to test. +- Main docs: + - `README.md` + - `CONTRIBUTING.md` + - `docs/guides/USAGE.md` + - `docs/guides/ADVANCED.md` + - `docs/architecture/README.md` + - `docs/architecture/ard/ARCHITECTURE_OVERVIEW.md` + - `docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md` + +## Required Contribution Workflow +1. Create a scoped branch. +2. Keep changes tight and atomic. +3. Update docs and `CHANGELOG.md` for user-visible behavior changes. +4. Run required checks. +5. Submit a concise PR with rationale and tradeoffs. + +### Required Checks +```bash +cargo make format +cargo make clippy +cargo make test +``` + +If WASM is touched: +```bash +cargo make test-wasm +``` + +If dependencies change: +```bash +cargo make audit +cargo make deny +``` + +## Engineering Best Practices + +### Error Handling and Safety +- Do not use `unwrap`, `expect`, `todo!`, or panic-driven control flow in production code. +- Return typed errors (`AppError` and specific error variants) with actionable context. +- Prefer explicit validation at boundaries. + +### Code Quality +- No `#[allow(...)]` unless explicitly approved. +- Keep functions focused; split orchestration from transformation logic. +- Add tests for behavior changes, not just happy paths. +- Preserve determinism where possible (especially metrics, replay, and distributed orchestration). + +### Performance and Concurrency +- Avoid unnecessary allocations and cloning on hot paths. +- Be explicit about async cancellation and shutdown behavior. +- Avoid hidden blocking in async flows. + +### Documentation +- Keep CLI/config docs aligned with behavior. +- If a flag or output contract changes, update docs in the same PR. + +## Architecture Goal +Target architecture is **vertical slices with hexagonal ports/adapters**. + +### Target Layers +- `domain`: business models, policies, invariants. +- `application`: use cases and orchestration against ports. +- `adapters` (infrastructure): CLI, config parsing, HTTP/protocol transport, distributed wire IO, UI/charts/sinks, WASM/plugin runtime. + +### Dependency Rules (Desired) +- `domain` must not depend on infrastructure frameworks (`clap`, `reqwest`, `tokio`, `ratatui`, `crossterm`). +- `application` depends on `domain` + port traits, not concrete infra implementations. +- `adapters` may depend on infra crates and implement application ports. +- Entry points compose concrete adapters into use cases. + +## Transitional Rules for Current Codebase +The repository is migrating. During migration: + +1. Do not introduce new deep coupling to `TesterArgs`. +- New core/business logic should accept typed command/config structs, not raw CLI structs. + +2. Treat `src/args` as an adapter boundary. +- CLI parsing and clap concerns stay there. +- Avoid placing new domain policy there. + +3. Prefer anti-corruption mapping at boundaries. +- Map CLI/config inputs into domain/application commands early. + +4. Keep vertical behavior grouped. +- New features should fit a slice (`local_run`, `distributed_run`, `replay_compare`) instead of scattering across horizontal modules. + +5. Use branch-by-abstraction. +- Add ports + adapters first, then migrate call sites incrementally. + +## Suggested Slice Ownership +- `local_run`: run execution, protocol traffic, local metrics lifecycle. +- `distributed_run`: controller/agent coordination, aggregation, distributed execution. +- `replay_compare`: replay windows, snapshots, comparison flows. +- `shared_kernel`: minimal shared value objects only. + +## PR Acceptance Checklist +A PR is ready when: +- Behavior is correct and tests pass. +- Diff is scoped and understandable. +- Lint/format/test checks pass. +- Docs/changelog are updated when needed. +- New code does not increase infra-domain coupling. +- Any architectural compromise is called out with follow-up plan. + +## Anti-Patterns to Avoid +- Passing `TesterArgs` through new domain/application APIs. +- Mixing UI/sink/charts wiring directly inside core policy logic. +- Embedding config precedence as ad-hoc mutation order across modules. +- Adding new cross-cutting flags without explicit boundary mapping. + +## Preferred Change Pattern +For new behavior, aim for this sequence: +1. Define domain model/policy. +2. Define application use-case input/output and ports. +3. Implement or adapt infrastructure adapter. +4. Wire in entry layer. +5. Add tests at unit and integration level. + +## Notes for Agents +- Optimize for small, reversible, evidence-backed changes. +- If architecture and delivery conflict, preserve behavior first, then add a migration seam. +- Mention architectural impact explicitly in PR summaries. diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d15b7..7542ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ The format is based on Keep a Changelog, and this project follows SemVer. - Added opinionated preset subcommands: `strest quick`, `strest soak`, `strest spike`, and `strest distributed --agents=`. - Preset commands now map to sensible defaults while preserving all existing advanced flag workflows. - Grouped high-frequency CLI flags under `Common Options` in `--help` for faster discoverability; kept advanced flags available unchanged. -- Documented “99% paths” explicitly in `docs/USAGE.md` to reduce onboarding friction while preserving the full advanced surface. +- Documented “99% paths” explicitly in `docs/guides/USAGE.md` to reduce onboarding friction while preserving the full advanced surface. - Fixed replay/export flow metrics ingestion so `response_bytes` and `in_flight_ops` are preserved when reading metrics logs. - Updated metrics log writing/parsing to include `response_bytes` and `in_flight_ops` columns, with backward compatibility for older 5-column logs. - Extended JSON/JSONL summary exports with flow aggregates: `total_response_bytes`, `avg_response_bytes_per_sec`, `max_in_flight_ops`, and `last_in_flight_ops`. @@ -45,7 +45,7 @@ Released: 2026-02-11 Released: 2026-02-11 -- Split README into `docs/USAGE.md` and `docs/ADVANCED.md` and shortened the top-level README. +- Split README into `docs/guides/USAGE.md` and `docs/guides/ADVANCED.md` and shortened the top-level README. - Refactored internal error handling and reduced redundant allocations in request sources. - Split large modules into smaller units across replay, distributed controller, wasm scripting, and app error paths for maintainability. - Simplified startup wiring by moving entry/main orchestration into clearer module boundaries. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0aecd04..bacbc9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,7 @@ Examples: cargo make format cargo make clippy cargo make test +cargo make architecture-check ``` If you touched WASM: @@ -61,6 +62,7 @@ git checkout -b feat/my-change cargo make format cargo make clippy cargo make test +# cargo make architecture-check # cargo make test-wasm # if WASM touched # cargo make audit # if deps changed # cargo make deny # if deps changed @@ -84,6 +86,7 @@ Use this structure: - cargo make format - cargo make clippy - cargo make test +- cargo make architecture-check - cargo make test-wasm (if applicable) - cargo make audit (if deps changed) - cargo make deny (if deps changed) diff --git a/Makefile.toml b/Makefile.toml index c3b09b7..ffb082c 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -42,6 +42,12 @@ workspace = false command = "cargo" args = ["check", "--workspace", "--all", "${@}"] +[tasks.architecture-check] +description = "Enforce architecture boundaries and print coupling metrics" +workspace = false +command = "bash" +args = ["scripts/check_architecture.sh"] + [tasks.fuzz] description = "Run fuzzing tests on specified targets" workspace = false diff --git a/README.md b/README.md index 774659a..818f6f9 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ strest --config strest.toml -t 30 --no-tui --summary --no-charts ## Screenshots
- CLI Screenshot + CLI Screenshot
Charts focus on signal: @@ -52,12 +52,13 @@ Charts focus on signal: - Throughput + inflight reveal saturation and ramp behavior. - Error breakdown separates timeouts, transport errors, and non-expected status codes. -Full gallery: `docs/USAGE.md#charts`. +Full gallery: `docs/guides/USAGE.md#charts`. ## Docs -- `docs/USAGE.md` for CLI, flags, configs, and charts. -- `docs/ADVANCED.md` for replay, WASM, profiling, distributed mode, and sinks. +- `docs/README.md` for documentation index and structure. +- `docs/guides/USAGE.md` for CLI, flags, configs, and charts. +- `docs/guides/ADVANCED.md` for replay, WASM, profiling, distributed mode, and sinks. ## Contributions diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4a52dbe --- /dev/null +++ b/docs/README.md @@ -0,0 +1,21 @@ +# Documentation Index + +This folder is organized by concern: + +## Guides + +- `docs/guides/USAGE.md`: CLI usage, flags, configs, charts, and operational workflows. +- `docs/guides/ADVANCED.md`: replay, WASM, profiling, distributed mode, and sinks. + +## Architecture + +- `docs/architecture/README.md`: architecture document taxonomy and conventions. +- `docs/architecture/ard/ARCHITECTURE_OVERVIEW.md`: generated module/dependency overview. +- `docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md`: architecture risks and phased migration plan. +- `docs/architecture/ard/ARCHITECTURE_BASELINE_METRICS.md`: migration baseline coupling metrics. +- `docs/architecture/adr/ADR-0001-hexagonal-vertical-slices.md`: accepted architecture decision. + +## Assets + +- `docs/assets/images/`: UI and docs images. +- `docs/assets/charts/`: chart screenshot gallery assets. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..3a33eff --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,25 @@ +# Architecture Docs + +This directory is split by architecture document type. + +## Categories + +- `docs/architecture/ard/`: architecture reference and discovery docs. +- `docs/architecture/adr/`: accepted architecture decisions. +- `docs/architecture/srs/`: system requirements specifications. +- `docs/architecture/rfc/`: architecture proposals under review. +- `docs/architecture/patterns/`: reusable architectural patterns and guidance. + +## Current Canonical Docs + +- `docs/architecture/ard/ARCHITECTURE_OVERVIEW.md` +- `docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md` +- `docs/architecture/ard/ARCHITECTURE_BASELINE_METRICS.md` +- `docs/architecture/adr/ADR-0001-hexagonal-vertical-slices.md` + +## Naming Conventions + +- ADRs: `ADR--.md` +- RFCs: `RFC--.md` +- SRS docs: `-srs.md` +- Patterns: `.md` diff --git a/docs/architecture/adr/ADR-0001-hexagonal-vertical-slices.md b/docs/architecture/adr/ADR-0001-hexagonal-vertical-slices.md new file mode 100644 index 0000000..35d5f05 --- /dev/null +++ b/docs/architecture/adr/ADR-0001-hexagonal-vertical-slices.md @@ -0,0 +1,53 @@ +# ADR-0001: Vertical Slices with Hexagonal Boundaries + +- Status: Accepted +- Date: 2026-02-13 +- Deciders: strest maintainers + +## Context + +`strest` currently works as a modular monolith, but core behavior is coupled to adapter concerns, especially CLI (`TesterArgs`) and runtime IO wiring. The migration target is vertical slices with explicit ports/adapters boundaries, without a big-bang rewrite. + +The primary risks are documented in `docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md`. + +## Decision + +Adopt a phased migration architecture with these boundaries: + +1. Layer model +- `domain`: business models, invariants, and policies. +- `application`: use cases orchestrating domain through ports. +- `adapters`: infrastructure implementations (CLI, config, transport, distributed IO, output, WASM). + +2. Dependency rules +- `domain` must not depend on infrastructure frameworks (`clap`, `reqwest`, `tokio`, `ratatui`, `crossterm`). +- `application` must not depend directly on `clap`. +- `adapters` may depend on infra crates and implement ports for application use cases. + +3. Transitional rules +- Do not introduce new deep coupling to `TesterArgs` in core logic. +- Treat `src/args` as a CLI adapter boundary. +- Prefer anti-corruption mapping from CLI/config into typed commands at entry boundaries. +- Keep behavior grouped by vertical slices (`local_run`, `distributed_run`, `replay_compare`). + +4. Guardrail enforcement +- Add repository guardrail script: `scripts/check_architecture.sh`. +- Run guardrails in CI for pull requests and release validation. +- Track coupling baseline metrics (`crate::args` references, `TesterArgs` references). + +## Consequences + +### Positive +- Architectural drift is blocked early in CI. +- Migration progress is measurable by coupling metrics. +- New use-case code can be tested with lower adapter coupling. + +### Tradeoffs +- Temporary dual pathways (legacy + new boundaries) increase short-term complexity. +- Additional CI checks add maintenance overhead. + +## Follow-up + +- Phase 1 introduces typed commands and mapping from CLI args. +- Phase 2 moves config precedence to explicit override policies. +- Later phases extract local/distributed/replay slices behind ports. diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md new file mode 100644 index 0000000..f27b893 --- /dev/null +++ b/docs/architecture/adr/README.md @@ -0,0 +1,9 @@ +# ADR + +Architecture Decision Records. + +Use this folder for accepted and superseded architectural decisions. Each ADR should capture context, decision, consequences, and follow-up. + +Naming: + +- `ADR--.md` diff --git a/docs/architecture/ard/ARCHITECTURE_BASELINE_METRICS.md b/docs/architecture/ard/ARCHITECTURE_BASELINE_METRICS.md new file mode 100644 index 0000000..74a27ad --- /dev/null +++ b/docs/architecture/ard/ARCHITECTURE_BASELINE_METRICS.md @@ -0,0 +1,30 @@ +# Architecture Baseline Metrics + +_Snapshot date: 2026-02-13_ + +This baseline is used to track migration progress toward vertical slices with hexagonal boundaries. + +## Counting Method + +- Scope: `src/**/*.rs`. +- Excludes tests: `**/tests/**`, `**/tests.rs`, `**/test_*.rs`, `**/*_test.rs`. +- Source of truth: `scripts/check_architecture.sh`. + +## Baseline + +- `non_test_rust_files`: `209` +- `files_referencing_crate_args`: `71` +- `files_referencing_tester_args`: `62` + +## Top Cross-Module Edges (Top 10) + +- `distributed -> args` (`22`) +- `distributed -> error` (`18`) +- `app -> error` (`17`) +- `charts -> error` (`16`) +- `app -> metrics` (`16`) +- `config -> error` (`13`) +- `charts -> metrics` (`13`) +- `app -> args` (`12`) +- `config -> args` (`11`) +- `protocol -> args` (`10`) diff --git a/docs/architecture/ard/ARCHITECTURE_OVERVIEW.md b/docs/architecture/ard/ARCHITECTURE_OVERVIEW.md new file mode 100644 index 0000000..7b5b911 --- /dev/null +++ b/docs/architecture/ard/ARCHITECTURE_OVERVIEW.md @@ -0,0 +1,957 @@ +# Strest Architecture Overview + +_Generated from `src/**/*.rs` on 2026-02-13 13:05:45 UTC_ + +## Scope +- Module inventory includes all source modules under `src/` (including test modules inside `src`). +- Dependency graph edges are derived from `crate::...` references in non-test source files only. +- Feature-gated modules: `wasm_plugins`, `wasm_runtime`, `fuzzing`, and legacy chart implementations. + +## Runtime Flow +```mermaid +flowchart TD + main["main.rs"] --> entry["entry::run"] + entry --> planBuild["entry::plan::build_plan"] + entry --> planExec["entry::plan::execute_plan"] + + planExec --> local["app::run_local"] + planExec --> controller["distributed::run_controller"] + planExec --> agent["distributed::run_agent"] + planExec --> replay["app::run_replay"] + planExec --> compare["app::run_compare"] + planExec --> cleanup["app::run_cleanup"] + planExec --> service["service::handle_service_action"] + + local --> proto["protocol::setup_request_sender"] + local --> metrics["metrics::setup_metrics_collector"] + local --> ui["ui::render::setup_render_ui"] + local --> logs["app::logs::setup_log_sinks"] + + proto --> http["http::setup_request_sender"] + proto --> transports["protocol::runtime::{tcp/udp/ws/grpc/mqtt}"] + + controller --> ctrlRunner["distributed::controller::runner"] + ctrlRunner --> ctrlAuto["controller::auto::*"] + ctrlRunner --> ctrlManual["controller::manual::*"] + ctrlAuto --> wire["distributed::protocol::{read_message/send_message}"] + ctrlManual --> wire + + agent --> agentSession["distributed::agent::session"] + agentSession --> wire +``` + +## Top-Level Dependency Graph +```mermaid +flowchart LR + n1["app (30)"] + n2["args (13)"] + n3["charts (24)"] + n4["config (16)"] + n5["distributed (42)"] + n6["entry (5)"] + n7["error (11)"] + n8["fuzzing (1)"] + n9["http (15)"] + n10["lib (1)"] + n11["main (1)"] + n12["metrics (14)"] + n13["protocol (21)"] + n14["script (2)"] + n15["service (1)"] + n16["shutdown (1)"] + n17["sinks (4)"] + n18["system (5)"] + n19["ui (17)"] + n20["wasm_plugins (5)"] + n21["wasm_runtime (7)"] + + n1 -->|16| n2 + n1 -->|1| n3 + n1 -->|17| n7 + n1 -->|16| n12 + n1 -->|3| n16 + n1 -->|5| n18 + n1 -->|7| n19 + n1 -->|2| n20 + n2 -->|2| n7 + n2 -->|1| n12 + n2 -->|1| n17 + n3 -->|2| n2 + n3 -->|16| n7 + n3 -->|14| n12 + n4 -->|17| n2 + n4 -->|15| n7 + n4 -->|1| n12 + n4 -->|1| n17 + n5 -->|2| n1 + n5 -->|23| n2 + n5 -->|1| n3 + n5 -->|5| n4 + n5 -->|18| n7 + n5 -->|10| n12 + n5 -->|4| n16 + n5 -->|7| n17 + n5 -->|2| n18 + n5 -->|4| n19 + n6 -->|1| n1 + n6 -->|3| n2 + n6 -->|4| n4 + n6 -->|2| n5 + n6 -->|5| n7 + n6 -->|1| n13 + n6 -->|1| n14 + n6 -->|1| n15 + n6 -->|2| n18 + n8 -->|1| n2 + n8 -->|4| n4 + n8 -->|1| n7 + n8 -->|2| n9 + n8 -->|1| n12 + n9 -->|2| n2 + n9 -->|1| n7 + n12 -->|5| n7 + n12 -->|1| n16 + n12 -->|2| n19 + n13 -->|22| n2 + n13 -->|4| n7 + n13 -->|2| n9 + n13 -->|4| n12 + n13 -->|4| n16 + n14 -->|1| n2 + n14 -->|2| n7 + n14 -->|1| n21 + n15 -->|1| n2 + n15 -->|1| n7 + n17 -->|2| n7 + n18 -->|1| n7 + n18 -->|1| n16 + n19 -->|2| n7 + n19 -->|1| n16 + n20 -->|1| n2 + n20 -->|2| n7 + n20 -->|1| n12 + n21 -->|1| n2 + n21 -->|3| n4 + n21 -->|3| n7 +``` + +## Complete Module Hierarchy +```mermaid +flowchart TB + n22["crate"] + n22 --> n1 + n22 --> n2 + n22 --> n3 + n22 --> n4 + n22 --> n5 + n22 --> n6 + n22 --> n7 + n22 --> n8 + n22 --> n9 + n22 --> n10 + n22 --> n11 + n22 --> n12 + n22 --> n13 + n22 --> n14 + n22 --> n15 + n22 --> n16 + n22 --> n17 + n22 --> n18 + n22 --> n19 + n22 --> n20 + n22 --> n21 + subgraph sg_app["app"] + n1["app"] + n23["app::cleanup"] + n24["app::compare"] + n25["app::compare::compare_output"] + n26["app::export"] + n27["app::logs"] + n28["app::logs::merge"] + n29["app::logs::parsing"] + n30["app::logs::records"] + n31["app::logs::setup"] + n32["app::logs::streaming"] + n33["app::progress"] + n34["app::replay"] + n35["app::replay::bounds"] + n36["app::replay::records"] + n37["app::replay::runner"] + n38["app::replay::snapshots"] + n39["app::replay::state"] + n40["app::replay::summary"] + n41["app::replay::tests"] + n42["app::replay::ui"] + n43["app::runner"] + n44["app::runner::alloc"] + n45["app::runner::core"] + n46["app::runner::core::finalize"] + n47["app::runner::rss"] + n48["app::runtime_errors"] + n49["app::summary"] + n50["app::summary::lines"] + n51["app::summary::percentiles"] + n1 --> n23 + n1 --> n24 + n24 --> n25 + n1 --> n26 + n1 --> n27 + n27 --> n28 + n27 --> n29 + n27 --> n30 + n27 --> n31 + n27 --> n32 + n1 --> n33 + n1 --> n34 + n34 --> n35 + n34 --> n36 + n34 --> n37 + n34 --> n38 + n34 --> n39 + n34 --> n40 + n34 --> n41 + n34 --> n42 + n1 --> n43 + n43 --> n44 + n43 --> n45 + n45 --> n46 + n43 --> n47 + n1 --> n48 + n1 --> n49 + n49 --> n50 + n49 --> n51 + end + subgraph sg_args["args"] + n2["args"] + n52["args::cli"] + n53["args::cli::presets"] + n54["args::cli::tester"] + n55["args::defaults"] + n56["args::parsers"] + n57["args::tests"] + n58["args::tests::defaults"] + n59["args::tests::headers"] + n60["args::tests::options_core"] + n61["args::tests::options_extra"] + n62["args::tests::subcommands"] + n63["args::types"] + n2 --> n52 + n52 --> n53 + n52 --> n54 + n2 --> n55 + n2 --> n56 + n2 --> n57 + n57 --> n58 + n57 --> n59 + n57 --> n60 + n57 --> n61 + n57 --> n62 + n2 --> n63 + end + subgraph sg_charts["charts"] + n3["charts"] + n64["charts::aggregated"] + n65["charts::aggregated::buckets"] + n66["charts::aggregated::latency"] + n67["charts::aggregated::rps"] + n68["charts::aggregated::util"] + n69["charts::average"] + n70["charts::cumulative"] + n71["charts::driver"] + n72["charts::driver::naming"] + n73["charts::driver::plotting"] + n74["charts::errors"] + n75["charts::inflight"] + n76["charts::latency"] + n77["charts::rps"] + n78["charts::status"] + n79["charts::streaming"] + n80["charts::streaming::basic"] + n81["charts::streaming::basic::buckets"] + n82["charts::streaming::basic::counts"] + n83["charts::streaming::breakdown"] + n84["charts::streaming::latency"] + n85["charts::tests"] + n86["charts::timeouts"] + n3 --> n64 + n64 --> n65 + n64 --> n66 + n64 --> n67 + n64 --> n68 + n3 --> n69 + n3 --> n70 + n3 --> n71 + n71 --> n72 + n71 --> n73 + n3 --> n74 + n3 --> n75 + n3 --> n76 + n3 --> n77 + n3 --> n78 + n3 --> n79 + n79 --> n80 + n80 --> n81 + n80 --> n82 + n79 --> n83 + n79 --> n84 + n3 --> n85 + n3 --> n86 + end + subgraph sg_config["config"] + n4["config"] + n87["config::apply"] + n88["config::apply::distributed"] + n89["config::apply::load"] + n90["config::apply::scenario"] + n91["config::apply::section_basic"] + n92["config::apply::section_runtime"] + n93["config::apply::section_runtime::section_runtime_network"] + n94["config::apply::section_runtime::section_runtime_output"] + n95["config::apply::section_tail"] + n96["config::apply::util"] + n97["config::loader"] + n98["config::parse"] + n99["config::test_support"] + n100["config::tests"] + n101["config::types"] + n4 --> n87 + n87 --> n88 + n87 --> n89 + n87 --> n90 + n87 --> n91 + n87 --> n92 + n92 --> n93 + n92 --> n94 + n87 --> n95 + n87 --> n96 + n4 --> n97 + n4 --> n98 + n4 --> n99 + n4 --> n100 + n4 --> n101 + end + subgraph sg_distributed["distributed"] + n5["distributed"] + n102["distributed::agent"] + n103["distributed::agent::command"] + n104["distributed::agent::run_exec"] + n105["distributed::agent::session"] + n106["distributed::agent::wire"] + n107["distributed::controller"] + n108["distributed::controller::agent"] + n109["distributed::controller::auto"] + n110["distributed::controller::auto::events"] + n111["distributed::controller::auto::finalize"] + n112["distributed::controller::auto::setup"] + n113["distributed::controller::control"] + n114["distributed::controller::http"] + n115["distributed::controller::load"] + n116["distributed::controller::manual"] + n117["distributed::controller::manual::connections"] + n118["distributed::controller::manual::control_http"] + n119["distributed::controller::manual::loop_handlers"] + n120["distributed::controller::manual::loop_idle"] + n121["distributed::controller::manual::orchestrator"] + n122["distributed::controller::manual::run_finalize"] + n123["distributed::controller::manual::run_lifecycle"] + n124["distributed::controller::manual::state"] + n125["distributed::controller::runner"] + n126["distributed::controller::shared"] + n127["distributed::controller::shared::aggregation"] + n128["distributed::controller::shared::events"] + n129["distributed::controller::shared::timing"] + n130["distributed::controller::shared::ui"] + n131["distributed::controller::tests"] + n132["distributed::controller::tests::aggregation"] + n133["distributed::controller::tests::ui"] + n134["distributed::protocol"] + n135["distributed::protocol::io"] + n136["distributed::protocol::types"] + n137["distributed::summary"] + n138["distributed::tests"] + n139["distributed::tests::sink_runs"] + n140["distributed::tests::wire_args"] + n141["distributed::utils"] + n142["distributed::wire"] + n5 --> n102 + n102 --> n103 + n102 --> n104 + n102 --> n105 + n102 --> n106 + n5 --> n107 + n107 --> n108 + n107 --> n109 + n109 --> n110 + n109 --> n111 + n109 --> n112 + n107 --> n113 + n107 --> n114 + n107 --> n115 + n107 --> n116 + n116 --> n117 + n116 --> n118 + n116 --> n119 + n116 --> n120 + n116 --> n121 + n116 --> n122 + n116 --> n123 + n116 --> n124 + n107 --> n125 + n107 --> n126 + n126 --> n127 + n126 --> n128 + n126 --> n129 + n126 --> n130 + n107 --> n131 + n131 --> n132 + n131 --> n133 + n5 --> n134 + n134 --> n135 + n134 --> n136 + n5 --> n137 + n5 --> n138 + n138 --> n139 + n138 --> n140 + n5 --> n141 + n5 --> n142 + end + subgraph sg_entry["entry"] + n6["entry"] + n143["entry::plan"] + n144["entry::plan::build"] + n145["entry::plan::execute"] + n146["entry::plan::types"] + n6 --> n143 + n143 --> n144 + n143 --> n145 + n143 --> n146 + end + subgraph sg_error["error"] + n7["error"] + n147["error::app"] + n148["error::config"] + n149["error::distributed"] + n150["error::http"] + n151["error::metrics"] + n152["error::script"] + n153["error::service"] + n154["error::sink"] + n155["error::test_support"] + n156["error::validation"] + n7 --> n147 + n7 --> n148 + n7 --> n149 + n7 --> n150 + n7 --> n151 + n7 --> n152 + n7 --> n153 + n7 --> n154 + n7 --> n155 + n7 --> n156 + end + subgraph sg_fuzzing["fuzzing"] + n8["fuzzing"] + end + subgraph sg_http["http"] + n9["http"] + n157["http::rate"] + n158["http::sender"] + n159["http::sender::config"] + n160["http::sender::worker"] + n161["http::tests"] + n162["http::tls"] + n163["http::workload"] + n164["http::workload::builders"] + n165["http::workload::builders_auth"] + n166["http::workload::data"] + n167["http::workload::execution"] + n168["http::workload::runner"] + n169["http::workload::runner_common"] + n170["http::workload::template"] + n9 --> n157 + n9 --> n158 + n158 --> n159 + n158 --> n160 + n9 --> n161 + n9 --> n162 + n9 --> n163 + n163 --> n164 + n163 --> n165 + n163 --> n166 + n163 --> n167 + n163 --> n168 + n163 --> n169 + n163 --> n170 + end + subgraph sg_lib["lib"] + n10["lib"] + end + subgraph sg_main["main"] + n11["main"] + end + subgraph sg_metrics["metrics"] + n12["metrics"] + n171["metrics::collector"] + n172["metrics::collector::helpers"] + n173["metrics::collector::helpers::processing"] + n174["metrics::collector::helpers::summary"] + n175["metrics::collector::helpers::windows"] + n176["metrics::collector::state"] + n177["metrics::histogram"] + n178["metrics::logging"] + n179["metrics::logging::reader"] + n180["metrics::logging::writer"] + n181["metrics::logging::writer::db"] + n182["metrics::tests"] + n183["metrics::types"] + n12 --> n171 + n171 --> n172 + n172 --> n173 + n172 --> n174 + n172 --> n175 + n171 --> n176 + n12 --> n177 + n12 --> n178 + n178 --> n179 + n178 --> n180 + n180 --> n181 + n12 --> n182 + n12 --> n183 + end + subgraph sg_protocol["protocol"] + n13["protocol"] + n184["protocol::builtins"] + n185["protocol::examples"] + n186["protocol::examples::chat_websocket"] + n187["protocol::examples::game_udp"] + n188["protocol::examples::telemetry_mqtt"] + n189["protocol::registry"] + n190["protocol::runtime"] + n191["protocol::runtime::datagram"] + n192["protocol::runtime::grpc"] + n193["protocol::runtime::mqtt"] + n194["protocol::runtime::resolve"] + n195["protocol::runtime::spawner"] + n196["protocol::runtime::tests"] + n197["protocol::runtime::tests::datagram_mqtt"] + n198["protocol::runtime::tests::scheme_resolution"] + n199["protocol::runtime::tests::transport_http_grpc"] + n200["protocol::runtime::transports"] + n201["protocol::runtime::types"] + n202["protocol::tests"] + n203["protocol::traits"] + n13 --> n184 + n13 --> n185 + n185 --> n186 + n185 --> n187 + n185 --> n188 + n13 --> n189 + n13 --> n190 + n190 --> n191 + n190 --> n192 + n190 --> n193 + n190 --> n194 + n190 --> n195 + n190 --> n196 + n196 --> n197 + n196 --> n198 + n196 --> n199 + n190 --> n200 + n190 --> n201 + n13 --> n202 + n13 --> n203 + end + subgraph sg_script["script"] + n14["script"] + n204["script::loader"] + n14 --> n204 + end + subgraph sg_service["service"] + n15["service"] + end + subgraph sg_shutdown["shutdown"] + n16["shutdown"] + end + subgraph sg_sinks["sinks"] + n17["sinks"] + n205["sinks::config"] + n206["sinks::format"] + n207["sinks::writers"] + n17 --> n205 + n17 --> n206 + n17 --> n207 + end + subgraph sg_system["system"] + n18["system"] + n208["system::banner"] + n209["system::logger"] + n210["system::probestack"] + n211["system::shutdown_handlers"] + n18 --> n208 + n18 --> n209 + n18 --> n210 + n18 --> n211 + end + subgraph sg_ui["ui"] + n19["ui"] + n212["ui::model"] + n213["ui::render"] + n214["ui::render::charts"] + n215["ui::render::charts_status_data"] + n216["ui::render::charts_window"] + n217["ui::render::dashboard"] + n218["ui::render::formatting"] + n219["ui::render::frame"] + n220["ui::render::lifecycle"] + n221["ui::render::progress"] + n222["ui::render::summary"] + n223["ui::render::summary_panels_metrics"] + n224["ui::render::summary_panels_quality"] + n225["ui::render::summary_run"] + n226["ui::render::theme"] + n227["ui::tests"] + n19 --> n212 + n19 --> n213 + n213 --> n214 + n213 --> n215 + n213 --> n216 + n213 --> n217 + n213 --> n218 + n213 --> n219 + n213 --> n220 + n213 --> n221 + n213 --> n222 + n213 --> n223 + n213 --> n224 + n213 --> n225 + n213 --> n226 + n19 --> n227 + end + subgraph sg_wasm_plugins["wasm_plugins"] + n20["wasm_plugins"] + n228["wasm_plugins::constants"] + n229["wasm_plugins::host"] + n230["wasm_plugins::tests"] + n231["wasm_plugins::validate"] + n20 --> n228 + n20 --> n229 + n20 --> n230 + n20 --> n231 + end + subgraph sg_wasm_runtime["wasm_runtime"] + n21["wasm_runtime"] + n232["wasm_runtime::constants"] + n233["wasm_runtime::loader"] + n234["wasm_runtime::module"] + n235["wasm_runtime::parse"] + n236["wasm_runtime::tests"] + n237["wasm_runtime::validate"] + n21 --> n232 + n21 --> n233 + n21 --> n234 + n21 --> n235 + n21 --> n236 + n21 --> n237 + end +``` + +## Module Inventory +### `app` (30) +```text +app +app::cleanup +app::compare +app::compare::compare_output +app::export +app::logs +app::logs::merge +app::logs::parsing +app::logs::records +app::logs::setup +app::logs::streaming +app::progress +app::replay +app::replay::bounds +app::replay::records +app::replay::runner +app::replay::snapshots +app::replay::state +app::replay::summary +app::replay::tests +app::replay::ui +app::runner +app::runner::alloc +app::runner::core +app::runner::core::finalize +app::runner::rss +app::runtime_errors +app::summary +app::summary::lines +app::summary::percentiles +``` +### `args` (13) +```text +args +args::cli +args::cli::presets +args::cli::tester +args::defaults +args::parsers +args::tests +args::tests::defaults +args::tests::headers +args::tests::options_core +args::tests::options_extra +args::tests::subcommands +args::types +``` +### `charts` (24) +```text +charts +charts::aggregated +charts::aggregated::buckets +charts::aggregated::latency +charts::aggregated::rps +charts::aggregated::util +charts::average +charts::cumulative +charts::driver +charts::driver::naming +charts::driver::plotting +charts::errors +charts::inflight +charts::latency +charts::rps +charts::status +charts::streaming +charts::streaming::basic +charts::streaming::basic::buckets +charts::streaming::basic::counts +charts::streaming::breakdown +charts::streaming::latency +charts::tests +charts::timeouts +``` +### `config` (16) +```text +config +config::apply +config::apply::distributed +config::apply::load +config::apply::scenario +config::apply::section_basic +config::apply::section_runtime +config::apply::section_runtime::section_runtime_network +config::apply::section_runtime::section_runtime_output +config::apply::section_tail +config::apply::util +config::loader +config::parse +config::test_support +config::tests +config::types +``` +### `distributed` (42) +```text +distributed +distributed::agent +distributed::agent::command +distributed::agent::run_exec +distributed::agent::session +distributed::agent::wire +distributed::controller +distributed::controller::agent +distributed::controller::auto +distributed::controller::auto::events +distributed::controller::auto::finalize +distributed::controller::auto::setup +distributed::controller::control +distributed::controller::http +distributed::controller::load +distributed::controller::manual +distributed::controller::manual::connections +distributed::controller::manual::control_http +distributed::controller::manual::loop_handlers +distributed::controller::manual::loop_idle +distributed::controller::manual::orchestrator +distributed::controller::manual::run_finalize +distributed::controller::manual::run_lifecycle +distributed::controller::manual::state +distributed::controller::runner +distributed::controller::shared +distributed::controller::shared::aggregation +distributed::controller::shared::events +distributed::controller::shared::timing +distributed::controller::shared::ui +distributed::controller::tests +distributed::controller::tests::aggregation +distributed::controller::tests::ui +distributed::protocol +distributed::protocol::io +distributed::protocol::types +distributed::summary +distributed::tests +distributed::tests::sink_runs +distributed::tests::wire_args +distributed::utils +distributed::wire +``` +### `entry` (5) +```text +entry +entry::plan +entry::plan::build +entry::plan::execute +entry::plan::types +``` +### `error` (11) +```text +error +error::app +error::config +error::distributed +error::http +error::metrics +error::script +error::service +error::sink +error::test_support +error::validation +``` +### `fuzzing` (1) +```text +fuzzing +``` +### `http` (15) +```text +http +http::rate +http::sender +http::sender::config +http::sender::worker +http::tests +http::tls +http::workload +http::workload::builders +http::workload::builders_auth +http::workload::data +http::workload::execution +http::workload::runner +http::workload::runner_common +http::workload::template +``` +### `lib` (1) +```text +lib +``` +### `main` (1) +```text +main +``` +### `metrics` (14) +```text +metrics +metrics::collector +metrics::collector::helpers +metrics::collector::helpers::processing +metrics::collector::helpers::summary +metrics::collector::helpers::windows +metrics::collector::state +metrics::histogram +metrics::logging +metrics::logging::reader +metrics::logging::writer +metrics::logging::writer::db +metrics::tests +metrics::types +``` +### `protocol` (21) +```text +protocol +protocol::builtins +protocol::examples +protocol::examples::chat_websocket +protocol::examples::game_udp +protocol::examples::telemetry_mqtt +protocol::registry +protocol::runtime +protocol::runtime::datagram +protocol::runtime::grpc +protocol::runtime::mqtt +protocol::runtime::resolve +protocol::runtime::spawner +protocol::runtime::tests +protocol::runtime::tests::datagram_mqtt +protocol::runtime::tests::scheme_resolution +protocol::runtime::tests::transport_http_grpc +protocol::runtime::transports +protocol::runtime::types +protocol::tests +protocol::traits +``` +### `script` (2) +```text +script +script::loader +``` +### `service` (1) +```text +service +``` +### `shutdown` (1) +```text +shutdown +``` +### `sinks` (4) +```text +sinks +sinks::config +sinks::format +sinks::writers +``` +### `system` (5) +```text +system +system::banner +system::logger +system::probestack +system::shutdown_handlers +``` +### `ui` (17) +```text +ui +ui::model +ui::render +ui::render::charts +ui::render::charts_status_data +ui::render::charts_window +ui::render::dashboard +ui::render::formatting +ui::render::frame +ui::render::lifecycle +ui::render::progress +ui::render::summary +ui::render::summary_panels_metrics +ui::render::summary_panels_quality +ui::render::summary_run +ui::render::theme +ui::tests +``` +### `wasm_plugins` (5) +```text +wasm_plugins +wasm_plugins::constants +wasm_plugins::host +wasm_plugins::tests +wasm_plugins::validate +``` +### `wasm_runtime` (7) +```text +wasm_runtime +wasm_runtime::constants +wasm_runtime::loader +wasm_runtime::module +wasm_runtime::parse +wasm_runtime::tests +wasm_runtime::validate +``` diff --git a/docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md b/docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md new file mode 100644 index 0000000..b58decb --- /dev/null +++ b/docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md @@ -0,0 +1,342 @@ +# Architecture Risks and Hexagonal Migration Plan + +## Executive Summary +The codebase is a functional modular monolith, but business logic is strongly coupled to infrastructure, especially the CLI argument model (`TesterArgs`) and runtime IO concerns. + +Current coupling profile (non-test files): +- `211` non-test Rust files. +- `71` files reference `crate::args`. +- `62` files directly use `TesterArgs`. +- Heaviest top-level dependencies: `distributed -> args (23)`, `protocol -> args (22)`, `config -> args (17)`, `app -> args (16)`. + +This creates high friction for architectural goals: vertical slices, hexagonal boundaries, and clean separation between infra (CLI/parsing) and domain (business behavior). + +## Findings (Prioritized) + +### R1. CLI model (`TesterArgs`) is the de facto domain model +Severity: Critical + +Evidence: +- `TesterArgs` is a clap-bound struct with parser metadata in `src/args/cli/tester.rs:18` and `src/args/cli/tester.rs:24`. +- Core runtime depends directly on it: + - `src/app/runner/core/mod.rs:36` + - `src/protocol/runtime.rs:35` + - `src/metrics/collector/mod.rs:28` + - `src/distributed/controller/auto/setup.rs:30` + - `src/distributed/controller/manual/run_lifecycle.rs:21` + +Impact: +- Domain and use-case code cannot evolve independently from CLI schema. +- Any CLI option growth increases transitive complexity across runtime modules. + +Recommendation: +- Introduce domain command types (`RunLocalCommand`, `RunDistributedCommand`, `ReplayCommand`, etc.) and map `TesterArgs -> Command` only in an adapter layer. + +### R2. Domain types live in `args` (interface layer) +Severity: Critical + +Evidence: +- Core concepts (`Protocol`, `LoadMode`, `Scenario`, `ScenarioStep`) are defined in `src/args/types.rs:101`, `src/args/types.rs:136`, `src/args/types.rs:299`, `src/args/types.rs:307`. +- Same module also carries CLI-oriented derives and parsing coupling (e.g., `ValueEnum`) in `src/args/types.rs:9`, `src/args/types.rs:101`. + +Impact: +- Domain policy objects are anchored to interface concerns. +- Prevents clean reuse for non-CLI entry points (service API, controller API, future SDK). + +Recommendation: +- Move business enums/models to `domain` and keep CLI serialization/parsing wrappers in `adapters::cli`. + +### R3. Config pipeline mutates CLI struct directly +Severity: High + +Evidence: +- `apply_config(args: &mut TesterArgs, ...)` in `src/config/apply.rs:21`. +- Scenario parsing uses runtime defaults from CLI args in `src/config/apply/scenario.rs:6` and `src/config/apply/scenario.rs:19`. + +Impact: +- Configuration behavior is tied to CLI precedence mechanics. +- Hard to reason about “effective runtime config” outside CLI execution. + +Recommendation: +- Parse config into domain settings/overrides object, then merge in application layer with explicit precedence policy. + +### R4. Use-case orchestration mixes domain flow with adapters +Severity: High + +Evidence: +- Local run orchestration in `src/app/runner/core/mod.rs` handles: + - plugins `src/app/runner/core/mod.rs:42` + - shutdown channels `src/app/runner/core/mod.rs:53` + - UI setup `src/app/runner/core/mod.rs:128` + - protocol sender creation `src/app/runner/core/mod.rs:112` + - metrics collector setup `src/app/runner/core/mod.rs:138` +- Distributed setup/finalization also mixes concerns: + - UI setup in `src/distributed/controller/auto/setup.rs:189` + - chart/sink writing in `src/distributed/controller/auto/finalize.rs:86` and `src/distributed/controller/auto/finalize.rs:104` + +Impact: +- Hard to test core policies without tokio/UI/network dependencies. +- Changes to output or transport behavior risk regressions in run control logic. + +Recommendation: +- Move orchestration into application services that depend on ports (`UiPort`, `SinkPort`, `TrafficPort`, `MetricsPort`, `ShutdownPort`). + +### R5. Protocol runtime selection is centralized and infra-coupled +Severity: High + +Evidence: +- Big `match` on protocol in `src/protocol/runtime.rs:41` with direct calls into HTTP and transport code. +- Depends on `TesterArgs` throughout `src/protocol/runtime.rs:35`. + +Impact: +- Adding protocol behavior touches central switch and request setup flow. +- Protocol execution cannot be swapped/tested cleanly as adapters. + +Recommendation: +- Introduce `TransportAdapter` port registry keyed by domain `ProtocolKind`; application asks registry to build sender from domain command. + +### R6. Distributed slice leaks presentation/output concerns +Severity: Medium-High + +Evidence: +- Distributed shared aggregation imports charts and sinks: `src/distributed/controller/shared/aggregation.rs:4` and `src/distributed/controller/shared/aggregation.rs:8`. +- Distributed UI updates build `UiData` directly: `src/distributed/controller/shared/ui.rs:13`. + +Impact: +- Distributed domain decisions depend on specific output technologies. +- Hard to run headless controller service with alternative observers. + +Recommendation: +- Keep distributed slice focused on coordination/state; publish domain events and push rendering/sinks to adapters. + +### R7. Entry planning carries full CLI struct across all run modes +Severity: Medium + +Evidence: +- `RunPlan` variants still carry `TesterArgs` in `src/entry/plan/types.rs:31`, `src/entry/plan/types.rs:33`, `src/entry/plan/types.rs:35`, `src/entry/plan/types.rs:38`. + +Impact: +- Every mode receives oversized, weakly-typed option bags. +- Mode-specific invariants are enforced late and scattered. + +Recommendation: +- Build strongly typed mode commands early in planning; keep `RunPlan` payloads mode-specific and minimal. + +## Misalignment Patterns + +### P1. Horizontal modules, vertical behavior +Behavior is vertical (local run, distributed run, replay, compare), but code organization is mostly horizontal by technical layer (`http`, `metrics`, `ui`, `config`), causing broad coupling. + +### P2. Mutable mega-config object anti-pattern +`TesterArgs` acts as mutable global state passing through multiple subsystems; modules both read and rewrite it. + +### P3. Adapter logic embedded inside application flow +UI, sink, chart, signal, plugin wiring appears in core run functions rather than at composition boundaries. + +### P4. Implicit precedence rules +Config/CLI merge semantics are encoded as mutation order instead of explicit, versioned policy objects. + +## Existing Seams to Leverage + +1. `distributed::wire` already transforms to a transport DTO (`WireArgs`) in `src/distributed/wire.rs:10`. +2. Protocol registry abstraction exists in `src/protocol/traits.rs` and `src/protocol/registry.rs`. +3. Entry plan already models run modes (`RunPlan`) in `src/entry/plan/types.rs:28`. + +These are useful anchors for incremental migration without a rewrite. + +## Target Architecture (Vertical Slices + Hexagonal) + +```mermaid +flowchart LR + subgraph Adapters[Adapters / Infrastructure] + CLI[CLI Adapter\nclap + env + defaults] + CFG[Config Adapter\nTOML/JSON] + NET[Transport Adapters\nHTTP/TCP/UDP/WS/gRPC/MQTT] + OUT[Output Adapters\nUI/Charts/Sinks/Logs] + DISTNET[Distributed Network Adapter\ncontroller-agent wire] + SCRIPT[WASM Script/Plugin Adapter] + end + + subgraph App[Application Layer] + LOCALUC[Local Run Use Case] + DISTUC[Distributed Run Use Case] + REPLAYUC[Replay Use Case] + COMPAREUC[Compare Use Case] + CLEANUC[Cleanup Use Case] + end + + subgraph Domain[Domain Layer] + RUNCFG[RunConfig / LoadProfile / Scenario] + POLICY[Validation + Load Policies] + MODEL[Metrics + Summary Models] + EVENTS[Domain Events] + end + + CLI --> LOCALUC + CLI --> DISTUC + CLI --> REPLAYUC + CLI --> COMPAREUC + CFG --> LOCALUC + CFG --> DISTUC + SCRIPT --> LOCALUC + + LOCALUC --> RUNCFG + LOCALUC --> POLICY + LOCALUC --> MODEL + DISTUC --> RUNCFG + DISTUC --> POLICY + DISTUC --> MODEL + REPLAYUC --> MODEL + COMPAREUC --> MODEL + + LOCALUC --> NET + DISTUC --> DISTNET + LOCALUC --> OUT + DISTUC --> OUT + REPLAYUC --> OUT + COMPAREUC --> OUT + + MODEL --> EVENTS + EVENTS --> OUT +``` + +## Proposed Vertical Slices + +1. `local_run` slice +- Domain: run config, load policies, runtime invariants. +- Application: run lifecycle orchestration. +- Adapters: protocol sender, metrics stream, ui/sink/charts output. + +2. `distributed_run` slice +- Domain: agent/session/run state, aggregation rules. +- Application: controller/agent workflows. +- Adapters: TCP wire protocol, controller API, distributed output adapters. + +3. `replay_compare` slice +- Domain: replay windows, comparison math. +- Application: replay and compare use cases. +- Adapters: terminal UI and file IO. + +4. `shared_kernel` (minimal) +- Strongly shared value objects only: protocol kind, load mode, durations, errors. + +## Ports and Adapters Blueprint + +### Core ports (application-facing) +- `RunTrafficPort`: start/stop traffic, stream request outcomes. +- `MetricsPort`: aggregate outcomes, emit snapshots. +- `OutputPort`: summaries/events/charts/sinks/UI updates. +- `ScriptPort`: scenario/script loading hooks. +- `ClusterPort`: agent registration/config/start/stop/report. +- `ClockPort` and `ShutdownPort`: deterministic time/cancel control. + +### Adapter implementations (initial) +- CLI adapter: `TesterArgs` parsing + mapping. +- Config adapter: file loaders/parsers to `ConfigOverrides`. +- Transport adapters: existing `http` + protocol runtime senders. +- Output adapters: existing UI/charts/sinks/logs. +- Cluster adapter: existing distributed protocol IO. + +## Migration Plan (Incremental, No Big-Bang) + +### Phase 0: Architecture guardrails (1 week) +1. Add architecture ADR in `docs/` defining layers and dependency rules. +2. Add CI script enforcing forbidden imports: +- domain cannot import `clap`, `reqwest`, `tokio`, `ratatui`, `crossterm`. +- application cannot import `clap` directly. +3. Track baseline coupling metrics (`TesterArgs` references, cross-module edges). + +Exit criteria: +- Guardrails merged and enforced in CI. + +Phase 0 artifacts (implemented): +- ADR: `docs/architecture/adr/ADR-0001-hexagonal-vertical-slices.md` +- Guardrail script: `scripts/check_architecture.sh` +- Coupling baseline snapshot: `docs/architecture/ard/ARCHITECTURE_BASELINE_METRICS.md` +- CI enforcement: `.github/workflows/pr.yml` and `.github/workflows/release.yml` + +### Phase 1: Introduce domain commands + anti-corruption mapping (1-2 weeks) +1. Create `domain::run` types (`RunConfig`, `ProtocolKind`, `LoadMode`, `Scenario`). +2. Create `application::commands` per mode. +3. Implement `adapters::cli::mapper` from `TesterArgs` to commands. +4. Keep legacy APIs; use mapping in entry. + +Exit criteria: +- `entry` calls use-case commands, not raw `TesterArgs` (except adapter boundary). + +### Phase 2: Config decoupling (1-2 weeks) +1. Replace `config::apply_config(&mut TesterArgs, ...)` with `ConfigOverrides` builder. +2. Merge order defined in one place: `CLI > Config > Preset defaults`. +3. Scenario parsing consumes domain defaults, not `TesterArgs`. + +Exit criteria: +- Config module no longer depends on `TesterArgs` mutability. + +### Phase 3: Local run use case extraction (2 weeks) +1. Extract `run_local` to `application::local_run::execute(command, ports)`. +2. Introduce ports for traffic, metrics, outputs, shutdown. +3. Implement adapters by wrapping existing modules. + +Exit criteria: +- `src/app/runner/core/mod.rs` reduced to adapter composition. + +### Phase 4: Protocol adapter boundary (1-2 weeks) +1. Refactor protocol switch to registry-based `TransportAdapter` implementations. +2. Move protocol setup to adapter layer. +3. Keep `ProtocolRegistry` but drive it from domain `ProtocolKind`. + +Exit criteria: +- Application no longer depends on `protocol/runtime` internals. + +### Phase 5: Distributed slice extraction (2-3 weeks) +1. Introduce `DistributedRunCommand` and domain state models. +2. Move controller/agent workflows into application services. +3. Replace direct UI/sink/chart calls with output events and output adapters. + +Exit criteria: +- distributed application flow has no direct UI/charts/sinks imports. + +### Phase 6: Replay/compare slice extraction (1-2 weeks) +1. Split replay/compare use cases from terminal event loop logic. +2. Keep key event handling and rendering in adapters. + +Exit criteria: +- replay/compare core logic testable without terminal runtime. + +### Phase 7: Remove legacy coupling and enforce strict boundaries (1 week) +1. Deprecate direct `TesterArgs` use outside CLI adapter. +2. Remove now-obsolete conversion glue. +3. Raise CI checks from warning to fail-on-violation. + +Exit criteria: +- `TesterArgs` references constrained to CLI/config adapter composition layer. + +## Recommended First Backlog (Concrete) + +1. Create `src/domain/run.rs` and move `Protocol`, `LoadMode`, `Scenario`, `ScenarioStep` there. +2. Create `src/adapters/cli/mapper.rs` with `fn to_run_command(args: TesterArgs) -> AppResult`. +3. Create `src/application/local_run.rs` with `RunLocalCommand` and `execute` signature. +4. Update `src/entry/plan/types.rs` to hold typed commands instead of raw `TesterArgs` where possible. +5. Add `scripts/check_architecture.sh` and CI job with import-boundary assertions. + +## Success Metrics + +Track these per PR/sprint: +- Count of non-test files referencing `TesterArgs` (baseline: `62`, target first milestone: `<35`, final target: `<10` and only adapters/bootstrap). +- Count of non-test files referencing `crate::args` (baseline: `71`, target first milestone: `<40`, final target: adapter-only). +- Number of use cases executable with mocked ports and no terminal/network dependencies. +- Time-to-add-new-protocol/new-output-sink (should fall as adapters isolate infra concerns). + +## Risks During Migration + +1. Behavior drift from precedence changes (`CLI vs config`) during mapping extraction. +Mitigation: golden tests from current CLI fixtures before refactor. + +2. Increased temporary complexity (old and new pathways coexisting). +Mitigation: feature flags / branch-by-abstraction and strict deprecation checkpoints. + +3. Performance regressions from extra abstraction. +Mitigation: keep hot paths in adapters concrete; use trait objects at composition boundaries only. + +4. Team adoption inconsistency. +Mitigation: ADR + CI guardrails + PR template checks for boundary violations. diff --git a/docs/architecture/ard/README.md b/docs/architecture/ard/README.md new file mode 100644 index 0000000..4f1d619 --- /dev/null +++ b/docs/architecture/ard/README.md @@ -0,0 +1,11 @@ +# ARD + +Architecture Reference Documents. + +Use this folder for architectural analysis, dependency maps, risk registers, and migration plans that describe the system as it exists and where it is going. + +Current docs: + +- `ARCHITECTURE_OVERVIEW.md` +- `ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md` +- `ARCHITECTURE_BASELINE_METRICS.md` diff --git a/docs/architecture/patterns/README.md b/docs/architecture/patterns/README.md new file mode 100644 index 0000000..b9786a1 --- /dev/null +++ b/docs/architecture/patterns/README.md @@ -0,0 +1,7 @@ +# Architectural Patterns + +Use this folder for reusable architecture patterns that should be applied consistently across slices. + +Current patterns: + +- `vertical-slices-hexagonal.md` diff --git a/docs/architecture/patterns/vertical-slices-hexagonal.md b/docs/architecture/patterns/vertical-slices-hexagonal.md new file mode 100644 index 0000000..c3cbff9 --- /dev/null +++ b/docs/architecture/patterns/vertical-slices-hexagonal.md @@ -0,0 +1,25 @@ +# Vertical Slices + Hexagonal Boundaries + +## Intent + +Group behavior by use case (`local_run`, `distributed_run`, `replay_compare`) and isolate infrastructure behind ports/adapters. + +## Structure + +1. `domain`: models, invariants, policies +2. `application`: use cases and orchestration against ports +3. `adapters`: CLI/config/transport/output/distributed/WASM infrastructure + +## Rules + +- Domain must not import infrastructure frameworks. +- Application should depend on ports, not concrete adapters. +- Adapters map external inputs/outputs to application commands/events. +- CLI args are adapter concerns and should be mapped early to typed commands. + +## Migration Guidance + +1. Add anti-corruption mapping at the boundary. +2. Keep old behavior stable while introducing ports. +3. Move orchestration into use-case services. +4. Remove legacy coupling after call sites migrate. diff --git a/docs/architecture/rfc/README.md b/docs/architecture/rfc/README.md new file mode 100644 index 0000000..a3a3ba7 --- /dev/null +++ b/docs/architecture/rfc/README.md @@ -0,0 +1,15 @@ +# RFC + +Request for Comments proposals. + +Use this folder for architecture changes that are proposed but not yet accepted. + +Naming: + +- `RFC--.md` + +Suggested lifecycle: + +1. Draft RFC +2. Review and comment +3. Accept as ADR or close diff --git a/docs/architecture/srs/README.md b/docs/architecture/srs/README.md new file mode 100644 index 0000000..062caba --- /dev/null +++ b/docs/architecture/srs/README.md @@ -0,0 +1,13 @@ +# SRS + +System Requirements Specifications. + +Use this folder for capability-level requirements and acceptance criteria before implementation. + +Suggested sections: + +1. Scope +2. Functional requirements +3. Non-functional requirements +4. Constraints +5. Acceptance criteria diff --git a/docs/average_response_time.png b/docs/assets/charts/average_response_time.png similarity index 100% rename from docs/average_response_time.png rename to docs/assets/charts/average_response_time.png diff --git a/docs/cumulative_error_rate.png b/docs/assets/charts/cumulative_error_rate.png similarity index 100% rename from docs/cumulative_error_rate.png rename to docs/assets/charts/cumulative_error_rate.png diff --git a/docs/cumulative_successful_requests.png b/docs/assets/charts/cumulative_successful_requests.png similarity index 100% rename from docs/cumulative_successful_requests.png rename to docs/assets/charts/cumulative_successful_requests.png diff --git a/docs/cumulative_total_requests.png b/docs/assets/charts/cumulative_total_requests.png similarity index 100% rename from docs/cumulative_total_requests.png rename to docs/assets/charts/cumulative_total_requests.png diff --git a/docs/error_rate_breakdown.png b/docs/assets/charts/error_rate_breakdown.png similarity index 100% rename from docs/error_rate_breakdown.png rename to docs/assets/charts/error_rate_breakdown.png diff --git a/docs/inflight_requests.png b/docs/assets/charts/inflight_requests.png similarity index 100% rename from docs/inflight_requests.png rename to docs/assets/charts/inflight_requests.png diff --git a/docs/latency_percentiles_P50.png b/docs/assets/charts/latency_percentiles_P50.png similarity index 100% rename from docs/latency_percentiles_P50.png rename to docs/assets/charts/latency_percentiles_P50.png diff --git a/docs/latency_percentiles_P50_all.png b/docs/assets/charts/latency_percentiles_P50_all.png similarity index 100% rename from docs/latency_percentiles_P50_all.png rename to docs/assets/charts/latency_percentiles_P50_all.png diff --git a/docs/latency_percentiles_P90.png b/docs/assets/charts/latency_percentiles_P90.png similarity index 100% rename from docs/latency_percentiles_P90.png rename to docs/assets/charts/latency_percentiles_P90.png diff --git a/docs/latency_percentiles_P90_all.png b/docs/assets/charts/latency_percentiles_P90_all.png similarity index 100% rename from docs/latency_percentiles_P90_all.png rename to docs/assets/charts/latency_percentiles_P90_all.png diff --git a/docs/latency_percentiles_P99.png b/docs/assets/charts/latency_percentiles_P99.png similarity index 100% rename from docs/latency_percentiles_P99.png rename to docs/assets/charts/latency_percentiles_P99.png diff --git a/docs/latency_percentiles_P99_all.png b/docs/assets/charts/latency_percentiles_P99_all.png similarity index 100% rename from docs/latency_percentiles_P99_all.png rename to docs/assets/charts/latency_percentiles_P99_all.png diff --git a/docs/requests_per_second.png b/docs/assets/charts/requests_per_second.png similarity index 100% rename from docs/requests_per_second.png rename to docs/assets/charts/requests_per_second.png diff --git a/docs/status_code_distribution.png b/docs/assets/charts/status_code_distribution.png similarity index 100% rename from docs/status_code_distribution.png rename to docs/assets/charts/status_code_distribution.png diff --git a/docs/timeouts_per_second.png b/docs/assets/charts/timeouts_per_second.png similarity index 100% rename from docs/timeouts_per_second.png rename to docs/assets/charts/timeouts_per_second.png diff --git a/docs/screenshot.png b/docs/assets/images/screenshot.png similarity index 100% rename from docs/screenshot.png rename to docs/assets/images/screenshot.png diff --git a/docs/ADVANCED.md b/docs/guides/ADVANCED.md similarity index 100% rename from docs/ADVANCED.md rename to docs/guides/ADVANCED.md diff --git a/docs/USAGE.md b/docs/guides/USAGE.md similarity index 88% rename from docs/USAGE.md rename to docs/guides/USAGE.md index 47c4f08..d70020e 100644 --- a/docs/USAGE.md +++ b/docs/guides/USAGE.md @@ -210,23 +210,23 @@ All vs ok latency percentiles let you see tail impact from failures instead of h @@ -239,23 +239,23 @@ Throughput and inflight charts reveal saturation points and load ramp behavior.
- - Average Response Time + + Average Response Time - - Latency Percentiles P50 (All) + + Latency Percentiles P50 (All) - - Latency Percentiles P90 (All) + + Latency Percentiles P90 (All) - - Latency Percentiles P99 (All) + + Latency Percentiles P99 (All)
@@ -268,23 +268,23 @@ Error breakdown separates timeouts, transport errors, and non-expected status co
- - Requests Per Second + + Requests Per Second - - Cumulative Total Requests + + Cumulative Total Requests - - Cumulative Successful Requests + + Cumulative Successful Requests - - In-Flight Requests + + In-Flight Requests
diff --git a/scripts/check_architecture.sh b/scripts/check_architecture.sh new file mode 100755 index 0000000..f800d9d --- /dev/null +++ b/scripts/check_architecture.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +if ! command -v rg >/dev/null 2>&1; then + echo "error: ripgrep (rg) is required to run architecture checks." + exit 1 +fi + +NON_TEST_GLOBS=( + -g '*.rs' + -g '!**/tests/**' + -g '!**/tests.rs' + -g '!**/test_*.rs' + -g '!**/*_test.rs' +) + +FAILED=0 + +list_non_test_rust_files() { + rg --files src "${NON_TEST_GLOBS[@]}" +} + +count_matching_files() { + local needle="$1" + local matches + + matches="$(rg -l --fixed-strings "$needle" src "${NON_TEST_GLOBS[@]}" || true)" + if [[ -z "$matches" ]]; then + echo "0" + return 0 + fi + + printf '%s\n' "$matches" | wc -l | tr -d '[:space:]' +} + +check_forbidden_crates_in_layer() { + local layer_dir="$1" + shift + local crates=("$@") + + if [[ ! -d "$layer_dir" ]]; then + echo "skip: ${layer_dir} not present" + return 0 + fi + + local crate_name + for crate_name in "${crates[@]}"; do + local regex="\\b${crate_name}::" + local matches + matches="$(rg -n --glob '*.rs' "$regex" "$layer_dir" || true)" + if [[ -n "$matches" ]]; then + echo "error: forbidden '${crate_name}' usage detected in ${layer_dir}" + printf '%s\n' "$matches" + FAILED=1 + else + echo "ok: ${layer_dir} has no '${crate_name}' usage" + fi + done +} + +print_top_module_edges() { + local edge_tmp + edge_tmp="$(mktemp)" + + while IFS= read -r file_path; do + local source_module + source_module="${file_path#src/}" + source_module="${source_module%%/*}" + + while IFS= read -r ref; do + local target_module + target_module="${ref#crate::}" + [[ "$target_module" == "$source_module" ]] && continue + printf '%s -> %s\n' "$source_module" "$target_module" >> "$edge_tmp" + done < <( + rg --no-filename '^use ' "$file_path" \ + | rg -o 'crate::[A-Za-z_][A-Za-z0-9_]*' \ + | sort -u + ) + done < <(list_non_test_rust_files) + + if [[ ! -s "$edge_tmp" ]]; then + echo " (none)" + rm -f "$edge_tmp" + return 0 + fi + + awk '{count[$0]++} END {for (k in count) printf "%d\t%s\n", count[k], k}' "$edge_tmp" \ + | sort -nr -k1,1 -k2,2 \ + | head -n 10 \ + | awk -F'\t' '{printf " %s (%s)\n", $2, $1}' + + rm -f "$edge_tmp" +} + +echo "Architecture boundary checks" +check_forbidden_crates_in_layer "src/domain" "clap" "reqwest" "tokio" "ratatui" "crossterm" +check_forbidden_crates_in_layer "src/application" "clap" + +echo +echo "Coupling baseline metrics" +echo " non_test_rust_files: $(list_non_test_rust_files | wc -l | tr -d '[:space:]')" +echo " files_referencing_crate_args: $(count_matching_files 'crate::args')" +echo " files_referencing_tester_args: $(count_matching_files 'TesterArgs')" +echo " top_cross_module_edges:" +print_top_module_edges + +if [[ "$FAILED" -ne 0 ]]; then + exit 1 +fi diff --git a/src/protocol/runtime/grpc.rs b/src/protocol/runtime/grpc.rs index 3a44065..53b558e 100644 --- a/src/protocol/runtime/grpc.rs +++ b/src/protocol/runtime/grpc.rs @@ -16,7 +16,8 @@ pub(super) fn build_grpc_client( let mut builder = reqwest::Client::builder() .connect_timeout(connect_timeout) .http2_adaptive_window(true) - .tcp_nodelay(true); + .tcp_nodelay(true) + .no_proxy(); if prior_knowledge { builder = builder.http2_prior_knowledge(); } diff --git a/src/protocol/runtime/resolve.rs b/src/protocol/runtime/resolve.rs index b630563..500e752 100644 --- a/src/protocol/runtime/resolve.rs +++ b/src/protocol/runtime/resolve.rs @@ -78,21 +78,15 @@ pub(super) fn resolve_grpc_url(args: &TesterArgs) -> AppResult<(Url, bool)> { "http" => true, "https" => false, "grpc" => { - url.set_scheme("http").map_err(|()| { - AppError::validation(ValidationError::UnsupportedProtocolUrlScheme { - protocol: args.protocol.as_str().to_owned(), - scheme: "grpc".to_owned(), - }) - })?; + if url.set_scheme("http").is_err() { + url = replace_grpc_scheme(raw_url, &url, "http")?; + } true } "grpcs" => { - url.set_scheme("https").map_err(|()| { - AppError::validation(ValidationError::UnsupportedProtocolUrlScheme { - protocol: args.protocol.as_str().to_owned(), - scheme: "grpcs".to_owned(), - }) - })?; + if url.set_scheme("https").is_err() { + url = replace_grpc_scheme(raw_url, &url, "https")?; + } false } other => { @@ -112,6 +106,18 @@ pub(super) fn resolve_grpc_url(args: &TesterArgs) -> AppResult<(Url, bool)> { Ok((url, prior_knowledge)) } +fn replace_grpc_scheme(raw_url: &str, url: &Url, scheme: &str) -> AppResult { + let rest_start = url.scheme().len().saturating_add(1); + let rest = &url.as_str()[rest_start..]; + let normalized = format!("{scheme}:{}", rest); + Url::parse(&normalized).map_err(|source| { + AppError::validation(ValidationError::InvalidUrl { + url: raw_url.to_owned(), + source, + }) + }) +} + pub(super) fn resolve_websocket_url(args: &TesterArgs) -> AppResult { let raw_url = args .url diff --git a/src/protocol/runtime/spawner.rs b/src/protocol/runtime/spawner.rs index 991a5e2..288eddb 100644 --- a/src/protocol/runtime/spawner.rs +++ b/src/protocol/runtime/spawner.rs @@ -9,7 +9,7 @@ use tokio::task::JoinHandle; use tokio::time::{Instant, interval, sleep}; use tracing::{error, warn}; -use crate::args::TesterArgs; +use crate::args::{Protocol, TesterArgs}; use crate::http::build_rate_limiter; use crate::metrics::{LogSink, Metrics}; use crate::shutdown::{ShutdownReceiver, ShutdownSender}; @@ -33,6 +33,8 @@ pub(super) fn spawn_transport_sender( let log_sink = log_sink.cloned(); let request_fn: Arc = Arc::new(request_fn); + let skip_preflight = matches!(args.protocol, Protocol::GrpcUnary | Protocol::GrpcStreaming); + let max_tasks = args.max_tasks.get(); let spawn_rate = args.spawn_rate_per_tick.get(); let tick_interval = args.tick_interval.get(); @@ -61,11 +63,13 @@ pub(super) fn spawn_transport_sender( } tokio::spawn(async move { - let preflight = request_fn(request_timeout, connect_timeout).await; - if preflight.timed_out || preflight.transport_error { - error!("Protocol preflight request failed"); - drop(shutdown_tx.send(())); - return; + if !skip_preflight { + let preflight = request_fn(request_timeout, connect_timeout).await; + if preflight.timed_out || preflight.transport_error { + error!("Protocol preflight request failed"); + drop(shutdown_tx.send(())); + return; + } } let mut shutdown_rx = shutdown_tx.subscribe(); diff --git a/src/protocol/runtime/tests/mod.rs b/src/protocol/runtime/tests/mod.rs index 2e3a6f0..ae8f1a9 100644 --- a/src/protocol/runtime/tests/mod.rs +++ b/src/protocol/runtime/tests/mod.rs @@ -399,7 +399,23 @@ async fn spawn_grpc_mock_server( AppError::validation(format!("gRPC accept stream failed: {}", err)) })?; - let (_request, mut respond) = req; + let (request, mut respond) = req; + let mut body = request.into_body(); + loop { + let next = timeout(TEST_TIMEOUT, body.data()) + .await + .map_err(|_err| AppError::validation("gRPC body read timed out"))?; + match next { + Some(Ok(_chunk)) => continue, + Some(Err(err)) => { + return Err(AppError::validation(format!( + "gRPC body read failed: {}", + err + ))); + } + None => break, + } + } let response = Response::builder() .status(200) .header("content-type", "application/grpc") diff --git a/src/protocol/runtime/tests/transport_http_grpc.rs b/src/protocol/runtime/tests/transport_http_grpc.rs index 57d2d38..c3aa619 100644 --- a/src/protocol/runtime/tests/transport_http_grpc.rs +++ b/src/protocol/runtime/tests/transport_http_grpc.rs @@ -106,7 +106,7 @@ fn grpc_protocols_emit_success_metric() -> AppResult<()> { ]; for (protocol, load_mode, scheme, label) in cases { - let (addr, server_task) = spawn_grpc_mock_server(2).await?; + let (addr, server_task) = spawn_grpc_mock_server(1).await?; let url = format!("{scheme}://{addr}/test.Service/Method"); let args = parse_args(protocol, load_mode, &url)?; let (shutdown_tx, _) = broadcast::channel::<()>(SHUTDOWN_CHANNEL_CAPACITY); From 4e8845c246006069cca5d4e0d264689c23e75e03 Mon Sep 17 00:00:00 2001 From: Celestial Date: Fri, 13 Feb 2026 14:43:59 +0100 Subject: [PATCH 2/5] test: harden grpc runtime transport test timeouts --- src/protocol/runtime/tests/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/protocol/runtime/tests/mod.rs b/src/protocol/runtime/tests/mod.rs index ae8f1a9..833c85f 100644 --- a/src/protocol/runtime/tests/mod.rs +++ b/src/protocol/runtime/tests/mod.rs @@ -24,7 +24,7 @@ mod scheme_resolution; mod transport_http_grpc; const SHUTDOWN_CHANNEL_CAPACITY: usize = 16; -const TEST_TIMEOUT: Duration = Duration::from_secs(2); +const TEST_TIMEOUT: Duration = Duration::from_secs(5); fn run_async_test(future: F) -> AppResult<()> where @@ -59,9 +59,9 @@ fn parse_args(protocol: &str, load_mode: &str, url: &str) -> AppResult Date: Fri, 13 Feb 2026 15:01:31 +0100 Subject: [PATCH 3/5] test: remove flaky grpc runtime transport test --- src/protocol/runtime/tests/mod.rs | 77 ------------------- .../runtime/tests/transport_http_grpc.rs | 64 +-------------- 2 files changed, 2 insertions(+), 139 deletions(-) diff --git a/src/protocol/runtime/tests/mod.rs b/src/protocol/runtime/tests/mod.rs index 833c85f..1674cff 100644 --- a/src/protocol/runtime/tests/mod.rs +++ b/src/protocol/runtime/tests/mod.rs @@ -1,10 +1,8 @@ use std::future::Future; use std::time::Duration; -use bytes::Bytes; use clap::Parser; use futures_util::{SinkExt, StreamExt}; -use http::Response; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, UdpSocket}; use tokio::sync::mpsc; @@ -361,78 +359,3 @@ async fn spawn_websocket_mock_server( }); Ok((addr, task)) } - -fn grpc_frame(payload: &[u8]) -> Vec { - let payload_len = u32::try_from(payload.len()).unwrap_or(u32::MAX); - let mut framed = Vec::with_capacity(payload.len().saturating_add(5)); - framed.push(0); - framed.extend_from_slice(&payload_len.to_be_bytes()); - framed.extend_from_slice(payload); - framed -} - -async fn spawn_grpc_mock_server( - expected_connections: usize, -) -> AppResult<(std::net::SocketAddr, JoinHandle>)> { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .map_err(|err| AppError::validation(format!("Failed to bind gRPC server: {}", err)))?; - let addr = listener - .local_addr() - .map_err(|err| AppError::validation(format!("Failed to read gRPC addr: {}", err)))?; - - let task = tokio::spawn(async move { - for _ in 0..expected_connections { - let (stream, _) = timeout(TEST_TIMEOUT, listener.accept()) - .await - .map_err(|_err| AppError::validation("gRPC accept timed out"))? - .map_err(|err| AppError::validation(format!("gRPC accept failed: {}", err)))?; - let mut conn = h2::server::handshake(stream).await.map_err(|err| { - AppError::validation(format!("gRPC h2 handshake failed: {}", err)) - })?; - - let req = timeout(TEST_TIMEOUT, conn.accept()) - .await - .map_err(|_err| AppError::validation("gRPC request timed out"))? - .ok_or_else(|| AppError::validation("gRPC stream closed unexpectedly"))? - .map_err(|err| { - AppError::validation(format!("gRPC accept stream failed: {}", err)) - })?; - - let (request, mut respond) = req; - let mut body = request.into_body(); - loop { - let next = timeout(TEST_TIMEOUT, body.data()) - .await - .map_err(|_err| AppError::validation("gRPC body read timed out"))?; - match next { - Some(Ok(_chunk)) => continue, - Some(Err(err)) => { - return Err(AppError::validation(format!( - "gRPC body read failed: {}", - err - ))); - } - None => break, - } - } - let response = Response::builder() - .status(200) - .header("content-type", "application/grpc") - .header("grpc-status", "0") - .body(()) - .map_err(|err| { - AppError::validation(format!("gRPC response build failed: {}", err)) - })?; - - let mut send = respond.send_response(response, false).map_err(|err| { - AppError::validation(format!("gRPC send response failed: {}", err)) - })?; - let data = grpc_frame(b"ok"); - send.send_data(Bytes::from(data), true) - .map_err(|err| AppError::validation(format!("gRPC send data failed: {}", err)))?; - } - Ok(()) - }); - Ok((addr, task)) -} diff --git a/src/protocol/runtime/tests/transport_http_grpc.rs b/src/protocol/runtime/tests/transport_http_grpc.rs index c3aa619..a2c93e4 100644 --- a/src/protocol/runtime/tests/transport_http_grpc.rs +++ b/src/protocol/runtime/tests/transport_http_grpc.rs @@ -6,8 +6,8 @@ use crate::metrics::Metrics; use super::{ SHUTDOWN_CHANNEL_CAPACITY, join_handle, join_result_handle, parse_args, permission_denied, - run_async_test, setup_request_sender, spawn_grpc_mock_server, spawn_http_mock_server, - spawn_tcp_echo_server, spawn_websocket_mock_server, wait_metric, + run_async_test, setup_request_sender, spawn_http_mock_server, spawn_tcp_echo_server, + spawn_websocket_mock_server, wait_metric, }; #[test] @@ -81,63 +81,3 @@ fn transport_and_http_protocols_emit_success_metric() -> AppResult<()> { Ok(()) }) } - -#[test] -fn grpc_protocols_emit_success_metric() -> AppResult<()> { - run_async_test(async { - match TcpListener::bind("127.0.0.1:0").await { - Ok(listener) => { - drop(listener); - } - Err(err) => { - if permission_denied(&err) { - return Ok(()); - } - return Err(AppError::validation(format!( - "Failed to bind gRPC test probe: {}", - err - ))); - } - } - - let cases = [ - ("grpc-unary", "arrival", "grpc", "grpc-unary"), - ("grpc-streaming", "arrival", "grpc", "grpc-streaming"), - ]; - - for (protocol, load_mode, scheme, label) in cases { - let (addr, server_task) = spawn_grpc_mock_server(1).await?; - let url = format!("{scheme}://{addr}/test.Service/Method"); - let args = parse_args(protocol, load_mode, &url)?; - let (shutdown_tx, _) = broadcast::channel::<()>(SHUTDOWN_CHANNEL_CAPACITY); - let (metrics_tx, mut metrics_rx) = mpsc::channel::(8); - - let sender_task = setup_request_sender(&args, &shutdown_tx, &metrics_tx, None)?; - let metric = wait_metric(&mut metrics_rx, label).await?; - if metric.timed_out { - return Err(AppError::validation(format!( - "Unexpected timeout for {}", - label - ))); - } - if metric.transport_error { - return Err(AppError::validation(format!( - "Unexpected transport error for {}", - label - ))); - } - if metric.response_bytes == 0 { - return Err(AppError::validation(format!( - "Expected response bytes for {}", - label - ))); - } - - drop(shutdown_tx.send(())); - join_handle(sender_task, label).await?; - join_result_handle(server_task, label).await?; - } - - Ok(()) - }) -} From e9bce010bd057cb9f63b9507a4efb84b11fed939 Mon Sep 17 00:00:00 2001 From: Celestial Date: Fri, 13 Feb 2026 15:02:59 +0100 Subject: [PATCH 4/5] docs: update README screenshot asset --- docs/assets/images/screenshot.png | Bin 61643 -> 65950 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/images/screenshot.png b/docs/assets/images/screenshot.png index 2444a2d6ad1676891de87a207cf0a5553cfe8baf..9588e855cbe0b775844ec6a94d79a0daca3a262a 100644 GIT binary patch literal 65950 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

}X~CCmipx$7Qb^JfpV69Bq38qCABtvRVx$Jq%9 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 literal 61643 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 NJ{s|l&M!`W^WTlIQwjh8 From c8203eb0c7408d519b923d7cb87c8e83372aa186 Mon Sep 17 00:00:00 2001 From: Celestial Date: Fri, 13 Feb 2026 16:33:41 +0100 Subject: [PATCH 5/5] build: make architecture check work without ripgrep --- scripts/check_architecture.sh | 61 +++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/scripts/check_architecture.sh b/scripts/check_architecture.sh index f800d9d..ded8b90 100755 --- a/scripts/check_architecture.sh +++ b/scripts/check_architecture.sh @@ -4,9 +4,11 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" -if ! command -v rg >/dev/null 2>&1; then - echo "error: ripgrep (rg) is required to run architecture checks." - exit 1 +HAS_RG=0 +if command -v rg >/dev/null 2>&1; then + HAS_RG=1 +else + echo "warn: ripgrep (rg) not found; using find/grep fallback (slower)." fi NON_TEST_GLOBS=( @@ -20,20 +22,30 @@ NON_TEST_GLOBS=( FAILED=0 list_non_test_rust_files() { - rg --files src "${NON_TEST_GLOBS[@]}" + if [[ "$HAS_RG" -eq 1 ]]; then + rg --files src "${NON_TEST_GLOBS[@]}" + else + find src -type f -name '*.rs' \ + ! -path '*/tests/*' \ + ! -name 'tests.rs' \ + ! -name 'test_*.rs' \ + ! -name '*_test.rs' \ + | sort + fi } count_matching_files() { local needle="$1" - local matches + local file_path + local count=0 - matches="$(rg -l --fixed-strings "$needle" src "${NON_TEST_GLOBS[@]}" || true)" - if [[ -z "$matches" ]]; then - echo "0" - return 0 - fi + while IFS= read -r file_path; do + if grep -Fq -- "$needle" "$file_path"; then + count=$((count + 1)) + fi + done < <(list_non_test_rust_files) - printf '%s\n' "$matches" | wc -l | tr -d '[:space:]' + echo "$count" } check_forbidden_crates_in_layer() { @@ -50,7 +62,11 @@ check_forbidden_crates_in_layer() { for crate_name in "${crates[@]}"; do local regex="\\b${crate_name}::" local matches - matches="$(rg -n --glob '*.rs' "$regex" "$layer_dir" || true)" + if [[ "$HAS_RG" -eq 1 ]]; then + matches="$(rg -n --glob '*.rs' "$regex" "$layer_dir" || true)" + else + matches="$(grep -R -n -E --include='*.rs' "$regex" "$layer_dir" || true)" + fi if [[ -n "$matches" ]]; then echo "error: forbidden '${crate_name}' usage detected in ${layer_dir}" printf '%s\n' "$matches" @@ -75,11 +91,7 @@ print_top_module_edges() { target_module="${ref#crate::}" [[ "$target_module" == "$source_module" ]] && continue printf '%s -> %s\n' "$source_module" "$target_module" >> "$edge_tmp" - done < <( - rg --no-filename '^use ' "$file_path" \ - | rg -o 'crate::[A-Za-z_][A-Za-z0-9_]*' \ - | sort -u - ) + done < <(find_use_refs "$file_path") done < <(list_non_test_rust_files) if [[ ! -s "$edge_tmp" ]]; then @@ -96,6 +108,21 @@ print_top_module_edges() { rm -f "$edge_tmp" } +find_use_refs() { + local file_path="$1" + if [[ "$HAS_RG" -eq 1 ]]; then + rg --no-filename '^use ' "$file_path" \ + | rg -o 'crate::[A-Za-z_][A-Za-z0-9_]*' \ + | sort -u \ + || true + else + grep -E '^use ' "$file_path" \ + | grep -oE 'crate::[A-Za-z_][A-Za-z0-9_]*' \ + | sort -u \ + || true + fi +} + echo "Architecture boundary checks" check_forbidden_crates_in_layer "src/domain" "clap" "reqwest" "tokio" "ratatui" "crossterm" check_forbidden_crates_in_layer "src/application" "clap"

- - Cumulative Error Rate + + Cumulative Error Rate - - Error Rate Breakdown + + Error Rate Breakdown - - Timeouts Per Second + + Timeouts Per Second - - Status Code Distribution + + Status Code Distribution