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/assets/images/screenshot.png b/docs/assets/images/screenshot.png new file mode 100644 index 0000000..9588e85 Binary files /dev/null and b/docs/assets/images/screenshot.png differ 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/docs/screenshot.png b/docs/screenshot.png deleted file mode 100644 index 2444a2d..0000000 Binary files a/docs/screenshot.png and /dev/null differ diff --git a/scripts/check_architecture.sh b/scripts/check_architecture.sh new file mode 100755 index 0000000..ded8b90 --- /dev/null +++ b/scripts/check_architecture.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +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=( + -g '*.rs' + -g '!**/tests/**' + -g '!**/tests.rs' + -g '!**/test_*.rs' + -g '!**/*_test.rs' +) + +FAILED=0 + +list_non_test_rust_files() { + 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 file_path + local count=0 + + while IFS= read -r file_path; do + if grep -Fq -- "$needle" "$file_path"; then + count=$((count + 1)) + fi + done < <(list_non_test_rust_files) + + echo "$count" +} + +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 + 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" + 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 < <(find_use_refs "$file_path") + 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" +} + +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" + +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..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; @@ -24,7 +22,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 +57,9 @@ fn parse_args(protocol: &str, load_mode: &str, url: &str) -> AppResult 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 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 57d2d38..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(2).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(()) - }) -}
- - Cumulative Error Rate + + Cumulative Error Rate - - Error Rate Breakdown + + Error Rate Breakdown - - Timeouts Per Second + + Timeouts Per Second - - Status Code Distribution + + Status Code Distribution