ci-queue 0.16.0 → 0.17.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/Rakefile +1 -0
  4. data/bin/console +1 -0
  5. data/ci-queue.gemspec +2 -0
  6. data/exe/minitest-queue +1 -0
  7. data/exe/rspec-queue +1 -0
  8. data/lib/ci/queue.rb +3 -0
  9. data/lib/ci/queue/bisect.rb +1 -0
  10. data/lib/ci/queue/build_record.rb +1 -0
  11. data/lib/ci/queue/circuit_breaker.rb +39 -0
  12. data/lib/ci/queue/common.rb +3 -2
  13. data/lib/ci/queue/configuration.rb +25 -11
  14. data/lib/ci/queue/file.rb +1 -0
  15. data/lib/ci/queue/grind.rb +23 -0
  16. data/lib/ci/queue/output_helpers.rb +1 -0
  17. data/lib/ci/queue/redis.rb +6 -0
  18. data/lib/ci/queue/redis/base.rb +1 -0
  19. data/lib/ci/queue/redis/build_record.rb +4 -3
  20. data/lib/ci/queue/redis/grind.rb +17 -0
  21. data/lib/ci/queue/redis/grind_record.rb +66 -0
  22. data/lib/ci/queue/redis/grind_supervisor.rb +13 -0
  23. data/lib/ci/queue/redis/retry.rb +1 -0
  24. data/lib/ci/queue/redis/supervisor.rb +2 -1
  25. data/lib/ci/queue/redis/test_time_record.rb +66 -0
  26. data/lib/ci/queue/redis/worker.rb +2 -1
  27. data/lib/ci/queue/static.rb +3 -1
  28. data/lib/ci/queue/version.rb +3 -1
  29. data/lib/minitest/queue.rb +10 -4
  30. data/lib/minitest/queue/build_status_recorder.rb +1 -0
  31. data/lib/minitest/queue/build_status_reporter.rb +1 -0
  32. data/lib/minitest/queue/error_report.rb +13 -0
  33. data/lib/minitest/queue/failure_formatter.rb +4 -0
  34. data/lib/minitest/queue/grind_recorder.rb +68 -0
  35. data/lib/minitest/queue/grind_reporter.rb +74 -0
  36. data/lib/minitest/queue/junit_reporter.rb +6 -5
  37. data/lib/minitest/queue/local_requeue_reporter.rb +1 -0
  38. data/lib/minitest/queue/order_reporter.rb +1 -0
  39. data/lib/minitest/queue/runner.rb +156 -32
  40. data/lib/minitest/queue/statsd.rb +1 -0
  41. data/lib/minitest/queue/test_data.rb +138 -0
  42. data/lib/minitest/queue/test_data_reporter.rb +32 -0
  43. data/lib/minitest/queue/test_time_recorder.rb +17 -0
  44. data/lib/minitest/queue/test_time_reporter.rb +70 -0
  45. data/lib/minitest/reporters/bisect_reporter.rb +1 -0
  46. data/lib/minitest/reporters/statsd_reporter.rb +1 -0
  47. data/lib/rspec/queue.rb +15 -14
  48. data/lib/rspec/queue/build_status_recorder.rb +1 -0
  49. data/lib/rspec/queue/order_recorder.rb +20 -0
  50. metadata +29 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bce03b836af2ca47628929525648bed8d667a434b3d09228d33e0429cbb6a3a4
4
- data.tar.gz: 99e6734a053804f6104409822ac2292c26bcd373d060f34611a1e0fd20be3aa3
3
+ metadata.gz: 85d4db3b22fa289a9fed0d6456071f79a5e4ced83c6fc4fdee7cc4d8725df647
4
+ data.tar.gz: e4635ddf545a73f4680eac9660bfd82e07962e52cc23281a7328e8c7beac6b7d
5
5
  SHA512:
6
- metadata.gz: 3c8cf25173e9282dc101252ca6f494b94dcbc5c7baa6dd9283320aae47e9413a6139f78956ba999ed24aa4a876278f0f45fcede73a8b821039960ce4d8326e22
7
- data.tar.gz: e96bbc23377e38e7bcd36239adb8e5b05c4d8488601972f744f5484d1b569e5a35de85fb56de6b89f912e525728ed3200c08c78c0c81db8ed932e79e86338c77
6
+ metadata.gz: 79b7d97bfb440d829695c56aaf6634805098f95967afd07a52a7140c7d8b610eca8ae07029cf6e81846a51890960be4eba41e2b430f27b10a43dd426e9c3a038
7
+ data.tar.gz: 199500511e7864d46d4de0a4cb8f01dab40149005137b3d8cb8ef17d7050c4a5d5c7bc2d4c088ff325a996929207a837badd5af82d5ee42136ecb342f93fa796
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  test/fixtures/log/junit.xml
2
2
  test/fixtures/log/test_order.log
3
+ test/fixtures/log/test_data.json
3
4
  /.byebug_history
4
5
  /.bundle/
5
6
  /.yardoc
@@ -11,4 +12,4 @@ test/fixtures/log/test_order.log
11
12
  /spec/reports/
12
13
  /tmp/
13
14
  /lib/ci/queue/redis/*.lua
14
-
15
+ test_order.log
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'bundler/gem_tasks'
2
3
  require 'rake/testtask'
3
4
  require 'ci/queue/version'
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "ci/queue"
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'ci/queue/version'
@@ -38,4 +39,5 @@ Gem::Specification.new do |spec|
38
39
 
39
40
  spec.add_development_dependency 'snappy'
40
41
  spec.add_development_dependency 'msgpack'
42
+ spec.add_development_dependency 'rubocop'
41
43
  end
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'minitest/queue/runner'
4
5
  Minitest::Queue::Runner.invoke(ARGV)
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'rspec/queue'
4
5
  RSpec::Queue::Runner.invoke
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'cgi'
3
5
 
@@ -9,6 +11,7 @@ require 'ci/queue/common'
9
11
  require 'ci/queue/build_record'
10
12
  require 'ci/queue/static'
11
13
  require 'ci/queue/file'
14
+ require 'ci/queue/grind'
12
15
  require 'ci/queue/bisect'
13
16
 
14
17
  module CI
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  class Bisect
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  class BuildRecord
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  class CircuitBreaker
@@ -13,6 +14,40 @@ module CI
13
14
  def open?
14
15
  false
15
16
  end
17
+
18
+ def message
19
+ ''
20
+ end
21
+ end
22
+
23
+ class Timeout
24
+ attr_reader :duration, :opened_at, :closes_at
25
+
26
+ def initialize(duration:)
27
+ @duration = duration
28
+ @opened_at = current_timestamp
29
+ @closes_at = @opened_at + duration
30
+ end
31
+
32
+ def report_failure!
33
+ end
34
+
35
+ def report_success!
36
+ end
37
+
38
+ def open?
39
+ closes_at < current_timestamp
40
+ end
41
+
42
+ def message
43
+ "This worker is exiting early because it reached its timeout of #{duration} seconds"
44
+ end
45
+
46
+ private
47
+
48
+ def current_timestamp
49
+ Time.now.to_i
50
+ end
16
51
  end
17
52
 
18
53
  def initialize(max_consecutive_failures:)
@@ -31,6 +66,10 @@ module CI
31
66
  def open?
32
67
  @consecutive_failures >= @max
33
68
  end
69
+
70
+ def message
71
+ 'This worker is exiting early because it encountered too many consecutive test failures, probably because of some corrupted state.'
72
+ end
34
73
  end
35
74
  end
36
75
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  module Common
@@ -15,11 +16,11 @@ module CI
15
16
  end
16
17
 
17
18
  def report_failure!
18
- config.circuit_breaker.report_failure!
19
+ config.circuit_breakers.each(&:report_failure!)
19
20
  end
20
21
 
21
22
  def report_success!
22
- config.circuit_breaker.report_success!
23
+ config.circuit_breakers.each(&:report_success!)
23
24
  end
24
25
 
25
26
  def rescue_connection_errors(handler = ->(err) { nil })
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  class Configuration
4
- attr_accessor :timeout, :build_id, :worker_id, :max_requeues
5
+ attr_accessor :timeout, :build_id, :worker_id, :max_requeues, :grind_count, :failure_file
5
6
  attr_accessor :requeue_tolerance, :namespace, :seed, :failing_test, :statsd_endpoint
6
- attr_reader :circuit_breaker
7
+ attr_accessor :max_test_duration, :max_test_duration_percentile
8
+ attr_reader :circuit_breakers
7
9
 
8
10
  class << self
9
11
  def from_env(env)
@@ -26,25 +28,37 @@ module CI
26
28
 
27
29
  def initialize(
28
30
  timeout: 30, build_id: nil, worker_id: nil, max_requeues: 0, requeue_tolerance: 0,
29
- namespace: nil, seed: nil, flaky_tests: [], statsd_endpoint: nil, max_consecutive_failures: nil
31
+ namespace: nil, seed: nil, flaky_tests: [], statsd_endpoint: nil, max_consecutive_failures: nil,
32
+ grind_count: nil, max_duration: nil, failure_file: nil, max_test_duration: nil,
33
+ max_test_duration_percentile: 0.5
30
34
  )
31
- @namespace = namespace
32
- @timeout = timeout
35
+ @circuit_breakers = [CircuitBreaker::Disabled]
33
36
  @build_id = build_id
34
- @worker_id = worker_id
37
+ @failure_file = failure_file
38
+ @flaky_tests = flaky_tests
39
+ @grind_count = grind_count
35
40
  @max_requeues = max_requeues
41
+ @namespace = namespace
36
42
  @requeue_tolerance = requeue_tolerance
37
43
  @seed = seed
38
- @flaky_tests = flaky_tests
39
44
  @statsd_endpoint = statsd_endpoint
45
+ @timeout = timeout
46
+ @worker_id = worker_id
47
+ @max_test_duration = max_test_duration
48
+ @max_test_duration_percentile = max_test_duration_percentile
49
+ self.max_duration = max_duration
40
50
  self.max_consecutive_failures = max_consecutive_failures
41
51
  end
42
52
 
43
53
  def max_consecutive_failures=(max)
44
- @circuit_breaker = if max
45
- CircuitBreaker.new(max_consecutive_failures: max)
46
- else
47
- CircuitBreaker::Disabled
54
+ if max
55
+ @circuit_breakers << CircuitBreaker.new(max_consecutive_failures: max)
56
+ end
57
+ end
58
+
59
+ def max_duration=(duration)
60
+ if duration
61
+ @circuit_breakers << CircuitBreaker::Timeout.new(duration: duration)
48
62
  end
49
63
  end
50
64
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'ci/queue/static'
2
3
 
3
4
  module CI
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ require 'ci/queue/static'
3
+
4
+ module CI
5
+ module Queue
6
+ class Grind < Static
7
+ class << self
8
+ def from_uri(uri, config)
9
+ new(uri.path, config)
10
+ end
11
+ end
12
+
13
+ def initialize(path, config)
14
+ io = path == '-' ? STDIN : ::File.open(path)
15
+
16
+ tests_to_run = io.each_line.map(&:strip).reject(&:empty?)
17
+ test_grinds = (tests_to_run * config.grind_count).sort
18
+
19
+ super(test_grinds, config)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'ansi'
2
3
 
3
4
  module CI
@@ -1,9 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'redis'
2
4
  require 'ci/queue/redis/build_record'
3
5
  require 'ci/queue/redis/base'
4
6
  require 'ci/queue/redis/worker'
7
+ require 'ci/queue/redis/grind_record'
8
+ require 'ci/queue/redis/grind'
5
9
  require 'ci/queue/redis/retry'
6
10
  require 'ci/queue/redis/supervisor'
11
+ require 'ci/queue/redis/grind_supervisor'
12
+ require 'ci/queue/redis/test_time_record'
7
13
 
8
14
  module CI
9
15
  module Queue
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  module Redis
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  module Redis
@@ -37,8 +38,8 @@ module CI
37
38
  redis.pipelined do
38
39
  redis.hset(
39
40
  key('error-reports'),
40
- id.force_encoding(Encoding::BINARY),
41
- payload.force_encoding(Encoding::BINARY),
41
+ id.dup.force_encoding(Encoding::BINARY),
42
+ payload.dup.force_encoding(Encoding::BINARY),
42
43
  )
43
44
  record_stats(stats)
44
45
  end
@@ -47,7 +48,7 @@ module CI
47
48
 
48
49
  def record_success(id, stats: nil)
49
50
  redis.pipelined do
50
- redis.hdel(key('error-reports'), id.force_encoding(Encoding::BINARY))
51
+ redis.hdel(key('error-reports'), id.dup.force_encoding(Encoding::BINARY))
51
52
  record_stats(stats)
52
53
  end
53
54
  nil
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module CI
3
+ module Queue
4
+ module Redis
5
+ class Grind < Worker
6
+
7
+ def build
8
+ @build ||= GrindRecord.new(self, redis, config)
9
+ end
10
+
11
+ def supervisor
12
+ GrindSupervisor.new(redis_url, config)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module CI
3
+ module Queue
4
+ module Redis
5
+ class GrindRecord
6
+
7
+ def initialize(queue, redis, config)
8
+ @queue = queue
9
+ @redis = redis
10
+ @config = config
11
+ end
12
+
13
+ def record_error(payload, stats: nil)
14
+ redis.pipelined do
15
+ redis.lpush(
16
+ key('error-reports'),
17
+ payload.force_encoding(Encoding::BINARY),
18
+ )
19
+ record_stats(stats)
20
+ end
21
+ nil
22
+ end
23
+
24
+ def record_success(stats: nil)
25
+ record_stats(stats)
26
+ end
27
+
28
+ def record_warning(_,_)
29
+ #do nothing
30
+ end
31
+
32
+ def error_reports
33
+ redis.lrange(key('error-reports'), 0, -1)
34
+ end
35
+
36
+ def fetch_stats(stat_names)
37
+ counts = redis.pipelined do
38
+ stat_names.each { |c| redis.hvals(key(c)) }
39
+ end
40
+ stat_names.zip(counts.map { |values| values.map(&:to_f).inject(:+).to_f }).to_h
41
+ end
42
+
43
+ def pop_warnings
44
+ []
45
+ end
46
+
47
+ alias failed_tests error_reports
48
+
49
+ private
50
+
51
+ attr_reader :redis, :config
52
+
53
+ def key(*args)
54
+ ['build', config.build_id, *args].join(':')
55
+ end
56
+
57
+ def record_stats(stats)
58
+ return unless stats
59
+ stats.each do |stat_name, stat_value|
60
+ redis.hset(key(stat_name), config.worker_id, stat_value)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module CI
3
+ module Queue
4
+ module Redis
5
+ class GrindSupervisor < Supervisor
6
+
7
+ def build
8
+ @build ||= GrindRecord.new(self, redis, config)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  module Redis
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module CI
2
3
  module Queue
3
4
  module Redis
@@ -34,4 +35,4 @@ module CI
34
35
  end
35
36
  end
36
37
  end
37
- end
38
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module CI
3
+ module Queue
4
+ module Redis
5
+ class TestTimeRecord < Worker
6
+ def record(test_name, duration)
7
+ record_test_time(test_name, duration)
8
+ record_test_name(test_name)
9
+ end
10
+
11
+ def fetch
12
+ fetch_all_test_names.each_with_object({}) do |test_name, test_time_hash|
13
+ test_time_hash[test_name] = fetch_test_time(test_name)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :redis
20
+
21
+ def record_test_time(test_name, duration)
22
+ redis.pipelined do
23
+ redis.lpush(
24
+ test_time_key(test_name),
25
+ duration.to_s.force_encoding(Encoding::BINARY),
26
+ )
27
+ end
28
+ nil
29
+ end
30
+
31
+ def record_test_name(test_name)
32
+ redis.pipelined do
33
+ redis.lpush(
34
+ all_test_names_key,
35
+ test_name.dup.force_encoding(Encoding::BINARY),
36
+ )
37
+ end
38
+ nil
39
+ end
40
+
41
+ def fetch_all_test_names
42
+ values = redis.pipelined do
43
+ redis.lrange(all_test_names_key, 0, -1)
44
+ end
45
+ values.flatten.map(&:to_s)
46
+ end
47
+
48
+ def fetch_test_time(test_name)
49
+ values = redis.pipelined do
50
+ key = test_time_key(test_name)
51
+ redis.lrange(key, 0, -1)
52
+ end
53
+ values.flatten.map(&:to_f)
54
+ end
55
+
56
+ def all_test_names_key
57
+ "build:#{config.build_id}:list_of_test_names".dup.force_encoding(Encoding::BINARY)
58
+ end
59
+
60
+ def test_time_key(test_name)
61
+ "build:#{config.build_id}:#{test_name}".dup.force_encoding(Encoding::BINARY)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end