182 lines
6.1 KiB
Bash
Executable File
182 lines
6.1 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
set -eu
|
|
|
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
|
|
COMPOSE_FILE="$REPO_ROOT/etc/servers/work-server/docker-compose.yml"
|
|
PROXY_SERVICE="${WORK_SERVER_PROXY_SERVICE:-work-server}"
|
|
PROXY_CONTAINER="${WORK_SERVER_PROXY_CONTAINER:-work-server}"
|
|
BLUE_SERVICE="${WORK_SERVER_BLUE_SERVICE:-work-server-blue}"
|
|
GREEN_SERVICE="${WORK_SERVER_GREEN_SERVICE:-work-server-green}"
|
|
BLUE_CONTAINER="${WORK_SERVER_BLUE_CONTAINER:-work-server-blue}"
|
|
GREEN_CONTAINER="${WORK_SERVER_GREEN_CONTAINER:-work-server-green}"
|
|
ACTIVE_SLOT_FILE="${WORK_SERVER_ACTIVE_SLOT_FILE:-$REPO_ROOT/etc/servers/work-server/.docker/runtime/active-slot}"
|
|
PROXY_CONFIG_FILE="${WORK_SERVER_PROXY_CONFIG_FILE:-$REPO_ROOT/etc/servers/work-server/.docker/proxy/default.conf}"
|
|
HEALTH_ENDPOINT="${WORK_SERVER_HEALTH_ENDPOINT:-http://127.0.0.1:3100/health}"
|
|
RUNTIME_ENDPOINT="${WORK_SERVER_RUNTIME_ENDPOINT:-http://127.0.0.1:3100/api/runtime}"
|
|
PREVIOUS_SLOT_DRAIN_TIMEOUT_SECONDS="${WORK_SERVER_PREVIOUS_SLOT_DRAIN_TIMEOUT_SECONDS:-900}"
|
|
|
|
cd "$REPO_ROOT"
|
|
|
|
mkdir -p "$(dirname "$ACTIVE_SLOT_FILE")" "$(dirname "$PROXY_CONFIG_FILE")"
|
|
|
|
read_active_slot() {
|
|
if [ -f "$ACTIVE_SLOT_FILE" ]; then
|
|
SLOT=$(tr -d '[:space:]' <"$ACTIVE_SLOT_FILE")
|
|
if [ "$SLOT" = "blue" ] || [ "$SLOT" = "green" ]; then
|
|
printf '%s' "$SLOT"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
printf 'blue'
|
|
}
|
|
|
|
container_is_running() {
|
|
CONTAINER_NAME="$1"
|
|
STATUS=$(docker inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || true)
|
|
[ "$STATUS" = "running" ]
|
|
}
|
|
|
|
resolve_active_slot() {
|
|
SLOT=$(read_active_slot)
|
|
|
|
if [ "$SLOT" = "blue" ] && ! container_is_running "$BLUE_CONTAINER" && container_is_running "$GREEN_CONTAINER"; then
|
|
printf 'green'
|
|
return 0
|
|
fi
|
|
|
|
if [ "$SLOT" = "green" ] && ! container_is_running "$GREEN_CONTAINER" && container_is_running "$BLUE_CONTAINER"; then
|
|
printf 'blue'
|
|
return 0
|
|
fi
|
|
|
|
printf '%s' "$SLOT"
|
|
}
|
|
|
|
write_proxy_config() {
|
|
SLOT="$1"
|
|
TARGET_CONTAINER="$BLUE_CONTAINER"
|
|
|
|
if [ "$SLOT" = "green" ]; then
|
|
TARGET_CONTAINER="$GREEN_CONTAINER"
|
|
fi
|
|
|
|
cat >"$PROXY_CONFIG_FILE" <<EOF2
|
|
server {
|
|
listen 3100;
|
|
server_name _;
|
|
|
|
location /ws/chat {
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header X-Forwarded-Host \$host;
|
|
proxy_set_header X-Forwarded-Port \$server_port;
|
|
proxy_set_header Upgrade \$http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_pass http://$TARGET_CONTAINER:3100;
|
|
}
|
|
|
|
location / {
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host \$host;
|
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
proxy_set_header X-Forwarded-Host \$host;
|
|
proxy_set_header X-Forwarded-Port \$server_port;
|
|
proxy_set_header Connection "";
|
|
proxy_pass http://$TARGET_CONTAINER:3100;
|
|
}
|
|
}
|
|
EOF2
|
|
}
|
|
|
|
wait_for_container_health() {
|
|
TARGET_CONTAINER="$1"
|
|
ATTEMPT=0
|
|
|
|
while [ "$ATTEMPT" -lt 60 ]; do
|
|
if docker exec "$TARGET_CONTAINER" node -e "fetch(process.argv[1]).then(async (response) => { if (!response.ok) process.exit(1); process.stdout.write(await response.text()); }).catch(() => process.exit(1));" "$HEALTH_ENDPOINT" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
ATTEMPT=$((ATTEMPT + 1))
|
|
sleep 2
|
|
done
|
|
|
|
echo "health check failed for $TARGET_CONTAINER" >&2
|
|
return 1
|
|
}
|
|
|
|
read_runtime_value() {
|
|
TARGET_CONTAINER="$1"
|
|
FIELD_NAME="$2"
|
|
|
|
docker exec "$TARGET_CONTAINER" node -e "fetch(process.argv[1]).then((response) => response.json()).then((payload) => { const value = payload?.[process.argv[2]]; if (typeof value === 'boolean') { process.stdout.write(value ? 'true' : 'false'); return; } if (value == null) { process.stdout.write(''); return; } process.stdout.write(String(value)); }).catch(() => process.exit(1));" "$RUNTIME_ENDPOINT" "$FIELD_NAME"
|
|
}
|
|
|
|
set_container_draining() {
|
|
TARGET_CONTAINER="$1"
|
|
DRAINING_VALUE="$2"
|
|
|
|
docker exec "$TARGET_CONTAINER" node -e "fetch(process.argv[1], { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ draining: process.argv[2] === 'true' }) }).then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1));" "${RUNTIME_ENDPOINT}/drain" "$DRAINING_VALUE"
|
|
}
|
|
|
|
wait_for_previous_slot_drain() {
|
|
TARGET_CONTAINER="$1"
|
|
ELAPSED=0
|
|
|
|
while [ "$ELAPSED" -lt "$PREVIOUS_SLOT_DRAIN_TIMEOUT_SECONDS" ]; do
|
|
ACTIVE_COUNT=$(read_runtime_value "$TARGET_CONTAINER" activeChatRequestCount 2>/dev/null || printf '0')
|
|
QUEUED_COUNT=$(read_runtime_value "$TARGET_CONTAINER" queuedChatRequestCount 2>/dev/null || printf '0')
|
|
|
|
if [ "${ACTIVE_COUNT:-0}" = "0" ] && [ "${QUEUED_COUNT:-0}" = "0" ]; then
|
|
return 0
|
|
fi
|
|
|
|
sleep 2
|
|
ELAPSED=$((ELAPSED + 2))
|
|
done
|
|
|
|
echo "drain timeout reached for $TARGET_CONTAINER" >&2
|
|
return 1
|
|
}
|
|
|
|
ensure_proxy_running() {
|
|
docker compose -f "$COMPOSE_FILE" up -d --no-deps "$PROXY_SERVICE" >/dev/null
|
|
docker exec "$PROXY_CONTAINER" nginx -s reload >/dev/null
|
|
}
|
|
|
|
ACTIVE_SLOT=$(resolve_active_slot)
|
|
TARGET_SLOT="green"
|
|
TARGET_SERVICE="$GREEN_SERVICE"
|
|
TARGET_CONTAINER="$GREEN_CONTAINER"
|
|
PREVIOUS_SERVICE="$BLUE_SERVICE"
|
|
PREVIOUS_CONTAINER="$BLUE_CONTAINER"
|
|
|
|
if [ "$ACTIVE_SLOT" = "green" ]; then
|
|
TARGET_SLOT="blue"
|
|
TARGET_SERVICE="$BLUE_SERVICE"
|
|
TARGET_CONTAINER="$BLUE_CONTAINER"
|
|
PREVIOUS_SERVICE="$GREEN_SERVICE"
|
|
PREVIOUS_CONTAINER="$GREEN_CONTAINER"
|
|
fi
|
|
|
|
docker compose -f "$COMPOSE_FILE" up -d --build --force-recreate --no-deps "$TARGET_SERVICE"
|
|
wait_for_container_health "$TARGET_CONTAINER"
|
|
write_proxy_config "$TARGET_SLOT"
|
|
ensure_proxy_running
|
|
printf '%s\n' "$TARGET_SLOT" >"$ACTIVE_SLOT_FILE"
|
|
|
|
if [ "$PREVIOUS_SERVICE" != "$TARGET_SERVICE" ]; then
|
|
set_container_draining "$PREVIOUS_CONTAINER" true
|
|
wait_for_previous_slot_drain "$PREVIOUS_CONTAINER"
|
|
docker compose -f "$COMPOSE_FILE" up -d --build --force-recreate --no-deps "$PREVIOUS_SERVICE"
|
|
wait_for_container_health "$PREVIOUS_CONTAINER"
|
|
fi
|
|
|
|
printf 'work-server zero-downtime switch completed: %s -> %s\n' "$ACTIVE_SLOT" "$TARGET_SLOT"
|