Files
rustfs/scripts/run_listobjects_verified_bench.sh
T
houseme 25193ee6cf feat(listobjects): add guarded metadata-fast listing (#4311)
* feat(listobjects): add phase 0 observability metrics

* perf(listobjects): reduce gather metadata decoding

* test(listobjects): add adversarial listing coverage

* feat(listobjects): add source-aware cursor fields

* feat(listobjects): define index source modes

* feat(listobjects): model index rebuild health

* feat(listobjects): add opt-in index fallback gate

* feat(listobjects): verify index candidate pages

* docs(listobjects): add rollout runbook

* docs(listobjects): untrack baseline fixtures

* feat(listobjects): add opt-in key-only provider

* feat(listobjects): add index verification metrics

* perf(listobjects): gate list metrics on get stage switch

* feat(listobjects): bind provider health generation

* feat(listobjects): add persistent key-only provider

* feat(listobjects): bind persistent provider lifecycle

* feat(listobjects): track persistent index mutations

* feat(listobjects): persist index mutation checkpoints

* fix(listobjects): aggregate benchmark metric snapshots

* feat(listobjects): store journal in system namespace

* chore(listobjects): report fallback reasons in bench

* test(listobjects): verify metadata-fast stale fallbacks

* feat(listobjects): serve metadata-fast snapshots behind guardrails

* test(listobjects): add metadata-fast chaos bench

* fix(listobjects): attribute metadata-fast fallback metrics

* test(listobjects): add metadata-fast fallback chaos probes

* test(listobjects): add quorum journal chaos guardrails

* fix(listobjects): satisfy pre-pr clippy gate

* docs(listobjects): untrack rollout runbook

* fix(ecstore): remove redundant heal error conversion

* test(filemeta): satisfy replication info clippy

----

Co-Authored-By: heihutu <heihutu@gmail.com>
2026-07-07 01:38:52 +08:00

1325 lines
50 KiB
Bash
Executable File

#!/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 <path> Output directory.
--data-root <path> RustFS data root for d1..d4.
--rustfs-bin <path> RustFS binary path.
--warp-bin <path> warp binary path.
--objects <n> Objects prepared by warp list (default: 10000).
--duration <dur> Timed benchmark duration (default: 20s).
--warmup-duration <d> Warmup duration before timed run (default: 5s).
--concurrency <n> warp --concurrent value (default: 16).
--max-keys <n> warp --max-keys value (default: 1000).
--release Build and run target/release/rustfs.
--skip-build Use existing RustFS binary.
--index-provider <v> Opt-in provider (default: persistent_key_only).
--key-index-path <p> Persistent key-only index path.
--journal-path <p> 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 <n> Metadata-fast chaos page size (default: 3).
--metadata-fast-staleness-ms <n>
Metadata-fast staleness SLA in milliseconds (default: 5000).
--prometheus-query-url <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 "<CompleteMultipartUpload>"
echo " <Part><PartNumber>1</PartNumber><ETag>${etag1}</ETag></Part>"
echo " <Part><PartNumber>2</PartNumber><ETag>${etag2}</ETag></Part>"
echo "</CompleteMultipartUpload>"
} >"$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 "$@"