ci-queue 0.67.0 → 0.68.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3a701add5867e968b8dcc2c8d9fe301f88b54ede5b0f2f615171fa1d620e6bd
4
- data.tar.gz: 401f2f2c45c09edbd609c177a077b1aec2fe361f376439d6f5b8a29770d7e488
3
+ metadata.gz: dfe678bd81982a8945fff3610d86021f2b32ab96f19cd00bbda28c7837374f0c
4
+ data.tar.gz: f71d4a43aa80012a863f3ec0c6fccebd3542dd1c92e4cc386ffe1bda80836254
5
5
  SHA512:
6
- metadata.gz: f32ff8fa093d89df2194ac043943de43acfedbc5cbcf942c3638d0cc6ccf9659eb107dbf87ef04b3f19ba8329adf1b1275cb2125b8bc9c3cafd7072b49de3e5c
7
- data.tar.gz: 01c75cfae762cb874df2160b04c7b96d14d7665631dfaaf642553587b14f7e86bd4d093aad6b9feefa3bee67b6626c3b2a143f3695f50b942579c59949eb16ed
6
+ metadata.gz: 7af733cedec42252b31b2851cdea9067d5dd3974e9549595c492edbacb366125a4655c0c3d7dccb817eba6ab4b8472e0f35c98bbc4bfeb86f6033f5ffd42d93b
7
+ data.tar.gz: 2ca6d5bf9b6b2d59c3907656afb536c84fb360eee42aee63c3e48136a7b880ef3c16584b50a7bcb0877917d0a62f1dc6f3a13d31277f1c6f7ec20718efd8c5a6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ci-queue (0.67.0)
4
+ ci-queue (0.68.0)
5
5
  logger
6
6
 
7
7
  GEM
@@ -7,14 +7,16 @@ module CI
7
7
  module Redis
8
8
  class << self
9
9
  attr_accessor :requeue_offset
10
+ attr_accessor :max_sleep_time
10
11
  end
11
12
  self.requeue_offset = 42
13
+ self.max_sleep_time = 2
12
14
 
13
15
  class Worker < Base
14
16
  attr_reader :total
15
17
 
16
18
  def initialize(redis, config)
17
- @reserved_test = nil
19
+ @reserved_tests = Set.new
18
20
  @shutdown_required = false
19
21
  super(redis, config)
20
22
  end
@@ -46,13 +48,21 @@ module CI
46
48
  @master
47
49
  end
48
50
 
51
+ DEFAULT_SLEEP_SECONDS = 0.5
52
+
49
53
  def poll
50
54
  wait_for_master
55
+ attempt = 0
51
56
  until shutdown_required? || config.circuit_breakers.any?(&:open?) || exhausted? || max_test_failed?
52
57
  if test = reserve
58
+ attempt = 0
53
59
  yield index.fetch(test)
54
60
  else
55
- sleep 0.05
61
+ # Adding exponential backoff to avoid hammering Redis
62
+ # we just stay online here in case a test gets retried or times out so we can afford to wait
63
+ sleep_time = [DEFAULT_SLEEP_SECONDS * (2 ** attempt), Redis.max_sleep_time].min
64
+ attempt += 1
65
+ sleep sleep_time
56
66
  end
57
67
  end
58
68
  redis.pipelined do |pipeline|
@@ -125,7 +135,7 @@ module CI
125
135
  argv: [config.max_requeues, global_max_requeues, test_key, offset],
126
136
  ) == 1
127
137
 
128
- @reserved_test = test_key unless requeued
138
+ reserved_tests << test_key unless requeued
129
139
  requeued
130
140
  end
131
141
 
@@ -142,25 +152,24 @@ module CI
142
152
 
143
153
  attr_reader :index
144
154
 
155
+ def reserved_tests
156
+ @reserved_tests ||= Set.new
157
+ end
158
+
145
159
  def worker_id
146
160
  config.worker_id
147
161
  end
148
162
 
149
- def raise_on_mismatching_test(test_key)
150
- if @reserved_test == test_key
151
- @reserved_test = nil
152
- else
153
- raise ReservationError, "Acknowledged #{test_key.inspect} but #{@reserved_test.inspect} was reserved"
163
+ def raise_on_mismatching_test(test)
164
+ unless reserved_tests.delete?(test)
165
+ raise ReservationError, "Acknowledged #{test.inspect} but only #{reserved_tests.map(&:inspect).join(", ")} reserved"
154
166
  end
155
167
  end
156
168
 
157
169
  def reserve
158
- if @reserved_test
159
- raise ReservationError, "#{@reserved_test.inspect} is already reserved. " \
160
- "You have to acknowledge it before you can reserve another one"
161
- end
162
-
163
- @reserved_test = (try_to_reserve_lost_test || try_to_reserve_test)
170
+ test = (try_to_reserve_lost_test || try_to_reserve_test)
171
+ reserved_tests << test
172
+ test
164
173
  end
165
174
 
166
175
  def try_to_reserve_test
@@ -89,14 +89,15 @@ module CI
89
89
  end
90
90
 
91
91
  def running
