-
Notifications
You must be signed in to change notification settings - Fork 0
ruby: Measure worker slack and let a sample of workers exit idle #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,10 +24,30 @@ def initialize(redis, config) | |
| @reserved_tests = Concurrent::Set.new | ||
| @shutdown_required = false | ||
| @idle_since = nil | ||
| @last_test_finished_at = nil | ||
| # A worker with nothing left to reserve normally stays online until the whole | ||
| # build drains, so it can pick up a test that times out or gets requeued. Only | ||
| # a sample needs to do that. Random.new rather than Kernel#rand: the global RNG | ||
| # is seeded from --seed, which is identical across workers, so every worker | ||
| # would draw the same number. | ||
| @waits_for_requeues = Random.new.rand > config.idle_exit_probability | ||
| super(redis, config) | ||
| end | ||
|
|
||
| attr_accessor :idle_since | ||
| attr_reader :last_test_finished_at | ||
|
|
||
| def waits_for_requeues? | ||
| @waits_for_requeues | ||
| end | ||
|
|
||
| # Time between the last test finishing and the worker leaving the queue: capacity | ||
| # the build paid for and did not use. Nil when the worker never reserved a test. | ||
| def slack_duration | ||
| return nil if @last_test_finished_at.nil? | ||
|
|
||
| CI::Queue.time_now - @last_test_finished_at | ||
| end | ||
|
|
||
| def distributed? | ||
| true | ||
|
|
@@ -75,6 +95,15 @@ def idle? | |
| !@idle_since.nil? | ||
| end | ||
|
|
||
| # Workers that did not draw the requeue duty leave once they have been idle for | ||
| # the grace period, instead of waiting for the whole build to drain. | ||
| def idle_exit? | ||
| return false if @waits_for_requeues | ||
| return false if @idle_since.nil? | ||
|
|
||
| CI::Queue.time_now - @idle_since >= config.idle_exit_grace | ||
| end | ||
|
|
||
| def poll | ||
| wait_for_master | ||
| if master? | ||
|
|
@@ -83,26 +112,28 @@ def poll | |
| master_id = master_worker_id | ||
| warn "Worker #{worker_id} saw master worker: #{master_id}" if master_id | ||
| end | ||
| idle_since = nil | ||
| @idle_since = nil | ||
| idle_state_printed = false | ||
| attempt = 0 | ||
| until shutdown_required? || config.circuit_breakers.any?(&:open?) || exhausted? || max_test_failed? | ||
| until shutdown_required? || config.circuit_breakers.any?(&:open?) || exhausted? || | ||
| max_test_failed? || idle_exit? | ||
| if id = reserve | ||
| attempt = 0 | ||
| idle_since = nil | ||
| @idle_since = nil | ||
| executable = resolve_executable(id) | ||
|
|
||
| if executable | ||
| with_heartbeat(id) do | ||
| yield executable | ||
| end | ||
| @last_test_finished_at = CI::Queue.time_now | ||
| else | ||
| warn("Warning: Could not resolve executable for ID #{id.inspect}. Acknowledging to remove from queue.") | ||
| acknowledge(id) | ||
| end | ||
| else | ||
| idle_since ||= CI::Queue.time_now | ||
| if CI::Queue.time_now - idle_since > 120 && !idle_state_printed | ||
| @idle_since ||= CI::Queue.time_now | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idle workers spam logs and RedisMedium Severity Writing Additional Locations (1)Reviewed by Cursor Bugbot for commit 86365bb. Configure here. |
||
| if CI::Queue.time_now - @idle_since > 120 && !idle_state_printed | ||
| puts "Worker #{worker_id} has been idle for 120 seconds. Printing global state..." | ||
| running_tests = redis.zrange(key('running'), 0, -1, withscores: true) | ||
| puts " Processed tests: #{redis.scard(key('processed'))}" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # frozen_string_literal: true | ||
| require 'test_helper' | ||
|
|
||
| module CI::Queue::Redis | ||
| class WorkerIdleExitTest < Minitest::Test | ||
| REDIS_URL = 'redis://localhost:6379/0' | ||
|
|
||
| def test_worker_that_drew_requeue_duty_never_exits_on_idle | ||
| worker = build_worker(idle_exit_probability: 0.0, idle_exit_grace: 0) | ||
| assert worker.waits_for_requeues? | ||
|
|
||
| worker.idle_since = CI::Queue.time_now - 3600 | ||
| refute worker.idle_exit? | ||
| end | ||
|
|
||
| def test_worker_without_requeue_duty_waits_out_the_grace_period | ||
| worker = build_worker(idle_exit_probability: 1.0, idle_exit_grace: 30) | ||
| refute worker.waits_for_requeues? | ||
|
|
||
| worker.idle_since = CI::Queue.time_now - 10 | ||
| refute worker.idle_exit? | ||
|
|
||
| worker.idle_since = CI::Queue.time_now - 31 | ||
| assert worker.idle_exit? | ||
| end | ||
|
|
||
| def test_a_busy_worker_never_exits_on_idle | ||
| worker = build_worker(idle_exit_probability: 1.0, idle_exit_grace: 0) | ||
| assert_nil worker.idle_since | ||
| refute worker.idle_exit? | ||
| end | ||
|
|
||
| def test_slack_duration_is_unknown_until_a_test_finishes | ||
| assert_nil build_worker.slack_duration | ||
| end | ||
|
|
||
| def test_slack_duration_measures_from_the_last_finished_test | ||
| worker = build_worker | ||
| worker.instance_variable_set(:@last_test_finished_at, CI::Queue.time_now - 12) | ||
| assert_in_delta 12, worker.slack_duration, 1 | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def build_worker(**options) | ||
| Worker.new(REDIS_URL, CI::Queue::Configuration.new(**options)) | ||
| end | ||
| end | ||
| end |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Zero idle grace is ignored
Low Severity
from_envuses|| 30.0afterto_f, soCI_QUEUE_IDLE_EXIT_GRACE=0becomes0.0and falls back to30.0. A zero grace is a valid “leave as soon as idle” setting and is already used that way on the constructor path.Reviewed by Cursor Bugbot for commit 86365bb. Configure here.