srs/skills/srs-develop/scripts/proxy-e2e-cluster-test.sh
winlin 3060bf8e7c Claude: Split lb interfaces, extract redisclient, drop race-prone globals.
- Split OriginLoadBalancer into OriginService / HLSService / RTCService;
  the original interface now embeds the three role interfaces. Generate
  counterfeiter fakes for all four.
- Extract internal/redisclient: RedisClient interface + New() factory.
  internal/lb/redis.go no longer imports github.com/go-redis/redis/v8.
- Add unit tests for lb.go (OriginServer.ID/String/Format/NewOriginServer)
  and for the full memory + redis load balancers.
- Replace package-level test seams (memoryKeepaliveInterval, newRedisClient,
  redisKeepaliveInterval, signal.signalNotify/osExit, rtmp.createBuffer) with
  per-instance struct fields so concurrent tests can't race on them.
- Promote signal.InstallSignals / InstallForceQuit onto a new signal.Handler
  type; update bootstrap to construct one.
- Move rtmp createBuffer onto amf0ObjectBase as bufFactory; the three AMF0
  marshalers and their tests use the per-instance factory.
- Make proxy test scripts locate the workspace by walking up to go.mod
  instead of brittle '../../../..' counting (symlink-aware).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:11:18 -04:00

347 lines
10 KiB
Bash
Executable File

