Alza soft capacity fase A e aggiunge quiet hours CPX notturne.

Home a 4 e cloud a 6 con MAX_NODES=12 (~76 soft); di notte (02–07 Europe/Rome) niente scale-out/warm-spare e sweeper che chiude i CPX idle.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 09:48:50 +02:00
co-authored by Cursor
parent d52434fb2e
commit 7c7b2bf14c
16 changed files with 513 additions and 25 deletions
+8 -4
View File
@@ -98,10 +98,11 @@ HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
STREAM_DNS_ZONE=mltv-stream.net
STREAM_DNS_TTL=60
STREAM_CLOUD_DNS_SUFFIX=mltv-stream.net
STREAM_CLOUD_MAX_PUBLISHERS=4
STREAM_CLOUD_MAX_PUBLISHERS=6
STREAM_NODE_HOME_MAX_PUBLISHERS=4
STREAM_NODE_ENV=collaudo
STREAM_CLOUD_PROVIDER=local_lab
RELAY_MAX_CONCURRENT=4
RELAY_MAX_CONCURRENT=6
YOUTUBE_RELAY_WORKER=1
# Worker home: STREAM_NODE_SLUG=home (default). Overflow: coda youtube_relay_<slug>
@@ -111,7 +112,10 @@ STREAM_AUTOSCALE_ALLOW_CLOUD=0
STREAM_AUTOSCALE_SOFT_FREE_SLOTS=2
STREAM_AUTOSCALE_WARM_SPARE=1
STREAM_AUTOSCALE_IDLE_MINUTES=30
STREAM_AUTOSCALE_MAX_NODES=5
STREAM_AUTOSCALE_MAX_NODES=12
STREAM_AUTOSCALE_NODE_EUR_PER_HOUR=0.015
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR=40
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR=150
STREAM_AUTOSCALE_QUIET_HOURS=02:00-07:00
STREAM_AUTOSCALE_QUIET_TZ=Europe/Rome
STREAM_NIGHT_SWEEP_ENABLED=1
STREAM_OVERFLOW_ORPHAN_HOURS=3
+14 -4
View File
@@ -131,11 +131,12 @@ HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
STREAM_DNS_ZONE=mltv-stream.net
STREAM_DNS_TTL=60
STREAM_CLOUD_DNS_SUFFIX=mltv-stream.net
STREAM_CLOUD_MAX_PUBLISHERS=4
STREAM_CLOUD_MAX_PUBLISHERS=6
STREAM_NODE_HOME_MAX_PUBLISHERS=4
STREAM_NODE_ENV=prod
# Lab locale di default; il bottone admin "Provisiona nodo Hetzner" usa comunque i provider hetzner.
STREAM_CLOUD_PROVIDER=local_lab
RELAY_MAX_CONCURRENT=4
RELAY_MAX_CONCURRENT=6
YOUTUBE_RELAY_WORKER=1
# Deve coincidere con cloud-init (default mediamtx_webhook_dev_secret) — home chiama l'agent :9100.
STREAM_NODE_AGENT_SECRET=mediamtx_webhook_dev_secret
@@ -147,8 +148,17 @@ STREAM_AUTOSCALE_ALLOW_CLOUD=0
STREAM_AUTOSCALE_SOFT_FREE_SLOTS=2
STREAM_AUTOSCALE_WARM_SPARE=1
STREAM_AUTOSCALE_IDLE_MINUTES=30
STREAM_AUTOSCALE_MAX_NODES=5
# Fase A (qualità-first): soft ≈ 4 + 12×6 = 76. Fasi successive (dopo misure):
# B: MAX_NODES=22 BUDGET=250 → ~136
# C: MAX_NODES=33 BUDGET=360 → ~202
# Densità 8 solo dopo misure QoS — non alzare CLOUD_MAX prima.
STREAM_AUTOSCALE_MAX_NODES=12
STREAM_AUTOSCALE_INTERVAL_SECS=60
STREAM_AUTOSCALE_NODE_EUR_PER_HOUR=0.015
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR=40
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR=150
# Quiet hours: zero CPX idle 02:0007:00 Europe/Rome (sweeper + no scale-out/warm-spare)
STREAM_AUTOSCALE_QUIET_HOURS=02:00-07:00
STREAM_AUTOSCALE_QUIET_TZ=Europe/Rome
STREAM_NIGHT_SWEEP_ENABLED=1
STREAM_NIGHT_SWEEP_INTERVAL_SECS=900
STREAM_OVERFLOW_ORPHAN_HOURS=3
+16 -8
View File
@@ -73,7 +73,7 @@ services:
YOUTUBE_REDIRECT_URI: ${YOUTUBE_REDIRECT_URI:-}
YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-}
STREAM_NODE_AGENT_SECRET: ${STREAM_NODE_AGENT_SECRET:-mediamtx_webhook_dev_secret}
STREAM_NODE_HOME_MAX_PUBLISHERS: ${STREAM_NODE_HOME_MAX_PUBLISHERS:-6}
STREAM_NODE_HOME_MAX_PUBLISHERS: ${STREAM_NODE_HOME_MAX_PUBLISHERS:-4}
STREAM_CLOUD_PROVIDER: ${STREAM_CLOUD_PROVIDER:-local_lab}
STREAM_DNS_PROVIDER: ${STREAM_DNS_PROVIDER:-hetzner}
STREAM_AUTOSCALE_ENABLED: ${STREAM_AUTOSCALE_ENABLED:-0}
@@ -81,17 +81,21 @@ services:
STREAM_AUTOSCALE_ALLOW_CLOUD: ${STREAM_AUTOSCALE_ALLOW_CLOUD:-0}
STREAM_AUTOSCALE_SOFT_FREE_SLOTS: ${STREAM_AUTOSCALE_SOFT_FREE_SLOTS:-2}
STREAM_AUTOSCALE_WARM_SPARE: ${STREAM_AUTOSCALE_WARM_SPARE:-1}
STREAM_AUTOSCALE_MAX_NODES: ${STREAM_AUTOSCALE_MAX_NODES:-3}
STREAM_AUTOSCALE_MAX_NODES: ${STREAM_AUTOSCALE_MAX_NODES:-12}
STREAM_AUTOSCALE_IDLE_MINUTES: ${STREAM_AUTOSCALE_IDLE_MINUTES:-30}
STREAM_AUTOSCALE_INTERVAL_SECS: ${STREAM_AUTOSCALE_INTERVAL_SECS:-60}
STREAM_AUTOSCALE_NODE_EUR_PER_HOUR: ${STREAM_AUTOSCALE_NODE_EUR_PER_HOUR:-0.015}
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR: ${STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR:-40}
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR: ${STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR:-150}
STREAM_AUTOSCALE_QUIET_HOURS: ${STREAM_AUTOSCALE_QUIET_HOURS:-02:00-07:00}
STREAM_AUTOSCALE_QUIET_TZ: ${STREAM_AUTOSCALE_QUIET_TZ:-Europe/Rome}
STREAM_NIGHT_SWEEP_ENABLED: ${STREAM_NIGHT_SWEEP_ENABLED:-1}
STREAM_NIGHT_SWEEP_INTERVAL_SECS: ${STREAM_NIGHT_SWEEP_INTERVAL_SECS:-900}
STREAM_CLOUD_PUBLIC_CONTROL: ${STREAM_CLOUD_PUBLIC_CONTROL:-1}
STREAM_NODE_ENV: ${STREAM_NODE_ENV:-prod}
STREAM_DNS_ZONE: ${STREAM_DNS_ZONE:-mltv-stream.net}
STREAM_DNS_TTL: ${STREAM_DNS_TTL:-60}
STREAM_CLOUD_DNS_SUFFIX: ${STREAM_CLOUD_DNS_SUFFIX:-mltv-stream.net}
STREAM_CLOUD_MAX_PUBLISHERS: ${STREAM_CLOUD_MAX_PUBLISHERS:-4}
STREAM_CLOUD_MAX_PUBLISHERS: ${STREAM_CLOUD_MAX_PUBLISHERS:-6}
HCLOUD_TOKEN: ${HCLOUD_TOKEN:-}
HCLOUD_LOCATION: ${HCLOUD_LOCATION:-nbg1}
HCLOUD_SERVER_TYPE: ${HCLOUD_SERVER_TYPE:-cpx12}
@@ -219,7 +223,7 @@ services:
YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-}
YOUTUBE_RELAY_WORKER: "1"
STREAM_NODE_SLUG: home
STREAM_NODE_HOME_MAX_PUBLISHERS: ${STREAM_NODE_HOME_MAX_PUBLISHERS:-6}
STREAM_NODE_HOME_MAX_PUBLISHERS: ${STREAM_NODE_HOME_MAX_PUBLISHERS:-4}
STREAM_CLOUD_PROVIDER: ${STREAM_CLOUD_PROVIDER:-local_lab}
STREAM_DNS_PROVIDER: ${STREAM_DNS_PROVIDER:-hetzner}
STREAM_AUTOSCALE_ENABLED: ${STREAM_AUTOSCALE_ENABLED:-0}
@@ -227,17 +231,21 @@ services:
STREAM_AUTOSCALE_ALLOW_CLOUD: ${STREAM_AUTOSCALE_ALLOW_CLOUD:-0}
STREAM_AUTOSCALE_SOFT_FREE_SLOTS: ${STREAM_AUTOSCALE_SOFT_FREE_SLOTS:-2}
STREAM_AUTOSCALE_WARM_SPARE: ${STREAM_AUTOSCALE_WARM_SPARE:-1}
STREAM_AUTOSCALE_MAX_NODES: ${STREAM_AUTOSCALE_MAX_NODES:-3}
STREAM_AUTOSCALE_MAX_NODES: ${STREAM_AUTOSCALE_MAX_NODES:-12}
STREAM_AUTOSCALE_IDLE_MINUTES: ${STREAM_AUTOSCALE_IDLE_MINUTES:-30}
STREAM_AUTOSCALE_INTERVAL_SECS: ${STREAM_AUTOSCALE_INTERVAL_SECS:-60}
STREAM_AUTOSCALE_NODE_EUR_PER_HOUR: ${STREAM_AUTOSCALE_NODE_EUR_PER_HOUR:-0.015}
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR: ${STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR:-40}
STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR: ${STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR:-150}
STREAM_AUTOSCALE_QUIET_HOURS: ${STREAM_AUTOSCALE_QUIET_HOURS:-02:00-07:00}
STREAM_AUTOSCALE_QUIET_TZ: ${STREAM_AUTOSCALE_QUIET_TZ:-Europe/Rome}
STREAM_NIGHT_SWEEP_ENABLED: ${STREAM_NIGHT_SWEEP_ENABLED:-1}
STREAM_NIGHT_SWEEP_INTERVAL_SECS: ${STREAM_NIGHT_SWEEP_INTERVAL_SECS:-900}
STREAM_CLOUD_PUBLIC_CONTROL: ${STREAM_CLOUD_PUBLIC_CONTROL:-1}
STREAM_NODE_ENV: ${STREAM_NODE_ENV:-prod}
STREAM_DNS_ZONE: ${STREAM_DNS_ZONE:-mltv-stream.net}
STREAM_DNS_TTL: ${STREAM_DNS_TTL:-60}
STREAM_CLOUD_DNS_SUFFIX: ${STREAM_CLOUD_DNS_SUFFIX:-mltv-stream.net}
STREAM_CLOUD_MAX_PUBLISHERS: ${STREAM_CLOUD_MAX_PUBLISHERS:-4}
STREAM_CLOUD_MAX_PUBLISHERS: ${STREAM_CLOUD_MAX_PUBLISHERS:-6}
HCLOUD_TOKEN: ${HCLOUD_TOKEN:-}
HCLOUD_LOCATION: ${HCLOUD_LOCATION:-nbg1}
HCLOUD_SERVER_TYPE: ${HCLOUD_SERVER_TYPE:-cpx12}
+1
View File
@@ -32,6 +32,7 @@ CRON_BLOCK="${MARKER}
0 8 * * * mkdir -p ${LOG_DIR} && ${RUNNER} recordings:expiry_warnings >> ${LOG_FILE} 2>&1
15 * * * * mkdir -p ${LOG_DIR} && ${RUNNER} analytics:aggregate >> ${LOG_DIR}/cron-analytics.log 2>&1
20 4 * * * mkdir -p ${LOG_DIR} && ${RUNNER} analytics:purge >> ${LOG_DIR}/cron-analytics.log 2>&1
*/15 * * * * mkdir -p ${LOG_DIR} && ${RUNNER} streams:night_cloud_sweep >> ${LOG_DIR}/cron-night-sweep.log 2>&1
*/10 * * * * mkdir -p ${LOG_DIR} && /bin/bash ${SCAN_LOGS} >> ${OPS_LOG_FILE} 2>&1
"
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Applica soft-limit capacity phases su infra/.env (qualità-first).
# Uso (sul server, in /opt/matchlivetv/infra):
# bash ../scripts/deploy/../infra/scripts/stream_capacity_phase.sh A|B|C
# Fase B/C solo DOPO misure QoS/costi — non saltare.
set -euo pipefail
PHASE="${1:-}"
INFRA="$(cd "$(dirname "$0")/.." && pwd)"
ENV_FILE="${INFRA}/.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Manca $ENV_FILE" >&2
exit 1
fi
case "$PHASE" in
A)
HOME_MAX=4; CLOUD_MAX=6; MAX_NODES=12; BUDGET=150; SOFT_FREE=2; WARM=1; RELAY=6
;;
B)
HOME_MAX=4; CLOUD_MAX=6; MAX_NODES=22; BUDGET=250; SOFT_FREE=3; WARM=2; RELAY=6
;;
C)
HOME_MAX=4; CLOUD_MAX=6; MAX_NODES=33; BUDGET=360; SOFT_FREE=4; WARM=2; RELAY=6
;;
*)
echo "Uso: $0 A|B|C" >&2
echo " A → soft ~76 (home4 + 12×6)" >&2
echo " B → soft ~136 (dopo misure)" >&2
echo " C → soft ~202 (tetto ≥200)" >&2
exit 1
;;
esac
cp -a "$ENV_FILE" "${ENV_FILE}.bak.phase${PHASE}-$(date +%Y%m%d%H%M%S)"
set_kv() {
local key="$1" val="$2"
if grep -qE "^${key}=" "$ENV_FILE"; then
sed -i "s|^${key}=.*|${key}=${val}|" "$ENV_FILE"
else
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
fi
}
set_kv STREAM_NODE_HOME_MAX_PUBLISHERS "$HOME_MAX"
set_kv STREAM_CLOUD_MAX_PUBLISHERS "$CLOUD_MAX"
set_kv STREAM_AUTOSCALE_MAX_NODES "$MAX_NODES"
set_kv STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR "$BUDGET"
set_kv STREAM_AUTOSCALE_SOFT_FREE_SLOTS "$SOFT_FREE"
set_kv STREAM_AUTOSCALE_WARM_SPARE "$WARM"
set_kv RELAY_MAX_CONCURRENT "$RELAY"
set_kv STREAM_AUTOSCALE_QUIET_HOURS "02:00-07:00"
set_kv STREAM_AUTOSCALE_QUIET_TZ "Europe/Rome"
set_kv STREAM_NIGHT_SWEEP_ENABLED "1"
echo "Fase $PHASE applicata su $ENV_FILE"
echo " home=$HOME_MAX cloud=$CLOUD_MAX max_nodes=$MAX_NODES budget=$BUDGET soft≈$((HOME_MAX + MAX_NODES * CLOUD_MAX))"
echo "Riavvia rails+sidekiq e: rails runner 'Streams::NodeRegistry.ensure_home_from_env!'"