#!/usr/bin/env bash set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" OUT_DIR="${OUT_DIR:-${PROJECT_ROOT}/target/bench/listobjects-verified-$(date +%Y%m%d-%H%M%S)}" DATA_ROOT="${DATA_ROOT:-/private/tmp/listobjects-verified-$(date +%Y%m%d-%H%M%S)}" BUILD_PROFILE="${BUILD_PROFILE:-debug}" RUSTFS_BIN_USER_SET="false" if [[ -n "${RUSTFS_BIN:-}" ]]; then RUSTFS_BIN_USER_SET="true" else RUSTFS_BIN="${PROJECT_ROOT}/target/${BUILD_PROFILE}/rustfs" fi WARP_BIN="${WARP_BIN:-warp}" ADDRESS="${ADDRESS:-127.0.0.1:9000}" ENDPOINT="${ENDPOINT:-http://${ADDRESS}}" ACCESS_KEY="${ACCESS_KEY:-minioadmin}" SECRET_KEY="${SECRET_KEY:-minioadmin}" REGION="${REGION:-us-east-1}" OBJECTS="${OBJECTS:-10000}" OBJECT_SIZE="${OBJECT_SIZE:-1KB}" CONCURRENCY="${CONCURRENCY:-16}" MAX_KEYS="${MAX_KEYS:-1000}" DURATION="${DURATION:-20s}" WARMUP_DURATION="${WARMUP_DURATION:-5s}" METER_INTERVAL="${METER_INTERVAL:-1}" RESOURCE_SAMPLE_INTERVAL="${RESOURCE_SAMPLE_INTERVAL:-1}" SKIP_BUILD="${SKIP_BUILD:-false}" INDEX_PROVIDER="${INDEX_PROVIDER:-persistent_key_only}" INDEX_GENERATION="${INDEX_GENERATION:-bench-$(date +%Y%m%d%H%M%S)}" KEY_INDEX_PATH="${KEY_INDEX_PATH:-${OUT_DIR}/persistent-key-only.index}" NAMESPACE_JOURNAL_PATH="${NAMESPACE_JOURNAL_PATH:-}" PREBUILD_KEY_INDEX="${PREBUILD_KEY_INDEX:-false}" PREPARE_ONLY="${PREPARE_ONLY:-false}" REUSE_PREPARED_DATA="${REUSE_PREPARED_DATA:-false}" RUN_METADATA_FAST="${RUN_METADATA_FAST:-true}" RUN_CHAOS="${RUN_CHAOS:-true}" RUN_QUORUM_JOURNAL_CHAOS="${RUN_QUORUM_JOURNAL_CHAOS:-true}" CHAOS_MAX_KEYS="${CHAOS_MAX_KEYS:-3}" METADATA_FAST_STALENESS_MS="${METADATA_FAST_STALENESS_MS:-5000}" PROMETHEUS_QUERY_URL="${PROMETHEUS_QUERY_URL:-http://localhost:9090/api/v1/query}" PROMETHEUS_JOB="${PROMETHEUS_JOB:-rustfs-app-metrics}" JOURNAL_CHAOS_ENABLED="" JOURNAL_CHAOS_BUCKET="" JOURNAL_CHAOS_SEQUENCE="" JOURNAL_CHAOS_STATUS="" SERVER_PID="" RESOURCE_SAMPLER_PID="" log_info() { printf '[INFO] %s\n' "$*"; } log_warn() { printf '[WARN] %s\n' "$*" >&2; } die() { printf '[ERROR] %s\n' "$*" >&2 exit 1 } usage() { cat <<'USAGE' Usage: scripts/run_listobjects_verified_bench.sh [options] Options: --out-dir Output directory. --data-root RustFS data root for d1..d4. --rustfs-bin RustFS binary path. --warp-bin warp binary path. --objects Objects prepared by warp list (default: 10000). --duration Timed benchmark duration (default: 20s). --warmup-duration Warmup duration before timed run (default: 5s). --concurrency warp --concurrent value (default: 16). --max-keys warp --max-keys value (default: 1000). --release Build and run target/release/rustfs. --skip-build Use existing RustFS binary. --index-provider Opt-in provider (default: persistent_key_only). --key-index-path

Persistent key-only index path. --journal-path

