Skip to content

Commit 98d01d5

Browse files
authored
Merge pull request #50 from figma/ebarajas/ruby-support-dynamic-deadline
ruby: add support for dynamic deadlines when running suites
2 parents 0b67b33 + 7f975e7 commit 98d01d5

File tree

8 files changed

+555
-15
lines changed

8 files changed

+555
-15
lines changed

redis/reserve.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ local test_group_timeout_key = KEYS[6]
77

88
local current_time = ARGV[1]
99
local use_dynamic_deadline = ARGV[2] == "true"
10+
local default_timeout = ARGV[3] or 0
1011

1112
local test = redis.call('rpop', queue_key)
1213
if test then
1314
if use_dynamic_deadline then
1415
local dynamic_timeout = redis.call('hget', test_group_timeout_key, test)
16+
if not dynamic_timeout then
17+
dynamic_timeout = default_timeout
18+
else
19+
dynamic_timeout = tonumber(dynamic_timeout)
20+
end
1521
redis.call('zadd', zset_key, current_time + dynamic_timeout, test)
1622
else
1723
redis.call('zadd', zset_key, current_time, test)

redis/reserve_lost.lua

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ local worker_queue_key = KEYS[3]
44
local owners_key = KEYS[4]
55
local test_group_timeout_key = KEYS[5]
66

7-
local current_time = ARGV[1]
8-
local timeout = ARGV[2]
7+
local current_time = tonumber(ARGV[1])
8+
local timeout = tonumber(ARGV[2])
99
local use_dynamic_deadline = ARGV[3] == "true"
10+
local default_timeout = tonumber(ARGV[4]) or 0
1011

1112
local lost_tests
1213
if use_dynamic_deadline then
@@ -19,9 +20,14 @@ for _, test in ipairs(lost_tests) do
1920
if redis.call('sismember', processed_key, test) == 0 then
2021
if use_dynamic_deadline then
2122
local dynamic_timeout = redis.call('hget', test_group_timeout_key, test)
23+
if not dynamic_timeout or dynamic_timeout == "" then
24+
dynamic_timeout = default_timeout
25+
else
26+
dynamic_timeout = tonumber(dynamic_timeout)
27+
end
2228
redis.call('zadd', zset_key, current_time + dynamic_timeout, test)
2329
else
24-
redis.call('zadd', zset_key, current_time, test)
30+
redis.call('zadd', zset_key, current_time + timeout, test)
2531
end
2632
redis.call('lpush', worker_queue_key, test)
2733
redis.call('hset', owners_key, test, worker_queue_key) -- Take ownership

ruby/lib/ci/queue/redis/worker.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,9 @@ def try_to_reserve_test
219219
key('processed'),
220220
key('worker', worker_id, 'queue'),
221221
key('owners'),
222+
key('test-group-timeout'),
222223
],
223-
argv: [CI::Queue.time_now.to_f],
224+
argv: [CI::Queue.time_now.to_f, 'true', config.timeout],
224225
)
225226
end
226227

@@ -232,8 +233,9 @@ def try_to_reserve_lost_test
232233
key('completed'),
233234
key('worker', worker_id, 'queue'),
234235
key('owners'),
236+
key('test-group-timeout'),
235237
],
236-
argv: [CI::Queue.time_now.to_f, timeout],
238+
argv: [CI::Queue.time_now.to_f, timeout, 'true', config.timeout],
237239
)
238240

239241
if lost_test.nil? && idle?
@@ -277,8 +279,8 @@ def register
277279

278280
def store_chunk_metadata(chunks)
279281
# Batch operations to avoid exceeding Redis multi operation limits
280-
# Each chunk requires 3 commands (set, expire, sadd), so batch conservatively
281-
batch_size = 7 # 7 chunks = 21 commands + 1 expire = 22 commands per batch
282+
# Each chunk requires 4 commands (set, expire, sadd, hset), so batch conservatively
283+
batch_size = 5 # 5 chunks = 20 commands + 2 expires = 22 commands per batch
282284

283285
chunks.each_slice(batch_size) do |chunk_batch|
284286
redis.multi do |transaction|
@@ -292,8 +294,14 @@ def store_chunk_metadata(chunks)
292294

293295
# Track all chunks for cleanup
294296
transaction.sadd(key('chunks'), chunk.id)
297+
298+
# Store dynamic timeout for this chunk
299+
# Timeout = default_timeout * number_of_tests
300+
chunk_timeout = config.timeout * chunk.test_count
301+
transaction.hset(key('test-group-timeout'), chunk.id, chunk_timeout)
295302
end
296303
transaction.expire(key('chunks'), config.redis_ttl)
304+
transaction.expire(key('test-group-timeout'), config.redis_ttl)
297305
end
298306
end
299307
end

ruby/lib/ci/queue/strategy/suite_bin_packing.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ def create_chunks_for_suite(suite_name, suite_tests, max_duration, buffer_percen
6262
# If suite fits in max duration, create full_suite chunk
6363
if total_duration <= max_duration
6464
chunk_id = "#{suite_name}:full_suite"
65-
# Don't store test_ids - worker will resolve from index
66-
return [TestChunk.new(chunk_id, suite_name, :full_suite, [], total_duration)]
65+
# Don't store test_ids in Redis - worker will resolve from index
66+
# But pass test_count for timeout calculation
67+
return [TestChunk.new(chunk_id, suite_name, :full_suite, [], total_duration, test_count: suite_tests.size)]
6768
end
6869

6970
# Suite too large - split into partial_suite chunks

ruby/lib/ci/queue/test_chunk.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ module Queue
55
class TestChunk
66
include Comparable
77

8-
attr_reader :id, :suite_name, :type, :test_ids, :estimated_duration
8+
attr_reader :id, :suite_name, :type, :test_ids, :estimated_duration, :test_count
99

10-
# type can be :full_suite or :partial_suite
11-
def initialize(id, suite_name, type, test_ids, estimated_duration = 0)
10+
def initialize(id, suite_name, type, test_ids, estimated_duration = 0, test_count: nil)
1211
@id = id
1312
@suite_name = suite_name
1413
@type = type
1514
@test_ids = test_ids.freeze
1615
@estimated_duration = estimated_duration
16+
@test_count = test_count || test_ids.size
1717
end
1818

1919
def full_suite?
@@ -38,7 +38,8 @@ def to_json(*args)
3838
data = {
3939
type: type.to_s,
4040
suite_name: suite_name,
41-
estimated_duration: estimated_duration
41+
estimated_duration: estimated_duration,
42+
test_count: test_count
4243
}
4344

4445
# Only include test_ids for partial suites
@@ -55,7 +56,8 @@ def self.from_json(chunk_id, json_string)
5556
data['suite_name'],
5657
data['type'].to_sym,
5758
data['test_ids'] || [], # Empty for full_suite
58-
data['estimated_duration']
59+
data['estimated_duration'],
60+
test_count: data['test_count']
5961
)
6062
end
6163
end

0 commit comments

Comments
 (0)