Skip to content

fix: auto-upgrade local state during install - #407

Merged
starrysky9959 merged 27 commits into
mainfrom
fix/installer-upgrade-docs
May 27, 2026
Merged

starrysky9959 merged 27 commits into
mainfrom
fix/installer-upgrade-docs

Conversation

@starrysky9959

@starrysky9959 starrysky9959 commented May 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • run Schema and local cluster topology upgrade complete automatically from when an existing local state directory is detected
  • document the automatic local-state migration behavior in the main upgrade/install docs
  • clarify the Docker E2E control-node and stress-test README instructions updated during the recent workflow changes

Verification

  • sh -n install.sh
  • updated README and upgrade docs reviewed locally

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Alertmanager and Feishu webhook integration for cluster monitoring
    • Enhanced cluster monitor component management with individual start/stop/restart/status/update operations
    • Added --download-only mode for rolling cluster upgrades
    • Improved artifact downloads with automatic resumption and integrity validation
  • Bug Fixes

    • Fixed installer to automatically migrate existing cluster metadata on upgrades
  • Documentation

    • Updated installation and upgrade guides with latest version examples
    • Expanded command reference documentation for cluster updates and monitor operations

Review Change Stack

Add a unified e2e test suite that launches an EloqKV cluster and runs
full Redis command coverage stress across Python (redis-py), Go
(go-redis/v9), and TypeScript (ioredis) SDKs.

Each SDK runs 16 workers split evenly between standalone and cluster
clients, covering 104 Redis commands with TLS enabled. The topology
verifies master/standby/voter deployment over MinIO-backed storage.

Key changes:
- cmd_stress_test.sh: single entry point for launch → stress → remove
- cmd_stress_py/main.go/main.ts: per-SDK stress clients with dual mode
- docker_env.sh: compose override support, configurable version/storage
- topology.yaml: version + storage parameterized via env vars
- GitHub Actions workflow with version/storage/workers inputs

Remove superseded legacy tests (test.sh, stress_test.sh, stress.py,
go_cluster_tls_stress, memtier_cluster_tls_stress).
Copilot AI review requested due to automatic review settings May 23, 2026 08:03
@coderabbitai

coderabbitai Bot commented May 23, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b65bdc1f-d01d-476e-982b-3d467112f5bf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds typed monitor control/update, GitHub Releases-based versioning, TLS-aware Redis probes, local extract + rsync uploads, new rolling-upgrade wait step, expanded configs/templates, replaces CI workflow and E2E with multi-language Docker stress harness, and updates docs/install scripts.

Changes

Cluster Manager and E2E Overhaul

