ci-queue 0.82.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,17 +6,20 @@ local zset_key = KEYS[4]
6
6
  local worker_queue_key = KEYS[5]
7
7
  local owners_key = KEYS[6]
8
8
  local error_reports_key = KEYS[7]
9
+ local requeued_by_key = KEYS[8]
9
10
 
10
11
  local max_requeues = tonumber(ARGV[1])
11
12
  local global_max_requeues = tonumber(ARGV[2])
12
- local test = ARGV[3]
13
- local offset = ARGV[4]
13
+ local entry = ARGV[3]
14
+ local test_id = ARGV[4]
15
+ local offset = ARGV[5]
16
+ local ttl = tonumber(ARGV[6])
14
17
 
15
- if redis.call('hget', owners_key, test) == worker_queue_key then
16
- redis.call('hdel', owners_key, test)
18
+ if redis.call('hget', owners_key, entry) == worker_queue_key then
19
+ redis.call('hdel', owners_key, entry)
17
20
  end
18
21
 
19
- if redis.call('sismember', processed_key, test) == 1 then
22
+ if redis.call('sismember', processed_key, test_id) == 1 then
20
23
  return false
21
24
  end
22
25
 
@@ -25,23 +28,28 @@ if global_requeues and global_requeues >= tonumber(global_max_requeues) then
25
28
  return false
26
29
  end
27
30
 
28
- local requeues = tonumber(redis.call('hget', requeues_count_key, test))
31
+ local requeues = tonumber(redis.call('hget', requeues_count_key, test_id))
29
32
  if requeues and requeues >= max_requeues then
30
33
  return false
31
34
  end
32
35
 
33
36
  redis.call('hincrby', requeues_count_key, '___total___', 1)
34
- redis.call('hincrby', requeues_count_key, test, 1)
37
+ redis.call('hincrby', requeues_count_key, test_id, 1)
35
38
 
36
- redis.call('hdel', error_reports_key, test)
39
+ redis.call('hdel', error_reports_key, test_id)
37
40
 
38
41
  local pivot = redis.call('lrange', queue_key, -1 - offset, 0 - offset)[1]
39
42
  if pivot then
40
- redis.call('linsert', queue_key, 'BEFORE', pivot, test)
43
+ redis.call('linsert', queue_key, 'BEFORE', pivot, entry)
41
44
  else
42
- redis.call('lpush', queue_key, test)
45
+ redis.call('lpush', queue_key, entry)
43
46
  end
44
47
 
45
- redis.call('zrem', zset_key, test)
48
+ redis.call('hset', requeued_by_key, entry, worker_queue_key)
49
+ if ttl and ttl > 0 then
50
+ redis.call('expire', requeued_by_key, ttl)
51
+ end
52
+
53
+ redis.call('zrem', zset_key, entry)
46
54
 
47
55
  return true
@@ -4,15 +4,54 @@ local zset_key = KEYS[2]
4
4
  local processed_key = KEYS[3]
5
5
  local worker_queue_key = KEYS[4]
6
6
  local owners_key = KEYS[5]
7
+ local requeued_by_key = KEYS[6]
8
+ local workers_key = KEYS[7]
7
9
 
8
10
  local current_time = ARGV[1]
11
+ local defer_offset = tonumber(ARGV[2]) or 0
12
+ local max_skip_attempts = 4
9
13
 
10
- local test = redis.call('rpop', queue_key)
11
- if test then
12
- redis.call('zadd', zset_key, current_time, test)
13
- redis.call('lpush', worker_queue_key, test)
14
- redis.call('hset', owners_key, test, worker_queue_key)
15
- return test
16
- else
17
- return nil
14
+ local function insert_with_offset(test)
15
+ local pivot = redis.call('lrange', queue_key, -1 - defer_offset, 0 - defer_offset)[1]
16
+ if pivot then
17
+ redis.call('linsert', queue_key, 'BEFORE', pivot, test)
18
+ else
19
+ redis.call('lpush', queue_key, test)
20
+ end
18
21
  end
