Avvia i relay YouTube overflow dal Sidekiq home e passa STREAM/HCLOUD in produzione.

Così un CPX nuovo non richiede worker Docker a mano, l'agent è nel cloud-init e gli E2E restano non in elenco.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 08:57:56 +02:00
co-authored by Cursor
parent dd94bf7e66
commit 841a6a15be
10 changed files with 452 additions and 31 deletions
+16
View File
@@ -35,4 +35,20 @@ class StreamNode < ApplicationRecord
def allocatable?
status == "ready" && free_slots.positive?
end
# Agent ffmpeg sul CPX (cloud-init :9100). Home Sidekiq lo chiama senza worker Docker per nodo.
def relay_agent_url
return unless role == "cloud"
ip = (metadata || {})["public_ip"].presence
if ip.blank?
host = URI.parse(api_base_url.to_s).host
ip = host if host.present? && host != "localhost"
end
return if ip.blank? || ip == "127.0.0.1"
"http://#{ip}:#{ENV.fetch("STREAM_NODE_AGENT_PORT", "9100")}"
rescue URI::InvalidURIError
nil
end
end
+58 -23
View File
@@ -35,7 +35,26 @@ module Streams
assigned_node_slug(session) == local_node_slug
end
# Home può avviare ffmpeg sull'agent del CPX (niente worker Docker per nodo).
def can_process?(session)
return false unless worker?
return true if node_matches?(session)
cloud_home_dispatch?(session)
end
def cloud_home_dispatch?(session)
local_node_slug == NodeRegistry::HOME_SLUG &&
session.stream_node&.role == "cloud" &&
session.stream_node.relay_agent_url.present?
end
def queue_for(session)
node = session.stream_node
if node&.role == "cloud" && node.relay_agent_url.present?
return "#{QUEUE_PREFIX}_cloud"
end
"#{QUEUE_PREFIX}_#{assigned_node_slug(session)}"
end
@@ -50,14 +69,14 @@ module Streams
end
def start(session)
return enqueue_ensure!(session) unless worker? && node_matches?(session)
return enqueue_ensure!(session) unless can_process?(session)
start_on_worker!(session)
end
# Non cancella owner/pid qui: solo il worker owner deve killare ffmpeg.
def stop(session)
if worker? && owner_is_local?(session.id) && node_matches?(session)
if worker? && owner_is_local?(session.id) && can_process?(session)
stop_on_worker!(session)
else
enqueue_stop!(session)
@@ -83,7 +102,7 @@ module Streams
return if session.terminal?
return if session.stream_key.blank?
if worker? && node_matches?(session)
if can_process?(session)
ensure_on_worker!(session)
else
enqueue_ensure!(session)
@@ -92,7 +111,7 @@ module Streams
def ensure_on_worker!(session)
return :not_worker unless worker?
unless node_matches?(session)
unless can_process?(session)
enqueue_ensure!(session, wait: 2.seconds)
Rails.logger.info(
"[YoutubeRelay] wrong_node local=#{local_node_slug} assigned=#{assigned_node_slug(session)} session=#{session.id}"
@@ -115,7 +134,7 @@ module Streams
return :already_running
end
if at_capacity?
if at_capacity_for?(session)
enqueue_ensure!(session, wait: 5.seconds)
Rails.logger.info("[YoutubeRelay] at capacity worker=#{worker_id} node=#{local_node_slug} session=#{session.id} requeue")
return :at_capacity
@@ -129,6 +148,7 @@ module Streams
:started
rescue Error => e
Rails.logger.warn("[YoutubeRelay] ensure_on_worker session=#{session.id}: #{e.message}")
enqueue_ensure!(session, wait: 5.seconds) unless session.terminal?
:error
end
@@ -161,6 +181,13 @@ module Streams
local_owned_count >= max_concurrent
end
# I relay sull'agent CPX non consumano ffmpeg locale: non applicare il cap del worker home.
def at_capacity_for?(session)
return false if cloud_home_dispatch?(session)
at_capacity?
end
def owner_is_local?(session_id)
owner = redis.get(format(OWNER_KEY, session_id))
owner.blank? || owner == worker_id
@@ -188,7 +215,7 @@ module Streams
intake_source = mediamtx_intake_source(session)
output = "rtmps://a.rtmps.youtube.com/live2/#{session.stream_key}"
pid = if relay_agent_enabled?
pid = if agent_dispatch?(session)
start_via_agent!(session)
else
spawn_ffmpeg!(youtube_ffmpeg_args(intake_source, output), log_path)
@@ -197,7 +224,7 @@ module Streams
claim_ownership!(session.id)
Rails.logger.info(
"[YoutubeRelay] started pid=#{pid} session=#{session.id} intake=#{intake_source.join(':')} " \
"worker=#{worker_id} agent=#{relay_agent_url || '-'}"
"worker=#{worker_id} agent=#{agent_base_url(session) || '-'}"
)
schedule_youtube_activate(session)
pid
@@ -205,16 +232,18 @@ module Streams
raise Error, "ffmpeg non disponibile: #{e.message}"
end
def relay_agent_url
ENV["STREAM_NODE_RELAY_AGENT_URL"].presence
def agent_base_url(session)
return if session.blank?
ENV["STREAM_NODE_RELAY_AGENT_URL"].presence || session.stream_node&.relay_agent_url
end
def relay_agent_enabled?
relay_agent_url.present?
def agent_dispatch?(session)
agent_base_url(session).present?
end
def relay_agent_secret
ENV["STREAM_NODE_AGENT_SECRET"].presence || ENV["MEDIAMTX_WEBHOOK_SECRET"].presence || ""
ENV["STREAM_NODE_AGENT_SECRET"].presence || "mediamtx_webhook_dev_secret"
end
def spawn_ffmpeg!(args, log_path)
@@ -229,19 +258,23 @@ module Streams
"path" => session.mediamtx_path_name,
"rtmps" => "rtmps://a.rtmps.youtube.com/live2/#{session.stream_key}"
}
res = agent_request(Net::HTTP::Post, "/relays", payload)
res = agent_request(Net::HTTP::Post, "/relays", payload, session: session)
pid = res["pid"].to_i
raise Error, "relay agent spawn failed: #{res.inspect}" if pid <= 0
File.write(
log_file(session),
"agent=#{relay_agent_url} pid=#{pid} path=#{session.mediamtx_path_name}\n"
"agent=#{agent_base_url(session)} pid=#{pid} path=#{session.mediamtx_path_name}\n"
)
pid
end
def agent_request(http_class, path, payload = nil)
uri = URI.parse("#{relay_agent_url.chomp('/')}#{path}")
def agent_request(http_class, path, payload = nil, session: nil, session_id: nil)
session ||= StreamSession.find_by(id: session_id) if session_id.present?
base = agent_base_url(session)
raise Error, "relay agent URL mancante" if base.blank?
uri = URI.parse("#{base.chomp('/')}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.open_timeout = 5
http.read_timeout = 10
@@ -304,7 +337,7 @@ module Streams
# Sul nodo assegnato ffmpeg legge MediaMTX in loopback (o hostname Docker locale).
def intake_bases(session)
if relay_agent_enabled? && node_matches?(session)
if agent_dispatch?(session) && (node_matches?(session) || cloud_home_dispatch?(session))
return ["rtmp://127.0.0.1:1935", "http://127.0.0.1:8888"]
end
@@ -376,7 +409,8 @@ module Streams
end
def process_alive?(pid, session_id: nil)
return agent_session_running?(session_id) if relay_agent_enabled? && session_id.present?
session = session_id.present? ? StreamSession.find_by(id: session_id) : nil
return agent_session_running?(session) if session && agent_dispatch?(session)
stat = File.read("/proc/#{pid.to_i}/stat")
return false if stat.split[2] == "Z"
@@ -387,16 +421,17 @@ module Streams
false
end
def agent_session_running?(session_id)
res = agent_request(Net::HTTP::Get, "/relays/#{session_id}")
def agent_session_running?(session)
res = agent_request(Net::HTTP::Get, "/relays/#{session.id}", session: session)
res["running"] == true
rescue Error
false
end
def terminate_pid(pid, session_id: nil)
if relay_agent_enabled? && session_id.present?
agent_request(Net::HTTP::Delete, "/relays/#{session_id}")
session = session_id.present? ? StreamSession.find_by(id: session_id) : nil
if session && agent_dispatch?(session)
agent_request(Net::HTTP::Delete, "/relays/#{session_id}", session: session)
return
end
@@ -405,7 +440,7 @@ module Streams
rescue Errno::ESRCH
nil
else
return if relay_agent_enabled?
return if session && agent_dispatch?(session)
begin
Process.kill("KILL", -pid.to_i)