ci-queue 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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