Optional local namespace mutation journal override path. --prebuild-key-index Build persistent key-only index from the bench script. --prepare-only Prepare bucket data and exit without running modes. --reuse-prepared-data Reuse --data-root and skip warp data preparation. --skip-metadata-fast Skip metadata-fast benchmark mode. --skip-chaos Skip metadata-fast stale/fallback chaos probes. --skip-quorum-journal-chaos Skip quorum-backed .rustfs.sys degraded/restore probe. --chaos-max-keys Metadata-fast chaos page size (default: 3). --metadata-fast-staleness-ms Metadata-fast staleness SLA in milliseconds (default: 5000). --prometheus-query-url Prometheus API query URL or UI /query URL. -h, --help Show this help. Environment: OUT_DIR, DATA_ROOT, RUSTFS_BIN, WARP_BIN, ADDRESS, ACCESS_KEY, SECRET_KEY, REGION, OBJECTS, OBJECT_SIZE, CONCURRENCY, MAX_KEYS, DURATION, WARMUP_DURATION, METER_INTERVAL, RESOURCE_SAMPLE_INTERVAL, SKIP_BUILD, BUILD_PROFILE, INDEX_PROVIDER, INDEX_GENERATION, KEY_INDEX_PATH, NAMESPACE_JOURNAL_PATH, PREBUILD_KEY_INDEX, PREPARE_ONLY, REUSE_PREPARED_DATA, RUN_METADATA_FAST, RUN_CHAOS, RUN_QUORUM_JOURNAL_CHAOS, CHAOS_MAX_KEYS, METADATA_FAST_STALENESS_MS, PROMETHEUS_QUERY_URL, PROMETHEUS_JOB. USAGE } arg_value() { local flag="$1" local value="${2:-}" [[ -n "$value" ]] || die "missing value for $flag" printf '%s\n' "$value" } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --out-dir) OUT_DIR="$(arg_value "$1" "${2:-}")"; shift 2 ;; --data-root) DATA_ROOT="$(arg_value "$1" "${2:-}")"; shift 2 ;; --rustfs-bin) RUSTFS_BIN="$(arg_value "$1" "${2:-}")"; RUSTFS_BIN_USER_SET="true"; shift 2 ;; --warp-bin) WARP_BIN="$(arg_value "$1" "${2:-}")"; shift 2 ;; --objects) OBJECTS="$(arg_value "$1" "${2:-}")"; shift 2 ;; --duration) DURATION="$(arg_value "$1" "${2:-}")"; shift 2 ;; --warmup-duration) WARMUP_DURATION="$(arg_value "$1" "${2:-}")"; shift 2 ;; --concurrency) CONCURRENCY="$(arg_value "$1" "${2:-}")"; shift 2 ;; --max-keys) MAX_KEYS="$(arg_value "$1" "${2:-}")"; shift 2 ;; --release) BUILD_PROFILE="release" if [[ "$RUSTFS_BIN_USER_SET" == "false" ]]; then RUSTFS_BIN="${PROJECT_ROOT}/target/release/rustfs" fi shift ;; --skip-build) SKIP_BUILD="true"; shift ;; --index-provider) INDEX_PROVIDER="$(arg_value "$1" "${2:-}")"; shift 2 ;; --key-index-path) KEY_INDEX_PATH="$(arg_value "$1" "${2:-}")"; shift 2 ;; --journal-path) NAMESPACE_JOURNAL_PATH="$(arg_value "$1" "${2:-}")"; shift 2 ;; --prebuild-key-index) PREBUILD_KEY_INDEX="true"; shift ;; --prepare-only) PREPARE_ONLY="true"; shift ;; --reuse-prepared-data) REUSE_PREPARED_DATA="true"; shift ;; --skip-metadata-fast) RUN_METADATA_FAST="false"; shift ;; --skip-chaos) RUN_CHAOS="false"; shift ;; --skip-quorum-journal-chaos) RUN_QUORUM_JOURNAL_CHAOS="false"; shift ;; --chaos-max-keys) CHAOS_MAX_KEYS="$(arg_value "$1" "${2:-}")"; shift 2 ;; --metadata-fast-staleness-ms) METADATA_FAST_STALENESS_MS="$(arg_value "$1" "${2:-}")"; shift 2 ;; --prometheus-query-url) PROMETHEUS_QUERY_URL="$(arg_value "$1" "${2:-}")"; shift 2 ;; -h|--help) usage; exit 0 ;; *) die "unknown arg: $1" ;; esac done } require_cmds() { command -v "$WARP_BIN" >/dev/null 2>&1 || die "warp binary not found: $WARP_BIN" command -v curl >/dev/null 2>&1 || die "curl not found" command -v python3 >/dev/null 2>&1 || die "python3 not found" command -v awk >/dev/null 2>&1 || die "awk not found" command -v sed >/dev/null 2>&1 || die "sed not found" } build_rustfs() { if [[ "$SKIP_BUILD" == "true" ]]; then [[ -x "$RUSTFS_BIN" ]] || die "RustFS binary is not executable: $RUSTFS_BIN" return fi if [[ "$BUILD_PROFILE" == "release" ]]; then (cd "$PROJECT_ROOT" && cargo build -p rustfs --release) else (cd "$PROJECT_ROOT" && cargo build -p rustfs) fi [[ -x "$RUSTFS_BIN" ]] || die "RustFS binary is not executable after build: $RUSTFS_BIN" } stop_server() { stop_resource_sampler if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" >/dev/null 2>&1; then kill "$SERVER_PID" >/dev/null 2>&1 || true wait "$SERVER_PID" >/dev/null 2>&1 || true fi SERVER_PID="" } stop_resource_sampler() { if [[ -n "$RESOURCE_SAMPLER_PID" ]] && kill -0 "$RESOURCE_SAMPLER_PID" >/dev/null 2>&1; then kill "$RESOURCE_SAMPLER_PID" >/dev/null 2>&1 || true wait "$RESOURCE_SAMPLER_PID" >/dev/null 2>&1 || true fi RESOURCE_SAMPLER_PID="" } start_resource_sampler() { local out_file="$1" local pid="$SERVER_PID" echo "unix_ts,pid,cpu_percent,rss_kib" >"$out_file" ( while kill -0 "$pid" >/dev/null 2>&1; do ps -p "$pid" -o pid=,%cpu=,rss= | awk -v ts="$(date +%s)" '{printf "%s,%s,%s,%s\n", ts, $1, $2, $3}' sleep "$RESOURCE_SAMPLE_INTERVAL" done ) >>"$out_file" 2>/dev/null & RESOURCE_SAMPLER_PID="$!" } wait_for_health() { local log_file="$1" local deadline=$((SECONDS + 60)) while (( SECONDS < deadline )); do if curl -fsS "${ENDPOINT}/health" >/dev/null 2>&1 || curl -fsS "${ENDPOINT}/health/ready" >/dev/null 2>&1; then return 0 fi if [[ -n "$SERVER_PID" ]] && ! kill -0 "$SERVER_PID" >/dev/null 2>&1; then tail -n 80 "$log_file" >&2 || true die "RustFS exited before health check passed" fi sleep 1 done tail -n 80 "$log_file" >&2 || true die "RustFS health check timed out" } start_server() { local mode="$1" local log_file="$2" local reset_data="${3:-false}" stop_server if [[ "$reset_data" == "true" ]]; then rm -rf "$DATA_ROOT" fi mkdir -p "$DATA_ROOT"/d{1,2,3,4} ( export RUST_LOG=off export RUSTFS_OBS_LOGGER_LEVEL=off export RUSTFS_OBS_TRACES_EXPORT_ENABLED=false export RUSTFS_OBS_METRICS_EXPORT_ENABLED=true export RUSTFS_OBS_LOGS_EXPORT_ENABLED=false export RUSTFS_OBS_PROFILING_EXPORT_ENABLED=false export RUSTFS_OBS_ENDPOINT=http://127.0.0.1:4318 export RUSTFS_OBS_METRIC_ENDPOINT=http://127.0.0.1:4318/v1/metrics export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics export RUSTFS_OBS_ENDPOINT_TIMEOUT_MILLIS=200 export RUSTFS_OBS_USE_STDOUT=true export RUSTFS_OBS_METER_INTERVAL="$METER_INTERVAL" export RUSTFS_OBS_LOG_STDOUT_ENABLED=false export RUSTFS_SCANNER_ENABLED=false export RUSTFS_CONSOLE_ENABLE=false export RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true export RUSTFS_ADDRESS="$ADDRESS" export RUSTFS_ACCESS_KEY="$ACCESS_KEY" export RUSTFS_SECRET_KEY="$SECRET_KEY" export RUSTFS_RPC_SECRET="rustfs-listobjects-verified-rpc-secret" export RUSTFS_REGION="$REGION" unset RUSTFS_LIST_OBJECTS_INDEX_MODE unset RUSTFS_LIST_OBJECTS_INDEX_PROVIDER unset RUSTFS_LIST_OBJECTS_INDEX_PROVIDER_PATH unset RUSTFS_LIST_OBJECTS_INDEX_PROVIDER_GENERATION unset RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_PATH unset RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_ENABLED unset RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_BUCKET unset RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_SEQUENCE unset RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_STATUS unset RUSTFS_LIST_OBJECTS_METADATA_FAST_ENABLED unset RUSTFS_LIST_OBJECTS_METADATA_FAST_STALENESS_MS if [[ "$mode" == "opt_in_verified" || "$mode" == "metadata_fast" ]]; then export RUSTFS_LIST_OBJECTS_INDEX_MODE=index_key_only if [[ "$mode" == "metadata_fast" ]]; then export RUSTFS_LIST_OBJECTS_INDEX_MODE=index_metadata_fast export RUSTFS_LIST_OBJECTS_METADATA_FAST_ENABLED=true export RUSTFS_LIST_OBJECTS_METADATA_FAST_STALENESS_MS="$METADATA_FAST_STALENESS_MS" fi export RUSTFS_LIST_OBJECTS_INDEX_PROVIDER="$INDEX_PROVIDER" if [[ "$INDEX_PROVIDER" == "persistent_key_only" || "$INDEX_PROVIDER" == "persisted_key_only" ]]; then export RUSTFS_LIST_OBJECTS_INDEX_PROVIDER_PATH="$KEY_INDEX_PATH" export RUSTFS_LIST_OBJECTS_INDEX_PROVIDER_GENERATION="$INDEX_GENERATION" if [[ -n "$NAMESPACE_JOURNAL_PATH" ]]; then export RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_PATH="$NAMESPACE_JOURNAL_PATH" fi if [[ -n "$JOURNAL_CHAOS_BUCKET" && -n "$JOURNAL_CHAOS_STATUS" ]]; then if [[ -n "$JOURNAL_CHAOS_ENABLED" ]]; then export RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_ENABLED="$JOURNAL_CHAOS_ENABLED" fi export RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_BUCKET="$JOURNAL_CHAOS_BUCKET" export RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_STATUS="$JOURNAL_CHAOS_STATUS" if [[ -n "$JOURNAL_CHAOS_SEQUENCE" ]]; then export RUSTFS_LIST_OBJECTS_NAMESPACE_JOURNAL_CHAOS_SEQUENCE="$JOURNAL_CHAOS_SEQUENCE" fi fi fi fi exec "$RUSTFS_BIN" server \ "${DATA_ROOT}/d1" \ "${DATA_ROOT}/d2" \ "${DATA_ROOT}/d3" \ "${DATA_ROOT}/d4" ) >"$log_file" 2>&1 & SERVER_PID="$!" wait_for_health "$log_file" } prepare_bucket() { local duration="$2" local log_file="$3" local bucket="$1" "$WARP_BIN" list \ --host "$ADDRESS" \ --access-key "$ACCESS_KEY" \ --secret-key "$SECRET_KEY" \ --region "$REGION" \ --bucket "$bucket" \ --objects "$OBJECTS" \ --obj.size "$OBJECT_SIZE" \ --concurrent "$CONCURRENCY" \ --max-keys "$MAX_KEYS" \ --duration "$duration" \ --noclear \ --no-color \ >"$log_file" 2>&1 } write_key_index() { local bucket="$1" local index_path="$2" local index_dir tmp_keys tmp_objects sorted_keys tmp_index token page response_file next_token key_count index_dir="$(dirname "$index_path")" tmp_keys="${index_path}.keys.tmp" tmp_objects="${index_path}.objects.tmp" sorted_keys="${index_path}.keys.sorted.tmp" tmp_index="${index_path}.tmp" mkdir -p "$index_dir" : >"$tmp_keys" : >"$tmp_objects" token="" page=0 while true; do local url="${ENDPOINT}/${bucket}?list-type=2&max-keys=${MAX_KEYS}" if [[ -n "$token" ]]; then local encoded_token encoded_token="$(printf '%s' "$token" | urlencode)" url="${url}&continuation-token=${encoded_token}" fi response_file="${index_path}.page-${page}.xml" curl -fsS \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ -o "$response_file" \ "$url" extract_xml_keys <"$response_file" >>"$tmp_keys" || true extract_metadata_object_rows <"$response_file" >>"$tmp_objects" || true next_token="$(extract_next_token <"$response_file" || true)" rm -f "$response_file" if [[ -z "$next_token" ]]; then break fi token="$next_token" page=$((page + 1)) done LC_ALL=C sort -u "$tmp_keys" >"$sorted_keys" key_count="$(awk 'END{print NR + 0}' "$sorted_keys")" { echo "# rustfs-listobjects-key-only-v1" echo "# bucket=${bucket}" echo "# generation=${INDEX_GENERATION}" echo "# checkpoint_high_water_mark=${key_count}" cat "$sorted_keys" cat "$tmp_objects" } >"$tmp_index" mv "$tmp_index" "$index_path" rm -f "$tmp_keys" "$tmp_objects" "$sorted_keys" } extract_xml_keys() { python3 -c 'import sys, xml.etree.ElementTree as ET root = ET.fromstring(sys.stdin.read()) for elem in root.iter(): if elem.tag.rsplit("}", 1)[-1] == "Key" and elem.text: print(elem.text)' } extract_next_token() { python3 -c 'import sys, xml.etree.ElementTree as ET root = ET.fromstring(sys.stdin.read()) for elem in root.iter(): if elem.tag.rsplit("}", 1)[-1] == "NextContinuationToken" and elem.text: print(elem.text) break' } xml_tag_text() { local tag="$1" python3 -c 'import sys, xml.etree.ElementTree as ET tag = sys.argv[1] root = ET.fromstring(sys.stdin.read()) for elem in root.iter(): if elem.tag.rsplit("}", 1)[-1] == tag and elem.text: print(elem.text) break' "$tag" } extract_metadata_object_rows() { python3 -c 'import base64, datetime, sys, xml.etree.ElementTree as ET def local_name(tag): return tag.rsplit("}", 1)[-1] def text(parent, name): for child in parent: if local_name(child.tag) == name: return child.text or "" return "" def enc(value): if not value: return "-" return base64.b64encode(value.encode("utf-8")).decode("ascii") def nanos(value): if not value: return "-" try: dt = datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) return str(int(dt.timestamp() * 1_000_000_000)) except ValueError: return "-" root = ET.fromstring(sys.stdin.read()) for contents in root.iter(): if local_name(contents.tag) != "Contents": continue key = text(contents, "Key") if not key: continue size = text(contents, "Size") or "0" etag = text(contents, "ETag").strip("\"") storage_class = text(contents, "StorageClass") print("# object\t{}\t{}\t{}\t{}\t{}".format( enc(key), size, nanos(text(contents, "LastModified")), enc(etag), enc(storage_class), ))' } signed_list_once() { local bucket="$1" local response_file="$2" local max_keys="${3:-1}" signed_list_page "$bucket" "$response_file" "$max_keys" "" } signed_list_page() { local bucket="$1" local response_file="$2" local max_keys="${3:-1}" local token="${4:-}" local url="${ENDPOINT}/${bucket}?list-type=2&max-keys=${max_keys}" if [[ -n "$token" ]]; then local encoded_token encoded_token="$(printf '%s' "$token" | urlencode)" url="${url}&continuation-token=${encoded_token}" fi curl -fsS \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ -o "$response_file" \ "$url" } urlencode_path() { python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe="/-_.~"))' "$1" } signed_put_object() { local bucket="$1" local key="$2" local body_file="$3" local out_file="$4" local encoded_key encoded_key="$(urlencode_path "$key")" curl -fsS \ --request PUT \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ --upload-file "$body_file" \ -o "$out_file" \ "${ENDPOINT}/${bucket}/${encoded_key}" } signed_delete_object() { local bucket="$1" local key="$2" local out_file="$3" local encoded_key encoded_key="$(urlencode_path "$key")" curl -fsS \ --request DELETE \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ -o "$out_file" \ "${ENDPOINT}/${bucket}/${encoded_key}" } response_etag() { awk 'BEGIN{IGNORECASE=1} /^etag:/ {gsub("\r", "", $0); sub("^[^:]*:[[:space:]]*", "", $0); print $0; exit}' "$1" } signed_multipart_complete() { local bucket="$1" local key="$2" local work_dir="$3" local encoded_key upload_xml upload_id encoded_upload_id part1_file part2_file part1_headers part2_headers etag1 etag2 complete_xml encoded_key="$(urlencode_path "$key")" upload_xml="$work_dir/multipart-init.xml" curl -fsS \ --request POST \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ -o "$upload_xml" \ "${ENDPOINT}/${bucket}/${encoded_key}?uploads" upload_id="$(xml_tag_text UploadId <"$upload_xml")" [[ -n "$upload_id" ]] || die "multipart initiate did not return UploadId" encoded_upload_id="$(printf '%s' "$upload_id" | urlencode)" part1_file="$work_dir/multipart-part-1.bin" part2_file="$work_dir/multipart-part-2.bin" part1_headers="$work_dir/multipart-part-1.headers" part2_headers="$work_dir/multipart-part-2.headers" python3 -c 'from pathlib import Path; Path(__import__("sys").argv[1]).write_bytes(b"a" * (5 * 1024 * 1024)); Path(__import__("sys").argv[2]).write_bytes(b"b")' "$part1_file" "$part2_file" curl -fsS \ --request PUT \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ --upload-file "$part1_file" \ -D "$part1_headers" \ -o "$work_dir/multipart-part-1.out" \ "${ENDPOINT}/${bucket}/${encoded_key}?partNumber=1&uploadId=${encoded_upload_id}" curl -fsS \ --request PUT \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ --upload-file "$part2_file" \ -D "$part2_headers" \ -o "$work_dir/multipart-part-2.out" \ "${ENDPOINT}/${bucket}/${encoded_key}?partNumber=2&uploadId=${encoded_upload_id}" etag1="$(response_etag "$part1_headers")" etag2="$(response_etag "$part2_headers")" [[ -n "$etag1" && -n "$etag2" ]] || die "multipart part upload did not return ETags" complete_xml="$work_dir/multipart-complete.xml" { echo "" echo " 1${etag1}" echo " 2${etag2}" echo "" } >"$complete_xml" curl -fsS \ --request POST \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ -H "Content-Type: application/xml" \ --data-binary "@${complete_xml}" \ -o "$work_dir/multipart-complete.out" \ "${ENDPOINT}/${bucket}/${encoded_key}?uploadId=${encoded_upload_id}" } urlencode() { python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=""))' } base64_token() { python3 -c 'import base64, sys; print(base64.b64encode(sys.stdin.read().encode("utf-8")).decode("ascii"))' } percentile_summary() { local values_file="$1" python3 - "$values_file" <<'PY' import math import statistics import sys path = sys.argv[1] values = [] with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line: values.append(float(line)) if not values: print("0,N/A,N/A,N/A,N/A") raise SystemExit(0) values.sort() def percentile(p: float) -> float: if len(values) == 1: return values[0] rank = math.ceil((p / 100.0) * len(values)) - 1 rank = min(max(rank, 0), len(values) - 1) return values[rank] print( f"{len(values)}," f"{statistics.fmean(values):.6f}," f"{percentile(50):.6f}," f"{percentile(95):.6f}," f"{percentile(99):.6f}" ) PY } run_signed_list_bench() { local mode="$1" local bucket="$2" local duration="$3" local out_dir="$4" local deadline=$((SECONDS + ${duration%s})) local latencies_file="$out_dir/list-latencies-ms.txt" local request_log="$out_dir/list-requests.csv" local total_keys=0 local total_requests=0 local iterations=0 : >"$latencies_file" echo "iteration,request,keys,latency_ms,truncated" >"$request_log" while (( SECONDS < deadline )); do local token="" local request_in_iteration=0 iterations=$((iterations + 1)) while (( SECONDS < deadline )); do local url="${ENDPOINT}/${bucket}?list-type=2&max-keys=${MAX_KEYS}" if [[ -n "$token" ]]; then local encoded_token encoded_token="$(printf '%s' "$token" | urlencode)" url="${url}&continuation-token=${encoded_token}" fi local response_file time_total status key_count next_token truncated response_file="$out_dir/response-${mode}-${iterations}-${request_in_iteration}.xml" time_total="$(curl -fsS \ --user "${ACCESS_KEY}:${SECRET_KEY}" \ --aws-sigv4 "aws:amz:${REGION}:s3" \ -H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \ -w '%{time_total}' \ -o "$response_file" \ "$url")" status=$? if [[ "$status" -ne 0 ]]; then log_warn "curl list failed for mode=$mode status=$status url=$url" return 1 fi key_count="$(extract_xml_keys <"$response_file" | grep -cv '^$' || true)" next_token="$(extract_next_token <"$response_file" || true)" truncated="false" [[ -n "$next_token" ]] && truncated="true" awk -v sec="$time_total" 'BEGIN{printf "%.6f\n", sec * 1000.0}' >>"$latencies_file" echo "$iterations,$request_in_iteration,$key_count,$(awk -v sec="$time_total" 'BEGIN{printf "%.6f", sec * 1000.0}'),$truncated" >>"$request_log" total_keys=$((total_keys + key_count)) total_requests=$((total_requests + 1)) request_in_iteration=$((request_in_iteration + 1)) if [[ -z "$next_token" ]]; then break fi token="$next_token" done done local percentile_line req_count avg_ms p50_ms p95_ms p99_ms objps percentile_line="$(percentile_summary "$latencies_file")" IFS=',' read -r req_count avg_ms p50_ms p95_ms p99_ms <<<"$percentile_line" objps="$(awk -v keys="$total_keys" -v duration="${duration%s}" 'BEGIN{if(duration<=0) print "N/A"; else printf "%.6f", keys/duration}')" { echo "mode,objects,object_size,max_keys,duration,requests,scan_iterations,total_keys,objps,latency_avg_ms,p50_ms,p95_ms,p99_ms" echo "$mode,$OBJECTS,$OBJECT_SIZE,$MAX_KEYS,$duration,$req_count,$iterations,$total_keys,$objps,$avg_ms,$p50_ms,$p95_ms,$p99_ms" } >"$out_dir/list-summary.csv" } last_metric_field_sum() { local metric="$1" local field="$2" local log_file="$3" awk -v metric="$metric" -v field="$field" ' function finish_block() { if (found) { snapshot_total += sum snapshot_seen = 1 found=0 sum=0 } } function finish_snapshot() { finish_block() if (snapshot_seen) { value=snapshot_total } snapshot_total=0 snapshot_seen=0 } /^Metric #0$/ { finish_snapshot() } /^Metric #[0-9]+/ { finish_block() } $0 ~ "Name[[:space:]]*:[[:space:]]*" metric "$" { found=1; sum=0; next } found && $0 ~ "^[[:space:]]*" field "[[:space:]]*:" { line=$0 sub("^[[:space:]]*" field "[[:space:]]*:[[:space:]]*", "", line) gsub(/[^0-9.eE+-]/, "", line) sum += line + 0 } END { finish_snapshot() if (value == "") print "0"; else print value } ' "$log_file" } last_metric_value() { local metric="$1" local log_file="$2" last_metric_field_sum "$metric" "Value" "$log_file" } last_metric_sum() { local metric="$1" local log_file="$2" last_metric_field_sum "$metric" "Sum" "$log_file" } last_metric_count() { local metric="$1" local log_file="$2" last_metric_field_sum "$metric" "Count" "$log_file" } write_ratio_summary() { local mode="$1" local log_file="$2" local csv_file="$3" local attempt fallback served verify_attempts verify_misses returned_objects amplification_sum amplification_count amplification stale_ratio fallback_ratio attempt="$(last_metric_value rustfs_s3_list_objects_index_attempt_total "$log_file")" fallback="$(last_metric_value rustfs_s3_list_objects_index_fallback_total "$log_file")" served="$(last_metric_value rustfs_s3_list_objects_index_served_total "$log_file")" verify_attempts="$(last_metric_sum rustfs_s3_list_objects_index_live_verify_attempts "$log_file")" verify_misses="$(last_metric_sum rustfs_s3_list_objects_index_live_verify_misses "$log_file")" returned_objects="$(last_metric_sum rustfs_s3_list_objects_index_returned_objects "$log_file")" amplification_sum="$(last_metric_sum rustfs_s3_list_objects_index_verification_io_amplification "$log_file")" amplification_count="$(last_metric_count rustfs_s3_list_objects_index_verification_io_amplification "$log_file")" amplification="$(awk -v sum="$amplification_sum" -v count="$amplification_count" 'BEGIN{if(count+0==0) print "0"; else printf "%.6f", (sum+0)/(count+0)}')" fallback_ratio="$(awk -v f="$fallback" -v a="$attempt" 'BEGIN{if(a+0==0) print "N/A"; else printf "%.6f", (f+0)/(a+0)}')" stale_ratio="$(awk -v m="$verify_misses" -v a="$verify_attempts" 'BEGIN{if(a+0==0) print "N/A"; else printf "%.6f", (m+0)/(a+0)}')" { echo "mode,index_attempt_total,index_fallback_total,index_served_total,live_verify_attempts,live_verify_misses,returned_objects,verification_io_amplification,fallback_ratio,stale_missing_candidate_ratio" echo "$mode,$attempt,$fallback,$served,$verify_attempts,$verify_misses,$returned_objects,$amplification,$fallback_ratio,$stale_ratio" } >"$csv_file" } write_fallback_reason_summary() { local mode="$1" local log_file="$2" local csv_file="$3" awk -v mode="$mode" ' BEGIN { print "mode,reason,index_fallback_total" } /Name[[:space:]]*: rustfs_s3_list_objects_index_fallback_total/ { in_metric = 1 value = "" next } in_metric && /Name[[:space:]]*:/ && $0 !~ /rustfs_s3_list_objects_index_fallback_total/ { in_metric = 0 value = "" next } in_metric && /Value[[:space:]]*:/ { line = $0 sub(/^.*Value[[:space:]]*:[[:space:]]*/, "", line) split(line, parts, /[[:space:]]+/) value = parts[1] next } in_metric && /->[[:space:]]*reason:/ { line = $0 sub(/^.*->[[:space:]]*reason:[[:space:]]*/, "", line) split(line, parts, /[[:space:]]+/) reason = parts[1] last[reason] = value + 0 seen[reason] = 1 value = "" next } END { count = 0 for (reason in seen) { printf "%s,%s,%.0f\n", mode, reason, last[reason] count += 1 } if (count == 0) { printf "%s,N/A,0\n", mode } } ' "$log_file" >"$csv_file" } prometheus_api_query_url() { local url="$PROMETHEUS_QUERY_URL" if [[ "$url" == */api/v1/query ]]; then printf '%s\n' "$url" elif [[ "$url" == */query ]]; then printf '%s/api/v1/query\n' "${url%/query}" else printf '%s\n' "$url" fi } query_prometheus_expr() { local expr="$1" local out_file="$2" local api_url api_url="$(prometheus_api_query_url)" curl -fsS --get --data-urlencode "query=${expr}" "$api_url" -o "$out_file" } write_prometheus_snapshot() { local mode="$1" local out_dir="$2" local prom_dir="$out_dir/prometheus" mkdir -p "$prom_dir" query_prometheus_expr "sum(rustfs_s3_list_objects_index_attempt_total)" "$prom_dir/index_attempt_total.json" || { log_warn "Prometheus query failed; skipping Prometheus snapshot for mode=$mode" return 0 } query_prometheus_expr "sum(rustfs_s3_list_objects_index_fallback_total)" "$prom_dir/index_fallback_total.json" || true query_prometheus_expr "sum(rustfs_s3_list_objects_index_served_total)" "$prom_dir/index_served_total.json" || true query_prometheus_expr "sum(rustfs_s3_list_objects_index_live_verify_attempts_sum)" "$prom_dir/live_verify_attempts_sum.json" || true query_prometheus_expr "sum(rustfs_s3_list_objects_index_live_verify_misses_sum)" "$prom_dir/live_verify_misses_sum.json" || true query_prometheus_expr "sum(rustfs_s3_list_objects_index_returned_objects_sum)" "$prom_dir/returned_objects_sum.json" || true query_prometheus_expr "sum(rustfs_s3_list_objects_index_verification_io_amplification_sum) / sum(rustfs_s3_list_objects_index_verification_io_amplification_count)" "$prom_dir/verification_io_amplification_avg.json" || true query_prometheus_expr "rate(process_cpu_seconds_total{job=\"${PROMETHEUS_JOB}\"}[1m])" "$prom_dir/process_cpu_rate_1m.json" || true query_prometheus_expr "process_resident_memory_bytes{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/process_resident_memory_bytes.json" || true query_prometheus_expr "rustfs_memory_allocator_reserved_bytes{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/allocator_reserved_bytes.json" || true query_prometheus_expr "rustfs_memory_allocator_committed_bytes{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/allocator_committed_bytes.json" || true query_prometheus_expr "rustfs_memory_allocator_page_committed_bytes{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/allocator_page_committed_bytes.json" || true query_prometheus_expr "rustfs_memory_allocator_malloc_requested_bytes{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/allocator_malloc_requested_bytes.json" || true query_prometheus_expr "rustfs_memory_allocator_malloc_requested_peak_bytes{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/allocator_malloc_requested_peak_bytes.json" || true query_prometheus_expr "rustfs_memory_allocator_malloc_requested_total_bytes{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/allocator_malloc_requested_total_bytes.json" || true query_prometheus_expr "rustfs_memory_allocator_heap_count{job=\"${PROMETHEUS_JOB}\"}" "$prom_dir/allocator_heap_count.json" || true } write_resource_summary() { local samples_file="$1" local summary_file="$2" awk -F',' ' NR == 1 { next } NF >= 4 { cpu += $3 rss += $4 if ($3 > max_cpu) max_cpu = $3 if ($4 > max_rss) max_rss = $4 count += 1 } END { print "samples,avg_cpu_percent,max_cpu_percent,avg_rss_mib,max_rss_mib" if (count == 0) { print "0,N/A,N/A,N/A,N/A" } else { printf "%d,%.6f,%.6f,%.6f,%.6f\n", count, cpu / count, max_cpu, (rss / count) / 1024.0, max_rss / 1024.0 } } ' "$samples_file" >"$summary_file" } run_metadata_fast_chaos() { local bucket="$1" local out_dir="$2" mkdir -p "$out_dir" local summary="$out_dir/chaos-summary.csv" local fallback_summary="$out_dir/fallback-probes.csv" local page1="$out_dir/page1-before-mutation.xml" local page2="$out_dir/page2-after-mutation.xml" local fresh_after="$out_dir/fresh-after-mutation.xml" local generation_mismatch="$out_dir/generation-mismatch.xml" local degraded_response="$out_dir/degraded-fallback.xml" local page1_keys="$out_dir/page1-before-mutation.keys" local page2_keys="$out_dir/page2-after-mutation.keys" local fresh_after_keys="$out_dir/fresh-after-mutation.keys" local expected_page2_keys="$out_dir/expected-page2-after-mutation.keys" local body_file="$out_dir/overwrite-body.bin" local delete_out="$out_dir/delete.out" local overwrite_out="$out_dir/overwrite.out" local token first_key second_key multipart_key overlap last_key monotonic_status no_skip_status page1_count page2_count fresh_max_keys forged_token journal_sequence echo "probe,status,detail" >"$summary" echo "probe,status,detail" >"$fallback_summary" signed_list_page "$bucket" "$page1" "$CHAOS_MAX_KEYS" "" extract_xml_keys <"$page1" >"$page1_keys" token="$(extract_next_token <"$page1" || true)" page1_count="$(awk 'END{print NR + 0}' "$page1_keys")" if [[ -z "$token" || "$page1_count" -lt 2 ]]; then echo "initial_page,skipped,requires at least two keys and a continuation token" >>"$summary" return 0 fi echo "initial_page,ok,keys=${page1_count}; continuation_token=yes" >>"$summary" first_key="$(sed -n '1p' "$page1_keys")" second_key="$(sed -n '2p' "$page1_keys")" printf 'metadata-fast overwrite probe %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$body_file" signed_put_object "$bucket" "$first_key" "$body_file" "$overwrite_out" echo "overwrite,ok,key=${first_key}" >>"$summary" signed_delete_object "$bucket" "$second_key" "$delete_out" echo "delete,ok,key=${second_key}" >>"$summary" multipart_key="zzzz-metadata-fast-chaos-multipart-$(date +%s).bin" signed_multipart_complete "$bucket" "$multipart_key" "$out_dir" echo "multipart_complete,ok,key=${multipart_key}" >>"$summary" signed_list_page "$bucket" "$page2" "$CHAOS_MAX_KEYS" "$token" extract_xml_keys <"$page2" >"$page2_keys" page2_count="$(awk 'END{print NR + 0}' "$page2_keys")" overlap="$(comm -12 <(LC_ALL=C sort "$page1_keys") <(LC_ALL=C sort "$page2_keys") | awk 'END{print NR + 0}')" if [[ "$overlap" -eq 0 ]]; then echo "continuation_no_duplicate,ok,overlap=0" >>"$summary" else echo "continuation_no_duplicate,failed,overlap=${overlap}" >>"$summary" return 1 fi last_key="$(tail -n 1 "$page1_keys")" monotonic_status="ok" if [[ -n "$last_key" ]] && ! awk -v last="$last_key" 'NF && $0 <= last { bad = 1 } END { exit bad }' "$page2_keys"; then monotonic_status="failed" fi echo "continuation_monotonic,${monotonic_status},page2_keys=${page2_count}; last_page1=${last_key}" >>"$summary" [[ "$monotonic_status" == "ok" ]] || return 1 fresh_max_keys=$((page1_count + page2_count + CHAOS_MAX_KEYS)) signed_list_page "$bucket" "$fresh_after" "$fresh_max_keys" "" extract_xml_keys <"$fresh_after" >"$fresh_after_keys" awk -v last="$last_key" 'NF && $0 > last { print }' "$fresh_after_keys" | head -n "$page2_count" >"$expected_page2_keys" no_skip_status="ok" if ! cmp -s "$expected_page2_keys" "$page2_keys"; then no_skip_status="failed" fi echo "continuation_no_skip,${no_skip_status},expected_next_keys=$(awk 'END{print NR + 0}' "$expected_page2_keys"); actual_page2_keys=${page2_count}" >>"$summary" [[ "$no_skip_status" == "ok" ]] || return 1 forged_token="$(printf '%s[rustfs_cache:v2,return:,src:index_metadata_fast,gen:%s-mismatch]' "$last_key" "$INDEX_GENERATION" | base64_token)" if signed_list_page "$bucket" "$generation_mismatch" "$CHAOS_MAX_KEYS" "$forged_token"; then echo "generation_mismatch,ok,forged_generation=${INDEX_GENERATION}-mismatch" >>"$fallback_summary" else echo "generation_mismatch,failed,forged_generation=${INDEX_GENERATION}-mismatch" >>"$fallback_summary" fi if [[ -z "$NAMESPACE_JOURNAL_PATH" ]]; then echo "degraded,skipped,requires --journal-path to avoid mutating quorum-backed journal state" >>"$fallback_summary" return 0 fi journal_sequence="$(journal_high_water_mark "$NAMESPACE_JOURNAL_PATH" "$bucket")" write_local_journal_state "$NAMESPACE_JOURNAL_PATH" "$bucket" "$journal_sequence" "degraded" if signed_list_page "$bucket" "$degraded_response" "$CHAOS_MAX_KEYS" ""; then echo "degraded,ok,journal_path=${NAMESPACE_JOURNAL_PATH}; high_water_mark=${journal_sequence}" >>"$fallback_summary" else echo "degraded,failed,journal_path=${NAMESPACE_JOURNAL_PATH}; high_water_mark=${journal_sequence}" >>"$fallback_summary" fi write_local_journal_state "$NAMESPACE_JOURNAL_PATH" "$bucket" "$journal_sequence" "healthy" echo "degraded_restore,ok,journal_path=${NAMESPACE_JOURNAL_PATH}; high_water_mark=${journal_sequence}" >>"$fallback_summary" } journal_high_water_mark() { local root="$1" local bucket="$2" local state_file="${root}/${bucket}.state" if [[ -f "$state_file" ]]; then awk -F= '/^# high_water_mark=/ {print $2; found=1; exit} END {if(!found) print "0"}' "$state_file" else echo "0" fi } write_local_journal_state() { local root="$1" local bucket="$2" local high_water_mark="$3" local status="$4" local state_file="${root}/${bucket}.state" local tmp_file="${state_file}.tmp" mkdir -p "$root" { echo "# rustfs-listobjects-namespace-journal-v1" echo "# bucket=${bucket}" echo "# high_water_mark=${high_water_mark}" echo "# status=${status}" } >"$tmp_file" mv "$tmp_file" "$state_file" } run_metadata_fast_quorum_journal_chaos() { local bucket="$1" local out_dir="$2" mkdir -p "$out_dir" local summary="$out_dir/quorum-journal-probes.csv" local degraded_response="$out_dir/quorum-degraded-fallback.xml" local disabled_gate_rebuild_response="$out_dir/quorum-disabled-gate-rebuild.xml" local disabled_gate_response="$out_dir/quorum-disabled-gate.xml" local restore_state_response="$out_dir/quorum-restore-state.xml" local restore_rebuild_response="$out_dir/quorum-restore-rebuild.xml" local restore_response="$out_dir/quorum-restore.xml" local disabled_gate_fallbacks echo "probe,status,detail" >"$summary" if [[ "$RUN_QUORUM_JOURNAL_CHAOS" != "true" ]]; then echo "quorum_degraded,skipped,RUN_QUORUM_JOURNAL_CHAOS=false" >>"$summary" return 0 fi if [[ -n "$NAMESPACE_JOURNAL_PATH" ]]; then echo "quorum_degraded,skipped,local namespace journal override is active" >>"$summary" return 0 fi stop_server JOURNAL_CHAOS_ENABLED="" JOURNAL_CHAOS_BUCKET="" JOURNAL_CHAOS_SEQUENCE="" JOURNAL_CHAOS_STATUS="" start_server opt_in_verified "$out_dir/quorum-disabled-gate-rebuild-rustfs.log" false if signed_list_once "$bucket" "$disabled_gate_rebuild_response" 1; then echo "quorum_disabled_gate_rebuild,ok,bucket=${bucket}; provider=rebuild" >>"$summary" else echo "quorum_disabled_gate_rebuild,failed,bucket=${bucket}; provider=rebuild" >>"$summary" fi stop_server JOURNAL_CHAOS_ENABLED="" JOURNAL_CHAOS_BUCKET="$bucket" JOURNAL_CHAOS_SEQUENCE="" JOURNAL_CHAOS_STATUS="degraded" start_server metadata_fast "$out_dir/quorum-disabled-gate-rustfs.log" false signed_list_page "$bucket" "$disabled_gate_response" "$CHAOS_MAX_KEYS" "" sleep "$((METER_INTERVAL + 2))" write_ratio_summary "metadata_fast_quorum_disabled_gate" "$out_dir/quorum-disabled-gate-rustfs.log" "$out_dir/quorum-disabled-gate-index-ratios.csv" write_fallback_reason_summary \ "metadata_fast_quorum_disabled_gate" \ "$out_dir/quorum-disabled-gate-rustfs.log" \ "$out_dir/quorum-disabled-gate-index-fallback-reasons.csv" disabled_gate_fallbacks="$(awk -F',' 'NR == 2 {print $3 + 0}' "$out_dir/quorum-disabled-gate-index-ratios.csv")" if [[ "$disabled_gate_fallbacks" -eq 0 ]]; then echo "quorum_disabled_gate,ok,bucket=${bucket}; status=degraded; enabled=unset" >>"$summary" else echo "quorum_disabled_gate,failed,bucket=${bucket}; status=degraded; enabled=unset; fallbacks=${disabled_gate_fallbacks}" >>"$summary" fi stop_server JOURNAL_CHAOS_ENABLED="true" JOURNAL_CHAOS_BUCKET="$bucket" JOURNAL_CHAOS_SEQUENCE="" JOURNAL_CHAOS_STATUS="degraded" start_server metadata_fast "$out_dir/quorum-degraded-rustfs.log" false if signed_list_page "$bucket" "$degraded_response" "$CHAOS_MAX_KEYS" ""; then echo "quorum_degraded,ok,bucket=${bucket}; status=degraded" >>"$summary" else echo "quorum_degraded,failed,bucket=${bucket}; status=degraded" >>"$summary" fi sleep "$((METER_INTERVAL + 2))" write_ratio_summary "metadata_fast_quorum_degraded" "$out_dir/quorum-degraded-rustfs.log" "$out_dir/quorum-degraded-index-ratios.csv" write_fallback_reason_summary \ "metadata_fast_quorum_degraded" \ "$out_dir/quorum-degraded-rustfs.log" \ "$out_dir/quorum-degraded-index-fallback-reasons.csv" stop_server JOURNAL_CHAOS_ENABLED="true" JOURNAL_CHAOS_STATUS="healthy" start_server metadata_fast "$out_dir/quorum-restore-state-rustfs.log" false if signed_list_page "$bucket" "$restore_state_response" "$CHAOS_MAX_KEYS" ""; then echo "quorum_restore_state,ok,bucket=${bucket}; status=healthy" >>"$summary" else echo "quorum_restore_state,failed,bucket=${bucket}; status=healthy" >>"$summary" fi stop_server JOURNAL_CHAOS_ENABLED="" JOURNAL_CHAOS_BUCKET="" JOURNAL_CHAOS_SEQUENCE="" JOURNAL_CHAOS_STATUS="" start_server opt_in_verified "$out_dir/quorum-restore-rebuild-rustfs.log" false if signed_list_once "$bucket" "$restore_rebuild_response" 1; then echo "quorum_restore_rebuild,ok,bucket=${bucket}; provider=rebuild" >>"$summary" else echo "quorum_restore_rebuild,failed,bucket=${bucket}; provider=rebuild" >>"$summary" fi stop_server start_server metadata_fast "$out_dir/quorum-restore-rustfs.log" false if signed_list_page "$bucket" "$restore_response" "$CHAOS_MAX_KEYS" ""; then echo "quorum_restore,ok,bucket=${bucket}; status=healthy; provider=metadata_fast" >>"$summary" else echo "quorum_restore,failed,bucket=${bucket}; status=healthy; provider=metadata_fast" >>"$summary" fi sleep "$((METER_INTERVAL + 2))" write_ratio_summary "metadata_fast_quorum_restore" "$out_dir/quorum-restore-rustfs.log" "$out_dir/quorum-restore-index-ratios.csv" write_fallback_reason_summary \ "metadata_fast_quorum_restore" \ "$out_dir/quorum-restore-rustfs.log" \ "$out_dir/quorum-restore-index-fallback-reasons.csv" stop_server } run_mode() { local mode="$1" local bucket="$2" local reset_data="${3:-false}" local mode_dir="$OUT_DIR/$mode" mkdir -p "$mode_dir" log_info "Starting RustFS for mode=$mode" start_server "$mode" "$mode_dir/rustfs.log" "$reset_data" start_resource_sampler "$mode_dir/resource-samples.csv" if [[ "$mode" == "opt_in_verified" && "$PREBUILD_KEY_INDEX" != "true" ]] \ && [[ "$INDEX_PROVIDER" == "persistent_key_only" || "$INDEX_PROVIDER" == "persisted_key_only" ]]; then log_info "Warming persistent key-only provider through production rebuild path" signed_list_once "$bucket" "$mode_dir/provider-warmup.xml" 1 fi log_info "Signed ListObjectsV2 bench for mode=$mode duration=$DURATION" run_signed_list_bench "$mode" "$bucket" "$DURATION" "$mode_dir" stop_resource_sampler sleep "$((METER_INTERVAL + 2))" write_ratio_summary "$mode" "$mode_dir/rustfs.log" "$mode_dir/index-ratios.csv" write_fallback_reason_summary "$mode" "$mode_dir/rustfs.log" "$mode_dir/index-fallback-reasons.csv" write_resource_summary "$mode_dir/resource-samples.csv" "$mode_dir/resource-summary.csv" write_prometheus_snapshot "$mode" "$mode_dir" if [[ "$mode" == "metadata_fast" && "$RUN_CHAOS" == "true" ]]; then log_info "Running metadata-fast chaos probes" run_metadata_fast_chaos "$bucket" "$mode_dir/chaos" sleep "$((METER_INTERVAL + 2))" write_ratio_summary "${mode}_after_chaos" "$mode_dir/rustfs.log" "$mode_dir/chaos/index-ratios-after-chaos.csv" write_fallback_reason_summary "${mode}_after_chaos" "$mode_dir/rustfs.log" "$mode_dir/chaos/index-fallback-reasons-after-chaos.csv" log_info "Running metadata-fast quorum-backed namespace journal degraded probe" run_metadata_fast_quorum_journal_chaos "$bucket" "$mode_dir/chaos" fi stop_server } write_manifest() { mkdir -p "$OUT_DIR" { echo "out_dir=$OUT_DIR" echo "data_root=$DATA_ROOT" echo "build_profile=$BUILD_PROFILE" echo "rustfs_bin=$RUSTFS_BIN" echo "warp_bin=$WARP_BIN" echo "address=$ADDRESS" echo "objects=$OBJECTS" echo "object_size=$OBJECT_SIZE" echo "concurrency=$CONCURRENCY" echo "max_keys=$MAX_KEYS" echo "duration=$DURATION" echo "warmup_duration=$WARMUP_DURATION" echo "meter_interval=$METER_INTERVAL" echo "resource_sample_interval=$RESOURCE_SAMPLE_INTERVAL" echo "index_provider=$INDEX_PROVIDER" echo "index_generation=$INDEX_GENERATION" echo "key_index_path=$KEY_INDEX_PATH" echo "namespace_journal_path=${NAMESPACE_JOURNAL_PATH:-.rustfs.sys/listobjects/ns-journal/v1}" echo "prebuild_key_index=$PREBUILD_KEY_INDEX" echo "prepare_only=$PREPARE_ONLY" echo "reuse_prepared_data=$REUSE_PREPARED_DATA" echo "run_metadata_fast=$RUN_METADATA_FAST" echo "run_chaos=$RUN_CHAOS" echo "run_quorum_journal_chaos=$RUN_QUORUM_JOURNAL_CHAOS" echo "chaos_max_keys=$CHAOS_MAX_KEYS" echo "metadata_fast_staleness_ms=$METADATA_FAST_STALENESS_MS" echo "prometheus_query_url=$PROMETHEUS_QUERY_URL" echo "prometheus_job=$PROMETHEUS_JOB" } >"$OUT_DIR/manifest.env" } summary_csv_section() { local title="$1" local file="$2" if [[ -f "$file" ]]; then echo "### $title" echo cat "$file" echo fi } write_combined_summary() { local summary="$OUT_DIR/summary.md" { echo "# ListObjects Verified Benchmark Summary" echo echo "## Manifest" echo sed 's/^/- /' "$OUT_DIR/manifest.env" echo echo "## Warp" echo summary_csv_section "Walker" "$OUT_DIR/walker/list-summary.csv" summary_csv_section "Opt-in verified" "$OUT_DIR/opt_in_verified/list-summary.csv" summary_csv_section "Metadata-fast" "$OUT_DIR/metadata_fast/list-summary.csv" echo echo "## Index Ratios" echo summary_csv_section "Walker" "$OUT_DIR/walker/index-ratios.csv" summary_csv_section "Opt-in verified" "$OUT_DIR/opt_in_verified/index-ratios.csv" summary_csv_section "Metadata-fast" "$OUT_DIR/metadata_fast/index-ratios.csv" echo echo "## Fallback Reasons" echo summary_csv_section "Walker" "$OUT_DIR/walker/index-fallback-reasons.csv" summary_csv_section "Opt-in verified" "$OUT_DIR/opt_in_verified/index-fallback-reasons.csv" summary_csv_section "Metadata-fast" "$OUT_DIR/metadata_fast/index-fallback-reasons.csv" echo echo "## Resource Samples" echo summary_csv_section "Walker" "$OUT_DIR/walker/resource-summary.csv" summary_csv_section "Opt-in verified" "$OUT_DIR/opt_in_verified/resource-summary.csv" summary_csv_section "Metadata-fast" "$OUT_DIR/metadata_fast/resource-summary.csv" if [[ -f "$OUT_DIR/metadata_fast/chaos/chaos-summary.csv" ]]; then echo echo "## Metadata-fast Chaos" echo summary_csv_section "Chaos probes" "$OUT_DIR/metadata_fast/chaos/chaos-summary.csv" summary_csv_section "Fallback probes" "$OUT_DIR/metadata_fast/chaos/fallback-probes.csv" summary_csv_section "Ratios after chaos" "$OUT_DIR/metadata_fast/chaos/index-ratios-after-chaos.csv" summary_csv_section "Fallback reasons after chaos" "$OUT_DIR/metadata_fast/chaos/index-fallback-reasons-after-chaos.csv" summary_csv_section "Quorum journal probes" "$OUT_DIR/metadata_fast/chaos/quorum-journal-probes.csv" summary_csv_section "Quorum disabled-gate ratios" "$OUT_DIR/metadata_fast/chaos/quorum-disabled-gate-index-ratios.csv" summary_csv_section "Quorum disabled-gate fallback reasons" "$OUT_DIR/metadata_fast/chaos/quorum-disabled-gate-index-fallback-reasons.csv" summary_csv_section "Quorum degraded ratios" "$OUT_DIR/metadata_fast/chaos/quorum-degraded-index-ratios.csv" summary_csv_section "Quorum degraded fallback reasons" "$OUT_DIR/metadata_fast/chaos/quorum-degraded-index-fallback-reasons.csv" summary_csv_section "Quorum restore ratios" "$OUT_DIR/metadata_fast/chaos/quorum-restore-index-ratios.csv" summary_csv_section "Quorum restore fallback reasons" "$OUT_DIR/metadata_fast/chaos/quorum-restore-index-fallback-reasons.csv" fi } >"$summary" } main() { parse_args "$@" require_cmds write_manifest build_rustfs trap stop_server EXIT INT TERM local bucket="rustfs-listobjects-bench" local prep_dir="$OUT_DIR/prepare" mkdir -p "$prep_dir" if [[ "$REUSE_PREPARED_DATA" == "true" ]]; then log_info "Reusing prepared data root: $DATA_ROOT" else log_info "Starting RustFS for data preparation" start_server walker "$prep_dir/rustfs.log" true log_info "Preparing bucket=$bucket objects=$OBJECTS with warp list" prepare_bucket "$bucket" "$WARMUP_DURATION" "$prep_dir/warp-prepare.log" || { tail -n 80 "$prep_dir/warp-prepare.log" >&2 || true return 1 } fi if [[ "$PREBUILD_KEY_INDEX" == "true" ]]; then if [[ "$REUSE_PREPARED_DATA" == "true" ]]; then log_info "Starting RustFS to build persistent key-only index from reused data" start_server walker "$prep_dir/rustfs.log" false fi log_info "Writing persistent key-only index from bench script: $KEY_INDEX_PATH" write_key_index "$bucket" "$KEY_INDEX_PATH" stop_server else log_info "Skipping bench-side key index generation; opt-in mode will rebuild through RustFS lifecycle" stop_server fi if [[ "$PREPARE_ONLY" == "true" ]]; then log_info "Prepare-only mode complete: $DATA_ROOT" return 0 fi run_mode walker "$bucket" false run_mode opt_in_verified "$bucket" false if [[ "$RUN_METADATA_FAST" == "true" ]]; then run_mode metadata_fast "$bucket" false fi write_combined_summary log_info "Summary: $OUT_DIR/summary.md" } main "$@"