@@ -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+ # Couldn't resolve - acknowledge and move on
82+ acknowledge_id ( id )
83+ end
6684 else
6785 idle_since ||= Time . now
6886 if Time . now - idle_since > 120 && !idle_state_printed
@@ -253,6 +271,131 @@ def push(tests)
253271 def register
254272 redis . sadd ( key ( 'workers' ) , [ worker_id ] )
255273 end
274+
275+ private
276+
277+ def acknowledge_id ( id )
278+ # Acknowledge by removing from processing
279+ redis . lrem ( key ( 'processing' ) , 1 , id )
280+ end
281+
282+ def store_chunk_metadata ( chunks )
283+ redis . multi do |transaction |
284+ chunks . each do |chunk |
285+ # Store chunk metadata with TTL
286+ transaction . set (
287+ key ( 'chunk' , chunk . id ) ,
288+ chunk . to_json
289+ )
290+ transaction . expire ( key ( 'chunk' , chunk . id ) , config . redis_ttl )
291+
292+ # Track all chunks for cleanup
293+ transaction . sadd ( key ( 'chunks' ) , chunk . id )
294+ end
295+ transaction . expire ( key ( 'chunks' ) , config . redis_ttl )
296+ end
297+ end
298+
299+ def chunk_id? ( id )
300+ id . include? ( ':full_suite' ) || id . include? ( ':chunk_' )
301+ end
302+
303+ def resolve_executable ( id )
304+ # Detect chunk by ID pattern
305+ if chunk_id? ( id )
306+ resolve_chunk ( id )
307+ else
308+ # Regular test - existing behavior
309+ index . fetch ( id )
310+ end
311+ end
312+
313+ def resolve_chunk ( chunk_id )
314+ # Fetch chunk metadata from Redis
315+ chunk_json = redis . get ( key ( 'chunk' , chunk_id ) )
316+ unless chunk_json
317+ warn "Warning: Chunk metadata not found for #{ chunk_id } "
318+ return nil
319+ end
320+
321+ chunk = CI ::Queue ::TestChunk . from_json ( chunk_id , chunk_json )
322+
323+ # Resolve test objects based on chunk type
324+ test_objects = if chunk . full_suite?
325+ resolve_full_suite_tests ( chunk . suite_name )
326+ else
327+ resolve_partial_suite_tests ( chunk . test_ids )
328+ end
329+
330+ if test_objects . empty?
331+ warn "Warning: No tests found for chunk #{ chunk_id } "
332+ return nil
333+ end
334+
335+ # Return enriched chunk with actual test objects
336+ ResolvedChunk . new ( chunk , test_objects )
337+ rescue JSON ::ParserError => e
338+ warn "Warning: Could not parse chunk metadata for #{ chunk_id } : #{ e . message } "
339+ nil
340+ rescue KeyError => e
341+ warn "Warning: Could not resolve test in chunk #{ chunk_id } : #{ e . message } "
342+ nil
343+ end
344+
345+ def resolve_full_suite_tests ( suite_name )
346+ # Filter index for all tests from this suite
347+ # Tests are added to index during populate() with format "SuiteName#test_method"
348+ prefix = "#{ suite_name } #"
349+ tests = index . select { |test_id , _ | test_id . start_with? ( prefix ) }
350+ . values
351+
352+ # Sort to maintain consistent order (alphabetical by test name)
353+ tests . sort_by ( &:id )
354+ end
355+
356+ def resolve_partial_suite_tests ( test_ids )
357+ # Fetch specific tests from index
358+ test_ids . map { |test_id | index . fetch ( test_id ) }
359+ end
360+
361+ public
362+
363+ def acknowledge_chunk ( chunk_id )
364+ # Remove chunk ID from processing set
365+ redis . lrem ( key ( 'processing' ) , 1 , chunk_id )
366+
367+ # Optionally track chunk completion
368+ if config . track_test_duration
369+ redis . sadd ( key ( 'completed-chunks' ) , chunk_id )
370+ end
371+ end
372+
373+ # Represents a chunk with resolved test objects
374+ class ResolvedChunk
375+ attr_reader :chunk_id , :suite_name , :tests
376+
377+ def initialize ( chunk , tests )
378+ @chunk_id = chunk . id
379+ @suite_name = chunk . suite_name
380+ @tests = tests . freeze
381+ end
382+
383+ def id
384+ chunk_id
385+ end
386+
387+ def chunk?
388+ true
389+ end
390+
391+ def flaky?
392+ tests . any? ( &:flaky? )
393+ end
394+
395+ def size
396+ tests . size
397+ end
398+ end
256399 end
257400 end
258401 end
0 commit comments