22
+
23
+ for attempt = 1, max_skip_attempts do
24
+ local test = redis.call('rpop', queue_key)
25
+ if not test then
26
+ return nil
27
+ end
28
+
29
+ local requeued_by = redis.call('hget', requeued_by_key, test)
30
+ if requeued_by == worker_queue_key then
31
+ -- If this build only has one worker, allow immediate self-pickup.
32
+ if redis.call('scard', workers_key) <= 1 then
33
+ redis.call('hdel', requeued_by_key, test)
34
+ redis.call('zadd', zset_key, current_time, test)
35
+ redis.call('lpush', worker_queue_key, test)
36
+ redis.call('hset', owners_key, test, worker_queue_key)
37
+ return test
38
+ end
39
+
40
+ insert_with_offset(test)
41
+
42
+ -- If this worker only finds its own requeued tests, defer once by returning nil,
43
+ -- then allow pickup on a subsequent reserve attempt.
44
+ if attempt == max_skip_attempts then
45
+ redis.call('hdel', requeued_by_key, test)
46
+ return nil
47
+ end
48
+ else
49
+ redis.call('hdel', requeued_by_key, test)
50
+ redis.call('zadd', zset_key, current_time, test)
51
+ redis.call('lpush', worker_queue_key, test)
52
+ redis.call('hset', owners_key, test, worker_queue_key)
53
+ return test
54
+ end
55
+ end
56
+
57
+ return nil
@@ -1,4 +1,6 @@
1
1
  -- AUTOGENERATED FILE DO NOT EDIT DIRECTLY
2
+ -- @include _entry_helpers
3
+
2
4
  local zset_key = KEYS[1]
3
5
  local processed_key = KEYS[2]
4
6
  local worker_queue_key = KEYS[3]
@@ -6,10 +8,12 @@ local owners_key = KEYS[4]
6
8
 
7
9
  local current_time = ARGV[1]
8
10
  local timeout = ARGV[2]
11
+ local entry_delimiter = ARGV[3]
9
12
 
10
13
  local lost_tests = redis.call('zrangebyscore', zset_key, 0, current_time - timeout)
11
14
  for _, test in ipairs(lost_tests) do
12
- if redis.call('sismember', processed_key, test) == 0 then
15
+ local test_id = test_id_from_entry(test, entry_delimiter)
16
+ if redis.call('sismember', processed_key, test_id) == 0 then
13
17
  redis.call('zadd', zset_key, current_time, test)
14
18
  redis.call('lpush', worker_queue_key, test)
15
19
  redis.call('hset', owners_key, test, worker_queue_key) -- Take ownership
@@ -9,7 +9,7 @@ module CI
9
9
  end
10
10
 
11
11
  def total
12
- wait_for_master(timeout: config.queue_init_timeout)
12
+ wait_for_master(timeout: config.queue_init_timeout, allow_streaming: true)
13
13
  redis.get(key('total')).to_i
14
14
  end
15
15
 
@@ -19,7 +19,7 @@ module CI
19
19
 
20
20
  def wait_for_workers
21
21
  duration = measure do
22
- wait_for_master(timeout: config.queue_init_timeout)
22
+ wait_for_master(timeout: config.queue_init_timeout, allow_streaming: true)
23
23
  end
24
24
 
25
25
  yield if block_given?
@@ -30,7 +30,7 @@ module CI
30
30
  @time_left -= 1
31
31
  sleep 1
32
32
 
33
- if active_workers?
33
+ if active_workers? || streaming?
34
34
  @time_left_with_no_workers = config.inactive_workers_timeout
35
35
  else
36
36
  @time_left_with_no_workers -= 1
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require 'ci/queue/static'
3
3
  require 'concurrent/set'
4
+ require 'concurrent/map'
4
5
 
5
6
  module CI
6
7
  module Queue
@@ -13,11 +14,13 @@ module CI
13
14
  self.max_sleep_time = 2
14
15
 
15
16
  class Worker < Base
16
- attr_reader :total
17
+ attr_accessor :entry_resolver
18
+ attr_reader :first_reserve_at
17
19
 
18
20
  def initialize(redis, config)
19
21
  @reserved_tests = Concurrent::Set.new
20
22
  @shutdown_required = false
23
+ @first_reserve_at = nil
21
24
  super(redis, config)
22
25
  end
23
26
 
@@ -27,15 +30,65 @@ module CI
27
30
 
28
31
  def populate(tests, random: Random.new)
29
32
  @index = tests.map { |t| [t.id, t] }.to_h
30
- tests = Queue.shuffle(tests, random)
31
- push(tests.map(&:id))
33
+ entries = Queue.shuffle(tests, random).map { |test| queue_entry_for(test) }
34
+ push(entries)
32
35
  self
33
36
  end
34
37
 