Layer / File(s) Summary
Monitor CLI/control and configs
src/cluster_mgr/src/cli/mod.rs, .../task/monitor_ctl_task.rs, .../task/group/monitor_ctl_group.rs, .../config/monitor.rs, .../config/mod.rs, .../config/config_base.rs, .../config/deployment.rs, .../config/*
Typed monitor subcommands; start/stop/restart/status/update across Prometheus, Grafana, Alertmanager, webhook adapter; config/templates and alert rules added; status parsing/wiring expanded.
Update/download/extract/upload pipeline
.../cli/cmd_base.rs, .../task/download_task.rs, .../task/local_extract_task.rs, .../task/upload/*, .../task/group/deployment_group.rs, .../task/group/scale_*_group.rs
GitHub Releases listing/resolution; resumable verified downloads; local extract task; directory rsync uploads; scale/deploy/rolling upgrade steps updated to use staged dirs.
Redis/TX control and rolling upgrade
.../task/eloq_tx_ctl_task.rs, .../task/redis_op_task.rs, .../task/rolling_upgrade/*, .../task/wait_replica_ready_task.rs
TLS-aware Redis probe; configurable topology retries; new WaitTxReplicaReady step/task; friendlier step names and readiness waits.
Task framework output and utilities
.../task/task_controller.rs, .../task/task_base.rs, .../task/task_utils.rs, .../cli/reconcile.rs, .../task/check_task.rs, .../task/unpack_file_task.rs
Stage labeling and error summarization; parsed TaskId; PID/status handling tweaks; new port checks; unpack name handling; run_context entrypoint.
CI/scripts, docs, and tests
.github/workflows/*, scripts/*, install.sh, README.md, doc/*, tests/**
Replaces CI workflow; speeds pre-push; installer auto-upgrade; docs updated for releases/monitor/update; Docker HA compose revamped; new multi-language stress (py/go/ts) and orchestration; old E2E scripts removed; .gitignore updated.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as eloqctl (Monitor Update)
  participant Cmd as CmdExecutor
  participant GH as GitHub API
  participant DL as DownloadTask
  participant LE as LocalExtractTask
  participant UP as UploadTask (rsync)
  participant SSH as Remote Host

  User->>CLI: monitor update --components grafana
  CLI->>Cmd: run_update_command(Monitor::Update)
  Cmd->>GH: fetch releases/list assets
  Cmd->>DL: create/download URLs (+sha256)
  DL-->>Cmd: cached/verified artifacts
  Cmd->>LE: extract staged dirs
  LE-->>Cmd: staged paths
  Cmd->>UP: rsync staged dirs to hosts
  UP->>SSH: transfer files
  SSH-->>UP: ok
  Cmd-->>CLI: update summary/status
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

In racks of rust and rivers of YAML, I hop,
Pulling versions from clouds that never stop.
I unzip the moon, rsync the sun,
Monitors wake—alerts on the run.
Three languages drum a cluster’s beat—
PASS lights blink; my whiskers tweet. 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/installer-upgrade-docs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances eloqctl installation and upgrade workflows by automatically upgrading local state during install, modernizing release/version resolution via GitHub Releases, and updating the Docker E2E stress-test harness and documentation to match the new workflow.

Changes:

  • Run eloqctl upgrade automatically during install.sh when an existing local state install is detected.
  • Switch version discovery / tarball selection to GitHub Releases (eloqdata/eloqkv), add local extraction + rsync-based syncing, and improve task stage labeling.
  • Replace legacy E2E scripts with a multi-SDK stress framework (Python/Go/TypeScript) and update Docker + docs accordingly.

Reviewed changes

Copilot reviewed 57 out of 61 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/README.md Updated E2E usage docs for new multi-SDK stress workflow.
tests/install_control_eloqctl.sh New helper to install host-built eloqctl into Docker control node.
tests/e2e/topology.yaml E2E topology updated (TLS/minio/version env).
tests/e2e/test.sh Removed legacy scenario-based E2E script.
tests/e2e/stress.py Removed legacy Python-only stress script.
tests/e2e/stress_test.sh Removed legacy stress wrapper script.
tests/e2e/cmd_stress_ts/tsconfig.json New TS stress client build config.
tests/e2e/cmd_stress_ts/package.json New TS stress client dependencies/scripts.
tests/e2e/cmd_stress_ts/package-lock.json Lockfile for TS stress client.
tests/e2e/cmd_stress_ts/main.ts New ioredis-based full-command stress client.
tests/e2e/cmd_stress_test.sh New orchestration script for launch + multi-SDK stress + cleanup.
tests/e2e/cmd_stress_py/requirements.txt Pinned redis-py dependency for Python stress client.
tests/e2e/cmd_stress_py/main.py New redis-py full-command stress client.
tests/e2e/cmd_stress_go/go.sum Go module lock for Go stress client.
tests/e2e/cmd_stress_go/go.mod Go module definition for Go stress client.
tests/docker_ha/topology.yaml Removed unused legacy docker_ha topology file.
tests/docker_ha/Dockerfile.stress_ts New Node-based stress container image.
tests/docker_ha/Dockerfile.stress_go New Go-based stress container image.
tests/docker_ha/Dockerfile.stress New Python-based stress container image.
tests/docker_ha/Dockerfile Expanded node image tooling (debugging + redis tools).
tests/docker_ha/docker-compose.yaml Adds MinIO + control node + stress containers to E2E compose.
tests/docker_env.sh Adds control-node helpers, MinIO bootstrap, topology rendering updates, improved progress output.
src/cluster_mgr/src/lib.rs Exposes new github_release module.
src/cluster_mgr/src/github_release.rs New GitHub Releases parsing + asset matching helpers (with tests).
src/cluster_mgr/src/config/mod.rs Adjusts download cache dir derivation for GitHub-hosted assets.
src/cluster_mgr/src/config/deployment.rs Adds optional event_dispatcher_num to hardware config.
src/cluster_mgr/src/config/config_base.rs Improves client connect string handling via service endpoint mapping.
src/cluster_mgr/src/cli/task/upload/upload_task.rs Generalizes upload task to use rsync for directory syncs.
src/cluster_mgr/src/cli/task/upload/upload_task_builder.rs Adds rsync arg builder; switches upload task inputs to transfer abstraction.
src/cluster_mgr/src/cli/task/upload/tx_conf_upload_builder.rs Initializes new delete_remote field for uploads.
src/cluster_mgr/src/cli/task/upload/proxy_upload_builder.rs Initializes new delete_remote field for uploads.
src/cluster_mgr/src/cli/task/upload/monitor_upload_builder.rs Initializes new delete_remote field for uploads.
src/cluster_mgr/src/cli/task/upload/eloq_upload_builder.rs Moves to local-extract + rsync directory sync approach; refactors upload grouping.
src/cluster_mgr/src/cli/task/upload/data_dir_upload_builder.rs Enables remote deletion when syncing data directories.
src/cluster_mgr/src/cli/task/unpack_file_task.rs Makes make_task_pair public for reuse.
src/cluster_mgr/src/cli/task/task_controller.rs Improves stage labels based on task composition.
src/cluster_mgr/src/cli/task/task_base.rs Adds run_context helper to run an explicit task context.
src/cluster_mgr/src/cli/task/rolling_upgrade/steps.rs Adds local extraction stage; removes now-unused unpack steps.
src/cluster_mgr/src/cli/task/monitor_ctl_task.rs Enables Prometheus lifecycle endpoint and updates unit test.
src/cluster_mgr/src/cli/task/mod.rs Exposes download_task, local_extract_task, monitor_ctl_task.
src/cluster_mgr/src/cli/task/local_extract_task.rs New local tar.gz extraction task for staging release directories.
src/cluster_mgr/src/cli/task/group/scale_log_group.rs Switches log scale to download+extract+rsync logservice dir and script uploads.
src/cluster_mgr/src/cli/task/group/scale_group.rs Switches scale add-node version sync to download+extract+rsync staged TX dir.
src/cluster_mgr/src/cli/task/group/deployment_group.rs Adds local extract stage; removes remote unpack stage in deployment pipeline.
src/cluster_mgr/src/cli/task/download_task.rs Adds optional sha256 validation via GitHub release asset digest and improved cache logic.
src/cluster_mgr/src/cli/mod.rs Extends update CLI with --download-only and --monitor modes.
src/cluster_mgr/src/cli/cmd_base.rs Implements GitHub-based version resolution; adds download-only & monitor-only update flows; enhances status output hints.
src/cluster_mgr/config/mono_prometheus.yaml Enables alertmanager target block by default.
src/cluster_mgr/config/alert.rules Adds new alert rules for ops/connection thresholds.
src/cluster_mgr/Cargo.toml Adds flate2, tar, sha2 dependencies for local extract + digest checks.
scripts/test-before-push.sh Removes local E2E run from pre-push; points to CI for full E2E.
README.md Updates install URLs + documents installer auto-upgrade behavior.
install.sh Detects existing state and runs eloqctl upgrade automatically.
doc/upgrade.md Documents auto migration + GitHub Releases version resolution and download-only/monitor update modes.
doc/cluster_mgr.md Updates command docs for new update/versions behavior.
Cargo.lock Updates lockfile for new Rust dependencies/version bump.
.gitignore Ignores local dev tmp dir + node_modules/pycache/Go stress binary.
.github/workflows/test.yml New consolidated workflow including formatting, clippy, unit tests, and Docker E2E stress run.
.github/workflows/quality.yml Removed older split quality workflow.
.githooks/pre-push Updates messaging to reflect “fast” local checks.
Files not reviewed (1)
  • tests/e2e/cmd_stress_ts/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

src/cluster_mgr/config/alert.rules:134

  • The alert description doesn’t match the rule: the expr triggers when connections are > 100, but the description says exceeded 1000. Align the description (and/or threshold) with the expr so on-call operators aren’t misled.
      - alert: eloqkv_active_connections_too_high
        expr: sum by (instance) (redis_connection_count) > 100
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "EloqKV active connections too high"
          description: "EloqKV instance {{ $labels.instance }} active connections have exceeded 1000 for 1 minute. Current value: {{ printf \"%.0f\" $value }}"

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +154 to +166
let segments = url
.path_segments()
.map(|segments| segments.collect_vec())
.unwrap_or_default();
if segments.len() < 8
|| segments[0] != "eloqdata"
|| segments[1] != "eloqkv"
|| segments[2] != "releases"
|| segments[3] != "download"
{
return Ok(None);
}

Comment on lines +407 to 413
let staged_dir = LocalExtractTask::staged_dir_for(&tx_download_url, &tx_home);
if !staged_dir.exists() {
bail!(
"Unable to determine TX service image filename from {}",
effective_tx_image_url
"TX service staged directory not found: {}",
staged_dir.display()
);
}
Comment on lines +282 to +287
if !staged_dir.exists() {
return Err(anyhow!(
"Log service staged directory not found: {}",
staged_dir.display()
));
}
Comment on lines +17 to +37
"startup-nodes": { type: "string", default: "127.0.0.1:6379,127.0.0.1:6380" },
password: { type: "string", default: "testpass" },
"cmd-timeout": { type: "string", default: "5000" },
"progress-interval": { type: "string", default: "5000" },
"key-count": { type: "string", default: "256" },
workers: { type: "string", default: "16" },
inflight: { type: "string", default: "4" },
repeat: { type: "string", default: "10" },
duration: { type: "string", default: "0" },
"tls-insecure": { type: "string", default: "true" },
"read-from-replicas": { type: "string", default: "true" },
"command-set": { type: "string", default: "full" },
"log-prefix": { type: "string", default: "" },
},
});

const startupNodes = args["startup-nodes"]!.split(",").map(s => s.trim());
const password = args.password!;
const CMD_TIMEOUT = parseInt(args["cmd-timeout"]!) * 1000;
const PROGRESS_INTERVAL = parseInt(args["progress-interval"]!) * 1000;
const KEY_COUNT = parseInt(args["key-count"]!);
Comment on lines +296 to +302
--repeat ${REPEAT} \
--progress-interval ${PROGRESS_INTERVAL} \
--key-count ${KEY_COUNT} \
--cmd-timeout ${CMD_TIMEOUT} \
--read-from-replicas 'false' \
--tls-insecure '${TLS_ENABLED}'" \
2>&1 | tee "${SCRIPT_DIR}/cmd-stress-ts.log"
Comment thread tests/README.md
Comment on lines +9 to +14
```sh
cd /home/starrysky/workspace/eloqdata-kernel/eloq_waiter

# Build eloqctl (one time)
scripts/install-dev.sh

Comment on lines +556 to +564
) -> TaskExecutionContext {
let mut executable = indexmap::IndexMap::new();
let mut barrier = vec![];

let download = DownloadTask::from_config(config).unwrap_or_default();
if !download.is_empty() {
barrier.push(download.len());
executable.extend(download);
}
Comment thread src/cluster_mgr/config/alert.rules Outdated
Comment on lines +118 to +126
- alert: eloqkv_instance_ops_too_low
expr: sum by (instance) (rate(redis_command_aggregated_total[1m])) < 100000
for: 1m
labels:
severity: critical
annotations:
summary: "EloqKV instance OPS too low"
description: "EloqKV instance {{ $labels.instance }} OPS has been below 10000 for 1 minutes. Current value: {{ printf \"%.2f\" $value }}"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/cluster_mgr/src/cli/task/upload/data_dir_upload_builder.rs (1)

31-35: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

delete_remote: true against tx_srv_home can wipe unrelated remote files.

With directory sync semantics, --delete will remove destination entries not present in datafarm. Since dest is tx_srv_home, this can delete non-datafarm content.

Suggested fix
 UploadFile {
     source: datafarm.clone(),
-    dest: txsrv_home.clone(),
+    dest: format!("{}/datafarm", txsrv_home),
     extension: "datafarm".to_string(),
     host: host.to_string(),
     copy_dir: true,
-    delete_remote: true,
+    delete_remote: true,
 }

Or keep dest: txsrv_home and set delete_remote: false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/upload/data_dir_upload_builder.rs` around lines
31 - 35, The upload builder is setting delete_remote: true while dest is
txsrv_home, which risks removing unrelated files in the remote txsrv_home;
update the builder used in data_dir_upload_builder.rs to either change dest to a
datafarm-specific subpath (e.g., a path that clearly targets the datafarm
directory) or set delete_remote: false so the sync won't prune files outside the
datafarm; locate the struct/variable initialization referencing dest:
txsrv_home, extension: "datafarm", copy_dir: true and modify the delete_remote
field accordingly.
src/cluster_mgr/src/config/config_base.rs (1)

429-435: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid panic in client_conn when parsing host:port.

split(':') with parts[1]/parts[0] can panic on malformed input. Use rsplit_once(':') and handle invalid entries safely.

Suggested fix
 pub fn client_conn(&self) -> String {
     let bin = self.deployment.client_bin();
-    let host_port = self.deployment.tx_service.tx_host_ports.first().unwrap();
-    let parts: Vec<&str> = host_port.split(':').collect();
-    let port = parts[1].parse::<u16>().unwrap_or(6379);
-    let (host, port) = self.service_endpoint(parts[0], port);
+    let host_port = self
+        .deployment
+        .tx_service
+        .tx_host_ports
+        .first()
+        .cloned()
+        .unwrap_or_else(|| "127.0.0.1:6379".to_string());
+    let (raw_host, raw_port) = host_port
+        .rsplit_once(':')
+        .unwrap_or(("127.0.0.1", "6379"));
+    let port = raw_port.parse::<u16>().unwrap_or(6379);
+    let (host, port) = self.service_endpoint(raw_host, port);
     format!("{bin} -h {host} -p {port}")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/config/config_base.rs` around lines 429 - 435, The
client_conn function currently unwraps parts from host_port.split(':') which can
panic on malformed input; update client_conn to use host_port.rsplit_once(':')
to safely split into (host, port_str) and parse port_str with
parse::<u16>().map_or(DEFAULT_PORT, |p| p) or similar, and handle the None case
by treating the whole host_port as host with a default port (e.g., 6379); remove
all unwraps—specifically change the logic around
deployment.tx_service.tx_host_ports.first().unwrap(), host_port.split,
parts[0]/parts[1], and the parse::<u16>().unwrap_or so errors are gracefully
handled and a sensible fallback connection string is returned by client_conn and
passed into service_endpoint.
🟠 Major comments (19)
.github/workflows/test.yml-46-61 (1)

46-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin mutable GitHub Actions refs, disable checkout token persistence, and sanitize workflow_dispatch inputs

  • Pin all uses: references in .github/workflows/test.yml (actions/checkout@v4, awalsh128/cache-apt-pkgs-action@latest, dtolnay/rust-toolchain@master, swatinem/rust-cache@v2) to full commit SHAs to avoid supply-chain drift.
  • Add persist-credentials: false to actions/checkout (currently missing), to reduce credential/token exposure surface.
  • The workflow_dispatch inputs are interpolated directly into the bash script (workers, duration, repeat, inflight, ELOQKV_VERSION), enabling command/variable injection if inputs contain quotes or shell metacharacters; validate/sanitize to expected formats (e.g., numeric-only) before use.
Suggested hardening diff
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<full_commit_sha_for_v4>
+        with:
+          persist-credentials: false

-      - name: Install build dependencies
-        uses: awalsh128/cache-apt-pkgs-action@latest
+      - name: Install build dependencies
+        uses: awalsh128/cache-apt-pkgs-action@<full_commit_sha>

-      - name: Set up Rust
-        uses: dtolnay/rust-toolchain@master
+      - name: Set up Rust
+        uses: dtolnay/rust-toolchain@<full_commit_sha>

-      - name: Cache Rust build artifacts
-        uses: swatinem/rust-cache@v2
+      - name: Cache Rust build artifacts
+        uses: swatinem/rust-cache@<full_commit_sha>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 46 - 61, Pin all mutable GitHub
Action refs and harden checkout and workflow inputs: replace floating refs
(actions/checkout@v4, awalsh128/cache-apt-pkgs-action@latest,
dtolnay/rust-toolchain@master, swatinem/rust-cache@v2) with their full commit
SHAs, add persist-credentials: false to the actions/checkout step, and
validate/sanitize workflow_dispatch inputs (workers, duration, repeat, inflight,
ELOQKV_VERSION) before interpolating into bash—e.g., enforce numeric-only for
counts/durations and whitelist/escape allowed characters for ELOQKV_VERSION—to
prevent supply-chain drift, credential persistence, and command/variable
injection.
.github/workflows/test.yml-82-85 (1)

82-85: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add strict validation for workflow_dispatch inputs before they’re interpolated into shell commands

workers/duration/repeat/inflight flow from github.event.inputs.* into tests/e2e/cmd_stress_test.sh, where the Go/TS stress paths build a bash -c " ... --workers ${WORKERS} ..." command without quoting, allowing input like 8; <cmd> to execute on the runner. version is also interpolated into the sed-based topology renderer without escaping.

Suggested validation pattern
       - name: Run E2E test
         run: |
           if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
             workers="${{ github.event.inputs.workers || '8' }}"
             duration="${{ github.event.inputs.duration || '20' }}"
             repeat="${{ github.event.inputs.repeat || '8' }}"
             inflight="${{ github.event.inputs.inflight || '4' }}"
+            version="${{ github.event.inputs.version || '1.2.2' }}"
           else
             workers="8"
             duration="20"
             repeat="8"
             inflight="4"
+            version="1.2.2"
           fi
+
+          case "$workers" in ''|*[!0-9]*) echo "invalid workers"; exit 1;; esac
+          case "$duration" in ''|*[!0-9]*) echo "invalid duration"; exit 1;; esac
+          case "$repeat" in ''|*[!0-9]*) echo "invalid repeat"; exit 1;; esac
+          case "$inflight" in ''|*[!0-9]*) echo "invalid inflight"; exit 1;; esac
+          case "$version" in ''|*[!0-9A-Za-z._-]*) echo "invalid version"; exit 1;; esac

           export DOCKER_E2E_DIR="$PWD/tests/docker_ha"
@@
-          export ELOQKV_VERSION="${{ github.event.inputs.version || '1.2.2' }}"
+          export ELOQKV_VERSION="${version}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 82 - 85, Add strict validation and
safer interpolation for workflow_dispatch inputs: ensure workers, duration,
repeat, and inflight are validated as positive integers (e.g., regex ^[0-9]+$)
in the workflow before exporting them to the environment and before invoking
tests/e2e/cmd_stress_test.sh (reject/exit on invalid input), and
validate/whitelist the version string (e.g., semver pattern or specific tags)
before it’s used by the sed topology renderer; also pass these values safely to
shell (quote variables and avoid unquoted bash -c interpolation) or call the
script directly with positional args to prevent command injection (references:
workflow variables workers/duration/repeat/inflight,
tests/e2e/cmd_stress_test.sh, exported env vars
WORKERS/DURATION/REPEAT/INFLIGHT, and the version used by the sed topology
renderer).
src/cluster_mgr/src/cli/cmd_base.rs-898-900 (1)

898-900: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid panic when monitor cluster is supplied only in subcommand args.

expect("monitor cluster is resolved") can panic when parent SubCommand::Monitor.cluster is None but child MonitorCommand::Update.cluster is present (a path you already accept via monitor_cluster). Resolve once via monitor_cluster(...) and reuse that value here instead of expect.

Suggested fix
-        let stop_cmd = SubCommand::Monitor {
-            cluster: Some(match cmd {
-                SubCommand::Update {
-                    cluster: Some(cluster),
-                    ..
-                } => cluster.clone(),
-                SubCommand::Monitor { cluster, .. } => {
-                    cluster.clone().expect("monitor cluster is resolved")
-                }
-                _ => unreachable!(),
-            }),
+        let resolved_cluster = match cmd {
+            SubCommand::Update {
+                cluster: Some(cluster),
+                ..
+            } => cluster.clone(),
+            SubCommand::Monitor { cluster, command } => {
+                Self::monitor_cluster(cluster, command)?
+            }
+            _ => unreachable!(),
+        };
+        let stop_cmd = SubCommand::Monitor {
+            cluster: Some(resolved_cluster.clone()),
             command: MonitorCommand::Stop {
                 cluster: None,
                 components: vec![],
             },
         };
         let start_cmd = SubCommand::Monitor {
-            cluster: Some(match cmd {
-                SubCommand::Update {
-                    cluster: Some(cluster),
-                    ..
-                } => cluster.clone(),
-                SubCommand::Monitor { cluster, .. } => {
-                    cluster.clone().expect("monitor cluster is resolved")
-                }
-                _ => unreachable!(),
-            }),
+            cluster: Some(resolved_cluster),
             command: MonitorCommand::Start {
                 cluster: None,
                 components: vec![],
             },
         };

Also applies to: 914-916

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/cmd_base.rs` around lines 898 - 900, The code
currently forces a panic by calling expect("monitor cluster is resolved") on
SubCommand::Monitor when the parent enum's cluster is None; instead call the
existing monitor_cluster(...) helper once to resolve the cluster (which checks
both the parent SubCommand::Monitor.cluster and the child MonitorCommand
variants like MonitorCommand::Update) and reuse that returned value here and at
the other occurrence (the similar block at the Monitor branch around the later
lines). Replace direct clone/expect usage in the SubCommand::Monitor arm with
the monitor_cluster(...) result so the logic is centralized and avoids panics.
src/cluster_mgr/src/cli/cmd_base.rs-1187-1190 (1)

1187-1190: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix OpenOptions access mode for quiet-mode result files

The quiet-mode OpenOptions create/truncate handlers open task-result / operation-result without any access mode flags, which can lead to write failures when the code later writes to the file. Add .write(true) to these blocks:

Suggested fix
                 let f = fs::OpenOptions::new()
+                    .write(true)
                     .create(true)
                     .truncate(true)
                     .open(self.home.join("task-result"))?;

Apply to: 1215-1218, 1319-1322, 1383-1386, 2255-2258, 2321-2324 (in addition to 1187-1190).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/cmd_base.rs` around lines 1187 - 1190, The
OpenOptions calls that create/truncate quiet-mode result files (calls building
OpenOptions::new().create(true).truncate(true).open(self.home.join("task-result"))
and similar for "operation-result") lack write permission, causing downstream
writes to fail; for each such OpenOptions block (the ones opening
self.home.join("task-result") and self.home.join("operation-result")), add
.write(true) to the builder chain so the file is opened for writing (i.e.,
change OpenOptions::new().create(true).truncate(true) to
OpenOptions::new().create(true).truncate(true).write(true)).
src/cluster_mgr/src/cli/task/local_extract_task.rs-144-167 (1)

144-167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Raw binary extraction hardcodes output filename.

unpack_raw_binary always writes to ALERTMANAGER_WEBHOOK_ADAPTER_BINARY regardless of the actual input file. If this method is called for a different raw binary, the output filename will be incorrect.

Consider deriving the output filename from the archive path or passing it as a parameter.

Suggested fix
     fn unpack_raw_binary(&self, archive_path: &Path, destination: &Path) -> Result<()> {
         let file = File::open(archive_path)?;
         let binary_len = file.metadata()?.len();
         self.pg_bar.set_length(binary_len);
         self.pg_bar.set_position(0);
         self.pg_bar.set_message(format!(
             "{} Extracting...",
             Self::archive_display_name(archive_path)
         ));
         let mut reader = self.pg_bar.wrap_read(file);
-        let output_path = destination.join(ALERTMANAGER_WEBHOOK_ADAPTER_BINARY);
+        let output_name = archive_path
+            .file_name()
+            .and_then(|n| n.to_str())
+            .unwrap_or(ALERTMANAGER_WEBHOOK_ADAPTER_BINARY);
+        let output_path = destination.join(output_name);
         let mut out_file = File::create(&output_path)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/local_extract_task.rs` around lines 144 - 167,
unpack_raw_binary currently hardcodes the output filename to
ALERTMANAGER_WEBHOOK_ADAPTER_BINARY causing incorrect filenames for other
binaries; change it to derive the output filename from archive_path (e.g., use
archive_path.file_name() or file_stem() plus extension) or add an output_name
parameter to unpack_raw_binary, then build output_path =
destination.join(derived_name) instead of joining
ALERTMANAGER_WEBHOOK_ADAPTER_BINARY; update any callers of unpack_raw_binary to
pass the desired name if you choose the parameter approach and keep the
progress/message text using the derived name.
src/cluster_mgr/src/cli/task/unpack_file_task.rs-38-40 (1)

38-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Narrow the linux.zip special-case mapping.

Line 38 currently maps any linux.zip artifact to alertmanager-webhook-adapter. That can send unrelated archives to the wrong unpack target and break rollout when another component also ships a generic linux.zip.

Suggested fix
-    if raw_file_name.contains("PrometheusAlert") || raw_file_name == "linux.zip" {
+    if raw_file_name.contains("PrometheusAlert") {
         return "alertmanager-webhook-adapter".to_string();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/unpack_file_task.rs` around lines 38 - 40, The
current check maps any raw_file_name == "linux.zip" to
"alertmanager-webhook-adapter", which is too broad; update the conditional in
the function that returns the component name (the block that currently checks
raw_file_name.contains("PrometheusAlert") || raw_file_name == "linux.zip") so
that the "linux.zip" branch is only taken for alertmanager artifacts (for
example require raw_file_name.contains("alertmanager") or match specific names
like "alertmanager-linux.zip"/"alertmanager-*.zip" instead of plain
"linux.zip"), keeping the existing PrometheusAlert check and returning
"alertmanager-webhook-adapter" only when the filename clearly identifies an
alertmanager artifact.
src/cluster_mgr/src/cli/task/upload/monitor_upload_builder.rs-30-37 (1)

30-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard remove_dir_all target to stay under upload root.

staged_dir includes cluster_name; if it contains traversal components, this can delete outside the intended upload workspace. Validate/canonicalize before removal.

Also applies to: 55-63

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/upload/monitor_upload_builder.rs` around lines
30 - 37, The deletion of staged_dir before recreating it is unsafe because
staged_dir is built from upload_dir(), config.deployment.cluster_name,
MONITOR_DIR and dir_name; canonicalize/normalize and verify that the resolved
path is a descendant of the upload root before calling fs::remove_dir_all.
Update the logic around staged_dir creation in monitor_upload_builder.rs (the
block that constructs staged_dir and the later similar block at the 55-63
region) to canonicalize both upload_dir() and staged_dir (or use join then
canonicalize), check that staged_dir.starts_with(canonicalized_upload_root) (or
equivalent Path::starts_with) and only then call fs::remove_dir_all(&staged_dir)
otherwise return an error; keep the subsequent fs::create_dir_all call unchanged
once the safety check passes.
src/cluster_mgr/src/cli/task/upload/upload_task_builder.rs-83-85 (1)

83-85: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid shell injection via unescaped dest in --rsync-path.

format!("mkdir -p {} && rsync", upload_file.dest.trim()) is executed by a remote shell. If dest contains shell metacharacters, command execution can be altered.

Suggested hardening
- "--rsync-path".to_string(),
- format!("mkdir -p {} && rsync", upload_file.dest.trim()),
+ "--rsync-path".to_string(),
+ format!("mkdir -p '{}' && rsync", upload_file.dest.trim().replace('\'', "'\\''")),

Also reject unsafe destination patterns early (e.g., newline/control chars).

Also applies to: 96-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/upload/upload_task_builder.rs` around lines 83 -
85, The "--rsync-path" argument currently interpolates upload_file.dest directly
into a shell command (format!("mkdir -p {} && rsync", upload_file.dest.trim())),
which allows shell injection; change this by either (a) performing the mkdir on
the remote host separately (e.g., run ssh remote "mkdir -p -- <dest>" without
embedding into --rsync-path) or (b) safely shell-quoting/escaping
upload_file.dest before embedding (use a proven quoting helper like shlex::quote
or implement single-quote escaping and reject inputs with control/newline
chars). Update the code that builds "--rsync-path" and any similar occurrences
(the other block at lines ~96-101) to validate/deny unsafe destination patterns
(newline/control chars) and only pass a safely quoted dest into the rsync-path
string or move the mkdir out of the rsync-path altogether.
src/cluster_mgr/src/config/monitor.rs-538-601 (1)

538-601: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Route can point to a missing Alertmanager receiver.

receiver_name is decided from raw URL presence, but receiver creation depends on parsed tokens. With invalid URLs, route points to alertmanager_webhook_adapter that is never defined.

Suggested fix
-        let receiver_name = if self
-            .alertmanager
-            .as_ref()
-            .and_then(|alertmanager| alertmanager.feishu_robot_urls.as_ref())
-            .is_some_and(|urls| !urls.is_empty())
-        {
-            "alertmanager_webhook_adapter"
-        } else {
-            "null"
-        };
+        let mut receiver_name = "null";
@@
-            if !webhook_entries.is_empty() {
+            if !webhook_entries.is_empty() {
+                receiver_name = "alertmanager_webhook_adapter";
                 let mut receiver = serde_yaml::Mapping::new();
@@
-        if let Some(route) = alertmanager_config_map
+        if let Some(route) = alertmanager_config_map
             .get_mut("route")
             .and_then(|v| v.as_mapping_mut())
         {
             route.insert(
                 Value::String("receiver".to_string()),
                 Value::String(receiver_name.to_string()),
             );
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/config/monitor.rs` around lines 538 - 601, The route may
reference "alertmanager_webhook_adapter" based on raw feishu_robot_urls even
when no valid tokens are parsed, causing a missing receiver; compute the parsed
webhook_entries/tokens first (using Self::extract_feishu_robot_token on
feishu_robot_urls) and then set receiver_name to "alertmanager_webhook_adapter"
only if webhook_entries is non-empty (otherwise "null"), and ensure the route
insertion uses that computed receiver_name and that receivers is populated from
the same webhook_entries variable (symbols: feishu_robot_urls,
extract_feishu_robot_token, webhook_entries, receiver_name, receivers, route).
tests/docker_ha/Dockerfile.stress_go-1-16 (1)

1-16: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run stress container as a non-root user.

This image currently runs as root, which increases risk and can leave root-owned files in the mounted repo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/docker_ha/Dockerfile.stress_go` around lines 1 - 16, The Dockerfile
runs as root; add steps to create a non-root user and switch to it before the
final CMD: create a group/user (e.g., groupadd/useradd or addgroup/adduser) with
a fixed non-root UID/GID (commonly 1000), chown the WORKDIR
(/workspace/eloq_waiter) and any needed runtime directories to that user, and
add a USER directive to switch to that account so the container executes CMD
["sleep", "infinity"] as non-root; reference the WORKDIR and CMD lines when
making these edits and ensure file ownership is updated so mounted repo files
aren't left owned by root.
tests/docker_ha/Dockerfile.stress-1-21 (1)

1-21: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run stress container as a non-root user.

This image runs as root by default, which can cause host-mounted workspace permission issues and weakens container isolation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/docker_ha/Dockerfile.stress` around lines 1 - 21, The Dockerfile
currently runs as root; modify it to create and switch to a non-root user (e.g.,
create a user like stressuser), ensure ownership of the WORKDIR
(/workspace/eloq_waiter) and any copied files
(/tmp/cmd_stress_py_requirements.txt) is set to that user (chown), and add a
USER instruction before CMD so the container runs unprivileged; locate the
relevant Dockerfile directives (FROM, RUN, COPY, WORKDIR, CMD) and add a RUN
that creates the user/group and adjusts ownership, then add USER stressuser.
tests/docker_ha/Dockerfile.stress_ts-1-16 (1)

1-16: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run stress container as a non-root user.

Default root execution is avoidable here and can create permission drift on mounted files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/docker_ha/Dockerfile.stress_ts` around lines 1 - 16, The Dockerfile
runs the stress container as root; modify it to create a non-root user (e.g.,
add a user/group like "eloq" or "appuser"), chown the WORKDIR
(/workspace/eloq_waiter) to that user, and switch to that user with a USER
instruction before the CMD so the container runs unprivileged; update the RUN
sequence that sets up packages to also create the user and set permissions, and
ensure WORKDIR and CMD remain correct for the new user.
tests/docker_env.sh-82-87 (1)

82-87: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

compose_down() ignores compose override and can leave test resources behind.

You added override support in compose(), but cleanup bypasses it. With COMPOSE_OVERRIDE_FILE, down may miss override-defined services/networks/volumes.

Suggested fix
 compose_down() {
-    if docker compose version >/dev/null 2>&1; then
-        timeout --kill-after=5s "${CLEANUP_TIMEOUT_SECONDS}s" docker compose -f "${DOCKER_E2E_DIR}/docker-compose.yaml" down -v --remove-orphans >/dev/null 2>&1 || true
-    else
-        timeout --kill-after=5s "${CLEANUP_TIMEOUT_SECONDS}s" docker-compose -f "${DOCKER_E2E_DIR}/docker-compose.yaml" down -v --remove-orphans >/dev/null 2>&1 || true
-    fi
+    timeout --kill-after=5s "${CLEANUP_TIMEOUT_SECONDS}s" \
+        compose down -v --remove-orphans >/dev/null 2>&1 || true
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/docker_env.sh` around lines 82 - 87, compose_down() currently calls
docker compose/docker-compose down with only the base
"${DOCKER_E2E_DIR}/docker-compose.yaml", so any COMPOSE_OVERRIDE_FILE
services/volumes/networks can be left running; modify compose_down() to mirror
compose() by checking if COMPOSE_OVERRIDE_FILE is set/non-empty and, if so, add
a second -f "${COMPOSE_OVERRIDE_FILE}" argument to the docker compose (and
docker-compose) down invocation, preserving timeout and existing flags (down -v
--remove-orphans) and behavior across both branches while keeping
CLEANUP_TIMEOUT_SECONDS and DOCKER_E2E_DIR intact.
tests/e2e/cmd_stress_py/main.py-56-60 (1)

56-60: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on invalid numeric args (key-count, workers, inflight, repeat).

Line 316 can raise ZeroDivisionError when --key-count is 0. Non-positive worker/inflight/repeat values should also be rejected up front.

Proposed fix
 args = parser.parse_args()

 if not args.startup_node:
     print("FAIL: at least one --startup-node is required", flush=True)
     sys.exit(1)
+if args.key_count <= 0 or args.workers <= 0 or args.inflight <= 0 or args.repeat <= 0:
+    print(
+        "FAIL: --key-count, --workers, --inflight, and --repeat must all be > 0",
+        flush=True,
+    )
+    sys.exit(1)

Also applies to: 304-317

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/cmd_stress_py/main.py` around lines 56 - 60, After parsing
arguments (after args = parser.parse_args()), validate that numeric args
key_count, workers, inflight, and repeat are positive integers (>0); if any are
non-positive, print a clear FAIL message and sys.exit(1). This prevents the
ZeroDivisionError at the calculation using key_count (referenced around line
316) and rejects invalid worker/inflight/repeat inputs up front—implement checks
against args.key_count, args.workers, args.inflight, and args.repeat immediately
after startup_node validation in main.
tests/e2e/cmd_stress_go/main.go-565-578 (1)

565-578: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate CLI bounds before launching workers.

--key-count 0 will panic at Line 577 (keyIdx % keyMod). Also, --startup-nodes "" passes the current len(addrs) check and fails later with a less clear error.

Proposed fix
  flag.Parse()
+ if workers <= 0 || inflight <= 0 || keyCount <= 0 || repeat <= 0 {
+     logger.Fatalf("invalid args: workers=%d inflight=%d key-count=%d repeat=%d (all must be > 0)",
+         workers, inflight, keyCount, repeat)
+ }

  addrs := strings.Split(startupNodes, ",")
- for i := range addrs {
-     addrs[i] = strings.TrimSpace(addrs[i])
- }
- if len(addrs) < 1 {
-     logger.Fatalf("need at least 1 startup node, got %q", startupNodes)
- }
+ cleaned := make([]string, 0, len(addrs))
+ for _, a := range addrs {
+     a = strings.TrimSpace(a)
+     if a != "" {
+         cleaned = append(cleaned, a)
+     }
+ }
+ if len(cleaned) < 1 {
+     logger.Fatalf("need at least 1 non-empty startup node, got %q", startupNodes)
+ }
+ addrs = cleaned

Also applies to: 616-642

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/cmd_stress_go/main.go` around lines 565 - 578, The worker loop in
stressWorker (and the similar block at lines 616-642) can panic when keyMod is
zero and will later fail confusingly if startup nodes are empty; validate CLI
bounds before launching workers by: ensure the key count / keyMod argument is >
0 (in the caller or before spawning stressWorker) and bail out with a clear
error/log if it is zero, and verify startup nodes (the addrs slice) is non-empty
and does not contain only an empty string before using it, returning/ cancelling
early with a descriptive error message so the code never reaches the modulo or
downstream failure points.
tests/e2e/cmd_stress_ts/main.ts-19-20 (1)

19-20: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Default timeout/progress values are scaled twice.

Defaults are set to "5000" but then multiplied by 1000, so default behavior becomes ~5000 seconds instead of 5 seconds.

Proposed fix
-    "cmd-timeout": { type: "string", default: "5000" },
-    "progress-interval": { type: "string", default: "5000" },
+    "cmd-timeout": { type: "string", default: "5" },
+    "progress-interval": { type: "string", default: "5" },

Also applies to: 35-36

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/cmd_stress_ts/main.ts` around lines 19 - 20, The defaults for the
CLI options "cmd-timeout" and "progress-interval" are set to "5000" (strings)
but later multiplied by 1000, producing ~5000 seconds instead of 5 seconds;
update the default values to "5" (or 5) for both "cmd-timeout" and
"progress-interval" in the options declaration in
tests/e2e/cmd_stress_ts/main.ts (also apply the same change for the other
occurrence at lines ~35-36), or alternatively remove the extra *1000
multiplication where the values are converted, so the final timeout/progress
values represent seconds correctly. Ensure the option keys "cmd-timeout" and
"progress-interval" and any variables that read them (e.g.,
cmdTimeout/progressInterval) are consistently treated as numeric seconds.
tests/e2e/cmd_stress_ts/main.ts-326-339 (1)

326-339: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Abort early if master discovery fails.

If no master is parsed, mkClient(masterAddr) receives an empty address and fails later with a less actionable error.

Proposed fix
   for (const line of nodesInfo.split("\n")) {
     const fields = line.trim().split(/\s+/);
     if (fields.length < 3) continue;
     const addr = fields[1].split("@")[0];
     if (fields[2].includes("master") && !masterAddr) masterAddr = addr;
     if (fields[2].includes("slave") && !replicaAddr) replicaAddr = addr;
   }
+  if (!masterAddr) {
+    throw new Error("no master found in CLUSTER NODES");
+  }
   console.log(P(`master=${masterAddr} replica=${replicaAddr}`));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/cmd_stress_ts/main.ts` around lines 326 - 339, The code parses
nodesInfo into masterAddr but doesn't abort if no master is found, causing
mkClient(masterAddr) to be called with an empty address; after the loop that
sets masterAddr and replicaAddr add an early check that masterAddr is non-empty
and if it is, throw an error or call process.exit with a clear message (e.g.,
"failed to discover master from cluster nodes"); keep the existing
mkClient(masterAddr) and replica logic but ensure the check prevents mkClient
from getting an empty masterAddr and references the variables masterAddr,
replicaAddr, mkClient, and nodesInfo to locate the code to change.
tests/e2e/cmd_stress_test.sh-195-200 (1)

195-200: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor TLS_ENABLED in topology discovery.

discover_master always creates a TLS Redis client. With TLS_ENABLED=0, this path can fail even when the cluster is healthy.

Proposed fix
 nodes_info=$(docker compose -f "${DOCKER_E2E_DIR}/docker-compose.yaml" exec -T stress-python python3 -c "
 import ssl
-TLS={'ssl':True,'ssl_cert_reqs':ssl.CERT_NONE,'ssl_check_hostname':False}
 from redis import Redis
-r=Redis(host='${N1}',port=6379,password='${PASSWD}',socket_timeout=5,**TLS)
+tls_enabled='${TLS_ENABLED}'
+kwargs = {'ssl': True, 'ssl_cert_reqs': ssl.CERT_NONE, 'ssl_check_hostname': False} if tls_enabled == '1' else {}
+r=Redis(host='${N1}',port=6379,password='${PASSWD}',socket_timeout=5,**kwargs)
 print(r.execute_command('CLUSTER','NODES').decode())
 r.close()
 " 2>/dev/null) || { echo "FAIL: cannot connect to cluster for discovery"; return 1; }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/cmd_stress_test.sh` around lines 195 - 200, discover_master
currently always builds a TLS Redis client (the TLS dict and passing **TLS to
Redis), ignoring the TLS_ENABLED flag; change the logic in discover_master so
that it checks TLS_ENABLED and only constructs/passes the TLS-related dict
(TLS={'ssl':True,...}) to Redis when TLS_ENABLED is set (non-zero/true),
otherwise create the Redis client without TLS kwargs; ensure the code paths that
reference TLS, the TLS variable, and the Redis(...) instantiation are guarded by
that check so topology discovery works for both TLS_ENABLED=0 and TLS-enabled
clusters.
tests/e2e/devctl.sh-62-66 (1)

62-66: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden control_exec bash -lc interpolation in tests/e2e/devctl.sh

tests/e2e/devctl.sh injects runtime ${cluster}/${url} directly into the remote bash -lc "..." string (lines 62-66, 72-75, 82-84). If either value contains quotes/metacharacters, this can break parsing or enable unintended command execution. export_topology also interpolates ${cluster} into /home/eloq/${cluster}-export.yaml without quoting (around lines 91-92).

Suggested hardening pattern
-    control_exec bash -lc "
-        eloqctl monitor update --cluster '${cluster}' --component grafana --url '${url}'
-    "
+    control_exec bash -lc '
+        cluster="$1"
+        url="$2"
+        eloqctl monitor update --cluster "$cluster" --component grafana --url "$url"
+    ' _ "$cluster" "$url"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/devctl.sh` around lines 62 - 66, The remote shell invocations in
tests/e2e/devctl.sh (the control_exec bash -lc "..." block that calls eloqctl
stop/remove/launch and the surrounding occurrences) and the export_topology
function currently inline ${cluster}/${url} unquoted, which can break or allow
injection; fix by shell-escaping or robustly quoting these values before
building the remote command (e.g. use printf '%q' or wrap values in single
quotes while escaping embedded single quotes) and pass them as environment
variables into control_exec rather than direct interpolation, and update
export_topology to quote the filename path /home/eloq/${cluster}-export.yaml
(escape ${cluster} the same way) so all uses of cluster/url in control_exec and
export_topology are safe against quotes and shell metacharacters.
🟡 Minor comments (12)
doc/upgrade.md-60-60 (1)

60-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align monitor-update command syntax with the command reference.

This page uses --cluster <cluster>, while doc/cluster_mgr.md shows positional <cluster>. Please standardize to one canonical form across docs to avoid copy/paste failures.

Also applies to: 66-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doc/upgrade.md` at line 60, The docs use a flag form for the cluster in the
monitor-update example but the command reference uses a positional parameter;
update the example command(s) to match the canonical positional syntax by
replacing occurrences of "--cluster <cluster>" with the positional "<cluster>"
in the `eloqctl monitor update` examples (e.g., change `eloqctl monitor update
--cluster <cluster> --component grafana` to `eloqctl monitor update <cluster>
--component grafana`) so both instances (line 60 and the one at 66) are
consistent with doc/cluster_mgr.md.
doc/cluster_mgr.md-57-57 (1)

57-57: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape pipe characters in command cell to keep table valid.

monitor start|stop|status <cluster> should escape | in table content; otherwise the row renders with extra columns.

Suggested fix
-| `monitor start|stop|status <cluster>` | Manage Prometheus, Grafana, and node exporter when monitor config is present. |
+| `monitor start\|stop\|status <cluster>` | Manage Prometheus, Grafana, and node exporter when monitor config is present. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doc/cluster_mgr.md` at line 57, The table row containing the command label
"monitor start|stop|status <cluster>" uses unescaped pipe characters which break
the Markdown table; update that cell to escape the pipe characters (e.g.,
replace "|" with "\|") so the cell renders as a single column. Locate the table
row that currently contains the literal monitor start|stop|status <cluster>
string and change it to use escaped pipes (monitor start\|stop\|status
<cluster>) to keep the table valid.
src/cluster_mgr/config/alert.rules-127-134 (1)

127-134: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix threshold mismatch in alert description.

Line 134 says “exceeded 1000” but Line 128 triggers at > 100. This will mislead on-call triage.

Suggested fix
-          description: "EloqKV instance {{ $labels.instance }} active connections have exceeded 1000 for 1 minute. Current value: {{ printf \"%.0f\" $value }}"
+          description: "EloqKV instance {{ $labels.instance }} active connections have exceeded 100 for 1 minute. Current value: {{ printf \"%.0f\" $value }}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/config/alert.rules` around lines 127 - 134, The alert
eloqkv_active_connections_too_high has a description that says “exceeded 1000”
but the expr triggers at > 100; update the description (and summary if needed)
to match the actual threshold (e.g., change “exceeded 1000” to “exceeded 100”)
or alternatively change the expr to > 1000 if the intended threshold is 1000;
locate the alert by name eloqkv_active_connections_too_high and make the
description and any annotations consistent with the expr value.
src/cluster_mgr/src/cli/task/group/db_cluster_ctrl_group.rs-279-287 (1)

279-287: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace expect() with proper error propagation.

Using expect("stop nodes error") can cause a panic if the task generation fails. Since this function returns a tuple (not Result), consider either propagating the error by changing the return type or logging and skipping the failed tasks gracefully.

Proposed fix
-                let stop_nodes = EloqTxCtlTask::from_config_with_channel(
-                    cmd.clone(),
-                    config,
-                    ServerType::Node,
-                    None,
-                )
-                .expect("stop nodes error");
-                barrier.push(stop_nodes.len());
-                executable.extend(stop_nodes);
+                match EloqTxCtlTask::from_config_with_channel(
+                    cmd.clone(),
+                    config,
+                    ServerType::Node,
+                    None,
+                ) {
+                    Ok(stop_nodes) => {
+                        barrier.push(stop_nodes.len());
+                        executable.extend(stop_nodes);
+                    }
+                    Err(err) => {
+                        warn!("Failed to generate stop tasks for nodes: {}", err);
+                    }
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/group/db_cluster_ctrl_group.rs` around lines 279
- 287, Replace the panicking expect call on
EloqTxCtlTask::from_config_with_channel with proper error handling: call
EloqTxCtlTask::from_config_with_channel(cmd.clone(), config, ServerType::Node,
None) and match its result — on Ok(stop_nodes) proceed with
barrier.push(stop_nodes.len()) and executable.extend(stop_nodes); on Err(e)
either return Err(e) from the enclosing function (update its signature to a
Result) or log the error and skip adding tasks (so the code continues without
panicking). Ensure you reference the same symbols
(EloqTxCtlTask::from_config_with_channel, stop_nodes, barrier.push,
executable.extend) when making the change.
src/cluster_mgr/src/cli/task/local_extract_task.rs-96-111 (1)

96-111: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

unwrap() on cache_dir() may panic.

Lines 99 and 106 call url.cache_dir().unwrap() which could panic if cache_dir() returns None. Consider using ok_or_else to return a proper error.

Suggested fix
     pub fn cached_archive_path(url: &DownloadUrl) -> PathBuf {
         match url {
             DownloadUrl::Local(_) => download_dir().join(url.file_name()),
-            DownloadUrl::Remote(_) => PathBuf::from(url.cache_dir().unwrap()).join(url.file_name()),
+            DownloadUrl::Remote(_) => {
+                let cache = url.cache_dir().unwrap_or_else(|| download_dir().to_string_lossy().to_string());
+                PathBuf::from(cache).join(url.file_name())
+            }
         }
     }

Or return Result<PathBuf> to allow proper error propagation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/local_extract_task.rs` around lines 96 - 111,
Both cached_archive_path and extract_root_for call url.cache_dir().unwrap()
which can panic; change them to handle the None case instead of unwrapping.
Update cached_archive_path and extract_root_for to return Result<PathBuf, E> (or
propagate an existing error type) and use url.cache_dir().ok_or_else(|| /*
construct error */)? to convert None into an Err; then build the path with
PathBuf::from(cache_dir) when DownloadUrl::Remote, keeping DownloadUrl::Local
using download_dir(), and continue to use Self::archive_stem(&url.file_name())
in extract_root_for.
src/cluster_mgr/src/cli/task/group/monitor_ctl_group.rs-69-74 (1)

