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 <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 18:42:35 +02:00
co-authored by Cursor
parent 4c5af99efa
commit 00fde25c50
2 changed files with 61 additions and 7 deletions
@@ -131,6 +131,7 @@ module Streams
def initialize(provisioner: nil) def initialize(provisioner: nil)
@provisioner = provisioner || NodeProvisioner.new @provisioner = provisioner || NodeProvisioner.new
@provisioned_this_round = []
end end
def reconcile! def reconcile!
@@ -154,6 +155,7 @@ module Streams
scale_in_candidates.each do |node| scale_in_candidates.each do |node|
next if keep_as_warm_spare?(node) next if keep_as_warm_spare?(node)
next if @provisioned_this_round.include?(node.id)
safe_scale_in!(node) safe_scale_in!(node)
actions << :"scale_in_#{node.slug}" actions << :"scale_in_#{node.slug}"
@@ -191,6 +193,7 @@ module Streams
end end
def provision_overflow! def provision_overflow!
node =
case self.class.kind case self.class.kind
when "cloud" when "cloud"
raise NodeProvisioner::Error, "Cloud autoscale disabilitato (STREAM_AUTOSCALE_ALLOW_CLOUD / HCLOUD_TOKEN)" unless self.class.allow_cloud? raise NodeProvisioner::Error, "Cloud autoscale disabilitato (STREAM_AUTOSCALE_ALLOW_CLOUD / HCLOUD_TOKEN)" unless self.class.allow_cloud?
@@ -199,6 +202,8 @@ module Streams
else else
@provisioner.provision_lab! @provisioner.provision_lab!
end end
@provisioned_this_round << node.id if node
node
end end
def safe_scale_in!(node) def safe_scale_in!(node)
@@ -91,6 +91,55 @@ RSpec.describe Streams::Autoscaler do
end end
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 it "scales in an idle overflow node when warm spare is not required" do
with_env( with_env(
"STREAM_AUTOSCALE_ENABLED" => "1", "STREAM_AUTOSCALE_ENABLED" => "1",