38
+ def stream_populate(tests, random: Random.new, batch_size: 10_000)
39
+ batch_size = batch_size.to_i
40
+ batch_size = 1 if batch_size < 1
41
+
42
+ value = key('setup', worker_id)
43
+ _, status = redis.pipelined do |pipeline|
44
+ pipeline.set(key('master-status'), value, nx: true)
45
+ pipeline.get(key('master-status'))
46
+ end
47
+
48
+ if @master = (value == status)
49
+ @total = 0
50
+ puts "Worker elected as leader, streaming tests to the queue."
51
+
52
+ duration = measure do
53
+ start_streaming!
54
+ buffer = []
55
+
56
+ tests.each do |test|
57
+ buffer << test
58
+
59
+ if buffer.size >= batch_size
60
+ push_batch(buffer, random)
61
+ buffer.clear
62
+ end
63
+ end
64
+
65
+ push_batch(buffer, random) unless buffer.empty?
66
+ finalize_streaming
67
+ end
68
+
69
+ puts "Streamed #{@total} tests in #{duration.round(2)}s."
70
+ $stdout.flush
71
+ end
72
+
73
+ register
74
+ redis.expire(key('workers'), config.redis_ttl)
75
+ self
76
+ rescue *CONNECTION_ERRORS
77
+ raise if @master
78
+ end
79
+
35
80
  def populated?
36
81
  !!defined?(@index)
37
82
  end
38
83
 
84
+ def total
85
+ return @total if defined?(@total) && @total
86
+
87
+ redis.get(key('total')).to_i
88
+ rescue *CONNECTION_ERRORS
89
+ @total || 0
90
+ end
91
+
39
92
  def shutdown!
40
93
  @shutdown_required = true
41
94
  end
@@ -51,13 +104,18 @@ module CI
51
104
  DEFAULT_SLEEP_SECONDS = 0.5
52
105
 
53
106
  def poll
54
- wait_for_master
107
+ wait_for_master(timeout: config.queue_init_timeout, allow_streaming: true)
55
108
  attempt = 0
56
109
  until shutdown_required? || config.circuit_breakers.any?(&:open?) || exhausted? || max_test_failed?
57
- if test = reserve
110
+ if entry = reserve
58
111
  attempt = 0
59
- yield index.fetch(test)
112
+ yield resolve_entry(entry)
60
113
  else
114
+ if still_streaming?
115
+ raise LostMaster, "Streaming stalled for more than #{config.lazy_load_streaming_timeout}s" if streaming_stale?
116
+ sleep 0.1
117
+ next
118
+ end
61
119
  # Adding exponential backoff to avoid hammering Redis
62
120
  # we just stay online here in case a test gets retried or times out so we can afford to wait
63
121
  sleep_time = [DEFAULT_SLEEP_SECONDS * (2 ** attempt), Redis.max_sleep_time].min
@@ -89,6 +147,7 @@ module CI
89
147
  def retry_queue
90
148
  failures = build.failed_tests.to_set
91
149
  log = redis.lrange(key('worker', worker_id, 'queue'), 0, -1)
150
+ log = log.map { |entry| queue_entry_test_id(entry) }
92
151
  log.select! { |id| failures.include?(id) }
93
152
  log.uniq!
94
153
  log.reverse!
@@ -103,23 +162,38 @@ module CI
103
162
  @build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config)
104
163
  end
105
164
 
165
+ def file_loader
166
+ @file_loader ||= CI::Queue::FileLoader.new
167
+ end
168
+
169
+ def worker_queue_length
170
+ redis.llen(key('worker', worker_id, 'queue'))
171
+ rescue *CONNECTION_ERRORS
172
+ nil
173
+ end
174
+
106
175
  def report_worker_error(error)
107
176
  build.report_worker_error(error)
108
177
  end
109
178
 
110
179
  def acknowledge(test_key, error: nil, pipeline: redis)
111
- raise_on_mismatching_test(test_key)
180
+ test_id = normalize_test_id(test_key)
181
+ assert_reserved!(test_id)
182
+ entry = reserved_entries.fetch(test_id, queue_entry_for(test_key))
183
+ unreserve_entry(test_id)
112
184
  eval_script(
113
185
  :acknowledge,
114
- keys: [key('running'), key('processed'), key('owners'), key('error-reports')],
115
- argv: [test_key, error.to_s, config.redis_ttl],
186
+ keys: [key('running'), key('processed'), key('owners'), key('error-reports'), key('requeued-by')],
187
+ argv: [entry, test_id, error.to_s, config.redis_ttl],
116
188
  pipeline: pipeline,
117
189
  ) == 1
