@@ -30,8 +30,19 @@ def distributed?
3030
3131 def populate ( tests , random : Random . new )
3232 @index = tests . map { |t | [ t . id , t ] } . to_h
33- tests = Queue . shuffle ( tests , random , config : config )
34- push ( tests . map ( &:id ) )
33+ executables = Queue . shuffle ( tests , random , config : config )
34+
35+ # Separate chunks from individual tests
36+ chunks = executables . select { |e | e . is_a? ( CI ::Queue ::TestChunk ) }
37+ individual_tests = executables . select { |e | !e . is_a? ( CI ::Queue ::TestChunk ) }
38+
39+ # Store chunk metadata in Redis (only master does this)
40+ store_chunk_metadata ( chunks ) if chunks . any?
41+
42+ # Push all IDs to queue (chunks + individual tests)
43+ all_ids = chunks . map ( &:id ) + individual_tests . map ( &:id )
44+ push ( all_ids )
45+
3546 self
3647 end
3748
@@ -60,9 +71,16 @@ def poll
6071 idle_since = nil
6172 idle_state_printed = false
6273 until shutdown_required? || config . circuit_breakers . any? ( &:open? ) || exhausted? || max_test_failed?
63- if test = reserve
74+ if id = reserve
6475 idle_since = nil
65- yield index . fetch ( test )
76+ executable = resolve_executable ( id )
77+
78+ if executable
79+ yield executable
80+ else
81+ warn ( "Warning: Could not resolve executable for ID #{ id . inspect } . Acknowledging to remove from queue." )
82+ acknowledge ( id )
83+ end
6684 else
6785 idle_since ||= Time . now
6886 if Time . now - idle_since > 120 && !idle_state_printed
@@ -121,8 +139,9 @@ def build
121139 @build ||= CI ::Queue ::Redis ::BuildRecord . new ( self , redis , config )
122140 end
123141
124- def acknowledge ( test )
125- test_key = test . id
142+ def acknowledge ( test_or_id )
143+ # Accept either an object with .id or a string ID
144+ test_key = test_or_id . respond_to? ( :id ) ? test_or_id . id : test_or_id
126145 raise_on_mismatching_test ( test_key )
127146 eval_script (
128147 :acknowledge ,
@@ -253,6 +272,114 @@ def push(tests)
253272 def register
254273 redis . sadd ( key ( 'workers' ) , [ worker_id ] )
255274 end
275+
276+ private
277+
278+ def store_chunk_metadata ( chunks )
279+ redis . multi do |transaction |
280+ chunks . each do |chunk |
281+ # Store chunk metadata with TTL
282+ transaction . set (
283+ key ( 'chunk' , chunk . id ) ,
284+ chunk . to_json
285+ )
286+ transaction . expire ( key ( 'chunk' , chunk . id ) , config . redis_ttl )
287+
288+ # Track all chunks for cleanup
289+ transaction . sadd ( key ( 'chunks' ) , chunk . id )
290+ end
291+ transaction . expire ( key ( 'chunks' ) , config . redis_ttl )
292+ end
293+ end
294+
295+ def chunk_id? ( id )
296+ id . include? ( ':full_suite' ) || id . include? ( ':chunk_' )
297+ end
298+
299+ def resolve_executable ( id )
300+ # Detect chunk by ID pattern
301+ if chunk_id? ( id )
302+ resolve_chunk ( id )
303+ else
304+ # Regular test - existing behavior
305+ index . fetch ( id )
306+ end
307+ end
308+
309+ def resolve_chunk ( chunk_id )
310+ # Fetch chunk metadata from Redis
311+ chunk_json = redis . get ( key ( 'chunk' , chunk_id ) )
312+ unless chunk_json
313+ warn "Warning: Chunk metadata not found for #{ chunk_id } "
314+ return nil
315+ end
316+
317+ chunk = CI ::Queue ::TestChunk . from_json ( chunk_id , chunk_json )
318+
319+ # Resolve test objects based on chunk type
320+ test_objects = if chunk . full_suite?
321+ resolve_full_suite_tests ( chunk . suite_name )
322+ else
323+ resolve_partial_suite_tests ( chunk . test_ids )
324+ end
325+
326+ if test_objects . empty?
327+ warn "Warning: No tests found for chunk #{ chunk_id } "
328+ return nil
329+ end
330+
331+ # Return enriched chunk with actual test objects
332+ ResolvedChunk . new ( chunk , test_objects )
333+ rescue JSON ::ParserError => e
334+ warn "Warning: Could not parse chunk metadata for #{ chunk_id } : #{ e . message } "
335+ nil
336+ rescue KeyError => e
337+ warn "Warning: Could not resolve test in chunk #{ chunk_id } : #{ e . message } "
338+ nil
339+ end
340+
341+ def resolve_full_suite_tests ( suite_name )
342+ # Filter index for all tests from this suite
343+ # Tests are added to index during populate() with format "SuiteName#test_method"
344+ prefix = "#{ suite_name } #"
345+ tests = index . select { |test_id , _ | test_id . start_with? ( prefix ) }
346+ . values
347+
348+ # Sort to maintain consistent order (alphabetical by test name)
349+ tests . sort_by ( &:id )
350+ end
351+
352+ def resolve_partial_suite_tests ( test_ids )
353+ # Fetch specific tests from index
354+ test_ids . map { |test_id | index . fetch ( test_id ) }
355+ end
356+
357+ # Represents a chunk with resolved test objects
358+ class ResolvedChunk
359+ attr_reader :chunk_id , :suite_name , :tests
360+
361+ def initialize ( chunk , tests )
362+ @chunk_id = chunk . id
363+ @suite_name = chunk . suite_name
364+ @tests = tests . freeze
365+ end
366+
367+ def id
368+ chunk_id
369+ end
370+
371+ def chunk?
372+ true
373+ end
374+
375+ def flaky?
376+ tests . any? ( &:flaky? )
377+ end
378+
379+ def size
380+ tests . size
381+ end
382+ end
256383 end
257384 end
258385 end
0 commit comments