- 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>
347 lines
10 KiB
Bash
Executable File
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 ==="
|