#!/bin/bash
# E2E test for RTMP proxy origin-cluster routing: starts one proxy + two SRS
# origins, publishes multiple RTMP streams, verifies playback via proxy, and
# verifies that different streams are assigned to different origin servers.
set -e
SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)"
# Walk up from SCRIPT_DIR looking for go.mod. This avoids brittle "../../../.."
# counting when the skills directory is reached via a symlink (which changes
# the symbolic vs. physical depth).
WORKSPACE="$SCRIPT_DIR"
while [[ "$WORKSPACE" != "/" && ! -f "$WORKSPACE/go.mod" ]]; do
WORKSPACE="$(dirname "$WORKSPACE")"
done
if [[ ! -f "$WORKSPACE/go.mod" ]]; then
echo "Error: go.mod not found walking up from: $SCRIPT_DIR" >&2
exit 1
fi
# Ports — use high ports to avoid conflicts with running services.
# The proxy starts ALL servers, so we must assign unique ports for each.
PROXY_RTMP_PORT=11935
PROXY_HTTP_API_PORT=11985
PROXY_HTTP_SERVER_PORT=18080
PROXY_WEBRTC_PORT=18000
PROXY_SRT_PORT=20080
PROXY_SYSTEM_API_PORT=12025
SOURCE_FLV="$WORKSPACE/trunk/doc/source.flv"
SRS_BINARY="$WORKSPACE/trunk/objs/srs"
# Origin ports from origin1-for-proxy.conf and origin2-for-proxy.conf.
ORIGIN1_RTMP_PORT=19351
ORIGIN1_HTTP_PORT=8081
ORIGIN1_API_PORT=19851
ORIGIN1_RTC_PORT=8001
ORIGIN1_SRT_PORT=10081
ORIGIN2_RTMP_PORT=19352
ORIGIN2_HTTP_PORT=8082
ORIGIN2_API_PORT=19853
ORIGIN2_RTC_PORT=8002
ORIGIN2_SRT_PORT=10082
# PIDs to clean up on exit.
PROXY_PID=""
ORIGIN_PIDS=()
FFMPEG_PIDS=()
cleanup() {
echo ""
echo "=== Cleaning up ==="
for pid in $PROXY_PID "${ORIGIN_PIDS[@]}" "${FFMPEG_PIDS[@]}"; do
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
sleep 1
for pid in $PROXY_PID "${ORIGIN_PIDS[@]}" "${FFMPEG_PIDS[@]}"; do
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
fi
done
echo "Cleanup done."
}
trap cleanup EXIT
wait_for_http() {
local url=$1
local name=$2
local i
for i in $(seq 1 30); do
if curl -fsS --max-time 2 "$url" >/dev/null 2>&1; then
echo "$name is ready."
return 0
fi
sleep 1
done
echo "Error: $name is not ready after 30s: $url" >&2
return 1
}
origin_has_stream() {
local api_port=$1
local stream=$2
curl -fsS --max-time 3 "http://127.0.0.1:$api_port/api/v1/streams/" 2>/dev/null | grep -q "$stream"
}
detect_origin_for_stream() {
local stream=$1
local i
for i in $(seq 1 10); do
local on_origin1=0
local on_origin2=0
if origin_has_stream "$ORIGIN1_API_PORT" "$stream"; then
on_origin1=1
fi
if origin_has_stream "$ORIGIN2_API_PORT" "$stream"; then
on_origin2=1
fi
if [[ $on_origin1 -eq 1 && $on_origin2 -eq 0 ]]; then
echo "origin1"
return 0
fi
if [[ $on_origin1 -eq 0 && $on_origin2 -eq 1 ]]; then
echo "origin2"
return 0
fi
if [[ $on_origin1 -eq 1 && $on_origin2 -eq 1 ]]; then
echo "Error: stream $stream exists on both origins; expected exactly one owner" >&2
return 1
fi
sleep 1
done
echo "Error: stream $stream was not found on either origin" >&2
return 1
}
verify_probe_has_av() {
local url=$1
local label=$2
local probe_output
probe_output=$(ffprobe -v error -rw_timeout 5000000 -show_streams "$url" 2>&1 || true)
if ! echo "$probe_output" | grep -q "codec_type=video"; then
echo "FAIL: No video stream detected for $label." >&2
echo "ffprobe output:" >&2
echo "$probe_output" >&2
exit 1
fi
if ! echo "$probe_output" | grep -q "codec_type=audio"; then
echo "FAIL: No audio stream detected for $label." >&2
echo "ffprobe output:" >&2
echo "$probe_output" >&2
exit 1
fi
echo "PASS: Audio/video detected for $label."
}
echo "=== E2E RTMP Proxy Origin Cluster Test ==="
echo "Workspace: $WORKSPACE"
echo ""
# --- Pre-checks ---
if [[ ! -f "$SOURCE_FLV" ]]; then
echo "Error: test source not found: $SOURCE_FLV" >&2
exit 1
fi
if ! command -v ffmpeg &>/dev/null; then
echo "Error: ffmpeg not found in PATH" >&2
exit 1
fi
if ! command -v ffprobe &>/dev/null; then
echo "Error: ffprobe not found in PATH" >&2
exit 1
fi
if ! command -v curl &>/dev/null; then
echo "Error: curl not found in PATH" >&2
exit 1
fi
# --- Step 0: Clean up stale state ---
# Remove stale SRS PID files that prevent restart.
rm -f "$WORKSPACE/trunk/objs/origin1.pid" "$WORKSPACE/trunk/objs/origin2.pid"
# Kill any leftover processes on our ports (proxy + origins).
ALL_PORTS="$PROXY_RTMP_PORT $PROXY_HTTP_API_PORT $PROXY_HTTP_SERVER_PORT $PROXY_WEBRTC_PORT $PROXY_SRT_PORT $PROXY_SYSTEM_API_PORT"
ALL_PORTS="$ALL_PORTS $ORIGIN1_RTMP_PORT $ORIGIN1_HTTP_PORT $ORIGIN1_API_PORT $ORIGIN1_RTC_PORT $ORIGIN1_SRT_PORT"
ALL_PORTS="$ALL_PORTS $ORIGIN2_RTMP_PORT $ORIGIN2_HTTP_PORT $ORIGIN2_API_PORT $ORIGIN2_RTC_PORT $ORIGIN2_SRT_PORT"
for port in $ALL_PORTS; do
lsof -ti :"$port" 2>/dev/null | xargs kill 2>/dev/null || true
done
sleep 1
# --- Step 1: Build proxy ---
echo "=== Step 1: Building proxy ==="
cd "$WORKSPACE"
make -s 2>&1
echo "Proxy built: $WORKSPACE/bin/srs-proxy"
# --- Step 2: Build SRS origins (if not already built) ---
if [[ ! -f "$SRS_BINARY" ]]; then
echo "=== Step 2: Building SRS origins ==="
cd "$WORKSPACE/trunk"
./configure && make 2>&1 | tail -3
echo "SRS origins built: $SRS_BINARY"
else
echo "=== Step 2: SRS origins already built ==="
fi
# --- Step 3: Start proxy ---
echo "=== Step 3: Starting proxy (RTMP :$PROXY_RTMP_PORT, System API :$PROXY_SYSTEM_API_PORT) ==="
cd "$WORKSPACE"
env PROXY_RTMP_SERVER=$PROXY_RTMP_PORT \
PROXY_HTTP_API=$PROXY_HTTP_API_PORT \
PROXY_HTTP_SERVER=$PROXY_HTTP_SERVER_PORT \
PROXY_WEBRTC_SERVER=$PROXY_WEBRTC_PORT \
PROXY_SRT_SERVER=$PROXY_SRT_PORT \
PROXY_SYSTEM_API=$PROXY_SYSTEM_API_PORT \
PROXY_LOAD_BALANCER_TYPE=memory \
./bin/srs-proxy >/tmp/srs-proxy-cluster-e2e.log 2>&1 &
PROXY_PID=$!
echo "Proxy PID: $PROXY_PID"
wait_for_http "http://127.0.0.1:$PROXY_SYSTEM_API_PORT/api/v1/versions" "Proxy System API"
if ! kill -0 "$PROXY_PID" 2>/dev/null; then
echo "Error: proxy failed to start. Logs:" >&2
cat /tmp/srs-proxy-cluster-e2e.log >&2
exit 1
fi
echo "Proxy started."
# --- Step 4: Start two SRS origins ---
echo "=== Step 4: Starting two SRS origins ==="
ulimit -n 10000 2>/dev/null || true
cd "$WORKSPACE/trunk"
./objs/srs -c conf/origin1-for-proxy.conf >/tmp/srs-origin1-cluster-e2e.log 2>&1 &
ORIGIN_PIDS+=($!)
echo "SRS origin1 PID: ${ORIGIN_PIDS[0]}"
./objs/srs -c conf/origin2-for-proxy.conf >/tmp/srs-origin2-cluster-e2e.log 2>&1 &
ORIGIN_PIDS+=($!)
echo "SRS origin2 PID: ${ORIGIN_PIDS[1]}"
wait_for_http "http://127.0.0.1:$ORIGIN1_API_PORT/api/v1/versions" "SRS origin1 HTTP API"
wait_for_http "http://127.0.0.1:$ORIGIN2_API_PORT/api/v1/versions" "SRS origin2 HTTP API"
# Wait for both SRS origins to register with proxy (heartbeat interval is 9s).
echo "Waiting for both SRS origins to register with proxy (up to 20s)..."
for i in $(seq 1 20); do
registered=$(grep -c "Register SRS media server" /tmp/srs-proxy-cluster-e2e.log 2>/dev/null || true)
if [[ $registered -ge 2 ]]; then
echo "Both origins registered."
break
fi
sleep 1
done
registered=$(grep -c "Register SRS media server" /tmp/srs-proxy-cluster-e2e.log 2>/dev/null || true)
if [[ $registered -lt 2 ]]; then
echo "Error: expected two origin registrations, got $registered. Proxy logs:" >&2
cat /tmp/srs-proxy-cluster-e2e.log >&2
exit 1
fi
for pid in "${ORIGIN_PIDS[@]}"; do
if ! kill -0 "$pid" 2>/dev/null; then
echo "Error: SRS origin failed to start. Origin1 logs:" >&2
cat /tmp/srs-origin1-cluster-e2e.log >&2
echo "Origin2 logs:" >&2
cat /tmp/srs-origin2-cluster-e2e.log >&2
exit 1
fi
done
echo "Two SRS origins started and registered."
# --- Step 5: Publish RTMP streams until both origins own at least one stream ---
echo "=== Step 5: Publishing multiple RTMP streams to proxy ==="
STREAM_PREFIX="cluster$(date +%s)"
STREAMS=()
STREAM_ORIGINS=()
origin1_count=0
origin2_count=0
# The memory load balancer picks a random healthy origin for each new stream and
# keeps that stream sticky. Publish several unique streams to verify distribution
# while keeping the test extremely unlikely to fail from random selection alone.
for i in $(seq 1 20); do
stream="${STREAM_PREFIX}_$i"
STREAMS+=("$stream")
ffmpeg -stream_loop -1 -re -i "$SOURCE_FLV" -c copy -f flv \
"rtmp://localhost:$PROXY_RTMP_PORT/live/$stream" >/tmp/srs-ffmpeg-cluster-e2e-$i.log 2>&1 &
FFMPEG_PIDS+=($!)
echo "Started publisher for live/$stream, PID: ${FFMPEG_PIDS[$((${#FFMPEG_PIDS[@]} - 1))]}"
sleep 3
if ! kill -0 "${FFMPEG_PIDS[$((${#FFMPEG_PIDS[@]} - 1))]}" 2>/dev/null; then
echo "Error: FFmpeg publisher failed for $stream. Logs:" >&2
cat "/tmp/srs-ffmpeg-cluster-e2e-$i.log" >&2
exit 1
fi
owner=$(detect_origin_for_stream "$stream")
STREAM_ORIGINS+=("$owner")
echo "Stream live/$stream is owned by $owner."
if [[ "$owner" == "origin1" ]]; then
origin1_count=$((origin1_count + 1))
else
origin2_count=$((origin2_count + 1))
fi
if [[ $origin1_count -gt 0 && $origin2_count -gt 0 ]]; then
break
fi
done
if [[ $origin1_count -eq 0 || $origin2_count -eq 0 ]]; then
echo "FAIL: streams were not distributed to both origins." >&2
echo "origin1_count=$origin1_count, origin2_count=$origin2_count" >&2
exit 1
fi
echo "PASS: stream distribution detected: origin1=$origin1_count, origin2=$origin2_count."
# --- Step 6: Verify playback via proxy and direct owning origins ---
echo "=== Step 6: Verifying playback and sticky origin ownership ==="
for i in "${!STREAMS[@]}"; do
stream="${STREAMS[$i]}"
owner="${STREAM_ORIGINS[$i]}"
# Verify proxy playback works for every published stream.
verify_probe_has_av "rtmp://localhost:$PROXY_RTMP_PORT/live/$stream" "proxy live/$stream"
# Verify the stream is on exactly one origin, and verify direct playback from
# the owning origin to prove the proxy really published to that backend.
current_owner=$(detect_origin_for_stream "$stream")
if [[ "$current_owner" != "$owner" ]]; then
echo "FAIL: stream live/$stream moved from $owner to $current_owner; expected sticky routing." >&2
exit 1
fi
if [[ "$owner" == "origin1" ]]; then
verify_probe_has_av "rtmp://localhost:$ORIGIN1_RTMP_PORT/live/$stream" "origin1 live/$stream"
else
verify_probe_has_av "rtmp://localhost:$ORIGIN2_RTMP_PORT/live/$stream" "origin2 live/$stream"
fi
done
echo ""
echo "=== E2E RTMP Proxy Origin Cluster Test PASSED ==="