From 045e245deaf5217820f33398669aae01439f87ae Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Sat, 7 Feb 2026 12:31:00 -0500 Subject: [PATCH] feat: add --shards flag and improve benchmark comparisons - add --shards N flag to ember-server to control shard count (defaults to available CPU cores) - update benchmark script to test both single-shard and multi-shard - add dragonfly support for multi-threaded comparison - add scaling efficiency section to show multi-core benefits - add --quick flag for reduced test matrix - document all new options in bench/README.md --- bench/README.md | 55 ++++- bench/compare-redis.sh | 308 +++++++++++++++++++------- crates/ember-protocol/src/command.rs | 8 +- crates/ember-server/src/connection.rs | 13 +- crates/ember-server/src/main.rs | 27 ++- crates/ember-server/src/server.rs | 5 +- crates/ember-server/src/slowlog.rs | 4 +- 7 files changed, 309 insertions(+), 111 deletions(-) diff --git a/bench/README.md b/bench/README.md index 6110e04e..e3a81f90 100644 --- a/bench/README.md +++ b/bench/README.md @@ -1,22 +1,26 @@ # benchmarks -performance benchmarks for ember, using `redis-benchmark` for system-level throughput/latency testing. +performance benchmarks for ember, comparing against redis (single-threaded) and dragonfly (multi-threaded). ## prerequisites - `redis-benchmark` (comes with redis: `brew install redis` on macOS) -- `redis-server` (optional, only needed for comparison mode) +- `redis-server` (optional, for single-threaded comparison) +- `dragonfly` (optional, for multi-threaded comparison — https://github.com/dragonflydb/dragonfly) - ember built in release mode (`cargo build --release -p ember-server`) ## running ```bash -# full comparison against redis +# full comparison (redis + dragonfly if available) make bench-compare -# ember only (no redis needed) +# ember only (no other servers needed) make bench-quick +# quick mode with reduced test matrix +bash bench/compare-redis.sh --ember-only --quick + # with custom parameters BENCH_REQUESTS=500000 BENCH_CLIENTS=100 bash bench/compare-redis.sh @@ -26,24 +30,63 @@ bash bench/compare-redis.sh --json ## what's measured +the benchmark runs three comparisons: + +### 1. single-threaded (ember 1 shard vs redis) + +apples-to-apples comparison of single-threaded performance. measures protocol efficiency, data structure speed, and per-core throughput. ember runs with `--shards 1`. + +### 2. multi-threaded (ember N shards vs dragonfly) + +apples-to-apples comparison of multi-threaded architectures. both ember and dragonfly use thread-per-core designs. ember runs with all available CPU cores. + +### 3. scaling efficiency + +compares ember multi-core vs ember single-core to show how well the sharded architecture scales. ideal scaling would be Nx on N cores. + +## test matrix + | test | what it measures | |------|-----------------| | SET (3B, P=16) | peak write throughput, pipelined | | GET (3B, P=16) | peak read throughput, pipelined | | SET/GET (64B, P=16) | throughput with realistic value sizes | | SET/GET (1KB, P=16) | throughput with larger payloads | -| SET/GET (3B, P=1) | single-request latency (no pipelining) | +| SET/GET (64B, P=1) | single-request latency (no pipelining) | ## environment variables | variable | default | description | |----------|---------|-------------| -| `EMBER_PORT` | 6379 | port for ember server | +| `EMBER_PORT` | 6379 | port for ember multi-core server | +| `EMBER_PORT_SINGLE` | 6378 | port for ember single-core server | | `REDIS_PORT` | 6399 | port for redis server | +| `DRAGONFLY_PORT` | 6389 | port for dragonfly server | | `BENCH_REQUESTS` | 100000 | total requests per test | | `BENCH_CLIENTS` | 50 | concurrent client connections | | `BENCH_PIPELINE` | 16 | pipeline depth for P>1 tests | | `EMBER_BIN` | ./target/release/ember-server | path to ember binary | +| `DRAGONFLY_BIN` | dragonfly | path to dragonfly binary | + +## command-line flags + +| flag | description | +|------|-------------| +| `--ember-only` | skip redis and dragonfly, only benchmark ember | +| `--quick` | reduced test matrix (64B only, P=16 and P=1) | +| `--json` | JSON output for CI integration | + +## ember server flags + +the benchmark uses these ember-server flags: + +```bash +# multi-core (uses all CPU cores by default) +./target/release/ember-server --port 6379 + +# single-core (for fair redis comparison) +./target/release/ember-server --port 6378 --shards 1 +``` ## results diff --git a/bench/compare-redis.sh b/bench/compare-redis.sh index 03e6c56a..0ee9759f 100755 --- a/bench/compare-redis.sh +++ b/bench/compare-redis.sh @@ -1,41 +1,52 @@ #!/usr/bin/env bash # -# Runs redis-benchmark against both Ember and Redis (optionally) and -# prints a side-by-side throughput comparison. Saves raw results to -# bench/results/ for historical tracking. +# Runs redis-benchmark against Ember, Redis, and Dragonfly to produce +# a meaningful performance comparison. Tests both single-threaded and +# multi-threaded configurations. # # Usage: # bash bench/compare-redis.sh # full comparison -# bash bench/compare-redis.sh --ember-only # skip redis +# bash bench/compare-redis.sh --ember-only # only benchmark ember +# bash bench/compare-redis.sh --quick # reduced test matrix # bash bench/compare-redis.sh --json # JSON output for CI # # Environment variables: -# EMBER_PORT ember server port (default: 6379) -# REDIS_PORT redis server port (default: 6399) -# BENCH_REQUESTS requests per test (default: 100000) -# BENCH_CLIENTS concurrent clients (default: 50) -# BENCH_PIPELINE pipeline depth for P>1 tests (default: 16) -# EMBER_BIN path to ember-server binary (default: ./target/release/ember-server) +# EMBER_PORT ember server port (default: 6379) +# REDIS_PORT redis server port (default: 6399) +# DRAGONFLY_PORT dragonfly server port (default: 6389) +# BENCH_REQUESTS requests per test (default: 100000) +# BENCH_CLIENTS concurrent clients (default: 50) +# BENCH_PIPELINE pipeline depth for P>1 tests (default: 16) +# EMBER_BIN path to ember-server binary (default: ./target/release/ember-server) +# DRAGONFLY_BIN path to dragonfly binary (default: dragonfly) set -euo pipefail # --- configuration --- EMBER_PORT="${EMBER_PORT:-6379}" +EMBER_PORT_SINGLE="${EMBER_PORT_SINGLE:-6378}" REDIS_PORT="${REDIS_PORT:-6399}" +DRAGONFLY_PORT="${DRAGONFLY_PORT:-6389}" REQUESTS="${BENCH_REQUESTS:-100000}" CLIENTS="${BENCH_CLIENTS:-50}" PIPELINE="${BENCH_PIPELINE:-16}" EMBER_BIN="${EMBER_BIN:-./target/release/ember-server}" +DRAGONFLY_BIN="${DRAGONFLY_BIN:-dragonfly}" RESULTS_DIR="bench/results" TIMESTAMP=$(date +%Y%m%d-%H%M%S) +# detect CPU cores +CPU_CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) + EMBER_ONLY=false +QUICK_MODE=false JSON_OUTPUT=false for arg in "$@"; do case "$arg" in --ember-only) EMBER_ONLY=true ;; + --quick) QUICK_MODE=true ;; --json) JSON_OUTPUT=true ;; *) echo "unknown flag: $arg"; exit 1 ;; esac @@ -44,18 +55,22 @@ done # --- helpers --- EMBER_PID="" +EMBER_SINGLE_PID="" REDIS_PID="" +DRAGONFLY_PID="" cleanup() { [[ -n "$EMBER_PID" ]] && kill "$EMBER_PID" 2>/dev/null && wait "$EMBER_PID" 2>/dev/null || true + [[ -n "$EMBER_SINGLE_PID" ]] && kill "$EMBER_SINGLE_PID" 2>/dev/null && wait "$EMBER_SINGLE_PID" 2>/dev/null || true [[ -n "$REDIS_PID" ]] && kill "$REDIS_PID" 2>/dev/null && wait "$REDIS_PID" 2>/dev/null || true + [[ -n "$DRAGONFLY_PID" ]] && kill "$DRAGONFLY_PID" 2>/dev/null && wait "$DRAGONFLY_PID" 2>/dev/null || true } trap cleanup EXIT wait_for_server() { local port=$1 local name=$2 - local retries=30 + local retries=50 while ! redis-cli -p "$port" ping > /dev/null 2>&1; do retries=$((retries - 1)) if [[ $retries -le 0 ]]; then @@ -67,7 +82,6 @@ wait_for_server() { } # extract requests/sec from redis-benchmark --csv output -# format: "TEST","rps"\n e.g. "SET","123456.78" extract_rps() { local csv_output=$1 local test_name=$2 @@ -87,11 +101,28 @@ populate_keys() { redis-benchmark -p "$port" -t set -n "$REQUESTS" -c "$CLIENTS" -P "$PIPELINE" -d 3 -q > /dev/null 2>&1 } +format_number() { + printf "%'d" "$1" +} + +calc_ratio() { + local a=$1 + local b=$2 + if [[ "$b" -gt 0 ]]; then + local ratio_x10=$(( (a * 10 + b / 2) / b )) + local ratio_int=$((ratio_x10 / 10)) + local ratio_frac=$((ratio_x10 % 10)) + echo "${ratio_int}.${ratio_frac}x" + else + echo "n/a" + fi +} + # --- checks --- if ! command -v redis-benchmark &> /dev/null; then echo "error: redis-benchmark not found. install redis tools first." >&2 - echo " brew install redis # macOS" >&2 + echo " brew install redis # macOS" >&2 echo " apt install redis-tools # debian/ubuntu" >&2 exit 1 fi @@ -101,77 +132,134 @@ if [[ ! -x "$EMBER_BIN" ]]; then cargo build --release -p ember-server fi -if [[ "$EMBER_ONLY" == "false" ]] && ! command -v redis-server &> /dev/null; then - echo "warning: redis-server not found, running ember-only mode" >&2 - EMBER_ONLY=true +HAS_REDIS=false +HAS_DRAGONFLY=false + +if [[ "$EMBER_ONLY" == "false" ]]; then + if command -v redis-server &> /dev/null; then + HAS_REDIS=true + else + echo "note: redis-server not found, skipping redis benchmarks" >&2 + fi + + if command -v "$DRAGONFLY_BIN" &> /dev/null; then + HAS_DRAGONFLY=true + else + echo "note: dragonfly not found, skipping dragonfly benchmarks" >&2 + echo " install from: https://github.com/dragonflydb/dragonfly" >&2 + fi fi mkdir -p "$RESULTS_DIR" # --- start servers --- -echo "starting ember on port $EMBER_PORT..." -"$EMBER_BIN" --port "$EMBER_PORT" & +echo "" +echo "=== benchmark configuration ===" +echo "cpu cores: $CPU_CORES" +echo "requests: $REQUESTS" +echo "clients: $CLIENTS" +echo "pipeline: $PIPELINE" +echo "" + +# ember with all cores +echo "starting ember ($CPU_CORES shards) on port $EMBER_PORT..." +"$EMBER_BIN" --port "$EMBER_PORT" > /dev/null 2>&1 & EMBER_PID=$! wait_for_server "$EMBER_PORT" "ember" -if [[ "$EMBER_ONLY" == "false" ]]; then +# ember with 1 shard (for single-threaded comparison) +echo "starting ember (1 shard) on port $EMBER_PORT_SINGLE..." +"$EMBER_BIN" --port "$EMBER_PORT_SINGLE" --shards 1 > /dev/null 2>&1 & +EMBER_SINGLE_PID=$! +wait_for_server "$EMBER_PORT_SINGLE" "ember-single" + +if [[ "$HAS_REDIS" == "true" ]]; then echo "starting redis on port $REDIS_PORT..." - redis-server --port "$REDIS_PORT" --save "" --appendonly no --loglevel warning & + redis-server --port "$REDIS_PORT" --save "" --appendonly no --loglevel warning > /dev/null 2>&1 & REDIS_PID=$! wait_for_server "$REDIS_PORT" "redis" fi +if [[ "$HAS_DRAGONFLY" == "true" ]]; then + echo "starting dragonfly on port $DRAGONFLY_PORT..." + "$DRAGONFLY_BIN" --port "$DRAGONFLY_PORT" --dbfilename "" > /dev/null 2>&1 & + DRAGONFLY_PID=$! + wait_for_server "$DRAGONFLY_PORT" "dragonfly" +fi + # --- define test matrix --- -# format: "label:data_size:pipeline" -TESTS=( - "SET (3B, P=$PIPELINE):3:$PIPELINE" - "GET (3B, P=$PIPELINE):3:$PIPELINE" - "SET (64B, P=$PIPELINE):64:$PIPELINE" - "GET (64B, P=$PIPELINE):64:$PIPELINE" - "SET (1KB, P=$PIPELINE):1024:$PIPELINE" - "GET (1KB, P=$PIPELINE):1024:$PIPELINE" - "SET (3B, P=1):3:1" - "GET (3B, P=1):3:1" -) + +if [[ "$QUICK_MODE" == "true" ]]; then + TESTS=( + "SET (64B, P=$PIPELINE):64:$PIPELINE" + "GET (64B, P=$PIPELINE):64:$PIPELINE" + "SET (64B, P=1):64:1" + "GET (64B, P=1):64:1" + ) +else + TESTS=( + "SET (3B, P=$PIPELINE):3:$PIPELINE" + "GET (3B, P=$PIPELINE):3:$PIPELINE" + "SET (64B, P=$PIPELINE):64:$PIPELINE" + "GET (64B, P=$PIPELINE):64:$PIPELINE" + "SET (1KB, P=$PIPELINE):1024:$PIPELINE" + "GET (1KB, P=$PIPELINE):1024:$PIPELINE" + "SET (64B, P=1):64:1" + "GET (64B, P=1):64:1" + ) +fi # --- run benchmarks --- echo "" -echo "running benchmarks ($REQUESTS requests, $CLIENTS clients)..." +echo "running benchmarks..." echo "" -# pre-populate +# pre-populate all servers populate_keys "$EMBER_PORT" -if [[ "$EMBER_ONLY" == "false" ]]; then - populate_keys "$REDIS_PORT" -fi +populate_keys "$EMBER_PORT_SINGLE" +[[ "$HAS_REDIS" == "true" ]] && populate_keys "$REDIS_PORT" +[[ "$HAS_DRAGONFLY" == "true" ]] && populate_keys "$DRAGONFLY_PORT" declare -a LABELS=() -declare -a EMBER_RESULTS=() +declare -a EMBER_MULTI_RESULTS=() +declare -a EMBER_SINGLE_RESULTS=() declare -a REDIS_RESULTS=() +declare -a DRAGONFLY_RESULTS=() for test_spec in "${TESTS[@]}"; do IFS=':' read -r label data_size pipeline <<< "$test_spec" LABELS+=("$label") - # determine if this is SET or GET from the label if [[ "$label" == SET* ]]; then test_type="SET" else test_type="GET" fi - # run ember + # ember multi-core csv=$(run_benchmark "$EMBER_PORT" "$data_size" "$pipeline") - ember_rps=$(extract_rps "$csv" "$test_type") - EMBER_RESULTS+=("${ember_rps:-0}") + rps=$(extract_rps "$csv" "$test_type") + EMBER_MULTI_RESULTS+=("${rps:-0}") + + # ember single-core + csv=$(run_benchmark "$EMBER_PORT_SINGLE" "$data_size" "$pipeline") + rps=$(extract_rps "$csv" "$test_type") + EMBER_SINGLE_RESULTS+=("${rps:-0}") - # run redis - if [[ "$EMBER_ONLY" == "false" ]]; then + # redis + if [[ "$HAS_REDIS" == "true" ]]; then csv=$(run_benchmark "$REDIS_PORT" "$data_size" "$pipeline") - redis_rps=$(extract_rps "$csv" "$test_type") - REDIS_RESULTS+=("${redis_rps:-0}") + rps=$(extract_rps "$csv" "$test_type") + REDIS_RESULTS+=("${rps:-0}") + fi + + # dragonfly + if [[ "$HAS_DRAGONFLY" == "true" ]]; then + csv=$(run_benchmark "$DRAGONFLY_PORT" "$data_size" "$pipeline") + rps=$(extract_rps "$csv" "$test_type") + DRAGONFLY_RESULTS+=("${rps:-0}") fi done @@ -180,72 +268,132 @@ done if [[ "$JSON_OUTPUT" == "true" ]]; then echo "{" echo " \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"," - echo " \"config\": { \"requests\": $REQUESTS, \"clients\": $CLIENTS, \"pipeline\": $PIPELINE }," - echo " \"ember\": {" + echo " \"config\": {" + echo " \"cpu_cores\": $CPU_CORES," + echo " \"requests\": $REQUESTS," + echo " \"clients\": $CLIENTS," + echo " \"pipeline\": $PIPELINE" + echo " }," + echo " \"ember_multi\": {" for i in "${!LABELS[@]}"; do - comma="" - [[ $i -lt $((${#LABELS[@]} - 1)) ]] && comma="," - echo " \"${LABELS[$i]}\": ${EMBER_RESULTS[$i]}$comma" + comma=$([[ $i -lt $((${#LABELS[@]} - 1)) ]] && echo "," || echo "") + echo " \"${LABELS[$i]}\": ${EMBER_MULTI_RESULTS[$i]}$comma" + done + echo " }," + echo " \"ember_single\": {" + for i in "${!LABELS[@]}"; do + comma=$([[ $i -lt $((${#LABELS[@]} - 1)) ]] && echo "," || echo "") + echo " \"${LABELS[$i]}\": ${EMBER_SINGLE_RESULTS[$i]}$comma" done echo " }" - if [[ "$EMBER_ONLY" == "false" ]]; then + if [[ "$HAS_REDIS" == "true" ]]; then echo " ,\"redis\": {" for i in "${!LABELS[@]}"; do - comma="" - [[ $i -lt $((${#LABELS[@]} - 1)) ]] && comma="," + comma=$([[ $i -lt $((${#LABELS[@]} - 1)) ]] && echo "," || echo "") echo " \"${LABELS[$i]}\": ${REDIS_RESULTS[$i]}$comma" done echo " }" fi + if [[ "$HAS_DRAGONFLY" == "true" ]]; then + echo " ,\"dragonfly\": {" + for i in "${!LABELS[@]}"; do + comma=$([[ $i -lt $((${#LABELS[@]} - 1)) ]] && echo "," || echo "") + echo " \"${LABELS[$i]}\": ${DRAGONFLY_RESULTS[$i]}$comma" + done + echo " }" + fi echo "}" else DATE=$(date +%Y-%m-%d) - if [[ "$EMBER_ONLY" == "false" ]]; then - echo "ember vs redis benchmark — $DATE" - echo "config: $REQUESTS requests, $CLIENTS clients" - echo "" - printf "%-24s %12s %12s %8s\n" "test" "ember (rps)" "redis (rps)" "ratio" - printf "%-24s %12s %12s %8s\n" "----" "-----------" "-----------" "-----" - + echo "========================================================================" + echo " ember benchmark results — $DATE" + echo "========================================================================" + echo "" + echo "system: $CPU_CORES cores, $REQUESTS requests, $CLIENTS clients" + echo "" + + # --- single-threaded comparison --- + echo "--- single-threaded comparison (ember 1 shard vs redis) ---" + echo "" + if [[ "$HAS_REDIS" == "true" ]]; then + printf "%-20s %14s %14s %10s\n" "test" "ember (1)" "redis" "ratio" + printf "%-20s %14s %14s %10s\n" "----" "---------" "-----" "-----" for i in "${!LABELS[@]}"; do - e=${EMBER_RESULTS[$i]} + e=${EMBER_SINGLE_RESULTS[$i]} r=${REDIS_RESULTS[$i]} - if [[ "$r" -gt 0 ]]; then - # integer ratio with one decimal: (e * 10 / r) then insert decimal - ratio_x10=$(( (e * 10 + r / 2) / r )) - ratio_int=$((ratio_x10 / 10)) - ratio_frac=$((ratio_x10 % 10)) - ratio="${ratio_int}.${ratio_frac}x" - else - ratio="n/a" - fi - printf "%-24s %12s %12s %8s\n" "${LABELS[$i]}" \ - "$(printf "%'d" "$e")" \ - "$(printf "%'d" "$r")" \ + ratio=$(calc_ratio "$e" "$r") + printf "%-20s %14s %14s %10s\n" "${LABELS[$i]}" \ + "$(format_number "$e")" \ + "$(format_number "$r")" \ "$ratio" done else - echo "ember benchmark — $DATE" - echo "config: $REQUESTS requests, $CLIENTS clients" + printf "%-20s %14s\n" "test" "ember (1)" + printf "%-20s %14s\n" "----" "---------" + for i in "${!LABELS[@]}"; do + printf "%-20s %14s\n" "${LABELS[$i]}" "$(format_number "${EMBER_SINGLE_RESULTS[$i]}")" + done echo "" - printf "%-24s %12s\n" "test" "ember (rps)" - printf "%-24s %12s\n" "----" "-----------" + echo "(redis not installed - single-threaded comparison unavailable)" + fi + + echo "" + # --- multi-threaded comparison --- + echo "--- multi-threaded comparison (ember $CPU_CORES shards vs dragonfly) ---" + echo "" + if [[ "$HAS_DRAGONFLY" == "true" ]]; then + printf "%-20s %14s %14s %10s\n" "test" "ember ($CPU_CORES)" "dragonfly" "ratio" + printf "%-20s %14s %14s %10s\n" "----" "----------" "---------" "-----" + for i in "${!LABELS[@]}"; do + e=${EMBER_MULTI_RESULTS[$i]} + d=${DRAGONFLY_RESULTS[$i]} + ratio=$(calc_ratio "$e" "$d") + printf "%-20s %14s %14s %10s\n" "${LABELS[$i]}" \ + "$(format_number "$e")" \ + "$(format_number "$d")" \ + "$ratio" + done + else + printf "%-20s %14s\n" "test" "ember ($CPU_CORES)" + printf "%-20s %14s\n" "----" "----------" for i in "${!LABELS[@]}"; do - printf "%-24s %12s\n" "${LABELS[$i]}" "$(printf "%'d" "${EMBER_RESULTS[$i]}")" + printf "%-20s %14s\n" "${LABELS[$i]}" "$(format_number "${EMBER_MULTI_RESULTS[$i]}")" done + echo "" + echo "(dragonfly not installed - multi-threaded comparison unavailable)" fi + + echo "" + + # --- scaling efficiency --- + echo "--- scaling efficiency (ember multi-core vs single-core) ---" + echo "" + printf "%-20s %14s %14s %10s\n" "test" "ember ($CPU_CORES)" "ember (1)" "scaling" + printf "%-20s %14s %14s %10s\n" "----" "----------" "---------" "-------" + for i in "${!LABELS[@]}"; do + m=${EMBER_MULTI_RESULTS[$i]} + s=${EMBER_SINGLE_RESULTS[$i]} + scaling=$(calc_ratio "$m" "$s") + printf "%-20s %14s %14s %10s\n" "${LABELS[$i]}" \ + "$(format_number "$m")" \ + "$(format_number "$s")" \ + "$scaling" + done + echo "" + echo "(ideal scaling on $CPU_CORES cores would be ${CPU_CORES}.0x)" fi # --- save raw results --- RESULT_FILE="$RESULTS_DIR/$TIMESTAMP.csv" { - echo "test,ember_rps,redis_rps" + echo "test,ember_multi_rps,ember_single_rps,redis_rps,dragonfly_rps" for i in "${!LABELS[@]}"; do redis_val="${REDIS_RESULTS[$i]:-}" - echo "${LABELS[$i]},${EMBER_RESULTS[$i]},${redis_val}" + dragonfly_val="${DRAGONFLY_RESULTS[$i]:-}" + echo "${LABELS[$i]},${EMBER_MULTI_RESULTS[$i]},${EMBER_SINGLE_RESULTS[$i]},${redis_val},${dragonfly_val}" done } > "$RESULT_FILE" diff --git a/crates/ember-protocol/src/command.rs b/crates/ember-protocol/src/command.rs index 7e9ecfa1..e565398b 100644 --- a/crates/ember-protocol/src/command.rs +++ b/crates/ember-protocol/src/command.rs @@ -1320,11 +1320,9 @@ fn parse_slowlog(args: &[Frame]) -> Result { match subcmd.as_str() { "GET" => { let count = if args.len() > 1 { - let n: usize = extract_string(&args[1])? - .parse() - .map_err(|_| { - ProtocolError::InvalidCommandFrame("invalid count for SLOWLOG GET".into()) - })?; + let n: usize = extract_string(&args[1])?.parse().map_err(|_| { + ProtocolError::InvalidCommandFrame("invalid count for SLOWLOG GET".into()) + })?; Some(n) } else { None diff --git a/crates/ember-server/src/connection.rs b/crates/ember-server/src/connection.rs index 89fb5190..6b8a90fd 100644 --- a/crates/ember-server/src/connection.rs +++ b/crates/ember-server/src/connection.rs @@ -74,8 +74,7 @@ pub async fn handle( match parse_frame(&buf) { Ok(Some((frame, consumed))) => { let _ = buf.split_to(consumed); - let response = - process(frame, &engine, ctx, slow_log).await; + let response = process(frame, &engine, ctx, slow_log).await; response.serialize(&mut out); } Ok(None) => break, // need more data @@ -353,9 +352,7 @@ async fn execute( Err(e) => Frame::Error(format!("ERR {e}")), }, - Command::Info { section } => { - render_info(engine, ctx, section.as_deref()).await - } + Command::Info { section } => render_info(engine, ctx, section.as_deref()).await, Command::BgSave => match engine.broadcast(|| ShardRequest::Snapshot).await { Ok(_) => Frame::Simple("Background saving started".into()), @@ -969,11 +966,7 @@ where /// With no argument, returns all sections. With a section name, /// returns only that section. Matches Redis convention of `#` headers /// followed by `key:value` pairs separated by `\r\n`. -async fn render_info( - engine: &Engine, - ctx: &Arc, - section: Option<&str>, -) -> Frame { +async fn render_info(engine: &Engine, ctx: &Arc, section: Option<&str>) -> Frame { let section_upper = section.map(|s| s.to_ascii_uppercase()); let want_all = section_upper.is_none(); let want = |name: &str| want_all || section_upper.as_deref() == Some(name); diff --git a/crates/ember-server/src/main.rs b/crates/ember-server/src/main.rs index 504667c9..981beb0f 100644 --- a/crates/ember-server/src/main.rs +++ b/crates/ember-server/src/main.rs @@ -58,6 +58,10 @@ struct Args { /// maximum number of entries in the slow log ring buffer #[arg(long, default_value_t = 128)] slowlog_max_len: usize, + + /// number of shards (worker threads). defaults to available CPU cores + #[arg(long)] + shards: Option, } #[tokio::main] @@ -87,9 +91,16 @@ async fn main() { std::process::exit(1); }); - let shard_count = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(1); + let shard_count = args.shards.unwrap_or_else(|| { + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) + }); + + if shard_count == 0 { + eprintln!("--shards must be at least 1"); + std::process::exit(1); + } // build persistence config if data-dir is set or appendonly is enabled let persistence = if args.appendonly || args.data_dir.is_some() { @@ -146,14 +157,12 @@ async fn main() { } let slowlog_config = slowlog::SlowLogConfig { - slower_than: std::time::Duration::from_micros( - args.slowlog_log_slower_than.max(0) as u64, - ), + slower_than: std::time::Duration::from_micros(args.slowlog_log_slower_than.max(0) as u64), max_len: args.slowlog_max_len, enabled: args.slowlog_log_slower_than >= 0, }; - info!("ember server starting..."); + info!(shards = shard_count, "ember server starting..."); if let Err(e) = server::run( addr, @@ -162,7 +171,9 @@ async fn main() { None, args.metrics_port.is_some(), slowlog_config, - ).await { + ) + .await + { eprintln!("server error: {e}"); std::process::exit(1); } diff --git a/crates/ember-server/src/server.rs b/crates/ember-server/src/server.rs index ff333108..5b7a6141 100644 --- a/crates/ember-server/src/server.rs +++ b/crates/ember-server/src/server.rs @@ -64,7 +64,10 @@ pub async fn run( .as_ref() .map(|p| p.append_only) .unwrap_or(false); - let max_memory = config.shard.max_memory.map(|per_shard| per_shard * shard_count); + let max_memory = config + .shard + .max_memory + .map(|per_shard| per_shard * shard_count); let engine = Engine::with_config(shard_count, config); diff --git a/crates/ember-server/src/slowlog.rs b/crates/ember-server/src/slowlog.rs index 9d5ee497..da3ccc2b 100644 --- a/crates/ember-server/src/slowlog.rs +++ b/crates/ember-server/src/slowlog.rs @@ -118,7 +118,9 @@ impl SlowLog { Ok(guard) => guard, Err(poisoned) => poisoned.into_inner(), }; - let n = count.unwrap_or(inner.entries.len()).min(inner.entries.len()); + let n = count + .unwrap_or(inner.entries.len()) + .min(inner.entries.len()); inner.entries.iter().rev().take(n).cloned().collect() }