118
190
  end
119
191
 
120
192
  def requeue(test, offset: Redis.requeue_offset)
121
- test_key = test.id
122
- raise_on_mismatching_test(test_key)
193
+ test_id = normalize_test_id(test)
194
+ assert_reserved!(test_id)
195
+ entry = reserved_entries.fetch(test_id, queue_entry_for(test))
196
+ unreserve_entry(test_id)
123
197
  global_max_requeues = config.global_max_requeues(total)
124
198
 
125
199
  requeued = config.max_requeues > 0 && global_max_requeues > 0 && eval_script(
@@ -132,11 +206,16 @@ module CI
132
206
  key('worker', worker_id, 'queue'),
133
207
  key('owners'),
134
208
  key('error-reports'),
209
+ key('requeued-by'),
135
210
  ],
136
- argv: [config.max_requeues, global_max_requeues, test_key, offset],
211
+ argv: [config.max_requeues, global_max_requeues, entry, test_id, offset, config.redis_ttl],
137
212
  ) == 1
138
213
 
139
- reserved_tests << test_key unless requeued
214
+ unless requeued
215
+ reserved_tests << test_id
216
+ reserved_entries[test_id] = entry
217
+ reserved_entry_ids[entry] = test_id
218
+ end
140
219
  requeued
141
220
  end
142
221
 
@@ -157,19 +236,131 @@ module CI
157
236
  @reserved_tests ||= Concurrent::Set.new
158
237
  end
159
238
 
239
+ def reserved_entries
240
+ @reserved_entries ||= Concurrent::Map.new
241
+ end
242
+
243
+ def reserved_entry_ids
244
+ @reserved_entry_ids ||= Concurrent::Map.new
245
+ end
246
+
160
247
  def worker_id
161
248
  config.worker_id
162
249
  end
163
250
 
164
- def raise_on_mismatching_test(test)
165
- unless reserved_tests.delete?(test)
166
- raise ReservationError, "Acknowledged #{test.inspect} but only #{reserved_tests.map(&:inspect).join(", ")} reserved"
251
+ def assert_reserved!(test_id)
252
+ unless reserved_tests.include?(test_id)
253
+ raise ReservationError, "Acknowledged #{test_id.inspect} but only #{reserved_tests.map(&:inspect).join(", ")} reserved"
254
+ end
255
+ end
256
+
257
+ def reserve_entry(entry)
258
+ test_id = queue_entry_test_id(entry)
259
+ reserved_tests << test_id
260
+ reserved_entries[test_id] = entry
261
+ reserved_entry_ids[entry] = test_id
262
+ end
263
+
264
+ def unreserve_entry(test_id)
265
+ entry = reserved_entries.delete(test_id)
266
+ reserved_tests.delete(test_id)
267
+ reserved_entry_ids.delete(entry) if entry
268
+ end
269
+
270
+ def normalize_test_id(test_key)
271
+ key = test_key.respond_to?(:id) ? test_key.id : test_key
272
+ if key.is_a?(String)
273
+ cached = reserved_entry_ids[key]
274
+ return cached if cached
275
+ end
276
+ queue_entry_test_id(key)
277
+ end
278
+
279
+ def queue_entry_test_id(entry)
280
+ CI::Queue::QueueEntry.test_id(entry)
281
+ end
282
+
283
+ def queue_entry_for(test)
284
+ return test.queue_entry if test.respond_to?(:queue_entry)
285
+ return test.id if test.respond_to?(:id)
286
+
287
+ test
288
+ end
289
+
290
+ def resolve_entry(entry)
291
+ test_id = reserved_entry_ids[entry] || queue_entry_test_id(entry)
292
+ if populated?
293
+ return index[test_id] if index.key?(test_id)
294
+ end
295
+
296
+ return entry_resolver.call(entry) if entry_resolver
297
+
298
+ entry
299
+ end
300
+
301
+ def still_streaming?
302
+ master_status == 'streaming'
303
+ end
304
+
305
+ def streaming_stale?
306
+ timeout = config.lazy_load_streaming_timeout.to_i
307
+ updated_at = redis.get(key('streaming-updated-at'))
308
+ return true unless updated_at
309
+
310
+ (CI::Queue.time_now.to_f - updated_at.to_f) > timeout
311
+ rescue *CONNECTION_ERRORS
312
+ false
313
+ end
314
+
315
+ def start_streaming!
316
+ timeout = config.lazy_load_streaming_timeout.to_i
317
+ with_redis_timeout(5) do
318
+ redis.multi do |transaction|
319
+ transaction.set(key('total'), 0)
320
+ transaction.set(key('master-status'), 'streaming')
321
+ transaction.set(key('streaming-updated-at'), CI::Queue.time_now.to_f)
322
+ transaction.expire(key('streaming-updated-at'), timeout)
323
+ transaction.expire(key('queue'), config.redis_ttl)
324
+ transaction.expire(key('total'), config.redis_ttl)
325
+ transaction.expire(key('master-status'), config.redis_ttl)
326
+ end
327
+ end
328
+ end
329
+
330
+ def push_batch(tests, random)
331
+ # Use plain shuffle instead of Queue.shuffle — the custom shuffler expects
332
+ # test objects with .id, but streaming entries are pre-formatted strings.
333
+ entries = tests.shuffle(random: random).map { |test| queue_entry_for(test) }
334
+ return if entries.empty?
335
+
336
+ @total += entries.size
337
+ timeout = config.lazy_load_streaming_timeout.to_i
338
+ redis.multi do |transaction|
339
+ transaction.lpush(key('queue'), entries)
340
+ transaction.incrby(key('total'), entries.size)
341
+ transaction.set(key('master-status'), 'streaming')
342
+ transaction.set(key('streaming-updated-at'), CI::Queue.time_now.to_f)
343
+ transaction.expire(key('streaming-updated-at'), timeout)
344
+ transaction.expire(key('queue'), config.redis_ttl)
345
+ transaction.expire(key('total'), config.redis_ttl)
346
+ transaction.expire(key('master-status'), config.redis_ttl)
347
+ end
348
+ end
349
+
350
+ def finalize_streaming
351
+ redis.multi do |transaction|
352
+ transaction.set(key('master-status'), 'ready')
353
+ transaction.expire(key('master-status'), config.redis_ttl)
354
+ transaction.del(key('streaming-updated-at'))
167
355
  end