69-74: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider handling None cluster gracefully instead of panicking.

The expect("monitor cluster is resolved") on Line 71 will panic if cluster is None. While the comment indicates an expectation that the caller resolves the cluster beforehand, a panic here could crash the application if this invariant is ever violated by a code path change.

Suggested improvement
-        let cluster_name = match &cmd_arg {
-            SubCommand::Monitor { cluster, .. } => {
-                cluster.clone().expect("monitor cluster is resolved")
-            }
-            _ => unreachable!(),
-        };
+        let cluster_name = match &cmd_arg {
+            SubCommand::Monitor { cluster, .. } => cluster
+                .clone()
+                .ok_or_else(|| anyhow::anyhow!("Monitor command requires cluster to be specified"))?,
+            _ => unreachable!(),
+        };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/group/monitor_ctl_group.rs` around lines 69 -
74, The current match on cmd_arg uses SubCommand::Monitor { cluster, .. } and
calls cluster.clone().expect("monitor cluster is resolved"), which will panic if
cluster is None; change this to handle None gracefully by matching the Option
and returning an error or printing a user-friendly message instead of
panicking—e.g., when cluster is Some(name) assign to cluster_name, and when None
return a Result::Err or call the CLI error/exit path used elsewhere in this
module so the function (where cmd_arg is inspected) does not unwind the whole
process.
src/cluster_mgr/src/cli/task/upload/monitor_upload_builder.rs-305-307 (1)

305-307: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add doc comments for new public API.

pub fn build_for_cmd should be documented with /// to describe intent and expected cmd values.

As per coding guidelines, "Document public API with doc comments (///)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/upload/monitor_upload_builder.rs` around lines
305 - 307, Add a doc comment for the public function
MonitorInfraConfUploadBuilder::build_for_cmd describing its purpose, the
expected values/format of the cmd parameter, and the return (IndexMap<TaskId,
TaskInstance>), using triple-slash (///) style comments placed immediately above
the fn declaration so it becomes part of the public API documentation.
src/cluster_mgr/src/config/deployment.rs-1491-1495 (1)

1491-1495: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate event_dispatcher_num to avoid zero dispatcher threads.

hw.event_dispatcher_num = Some(0) currently writes 0 to config, which is not a valid runtime setting for dispatcher threads.

Suggested fix
 let key = "event_dispatcher_num";
 let val = set_by_user!(ini.get(SECTION_LOCAL, key), u16);
 if val.is_none() {
-    let core_io = hw.event_dispatcher_num.unwrap_or(core_tx.div_ceil(8));
+    let core_io = hw
+        .event_dispatcher_num
+        .filter(|v| *v > 0)
+        .unwrap_or_else(|| core_tx.div_ceil(8).max(1));
     ini.set(SECTION_LOCAL, key, Some(core_io.to_string()));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/config/deployment.rs` around lines 1491 - 1495, Validate
and reject zero for event_dispatcher_num before writing it: when reading
hw.event_dispatcher_num (the value used to populate key "event_dispatcher_num"
via val and ini.set), treat Some(0) as invalid and fall back to the computed
default (core_io = hw.event_dispatcher_num.unwrap_or(core_tx.div_ceil(8))
behavior) instead of writing "0" into ini; i.e., if hw.event_dispatcher_num ==
Some(0) ignore it and use core_io.to_string() when calling
ini.set(SECTION_LOCAL, key, ...).
tests/README.md-10-10 (1)

10-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace machine-specific absolute paths in docs.

The hardcoded /home/starrysky/... paths are not portable and will fail for other contributors. Please switch to repo-relative instructions (for example cd <repo-root>).

Also applies to: 59-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/README.md` at line 10, Replace the machine-specific absolute path
string "/home/starrysky/workspace/eloqdata-kernel/eloq_waiter" in
tests/README.md with a repo-relative instruction such as "cd
<repo-root>/eloq_waiter" or simply "cd eloq_waiter"; update the other occurrence
noted (line 59) similarly so all documentation uses portable, relative paths
instead of hardcoded user home paths.
tests/docker_env.sh-141-144 (1)

141-144: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape sed replacement value before injecting ELOQCTL_FEISHU_ROBOT_URL.

Raw URL interpolation can corrupt output when the value contains & (or delimiter-sensitive chars), which is a realistic webhook URL case.

Suggested fix
 render_topology_for_control() {
     local source_topology="$1"
     local rendered_topology="$2"
+    local escaped_feishu_url
+    escaped_feishu_url=$(printf '%s' "${ELOQCTL_FEISHU_ROBOT_URL}" | sed 's/[&|]/\\&/g')
     sed -e "s|\${ELOQCTL_DOCKER_SSH_KEY}|${ELOQCTL_CONTROL_SSH_KEY}|g" \
         -e "s|\${ELOQKV_VERSION}|${ELOQKV_VERSION}|g" \
-        -e "s|\${ELOQCTL_FEISHU_ROBOT_URL}|${ELOQCTL_FEISHU_ROBOT_URL}|g" \
+        -e "s|\${ELOQCTL_FEISHU_ROBOT_URL}|${escaped_feishu_url}|g" \
         "${source_topology}" > "${rendered_topology}"
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/docker_env.sh` around lines 141 - 144, The sed replacement injects
ELOQCTL_FEISHU_ROBOT_URL raw, which will break when the URL contains & or
delimiter-sensitive chars; before running sed, create an escaped version (e.g.
ELOQCTL_FEISHU_ROBOT_URL_ESC) that escapes ampersands and any chosen sed
delimiter characters, then use that escaped variable in the sed -e
"s|...|${ELOQCTL_FEISHU_ROBOT_URL_ESC}|g" replacement (keep using
source_topology and rendered_topology as targets); this ensures special chars in
ELOQCTL_FEISHU_ROBOT_URL do not corrupt the rendered_topology.
tests/e2e/devctl.sh-33-39 (1)

33-39: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

CLUSTER_NAME override is only partially applied.

stop/remove/status/export use cluster_name(), but launch relies on topology content. If CLUSTER_NAME is overridden, commands can target different clusters.

Also applies to: 59-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/devctl.sh` around lines 33 - 39, The CLUSTER_NAME override is only
applied by cluster_name() but not when rendering topology for launch, causing
launch to target a different cluster; update the topology rendering and the
launch flow to use the same cluster_name() helper (or read
${CLUSTER_NAME:-${DEFAULT_CLUSTER}}) so all commands (launch, stop, remove,
status, export) reference the identical cluster name; locate the topology
rendering logic (the function or code that builds the topology used by launch)
and replace hardcoded/default usage with a call to cluster_name() or the same
expansion so overrides are honored everywhere.
tests/install_control_eloqctl.sh-64-68 (1)

64-68: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use INSTALL_ROOT when generating the eloqctl wrapper.

The wrapper currently hardcodes /home/eloq/.eloqctl/bin/cluster_mgr, which can diverge from CONTROL_ELOQCTL_HOME.

Proposed fix
-    printf '%s\n' '#!/bin/bash' \
-        'export HOME=/home/eloq' \
-        'export ELOQCTL_HOME=/home/eloq/.eloqctl' \
-        'exec /home/eloq/.eloqctl/bin/cluster_mgr "\$@"' \
+    printf '%s\n' '#!/bin/bash' \
+        'export HOME=/home/eloq' \
+        'export ELOQCTL_HOME='"'${INSTALL_ROOT}'" \
+        'exec '"${INSTALL_ROOT}"'/bin/cluster_mgr "\$@"' \
         > /usr/local/bin/eloqctl
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/install_control_eloqctl.sh` around lines 64 - 68, The eloqctl wrapper
currently hardcodes the path '/home/eloq/.eloqctl/bin/cluster_mgr'; change the
printf block that writes /usr/local/bin/eloqctl to use the INSTALL_ROOT (or
CONTROL_ELOQCTL_HOME) variable when constructing the exec line so the wrapper
uses "${INSTALL_ROOT}/.eloqctl/bin/cluster_mgr" or
"${CONTROL_ELOQCTL_HOME}/bin/cluster_mgr" (and preserve the existing exported
HOME/ELOQCTL_HOME lines and quoting for "$@") to avoid divergence from
CONTROL_ELOQCTL_HOME.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: faa35615-d3e3-4508-9b8d-32b308fe2456

📥 Commits

Reviewing files that changed from the base of the PR and between 565c72e and 5c276bc.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • tests/e2e/cmd_stress_go/go.sum is excluded by !**/*.sum
  • tests/e2e/cmd_stress_ts/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (81)
  • .githooks/pre-push
  • .github/workflows/quality.yml
  • .github/workflows/test.yml
  • .gitignore
  • README.md
  • doc/cluster_mgr.md
  • doc/upgrade.md
  • install.sh
  • scripts/test-before-push.sh
  • src/cluster_mgr/Cargo.toml
  • src/cluster_mgr/config/alert.rules
  • src/cluster_mgr/config/feishu.zh.tmpl
  • src/cluster_mgr/config/grafana_config_v13.ini
  • src/cluster_mgr/config/mono_alertmanager.yaml
  • src/cluster_mgr/config/mono_prometheus.yaml
  • src/cluster_mgr/config/runtime_deps_ubuntu
  • src/cluster_mgr/src/cli/cmd_base.rs
  • src/cluster_mgr/src/cli/mod.rs
  • src/cluster_mgr/src/cli/reconcile.rs
  • src/cluster_mgr/src/cli/task/check_task.rs
  • src/cluster_mgr/src/cli/task/download_task.rs
  • src/cluster_mgr/src/cli/task/eloq_tx_ctl_task.rs
  • src/cluster_mgr/src/cli/task/group/backup_group.rs
  • src/cluster_mgr/src/cli/task/group/check_group.rs
  • src/cluster_mgr/src/cli/task/group/db_cluster_ctrl_group.rs
  • src/cluster_mgr/src/cli/task/group/deployment_group.rs
  • src/cluster_mgr/src/cli/task/group/launch_group.rs
  • src/cluster_mgr/src/cli/task/group/monitor_ctl_group.rs
  • src/cluster_mgr/src/cli/task/group/remove_group.rs
  • src/cluster_mgr/src/cli/task/group/scale_group.rs
  • src/cluster_mgr/src/cli/task/group/scale_log_group.rs
  • src/cluster_mgr/src/cli/task/local_extract_task.rs
  • src/cluster_mgr/src/cli/task/mod.rs
  • src/cluster_mgr/src/cli/task/monitor_ctl_task.rs
  • src/cluster_mgr/src/cli/task/redis_op_task.rs
  • src/cluster_mgr/src/cli/task/rolling_upgrade/mod.rs
  • src/cluster_mgr/src/cli/task/rolling_upgrade/steps.rs
  • src/cluster_mgr/src/cli/task/task_base.rs
  • src/cluster_mgr/src/cli/task/task_controller.rs
  • src/cluster_mgr/src/cli/task/task_utils.rs
  • src/cluster_mgr/src/cli/task/unpack_file_task.rs
  • src/cluster_mgr/src/cli/task/upload/data_dir_upload_builder.rs
  • src/cluster_mgr/src/cli/task/upload/eloq_upload_builder.rs
  • src/cluster_mgr/src/cli/task/upload/mod.rs
  • src/cluster_mgr/src/cli/task/upload/monitor_upload_builder.rs
  • src/cluster_mgr/src/cli/task/upload/proxy_upload_builder.rs
  • src/cluster_mgr/src/cli/task/upload/tx_conf_upload_builder.rs
  • src/cluster_mgr/src/cli/task/upload/upload_task.rs
  • src/cluster_mgr/src/cli/task/upload/upload_task_builder.rs
  • src/cluster_mgr/src/cli/task/wait_replica_ready_task.rs
  • src/cluster_mgr/src/config/config_base.rs
  • src/cluster_mgr/src/config/deployment.rs
  • src/cluster_mgr/src/config/mod.rs
  • src/cluster_mgr/src/config/monitor.rs
  • src/cluster_mgr/src/github_release.rs
  • src/cluster_mgr/src/lib.rs
  • src/cluster_mgr/src/state/state_mgr.rs
  • src/rest_api/src/handler.rs
  • tests/README.md
  • tests/docker_env.sh
  • tests/docker_ha/Dockerfile
  • tests/docker_ha/Dockerfile.stress
  • tests/docker_ha/Dockerfile.stress_go
  • tests/docker_ha/Dockerfile.stress_ts
  • tests/docker_ha/docker-compose.yaml
  • tests/docker_ha/topology.yaml
  • tests/e2e/cmd_stress_go/cmd_stress_go
  • tests/e2e/cmd_stress_go/go.mod
  • tests/e2e/cmd_stress_go/main.go
  • tests/e2e/cmd_stress_py/main.py
  • tests/e2e/cmd_stress_py/requirements.txt
  • tests/e2e/cmd_stress_test.sh
  • tests/e2e/cmd_stress_ts/main.ts
  • tests/e2e/cmd_stress_ts/package.json
  • tests/e2e/cmd_stress_ts/tsconfig.json
  • tests/e2e/devctl.sh
  • tests/e2e/stress.py
  • tests/e2e/stress_test.sh
  • tests/e2e/test.sh
  • tests/e2e/topology.yaml
  • tests/install_control_eloqctl.sh
💤 Files with no reviewable changes (5)
  • tests/docker_ha/topology.yaml
  • .github/workflows/quality.yml
  • tests/e2e/stress.py
  • tests/e2e/stress_test.sh
  • tests/e2e/test.sh

Comment on lines +407 to 413
let staged_dir = LocalExtractTask::staged_dir_for(&tx_download_url, &tx_home);
if !staged_dir.exists() {
bail!(
"Unable to determine TX service image filename from {}",
effective_tx_image_url
"TX service staged directory not found: {}",
staged_dir.display()
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Premature directory existence check will always fail on fresh runs.

The staged_dir.exists() check at Line 408 runs during task graph construction, before the DownloadTask and LocalExtractTask have executed. On a fresh run where the archive hasn't been downloaded/extracted yet, this check will fail and bail out even though the preceding tasks would have created the directory.

This check should either be removed (trusting the preceding tasks to succeed), or the verification should be moved into a separate task that executes after extraction.

Suggested fix: Remove the premature check
                 if !extract_tasks.is_empty() {
                     barrier.push(extract_tasks.len());
                     executable.extend(extract_tasks);
                 }

-                let staged_dir = LocalExtractTask::staged_dir_for(&tx_download_url, &tx_home);
-                if !staged_dir.exists() {
-                    bail!(
-                        "TX service staged directory not found: {}",
-                        staged_dir.display()
-                    );
-                }
+                let staged_dir = LocalExtractTask::staged_dir_for(&tx_download_url, &tx_home);

                 info!(

The upload task will naturally fail if the source directory doesn't exist after extraction.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let staged_dir = LocalExtractTask::staged_dir_for(&tx_download_url, &tx_home);
if !staged_dir.exists() {
bail!(
"Unable to determine TX service image filename from {}",
effective_tx_image_url
"TX service staged directory not found: {}",
staged_dir.display()
);
}
let staged_dir = LocalExtractTask::staged_dir_for(&tx_download_url, &tx_home);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/group/scale_group.rs` around lines 407 - 413,
The staged_dir existence check is premature because it runs during graph
construction before DownloadTask/LocalExtractTask execute; remove the call to
staged_dir.exists() and the bail! that references
LocalExtractTask::staged_dir_for(&tx_download_url, &tx_home), and instead rely
on the upload task to fail if the directory is missing at runtime (or, if
stronger verification is required, move that check into a new follow-up
verification task that depends on DownloadTask and LocalExtractTask completing).
Ensure the upload task still depends on DownloadTask and LocalExtractTask so
extraction happens before any upload attempt.

Comment on lines +280 to +287
let staged_dir =
LocalExtractTask::staged_dir_for(&log_download_url, LOG_SERVICE_HOME);
if !staged_dir.exists() {
return Err(anyhow!(
"Log service staged directory not found: {}",
staged_dir.display()
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Premature directory existence check will always fail on fresh runs.

Same issue as in scale_group.rs: the staged_dir.exists() check at Line 282 runs during task graph construction, before DownloadTask and LocalExtractTask have executed. This will cause the command to fail on systems where the archive hasn't been previously downloaded and extracted.

Suggested fix: Remove the premature check
                 if !extract_tasks.is_empty() {
                     barrier.push(extract_tasks.len());
                     executable.extend(extract_tasks);
                 }

-                let staged_dir =
-                    LocalExtractTask::staged_dir_for(&log_download_url, LOG_SERVICE_HOME);
-                if !staged_dir.exists() {
-                    return Err(anyhow!(
-                        "Log service staged directory not found: {}",
-                        staged_dir.display()
-                    ));
-                }
+                let staged_dir =
+                    LocalExtractTask::staged_dir_for(&log_download_url, LOG_SERVICE_HOME);

The upload task will fail with a clear error if the source doesn't exist.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let staged_dir =
LocalExtractTask::staged_dir_for(&log_download_url, LOG_SERVICE_HOME);
if !staged_dir.exists() {
return Err(anyhow!(
"Log service staged directory not found: {}",
staged_dir.display()
));
}
let staged_dir =
LocalExtractTask::staged_dir_for(&log_download_url, LOG_SERVICE_HOME);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_mgr/src/cli/task/group/scale_log_group.rs` around lines 280 -
287, Remove the premature filesystem existence check that runs during task graph
construction: delete the staged_dir.exists() conditional (the block that returns
Err with "Log service staged directory not found") so that
LocalExtractTask::staged_dir_for(&log_download_url, LOG_SERVICE_HOME) is not
validated at construction time; let the downstream DownloadTask/LocalExtractTask
(and the upload task) perform the actual extraction/download and surface any
errors at runtime instead of failing early during graph build.

@starrysky9959
starrysky9959 merged commit 8a1a574 into main May 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants