162 lines
4.8 KiB
Bash
Executable File
162 lines
4.8 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
umask 077
|
|
|
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
|
# shellcheck source=scripts/scale-common.sh
|
|
. "$ROOT/scripts/scale-common.sh"
|
|
scale_load_environment
|
|
|
|
K6_IMAGE="grafana/k6@sha256:65c920dc067d5e2e00befbf982af6ad6ad0117034e8b1c65817c7975c52d4669"
|
|
STATE_FILE=${WNH_SCALE_BROWSER_STATE:-"$ROOT/output/runtime/scale-browser-state.json"}
|
|
VUS=${WNH_SCALE_READONLY_VUS:?WNH_SCALE_READONLY_VUS is required}
|
|
DURATION=${WNH_SCALE_READONLY_DURATION:?WNH_SCALE_READONLY_DURATION is required}
|
|
THINK_SECONDS=${WNH_SCALE_READONLY_THINK_SECONDS:?WNH_SCALE_READONLY_THINK_SECONDS is required}
|
|
LABEL=${WNH_SCALE_READONLY_LABEL:-"scale-readonly-$(date -u +%Y%m%dT%H%M%SZ)"}
|
|
OUTPUT_DIR="$ROOT/output/performance/$LABEL"
|
|
|
|
case "$VUS" in
|
|
''|*[!0-9]*|0)
|
|
echo "WNH_SCALE_READONLY_VUS must be a positive integer." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
if ! printf '%s\n' "$DURATION" | grep -Eq '^[1-9][0-9]*(ms|s|m|h)$'; then
|
|
echo "WNH_SCALE_READONLY_DURATION must be a positive k6 duration." >&2
|
|
exit 2
|
|
fi
|
|
|
|
if ! printf '%s\n' "$THINK_SECONDS" |
|
|
grep -Eq '^([0-9]+([.][0-9]+)?|[.][0-9]+)$'; then
|
|
echo "WNH_SCALE_READONLY_THINK_SECONDS must be a non-negative number." >&2
|
|
exit 2
|
|
fi
|
|
|
|
case "$LABEL" in
|
|
''|*[!A-Za-z0-9._-]*)
|
|
echo "WNH_SCALE_READONLY_LABEL contains unsupported characters." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
if [ ! -f "$STATE_FILE" ]; then
|
|
echo "Missing authenticated scale browser state: $STATE_FILE" >&2
|
|
exit 2
|
|
fi
|
|
|
|
session_cookie=$(
|
|
jq -r '
|
|
.cookies[]
|
|
| select(.domain == "scale.localhost" and .name == "_who_need_help_key")
|
|
| "\(.name)=\(.value)"
|
|
' "$STATE_FILE"
|
|
)
|
|
|
|
if [ -z "$session_cookie" ]; then
|
|
echo "The browser state does not contain the scale session cookie." >&2
|
|
exit 2
|
|
fi
|
|
|
|
web_id=$(scale_compose ps -q web)
|
|
worker_id=$(scale_compose ps -q worker)
|
|
db_id=$(scale_compose ps -q db)
|
|
|
|
for pair in "web:$web_id" "worker:$worker_id" "db:$db_id"; do
|
|
service=${pair%%:*}
|
|
container=${pair#*:}
|
|
|
|
if [ -z "$container" ] || [ "$(docker inspect --format '{{.State.Status}}' "$container")" != "running" ]; then
|
|
echo "Scale service $service is not running." >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
|
|
actual_database=$(
|
|
scale_compose exec -T db \
|
|
psql -XAt -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
|
|
-c 'SELECT current_database()'
|
|
)
|
|
|
|
if [ "$actual_database" != "$WNH_SCALE_EXPECTED_DATABASE" ]; then
|
|
echo "Refusing unexpected database: $actual_database" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [ -e "$OUTPUT_DIR" ]; then
|
|
echo "Output path already exists: $OUTPUT_DIR" >&2
|
|
exit 2
|
|
fi
|
|
|
|
mkdir -p "$OUTPUT_DIR"
|
|
chmod 700 "$ROOT/output" "$ROOT/output/performance" "$OUTPUT_DIR"
|
|
|
|
snapshot_counts() {
|
|
destination=$1
|
|
|
|
scale_compose exec -T db \
|
|
psql -XAt -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
|
|
-c "
|
|
SELECT json_build_object(
|
|
'captured_at', clock_timestamp(),
|
|
'database', current_database(),
|
|
'users', (SELECT count(*) FROM users),
|
|
'users_tokens', (SELECT count(*) FROM users_tokens),
|
|
'help_requests', (SELECT count(*) FROM help_requests),
|
|
'activities', (SELECT count(*) FROM activities),
|
|
'help_assignments', (SELECT count(*) FROM help_assignments),
|
|
'activity_participants', (SELECT count(*) FROM activity_participants),
|
|
'messages', (SELECT count(*) FROM messages),
|
|
'activity_messages', (SELECT count(*) FROM activity_messages),
|
|
'rate_limit_buckets', (SELECT count(*) FROM rate_limit_buckets)
|
|
)
|
|
" >"$destination"
|
|
}
|
|
|
|
snapshot_counts "$OUTPUT_DIR/counts-before.json"
|
|
|
|
cat >"$OUTPUT_DIR/scope.txt" <<EOF
|
|
target=http://web:4000
|
|
host_header=scale.localhost
|
|
compose_project=$LOAD_PROJECT
|
|
database=$actual_database
|
|
database_volume=${LOAD_PROJECT}_postgres_data
|
|
cpuset=$CPU_REPLAY_CPUSET
|
|
vus=$VUS
|
|
duration=$DURATION
|
|
think_seconds=$THINK_SECONDS
|
|
scope=authenticated GET-only static LiveView shells; discovery SQL is measured separately by scripts/scale-verify.sh
|
|
mutating_routes=false
|
|
test_or_production_targets=false
|
|
latency_or_capacity_thresholds_applied=false
|
|
EOF
|
|
|
|
docker run --rm \
|
|
--user "$(id -u):$(id -g)" \
|
|
--cpuset-cpus "$CPU_REPLAY_CPUSET" \
|
|
--network "${LOAD_PROJECT}_internal" \
|
|
--env BASE_URL=http://web:4000 \
|
|
--env "SESSION_COOKIE=$session_cookie" \
|
|
--env "VUS=$VUS" \
|
|
--env "DURATION=$DURATION" \
|
|
--env "THINK_SECONDS=$THINK_SECONDS" \
|
|
--volume "$ROOT/load/k6:/scripts:ro" \
|
|
--volume "$OUTPUT_DIR:/output" \
|
|
"$K6_IMAGE" run \
|
|
--summary-export /output/k6-summary.json \
|
|
/scripts/scale-readonly.js
|
|
|
|
snapshot_counts "$OUTPUT_DIR/counts-after.json"
|
|
|
|
jq -e '
|
|
del(.captured_at) as $before
|
|
| $before == (input | del(.captured_at))
|
|
' "$OUTPUT_DIR/counts-before.json" "$OUTPUT_DIR/counts-after.json" >/dev/null
|
|
|
|
docker run --rm \
|
|
--volume "$OUTPUT_DIR:/output" \
|
|
alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 \
|
|
chown -R "$(id -u):$(id -g)" /output
|
|
|
|
printf 'Read-only scale result: %s\n' "$OUTPUT_DIR"
|