92
- @reserved_test ? 1 : 0
92
+ reserved_tests.empty? ? 0 : 1
93
93
  end
94
94
 
95
95
  def poll
96
- while !@shutdown && config.circuit_breakers.none?(&:open?) && !max_test_failed? && @reserved_test = @queue.shift
97
- yield index.fetch(@reserved_test)
96
+ while !@shutdown && config.circuit_breakers.none?(&:open?) && !max_test_failed? && reserved_test = @queue.shift
97
+ reserved_tests << reserved_test
98
+ yield index.fetch(reserved_test)
98
99
  end
99
- @reserved_test = nil
100
+ reserved_tests.clear
100
101
  end
101
102
 
102
103
  def exhausted?
@@ -142,6 +143,10 @@ module CI
142
143
  def requeues
143
144
  @requeues ||= Hash.new(0)
144
145
  end
146
+
147
+ def reserved_tests
148
+ @reserved_tests ||= Set.new
149
+ end
145
150
  end
146
151
  end
147
152
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CI
4
4
  module Queue
5
- VERSION = '0.67.0'
5
+ VERSION = '0.68.0'
6
6
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
7
7
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
8
8
  end
@@ -108,7 +108,16 @@ module Minitest
108
108
  build.requeued_tests
109
109
  end
110
110
 
111
+ APPLICATION_ERROR_EXIT_CODE = 42
112
+ TIMED_OUT_EXIT_CODE = 43
113
+ TOO_MANY_FAILED_TESTS_EXIT_CODE = 44
114
+ WORKERS_DIED_EXIT_CODE = 45
115
+ SUCCESS_EXIT_CODE = 0
116
+ TEST_FAILURE_EXIT_CODE = 1
117
+
111
118
  def report
119
+ exit_code = TEST_FAILURE_EXIT_CODE
120
+
112
121
  if requeued_tests.to_a.any?
113
122
  step("Requeued #{requeued_tests.size} tests")
114
123
  requeued_tests.to_a.sort.each do |test_id, count|
@@ -131,10 +140,14 @@ module Minitest
131
140
  if remaining_tests.size > 10
132
141
  puts " ..."
133
142
  end
143
+
144
+ exit_code = TIMED_OUT_EXIT_CODE
134
145
  elsif supervisor.time_left_with_no_workers.to_i <= 0
135
146
  puts red("All workers died.")
147
+ exit_code = WORKERS_DIED_EXIT_CODE
136
148
  elsif supervisor.max_test_failed?
137
149
  puts red("Encountered too many failed tests. Test run was ended early.")
150
+ exit_code = TOO_MANY_FAILED_TESTS_EXIT_CODE
138
151
  end
139
152
 
140
153
  puts
@@ -146,9 +159,10 @@ module Minitest
146
159
  puts red("Worker #{worker_id } crashed")
147
160
  puts error
148
161
  puts ""
162
+ exit_code = APPLICATION_ERROR_EXIT_CODE
149
163
  end
150
164
 
151
- success?
165
+ success? ? SUCCESS_EXIT_CODE : exit_code
152
166
  end
153
167
 
154
168
  def success?
@@ -253,25 +253,24 @@ module Minitest
253
253
 
254
254
  unless supervisor.wait_for_workers { display_warnings(supervisor.build) }
255
255
  unless supervisor.queue_initialized?
256
- abort! "No master was elected. Did all workers crash?", 40
256
+ abort! "No leader was elected. This typically means no worker was able to start. Were there any errors during application boot?", 40
257
257
  end
258
258
 
259
259
  unless supervisor.exhausted?
260
260
  reporter = BuildStatusReporter.new(supervisor: supervisor)
261
- reporter.report
261
+ exit_code = reporter.report
262
262
  reporter.write_failure_file(queue_config.failure_file) if queue_config.failure_file
263
263
  reporter.write_flaky_tests_file(queue_config.export_flaky_tests_file) if queue_config.export_flaky_tests_file
264
264
 
265
- abort!("#{supervisor.size} tests weren't run.")
265
+ abort!("#{supervisor.size} tests weren't run.", exit_code)
266
266
  end
267
267
  end
268
268
 
269
269
  reporter = BuildStatusReporter.new(supervisor: supervisor)
270
270
  reporter.write_failure_file(queue_config.failure_file) if queue_config.failure_file
271
271
  reporter.write_flaky_tests_file(queue_config.export_flaky_tests_file) if queue_config.export_flaky_tests_file
272
- reporter.report
273
-
274
- exit! reporter.success? ? 0 : 1
272
+ exit_code = reporter.report
273
+ exit! exit_code
275
274
  end
276
275
 
277
276
  def report_grind_command
@@ -107,7 +107,7 @@ module Minitest
107
107
  end
108
108
 
109
109
  module Queue
110
- include ::CI::Queue::OutputHelpers
110
+ extend ::CI::Queue::OutputHelpers
111
111
  attr_writer :run_command_formatter, :project_root
112
112
 
113
113
  def run_command_formatter
@@ -149,8 +149,79 @@ module Minitest
149
149
  path
150
150
  end
151
151
 
152
+ class << self
153
+ def queue
154
+ Minitest.queue
155
+ end
156
+
157
+ def run(reporter, *)
158
+ rescue_run_errors do
159
+ queue.poll do |example|
160
+ result = queue.with_heartbeat(example.id) do
161
+ example.run
162
+ end
163
+
164
+ handle_test_result(reporter, example, result)
165
+ end
166
+
167
+ queue.stop_heartbeat!
168
+ end
169
+ end
170
+
171
+ def handle_test_result(reporter, example, result)
172
+ failed = !(result.passed? || result.skipped?)
173
+
174
+ if example.flaky?
175
+ result.mark_as_flaked!
176
+ failed = false
177
+ end
178
+
179
+ if failed && queue.config.failing_test && queue.config.failing_test != example.id
180
+ # When we do a bisect, we don't care about the result other than the test we're running the bisect on
181
+ result.mark_as_flaked!
182
+ failed = false
183
+ elsif failed
184
+ queue.report_failure!
185
+ else
186
+ queue.report_success!
187
+ end
188
+
189
+ if failed && CI::Queue.requeueable?(result) && queue.requeue(example)
190
+ result.requeue!
191
+ end
192
+ reporter.record(result)
193
+ end
194
+
195
+ private
196
+
197
+ def rescue_run_errors(&block)
198
+ block.call
199
+ rescue Errno::EPIPE
200
+ # This happens when the heartbeat process dies
201
+ reopen_previous_step
202
+ puts red("The heartbeat process died. This worker is exiting early.")
203
+ exit!(41)
204
+ rescue CI::Queue::Error => error
205
+ reopen_previous_step
206
+ puts red("#{error.class}: #{error.message}")
207
+ error.backtrace.each do |frame|
208
+ puts red(frame)
209
+ end
210
+ exit!(41)
211
+ rescue => error
212
+ reopen_previous_step
213
+ Minitest.queue.report_worker_error(error)
214
+ puts red("This worker exited because of an uncaught application error:")
215
+ puts red("#{error.class}: #{error.message}")
216
+ error.backtrace.each do |frame|
217
+ puts red(frame)
218
+ end
219
+ exit!(42)
220
+ end
221
+ end
222
+
152
223
  class SingleExample
153
- attr_reader :method_name
224
+ attr_reader :runnable, :method_name
154
225
 
155
226
  def initialize(runnable, method_name)
156
227
  @runnable = runnable
@@ -212,7 +283,7 @@ module Minitest
212
283
 
213
284
  def __run(*args)
214
285
  if queue
215
- run_from_queue(*args)
286
+ Queue.run(*args)
216
287
 
217
288
  if queue.config.circuit_breakers.any?(&:open?)
218
289
  STDERR.puts queue.config.circuit_breakers.map(&:message).join(' ').strip
@@ -225,58 +296,6 @@ module Minitest
225
296
  super
226
297
  end
227
298
  end
228
-
229
- def run_from_queue(reporter, *)
230
- queue.poll do |example|
231
- result = queue.with_heartbeat(example.id) do
232
- example.run
233
- end
234
-
235
- failed = !(result.passed? || result.skipped?)
236
-
237
- if example.flaky?
238
- result.mark_as_flaked!
239
- failed = false
240
- end
241
-
242
- if failed && queue.config.failing_test && queue.config.failing_test != example.id
243
- # When we do a bisect, we don't care about the result other than the test we're running the bisect on
244
- result.mark_as_flaked!
245
- failed = false
246
- elsif failed
247
- queue.report_failure!
248
- else
249
- queue.report_success!
250
- end
251
-
252
- if failed && CI::Queue.requeueable?(result) && queue.requeue(example)
253
- result.requeue!
254
- end
255
- reporter.record(result)
256
- end
257
- queue.stop_heartbeat!
258
- rescue Errno::EPIPE
259
- # This happens when the heartbeat process dies
260
- reopen_previous_step
261
- puts red("The heartbeat process died. This worker is exiting early.")
262
- exit!(41)
263
- rescue CI::Queue::Error => error
264
- reopen_previous_step
265
- puts red("#{error.class}: #{error.message}")
266
- error.backtrace.each do |frame|
267
- puts red(frame)
268
- end
269
- exit!(41)
270
- rescue => error
271
- reopen_previous_step
272
- queue.report_worker_error(error)
273
- puts red("This worker exited because of an uncaught application error:")
274
- puts red("#{error.class}: #{error.message}")
275
- error.backtrace.each do |frame|
276
- puts red(frame)
277
- end
278
- exit!(42)
279
- end
280
299
  end
281
300
  end
282
301
 
data/lib/rspec/queue.rb CHANGED
@@ -283,7 +283,7 @@ module RSpec
283
283
 
284
284
  unless supervisor.wait_for_workers
285
285
  unless supervisor.queue_initialized?
286
- abort! "No master was elected. Did all workers crash?"
286
+ abort! "No leader was elected. This typically means no worker was able to start. Were there any errors during application boot?"
287
287
  end
288
288
 
289
289
  unless supervisor.exhausted?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ci-queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.67.0
4
+ version: 0.68.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier