From 00fde25c50c1065c02646bb882512627f149cfe9 Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Tue, 11 Aug 2026 18:42:35 +0200 Subject: [PATCH] Evita scale-in del nodo appena creato nello stesso reconcile. Con IDLE_MINUTES=0 (o race stretta) lo scale-out veniva annullato subito; lo smoke AutoscalerJob Cloud su collaudo ora completa scale-out e scale-in. Co-authored-by: Cursor --- backend/app/services/streams/autoscaler.rb | 19 ++++--- .../spec/services/streams/autoscaler_spec.rb | 49 +++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/backend/app/services/streams/autoscaler.rb b/backend/app/services/streams/autoscaler.rb index 9076db8..902eb52 100644 --- a/backend/app/services/streams/autoscaler.rb +++ b/backend/app/services/streams/autoscaler.rb @@ -131,6 +131,7 @@ module Streams def initialize(provisioner: nil) @provisioner = provisioner || NodeProvisioner.new + @provisioned_this_round = [] end def reconcile! @@ -154,6 +155,7 @@ module Streams scale_in_candidates.each do |node| next if keep_as_warm_spare?(node) + next if @provisioned_this_round.include?(node.id) safe_scale_in!(node) actions << :"scale_in_#{node.slug}" @@ -191,14 +193,17 @@ module Streams end def provision_overflow! - case self.class.kind - when "cloud" - raise NodeProvisioner::Error, "Cloud autoscale disabilitato (STREAM_AUTOSCALE_ALLOW_CLOUD / HCLOUD_TOKEN)" unless self.class.allow_cloud? + node = + case self.class.kind + when "cloud" + raise NodeProvisioner::Error, "Cloud autoscale disabilitato (STREAM_AUTOSCALE_ALLOW_CLOUD / HCLOUD_TOKEN)" unless self.class.allow_cloud? - @provisioner.provision_cloud! - else - @provisioner.provision_lab! - end + @provisioner.provision_cloud! + else + @provisioner.provision_lab! + end + @provisioned_this_round << node.id if node + node end def safe_scale_in!(node) diff --git a/backend/spec/services/streams/autoscaler_spec.rb b/backend/spec/services/streams/autoscaler_spec.rb index a40408a..1ab061e 100644 --- a/backend/spec/services/streams/autoscaler_spec.rb +++ b/backend/spec/services/streams/autoscaler_spec.rb @@ -91,6 +91,55 @@ RSpec.describe Streams::Autoscaler do end end + it "does not scale in a node provisioned in the same reconcile round" do + with_env( + "STREAM_AUTOSCALE_ENABLED" => "1", + "STREAM_AUTOSCALE_SOFT_FREE_SLOTS" => "2", + "STREAM_AUTOSCALE_WARM_SPARE" => "0", + "STREAM_AUTOSCALE_IDLE_MINUTES" => "0", + "STREAM_AUTOSCALE_KIND" => "lab", + "STREAM_AUTOSCALE_MAX_NODES" => "5", + "STREAM_CLOUD_PROVIDER" => "local_lab", + "STREAM_DNS_PROVIDER" => "lab", + "STREAM_NODE_HOME_MAX_PUBLISHERS" => "2", + "MEDIAMTX_API_URL" => "http://mtx-home:9997", + "MEDIAMTX_RTMP_URL" => "rtmp://home.example:1935", + "HLS_PUBLIC_URL" => "https://home.example/hls" + ) do + home = Streams::NodeRegistry.ensure_home_from_env! + user = User.create!(email: "same@example.com", name: "S", password: "Password123", role: "coach") + club = Club.create!(name: "Same", sport: "volleyball") + team = club.teams.create!(name: "T", sport: "volleyball", slug: "same-t") + match = team.matches.create!(opponent_name: "X", scheduled_at: 1.hour.from_now) + 2.times do + StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "live", stream_node: home) + end + + provisioner = instance_double(Streams::NodeProvisioner) + created = nil + allow(provisioner).to receive(:provision_lab!) do + created = StreamNode.create!( + slug: "ingest-lab-new", + hostname: "new.lab", + role: "lab", + status: "ready", + provider: "local", + rtmp_base_url: "rtmp://h:1935", + hls_base_url: "https://h/hls", + api_base_url: "http://h:9997", + max_publishers: 4, + max_relays: 2 + ) + end + expect(provisioner).not_to receive(:decommission!) + + result = described_class.reconcile!(provisioner: provisioner) + expect(result.actions).to include(:scale_out) + expect(result.actions.map(&:to_s)).not_to include("scale_in_ingest-lab-new") + expect(StreamNode.find_by(slug: "ingest-lab-new")).to be_present + end + end + it "scales in an idle overflow node when warm spare is not required" do with_env( "STREAM_AUTOSCALE_ENABLED" => "1",