168
356
  end
169
357
 
170
358
  def reserve
171
- (try_to_reserve_lost_test || try_to_reserve_test).tap do |test|
172
- reserved_tests << test if test
359
+ (try_to_reserve_lost_test || try_to_reserve_test).tap do |entry|
360
+ if entry
361
+ @first_reserve_at ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
362
+ reserve_entry(entry)
363
+ end
173
364
  end
174
365
  end
175
366
 
@@ -182,8 +373,10 @@ module CI
182
373
  key('processed'),
183
374
  key('worker', worker_id, 'queue'),
184
375
  key('owners'),
376
+ key('requeued-by'),
377
+ key('workers'),
185
378
  ],
186
- argv: [CI::Queue.time_now.to_f],
379
+ argv: [CI::Queue.time_now.to_f, Redis.requeue_offset],
187
380
  )
188
381
  end
189
382
 
@@ -198,7 +391,7 @@ module CI
198
391
  key('worker', worker_id, 'queue'),
199
392
  key('owners'),
200
393
  ],
201
- argv: [CI::Queue.time_now.to_f, timeout],
394
+ argv: [CI::Queue.time_now.to_f, timeout, CI::Queue::QueueEntry::DELIMITER],
202
395
  )
203
396
 
204
397
  if lost_test
@@ -208,8 +401,8 @@ module CI
208
401
  lost_test
209
402
  end
210
403
 
211
- def push(tests)
212
- @total = tests.size
404
+ def push(entries)
405
+ @total = entries.size
213
406
 
214
407
  # We set a unique value (worker_id) and read it back to make "SET if Not eXists" idempotent in case of a retry.
215
408
  value = key('setup', worker_id)
@@ -227,7 +420,7 @@ module CI
227
420
  with_redis_timeout(5) do
228
421
  redis.without_reconnect do
229
422
  redis.multi do |transaction|
230
- transaction.lpush(key('queue'), tests) unless tests.empty?
423
+ transaction.lpush(key('queue'), entries) unless entries.empty?
231
424
  transaction.set(key('total'), @total)
232
425
  transaction.set(key('master-status'), 'ready')
233
426
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CI
4
4
  module Queue
5
- VERSION = '0.82.0'
5
+ VERSION = '0.83.0'
6
6
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
7
7
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
8
8
  end
data/lib/ci/queue.rb CHANGED
@@ -14,6 +14,9 @@ require 'ci/queue/static'
14
14
  require 'ci/queue/file'
15
15
  require 'ci/queue/grind'
16
16
  require 'ci/queue/bisect'
17
+ require 'ci/queue/queue_entry'
18
+ require 'ci/queue/class_resolver'
19
+ require 'ci/queue/file_loader'
17
20
 
18
21
  module CI
19
22
  module Queue
@@ -22,6 +25,18 @@ module CI
22
25
  attr_accessor :shuffler, :requeueable
23
26
 
24
27
  Error = Class.new(StandardError)
28
+ ClassNotFoundError = Class.new(Error)
29
+
30
+ class FileLoadError < Error
31
+ attr_reader :file_path, :original_error
32
+
33
+ def initialize(file_path, original_error)
34
+ @file_path = file_path
35
+ @original_error = original_error
36
+ super("Failed to load #{file_path}: #{original_error.class}: #{original_error.message}")
37
+ set_backtrace(original_error.backtrace)
38
+ end
39
+ end
25
40
 
26
41
  module Warnings
27
42
  RESERVED_LOST_TEST = :RESERVED_LOST_TEST
@@ -48,6 +63,17 @@ module CI
48
63
  end
49
64
  end
50
65
 
66
+ def debug?
67
+ return @debug if defined?(@debug)
68
+
69
+ value = ENV['CI_QUEUE_DEBUG']
70
+ @debug = !!(value && !value.strip.empty? && !%w[0 false].include?(value.strip.downcase))
71
+ end
72
+
73
+ def reset_debug!
74
+ remove_instance_variable(:@debug) if defined?(@debug)
75
+ end
76
+
51
77
  def from_uri(url, config)
52
78
  uri = URI(url)
53
79
  implementation = case uri.scheme
@@ -65,3 +91,4 @@ module CI
65
91
  end
66
92
  end
67
93
  end
94
+
@@ -136,9 +136,9 @@ module Minitest
136
136
  result[:time] = 0
137
137
  tests.each do |test|
138
138
  result[:"#{result(test)}_count"] += 1
139
- result[:assertion_count] += test.assertions
139
+ result[:assertion_count] += test.assertions || 0
140
140
  result[:test_count] += 1
141
- result[:time] += test.time
141
+ result[:time] += test.time || 0
142
142
  end
143
143
  result
144
144
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Queue
5
+ class LazyEntryResolver
6
+ def initialize(loader:, resolver:)
7
+ @loader = loader
8
+ @resolver = resolver
9
+ end
10
+
11
+ def call(entry)
12
+ parsed = CI::Queue::QueueEntry.parse(entry)
13
+ class_name, method_name = parsed.fetch(:test_id).split('#', 2)
14
+ if CI::Queue::QueueEntry.load_error_payload?(parsed[:file_path])
15
+ payload = CI::Queue::QueueEntry.decode_load_error(parsed[:file_path])
16
+ if payload
17
+ error = StandardError.new("#{payload['error_class']}: #{payload['error_message']}")
18
+ error.set_backtrace(payload['backtrace']) if payload['backtrace']
19
+ load_error = CI::Queue::FileLoadError.new(payload['file_path'], error)
20
+ return Minitest::Queue::LazySingleExample.new(
21
+ class_name,
22
+ method_name,
23
+ payload['file_path'],
24
+ loader: @loader,
25
+ resolver: @resolver,
26
+ load_error: load_error,
27
+ queue_entry: entry,
28
+ )
29
+ else
30
+ error = StandardError.new("Corrupt load error payload for #{class_name}##{method_name}")
31
+ load_error = CI::Queue::FileLoadError.new("unknown", error)
32
+ return Minitest::Queue::LazySingleExample.new(
33
+ class_name,
34
+ method_name,
35
+ nil,
36
+ loader: @loader,
37
+ resolver: @resolver,
38
+ load_error: load_error,
39
+ queue_entry: entry,
40
+ )
41
+ end
42
+ end
43
+
44
+ Minitest::Queue::LazySingleExample.new(
45
+ class_name,
46
+ method_name,
47
+ parsed[:file_path],
48
+ loader: @loader,
49
+ resolver: @resolver,
50
+ queue_entry: entry,
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end