buildkite-test_collector 2.0.0.pre → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7fc00254b8a2002e8680ca265b00f5b2700dace5d6948aa56f779102f8ce0c6
4
- data.tar.gz: 6c05b872c740262ca0c66f4d6c09b5b253a366466241513b74706c8d0a56328a
3
+ metadata.gz: 62d15c76f63986e5fbef7b57ef7319d4e13dbfe0224d7359f056c5be3d1a6016
4
+ data.tar.gz: 2e80852362308fb6a3b7e36f3ab2c9b7927386a446bead51d8d7dc799efef051
5
5
  SHA512:
6
- metadata.gz: 2f646c1a4564960ba78494efef9ec64fa43eb9a4107d7c004884d977649d5cdf07aa19abec49b887d046d1c7a15ec7b61afbde60f4b12769c77f7578ba55df8e
7
- data.tar.gz: b80f9fd21f045f396bceea9af860e471191669dc80b1d3e57ae38c5d1b7dcebe3fdfc1646774af043d6b8f243cd7335c9b8f044aff9232146e3c0a4dad8a58d8
6
+ metadata.gz: 0cffd41ec45351cde0532bcb79e26e24fbeed39bec9528189fed439bb82e69b8a5f1addd164d0d1e6c75e3915d48bbf0647c354ec3fdf68edc92e34e3dd995de
7
+ data.tar.gz: 6fe2ff7e9832bbe3c666882710c7609f90562c6e32072a70fd7833cf0e46999affa1bb34c47e7251cd00d3a283fe9f21443831b4f4dfcb5a047fe8571b54213e
data/CHANGELOG.md CHANGED
@@ -1,13 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v2.1.0
4
+ - Major change: deprecates websocket connection in favour of sending HTTP requests to the Upload API. In future, websocket support will be completely removed from Buildkite and only version 2.1+ of this gem will continue to work.
5
+
6
+ ## v2.1.0.pre
7
+ - Minitest plugin to use HTTP Upload API instead of websocket connection to send test data #178 #179 - @niceking
8
+
3
9
  ## v2.0.0.pre
4
10
 
5
11
  - Major change: RSpec plugin to use HTTP Upload API instead of websocket connection to send test data #174 #175 - @niceking
6
12
  - `identifier` field removed from trace #176 - @amybiyuliu
13
+ - Only warn on EOF errors and also catch SSLErrors #160 - @gchan
7
14
 
8
15
  ## v1.5.0
9
16
 
10
- - Send `failure_expanded` from minitest #171 - @nprizal
17
+ - Send `failure_expanded` from minitest #171 - @nprizal
11
18
 
12
19
  ## v1.4.2
13
20
 
data/DESIGN.md CHANGED
@@ -2,17 +2,7 @@
2
2
 
3
3
  ## Threads
4
4
 
5
- The Buildkite ruby collector uses websockets and ActionCable to send and
6
- receive data with Buildkite. Execution information starts transmitting as soon
7
- as possible, without waiting for the test suite to finish running.
8
-
9
- This gem uses 3 ruby threads:
10
-
11
- * main thread: acts as the producer. It collects span data from the
12
- test suite and enqueues it into the send queue.
13
- * write thread: acts as the consumer. Removes data from the send queue and
14
- sends it to Buildkite.
15
- * read thread: receives and processes messages from Buildkite.
5
+ The Buildkite ruby collector uses threads to send data to the Upload API in the background. When the test suite has finished running, the collector waits UPLOAD_SESSION_TIMEOUT seconds for the Upload API requests to finish before exiting.
16
6
 
17
7
  ## Data
18
8
 
data/Gemfile.lock CHANGED
@@ -1,14 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- buildkite-test_collector (2.0.0.pre)
4
+ buildkite-test_collector (2.1.0)
5
5
  activesupport (>= 4.2)
6
- websocket (~> 1.2)
7
6
 
8
7
  GEM
9
8
  remote: https://rubygems.org/
10
9
  specs:
11
- activesupport (7.0.4.2)
10
+ activesupport (7.0.4.3)
12
11
  concurrent-ruby (~> 1.0, >= 1.0.2)
13
12
  i18n (>= 1.6, < 2)
14
13
  minitest (>= 5.1)
@@ -34,7 +33,6 @@ GEM
34
33
  rspec-support (3.10.3)
35
34
  tzinfo (2.0.6)
36
35
  concurrent-ruby (~> 1.0)
37
- websocket (1.2.9)
38
36
 
39
37
  PLATFORMS
40
38
  ruby
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Buildkite Collectors for Ruby
2
2
 
3
+ **DEPRECATION NOTICE**
4
+ Versions prior to 2.1.x are unsupported and will not work after mid-2023. Please upgrade to the latest version.
5
+
3
6
  Official [Buildkite Test Analytics](https://buildkite.com/test-analytics) collectors for Ruby test frameworks ✨
4
7
 
5
8
  ⚒ **Supported test frameworks:** RSpec, Minitest, and [more coming soon](https://github.com/buildkite/test-collector-ruby/issues?q=is%3Aissue+is%3Aopen+label%3A%22test+frameworks%22).
@@ -89,10 +92,6 @@ BUILDKITE_ANALYTICS_EXECUTION_NAME_PREFIX
89
92
  BUILDKITE_ANALYTICS_EXECUTION_NAME_SUFFIX
90
93
  ```
91
94
 
92
- ## 🔍 Debugging
93
-
94
- To enable debugging output, set the `BUILDKITE_ANALYTICS_DEBUG_ENABLED` environment variable to `true`.
95
-
96
95
  ## 🔜 Roadmap
97
96
 
98
97
  See the [GitHub 'enhancement' issues](https://github.com/buildkite/test-collector-ruby/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) for planned features. Pull requests are always welcome, and we’ll give you feedback and guidance if you choose to contribute 💚
@@ -25,7 +25,6 @@ Gem::Specification.new do |spec|
25
25
  spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
26
26
 
27
27
  spec.add_dependency "activesupport", ">= 4.2"
28
- spec.add_dependency "websocket", '~> 1.2'
29
28
 
30
29
  spec.add_development_dependency "rspec-core", '~> 3.10'
31
30
  spec.add_development_dependency "rspec-expectations", '~> 3.10'
@@ -34,7 +34,6 @@ class Buildkite::TestCollector::CI
34
34
  "number" => ENV["BUILDKITE_ANALYTICS_NUMBER"],
35
35
  "job_id" => ENV["BUILDKITE_ANALYTICS_JOB_ID"],
36
36
  "message" => ENV["BUILDKITE_ANALYTICS_MESSAGE"],
37
- "debug" => ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"],
38
37
  "execution_name_prefix" => ENV["BUILDKITE_ANALYTICS_EXECUTION_NAME_PREFIX"],
39
38
  "execution_name_suffix" => ENV["BUILDKITE_ANALYTICS_EXECUTION_NAME_SUFFIX"],
40
39
  "version" => Buildkite::TestCollector::VERSION,
@@ -10,24 +10,6 @@ module Buildkite::TestCollector
10
10
  @authorization_header = "Token token=\"#{Buildkite::TestCollector.api_token}\""
11
11
  end
12
12
 
13
- def post
14
- contact_uri = URI.parse(url)
15
-
16
- http = Net::HTTP.new(contact_uri.host, contact_uri.port)
17
- http.use_ssl = contact_uri.scheme == "https"
18
-
19
- contact = Net::HTTP::Post.new(contact_uri.path, {
20
- "Authorization" => authorization_header,
21
- "Content-Type" => "application/json",
22
- })
23
- contact.body = {
24
- run_env: Buildkite::TestCollector::CI.env,
25
- format: "websocket"
26
- }.to_json
27
-
28
- http.request(contact)
29
- end
30
-
31
13
  def post_json(data)
32
14
  contact_uri = URI.parse(url)
33
15
 
@@ -12,4 +12,4 @@ end
12
12
 
13
13
  Buildkite::TestCollector.enable_tracing!
14
14
 
15
- Buildkite::TestCollector.safe { Buildkite::TestCollector::Uploader.configure }
15
+ Buildkite::TestCollector.session = Buildkite::TestCollector::Session.new
@@ -13,7 +13,7 @@ module Buildkite::TestCollector::MinitestPlugin
13
13
 
14
14
  if Buildkite::TestCollector.uploader
15
15
  if trace = Buildkite::TestCollector.uploader.traces[result.source_location]
16
- Buildkite::TestCollector.session&.write_result(trace)
16
+ Buildkite::TestCollector.session.add_example_to_send_queue(result.source_location)
17
17
  end
18
18
  end
19
19
  end
@@ -21,16 +21,8 @@ module Buildkite::TestCollector::MinitestPlugin
21
21
  def report
22
22
  super
23
23
 
24
- if Buildkite::TestCollector.session.present?
25
- examples_count = {
26
- examples: count,
27
- failed: failures,
28
- pending: skips,
29
- errors_outside_examples: 0, # Minitest does not report this
30
- }
31
-
32
- Buildkite::TestCollector.session.close(examples_count)
33
- end
24
+ Buildkite::TestCollector.session.send_remaining_data
25
+ Buildkite::TestCollector.session.close
34
26
  end
35
27
  end
36
28
  end
@@ -27,40 +27,6 @@ module Buildkite::TestCollector
27
27
  EOFError
28
28
  ]
29
29
 
30
- def self.configure
31
- Buildkite::TestCollector.logger.debug("hello from main thread")
32
-
33
- if Buildkite::TestCollector.api_token
34
- http = Buildkite::TestCollector::HTTPClient.new(Buildkite::TestCollector.url)
35
-
36
- response = begin
37
- http.post
38
- rescue *Buildkite::TestCollector::Uploader::REQUEST_EXCEPTIONS => e
39
- Buildkite::TestCollector.logger.error "Buildkite Test Analytics: Error communicating with the server: #{e.message}"
40
- end
41
-
42
- return unless response
43
-
44
- case response.code
45
- when "401"
46
- Buildkite::TestCollector.logger.info "Buildkite Test Analytics: Invalid Suite API key. Please double check your Suite API key."
47
- when "200"
48
- json = JSON.parse(response.body)
49
-
50
- if (socket_url = json["cable"]) && (channel = json["channel"])
51
- Buildkite::TestCollector.session = Buildkite::TestCollector::SocketSession.new(socket_url, http.authorization_header, channel)
52
- end
53
- else
54
- request_id = response.to_hash["x-request-id"]
55
- Buildkite::TestCollector.logger.info "buildkite-test_collector could not establish an initial connection with Buildkite. You may be missing some data for this test suite, please contact support with request ID #{request_id}."
56
- end
57
- else
58
- if !!ENV["BUILDKITE_BUILD_ID"]
59
- Buildkite::TestCollector.logger.info "Buildkite Test Analytics: No Suite API key provided. You can get the API key from your Suite settings page."
60
- end
61
- end
62
- end
63
-
64
30
  def self.tracer
65
31
  Thread.current[:_buildkite_tracer]
66
32
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Buildkite
4
4
  module TestCollector
5
- VERSION = "2.0.0.pre"
5
+ VERSION = "2.1.0"
6
6
  NAME = "buildkite-test_collector"
7
7
  end
8
8
  end
@@ -8,13 +8,10 @@ end
8
8
  require "json"
9
9
  require "logger"
10
10
  require "net/http"
11
- require "openssl"
12
11
  require "time"
13
12
  require "timeout"
14
13
  require "tmpdir"
15
14
  require "securerandom"
16
- require "socket"
17
- require "websocket"
18
15
 
19
16
  require "active_support/core_ext/object/blank"
20
17
  require "active_support/core_ext/hash/indifferent_access"
@@ -22,15 +19,12 @@ require "active_support/notifications"
22
19
 
23
20
  require_relative "test_collector/version"
24
21
  require_relative "test_collector/error"
25
- require_relative "test_collector/logger"
26
22
  require_relative "test_collector/ci"
27
23
  require_relative "test_collector/http_client"
28
24
  require_relative "test_collector/uploader"
29
25
  require_relative "test_collector/network"
30
26
  require_relative "test_collector/object"
31
27
  require_relative "test_collector/tracer"
32
- require_relative "test_collector/socket_connection"
33
- require_relative "test_collector/socket_session"
34
28
  require_relative "test_collector/session"
35
29
 
36
30
  module Buildkite
@@ -43,17 +37,15 @@ module Buildkite
43
37
  attr_accessor :url
44
38
  attr_accessor :uploader
45
39
  attr_accessor :session
46
- attr_accessor :debug_enabled
47
40
  attr_accessor :tracing_enabled
48
41
  attr_accessor :artifact_path
49
42
  attr_accessor :env
50
43
  attr_accessor :batch_size
51
44
  end
52
45
 
53
- def self.configure(hook:, token: nil, url: nil, debug_enabled: false, tracing_enabled: true, artifact_path: nil, env: {})
46
+ def self.configure(hook:, token: nil, url: nil, tracing_enabled: true, artifact_path: nil, env: {})
54
47
  self.api_token = (token || ENV["BUILDKITE_ANALYTICS_TOKEN"])&.strip
55
48
  self.url = url || DEFAULT_URL
56
- self.debug_enabled = debug_enabled || !!(ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"])
57
49
  self.tracing_enabled = tracing_enabled
58
50
  self.artifact_path = artifact_path
59
51
  self.env = env
@@ -74,30 +66,6 @@ module Buildkite
74
66
  tracer&.leave
75
67
  end
76
68
 
77
- def self.log_formatter
78
- @log_formatter ||= Buildkite::TestCollector::Logger::Formatter.new
79
- end
80
-
81
- def self.log_formatter=(log_formatter)
82
- @log_formatter = log_formatter
83
- logger.formatter = log_formatter
84
- end
85
-
86
- def self.logger=(logger)
87
- @logger = logger
88
- end
89
-
90
- def self.logger
91
- return @logger if defined?(@logger)
92
-
93
- debug_mode = ENV.fetch("BUILDKITE_ANALYTICS_DEBUG_ENABLED") do
94
- $DEBUG
95
- end
96
-
97
- level = !!debug_mode ? ::Logger::DEBUG : ::Logger::WARN
98
- @logger ||= Buildkite::TestCollector::Logger.new($stderr, level: level)
99
- end
100
-
101
69
  def self.enable_tracing!
102
70
  return unless self.tracing_enabled
103
71
 
@@ -108,12 +76,5 @@ module Buildkite
108
76
  Buildkite::TestCollector::Uploader.tracer&.backfill(:sql, finish - start, **{ query: payload[:sql] })
109
77
  end
110
78
  end
111
-
112
- def self.safe(&block)
113
- block.call
114
- rescue StandardError => e
115
- logger.error("Buildkite::TestCollector received exception: #{e}")
116
- logger.error("Backtrace:\n#{e.backtrace.join("\n")}")
117
- end
118
79
  end
119
80
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: buildkite-test_collector
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Buildkite
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-07 00:00:00.000000000 Z
11
+ date: 2023-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.2'
27
- - !ruby/object:Gem::Dependency
28
- name: websocket
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.2'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.2'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rspec-core
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -94,7 +80,6 @@ files:
94
80
  - lib/buildkite/test_collector/http_client.rb
95
81
  - lib/buildkite/test_collector/library_hooks/minitest.rb
96
82
  - lib/buildkite/test_collector/library_hooks/rspec.rb
97
- - lib/buildkite/test_collector/logger.rb
98
83
  - lib/buildkite/test_collector/minitest_plugin.rb
99
84
  - lib/buildkite/test_collector/minitest_plugin/reporter.rb
100
85
  - lib/buildkite/test_collector/minitest_plugin/trace.rb
@@ -103,8 +88,6 @@ files:
103
88
  - lib/buildkite/test_collector/rspec_plugin/reporter.rb
104
89
  - lib/buildkite/test_collector/rspec_plugin/trace.rb
105
90
  - lib/buildkite/test_collector/session.rb
106
- - lib/buildkite/test_collector/socket_connection.rb
107
- - lib/buildkite/test_collector/socket_session.rb
108
91
  - lib/buildkite/test_collector/tracer.rb
109
92
  - lib/buildkite/test_collector/uploader.rb
110
93
  - lib/buildkite/test_collector/version.rb
@@ -126,9 +109,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
126
109
  version: 2.3.0
127
110
  required_rubygems_version: !ruby/object:Gem::Requirement
128
111
  requirements:
129
- - - ">"
112
+ - - ">="
130
113
  - !ruby/object:Gem::Version
131
- version: 1.3.1
114
+ version: '0'
132
115
  requirements: []
133
116
  rubygems_version: 3.1.6
134
117
  signing_key:
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Buildkite::TestCollector
4
- class Logger < ::Logger
5
- class Formatter < ::Logger::Formatter
6
- def call(severity, time, _program, message)
7
- "#{time.utc.iso8601(9)} pid=#{::Process.pid} tid=#{::Thread.current.object_id} #{severity}: #{message}\n"
8
- end
9
- end
10
-
11
- def initialize(*args, **kwargs)
12
- super
13
- self.formatter = Buildkite::TestCollector.log_formatter
14
- end
15
- end
16
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Buildkite::TestCollector
4
- class SocketConnection
5
- class HandshakeError < StandardError; end
6
- class SocketError < StandardError; end
7
-
8
- def initialize(session, url, headers)
9
- uri = URI.parse(url)
10
- @session = session
11
- protocol = "http"
12
-
13
- begin
14
- socket = TCPSocket.new(uri.host, uri.port || (uri.scheme == "wss" ? 443 : 80))
15
-
16
- if uri.scheme == "wss"
17
- ctx = OpenSSL::SSL::SSLContext.new
18
- protocol = "https"
19
-
20
- ctx.min_version = :TLS1_2
21
- ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
22
- ctx.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
23
-
24
- socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
25
- socket.connect
26
- end
27
- rescue
28
- # We are rescuing all here, as there are a range of Errno errors that could be
29
- # raised when we fail to establish a TCP connection
30
- raise SocketError
31
- end
32
-
33
- @socket = socket
34
-
35
- headers = { "Origin" => "#{protocol}://#{uri.host}" }.merge(headers)
36
- handshake = WebSocket::Handshake::Client.new(url: url, headers: headers)
37
-
38
- @socket.write handshake.to_s
39
-
40
- until handshake.finished?
41
- if byte = @socket.getc
42
- handshake << byte
43
- end
44
- end
45
-
46
- # The errors below are raised when we establish the TCP connection, but get back
47
- # an error, i.e. in dev we can still connect to puma-dev while nginx isn't
48
- # running, or in prod we can hit a load balancer while app is down
49
- unless handshake.valid?
50
- case handshake.error
51
- when Exception, String
52
- raise HandshakeError.new(handshake.error)
53
- when nil
54
- raise HandshakeError.new("Invalid handshake")
55
- else
56
- raise HandshakeError.new(handshake.error.inspect)
57
- end
58
- end
59
-
60
- @version = handshake.version
61
-
62
- # Setting up a new thread that listens on the socket, and processes incoming
63
- # comms from the server
64
- @read_thread = Thread.new do
65
- Buildkite::TestCollector.logger.debug("listening in on socket")
66
- frame = WebSocket::Frame::Incoming::Client.new
67
-
68
- while @socket
69
- frame << @socket.readpartial(4096)
70
-
71
- while data = frame.next
72
- @session.handle(self, data.data)
73
- end
74
- end
75
- # These get re-raise from session, we should fail gracefully
76
- rescue *Buildkite::TestCollector::SocketSession::DISCONNECTED_EXCEPTIONS => e
77
- Buildkite::TestCollector.logger.error("We could not establish a connection with Buildkite Test Analytics. The error was: #{e.message}. If this is a problem, please contact support.")
78
- rescue EOFError, OpenSSL::SSL::SSLError => e
79
- # https://github.com/buildkite/test-collector-ruby/pull/147#issuecomment-1250485611
80
- raise if e.class == OpenSSL::SSL::SSLError && e.message != "SSL_read: unexpected eof while reading"
81
-
82
- Buildkite::TestCollector.logger.warn("#{e}")
83
- if @socket
84
- Buildkite::TestCollector.logger.warn("attempting disconnected flow")
85
- @session.disconnected(self)
86
- disconnect
87
- end
88
- rescue Errno::ECONNRESET, Errno::ETIMEDOUT => e
89
- Buildkite::TestCollector.logger.error("#{e}")
90
- if @socket
91
- Buildkite::TestCollector.logger.error("attempting disconnected flow")
92
- @session.disconnected(self)
93
- disconnect
94
- end
95
- rescue IOError
96
- # This is fine to ignore
97
- Buildkite::TestCollector.logger.error("IOError")
98
- rescue IndexError
99
- # I don't like that we're doing this but I think it's the best of the options
100
- #
101
- # This relates to this issue https://github.com/ruby/openssl/issues/452
102
- # A fix for it has been released but the repercussions of overriding
103
- # the OpenSSL version in the stdlib seem worse than catching this error here.
104
- Buildkite::TestCollector.logger.error("IndexError")
105
- if @socket
106
- Buildkite::TestCollector.logger.error("attempting disconnected flow")
107
- @session.disconnected(self)
108
- disconnect
109
- end
110
- end
111
- end
112
-
113
- def transmit(data, type: :text)
114
- # this line prevents us from calling disconnect twice
115
- return if @socket.nil?
116
-
117
- raw_data = data.to_json
118
- frame = WebSocket::Frame::Outgoing::Client.new(data: raw_data, type: :text, version: @version)
119
- @socket.write(frame.to_s)
120
- rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError => e
121
- return unless @socket
122
- return if type == :close
123
- Buildkite::TestCollector.logger.error("got #{e}, attempting disconnected flow")
124
- @session.disconnected(self)
125
- disconnect
126
- rescue IndexError
127
- # I don't like that we're doing this but I think it's the best of the options
128
- #
129
- # This relates to this issue https://github.com/ruby/openssl/issues/452
130
- # A fix for it has been released but the repercussions of overriding
131
- # the OpenSSL version in the stdlib seem worse than catching this error here.
132
- Buildkite::TestCollector.logger.error("IndexError")
133
- if @socket
134
- Buildkite::TestCollector.logger.error("attempting disconnected flow")
135
- @session.disconnected(self)
136
- disconnect
137
- end
138
- end
139
-
140
- def close
141
- Buildkite::TestCollector.logger.debug("socket close")
142
- transmit(nil, type: :close)
143
- disconnect
144
- end
145
-
146
- private
147
-
148
- def disconnect
149
- Buildkite::TestCollector.logger.debug("socket disconnect")
150
- socket = @socket
151
- @socket = nil
152
- socket&.close
153
- @read_thread&.join unless @read_thread == Thread.current
154
- end
155
- end
156
- end
@@ -1,333 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Buildkite::TestCollector
4
- class SocketSession
5
- # Picked 75 as the magic timeout number as it's longer than the TCP timeout of 60s 🤷‍♀️
6
- CONFIRMATION_TIMEOUT = ENV.fetch("BUILDKITE_ANALYTICS_CONFIRMATION_TIMEOUT") { 75 }.to_i
7
- MAX_RECONNECTION_ATTEMPTS = ENV.fetch("BUILDKITE_ANALYTICS_RECONNECTION_ATTEMPTS") { 3 }.to_i
8
- WAIT_BETWEEN_RECONNECTIONS = ENV.fetch("BUILDKITE_ANALYTICS_RECONNECTION_WAIT") { 5 }.to_i
9
-
10
- # We keep a private reference so that mocking libraries won't break JSON
11
- JSON_PARSE = JSON.method(:parse)
12
- private_constant :JSON_PARSE
13
-
14
- class RejectedSubscription < StandardError; end
15
- class InitialConnectionFailure < StandardError; end
16
-
17
- DISCONNECTED_EXCEPTIONS = [
18
- Buildkite::TestCollector::SocketConnection::HandshakeError,
19
- Buildkite::TestCollector::TimeoutError,
20
- Buildkite::TestCollector::SocketConnection::SocketError,
21
- RejectedSubscription,
22
- InitialConnectionFailure,
23
- ]
24
-
25
- def initialize(url, authorization_header, channel)
26
- @establish_subscription_queue = Queue.new
27
- @channel = channel
28
-
29
- @unconfirmed_idents = {}
30
- @idents_mutex = Mutex.new
31
- @send_queue = Queue.new
32
- @empty = ConditionVariable.new
33
- @closing = false
34
- @eot_queued = false
35
- @eot_queued_mutex = Mutex.new
36
- @reconnection_mutex = Mutex.new
37
-
38
- @url = url
39
- @authorization_header = authorization_header
40
-
41
- reconnection_count = 0
42
-
43
- begin
44
- reconnection_count += 1
45
- connect
46
- rescue Buildkite::TestCollector::TimeoutError, InitialConnectionFailure => e
47
- Buildkite::TestCollector.logger.warn("buildkite-test_collector could not establish an initial connection with Buildkite due to #{e}. Attempting retry #{reconnection_count} of #{MAX_RECONNECTION_ATTEMPTS}...")
48
- if reconnection_count > MAX_RECONNECTION_ATTEMPTS
49
- Buildkite::TestCollector.logger.error "buildkite-test_collector could not establish an initial connection with Buildkite due to #{e.message} after #{MAX_RECONNECTION_ATTEMPTS} attempts. You may be missing some data for this test suite, please contact support if this issue persists."
50
- else
51
- sleep(WAIT_BETWEEN_RECONNECTIONS)
52
- Buildkite::TestCollector.logger.warn("retrying reconnection")
53
- retry
54
- end
55
- end
56
- init_write_thread
57
- end
58
-
59
- def disconnected(connection)
60
- @reconnection_mutex.synchronize do
61
- # When the first thread detects a disconnection, it calls the disconnect method
62
- # with the current connection. This thread grabs the reconnection mutex and does the
63
- # reconnection, which then updates the value of @connection.
64
- #
65
- # At some point in that process, the second thread would have detected the
66
- # disconnection too, and it also calls it with the current connection. However, the
67
- # second thread can't run the reconnection code because of the mutex. By the
68
- # time the mutex is released, the value of @connection has been refreshed, and so
69
- # the second thread returns early and does not reattempt the reconnection.
70
- return unless connection == @connection
71
- Buildkite::TestCollector.logger.debug("starting reconnection")
72
-
73
- reconnection_count = 0
74
-
75
- begin
76
- reconnection_count += 1
77
- connect
78
- init_write_thread
79
- rescue *DISCONNECTED_EXCEPTIONS => e
80
- Buildkite::TestCollector.logger.warn("failed reconnection attempt #{reconnection_count} due to #{e}")
81
- if reconnection_count > MAX_RECONNECTION_ATTEMPTS
82
- Buildkite::TestCollector.logger.error "buildkite-test_collector experienced a disconnection and could not reconnect to Buildkite due to #{e.message}. Please contact support."
83
- raise e
84
- else
85
- sleep(WAIT_BETWEEN_RECONNECTIONS)
86
- Buildkite::TestCollector.logger.warn("retrying reconnection")
87
- retry
88
- end
89
- end
90
- end
91
- retransmit
92
- end
93
-
94
- def close(examples_count)
95
- @closing = true
96
- @examples_count = examples_count
97
- Buildkite::TestCollector.logger.debug("closing socket connection")
98
-
99
- # Because the server only sends us confirmations after every 10mb of
100
- # data it uploads to S3, we'll never get confirmation of the
101
- # identifiers of the last upload part unless we send an explicit finish,
102
- # to which the server will respond with the last bits of data
103
- send_eot
104
-
105
- # After EOT, we wait for 75 seconds for the send queue to be drained and for the
106
- # server to confirm the last idents. If everything has already been confirmed we can
107
- # proceed without waiting.
108
- @idents_mutex.synchronize do
109
- if @unconfirmed_idents.any?
110
- Buildkite::TestCollector.logger.debug "Waiting for Buildkite Test Analytics to send results..."
111
- Buildkite::TestCollector.logger.debug("waiting for last confirm")
112
-
113
- @empty.wait(@idents_mutex, CONFIRMATION_TIMEOUT)
114
- end
115
- end
116
-
117
- # Then we always disconnect cos we can't wait forever? 🤷‍♀️
118
- @connection.close
119
- # We kill the write thread cos it's got a while loop in it, so it won't finish otherwise
120
- @write_thread&.kill
121
-
122
- Buildkite::TestCollector.logger.info "Buildkite Test Analytics completed"
123
- Buildkite::TestCollector.logger.debug("socket connection closed")
124
- end
125
-
126
- def handle(_connection, data)
127
- data = JSON_PARSE.call(data)
128
- case data["type"]
129
- when "ping"
130
- # In absence of other message, the server sends us a ping every 3 seconds
131
- # We are currently not doing anything with these
132
- Buildkite::TestCollector.logger.debug("received ping")
133
- when "welcome", "confirm_subscription"
134
- # Push these two messages onto the queue, so that we block on waiting for the
135
- # initializing phase to complete
136
- @establish_subscription_queue.push(data)
137
- Buildkite::TestCollector.logger.debug("received #{data['type']}")
138
- when "reject_subscription"
139
- Buildkite::TestCollector.logger.debug("received rejected_subscription")
140
- raise RejectedSubscription
141
- else
142
- process_message(data)
143
- end
144
- end
145
-
146
- def write_result(result)
147
- queue_and_track_result(result.id, result.as_hash)
148
-
149
- Buildkite::TestCollector.logger.debug("added #{result.id} to send queue")
150
- end
151
-
152
- def unconfirmed_idents_count
153
- @idents_mutex.synchronize do
154
- @unconfirmed_idents.count
155
- end
156
- end
157
-
158
- private
159
-
160
- def connect
161
- Buildkite::TestCollector.logger.debug("starting socket connection process")
162
-
163
- @connection = SocketConnection.new(self, @url, {
164
- "Authorization" => @authorization_header,
165
- })
166
-
167
- wait_for_welcome
168
-
169
- @connection.transmit({
170
- "command" => "subscribe",
171
- "identifier" => @channel
172
- })
173
-
174
- wait_for_confirm
175
-
176
- Buildkite::TestCollector.logger.info "Connected to Buildkite Test Analytics!"
177
- Buildkite::TestCollector.logger.debug("connected")
178
- end
179
-
180
- def init_write_thread
181
- # As this method can be called multiple times in the
182
- # reconnection process, kill prev write threads (if any) before
183
- # setting up the new one
184
- @write_thread&.kill
185
-
186
- @write_thread = Thread.new do
187
- Buildkite::TestCollector.logger.debug("hello from write thread")
188
- # Pretty sure this eternal loop is fine cos the call to queue.pop is blocking
189
- loop do
190
- data = @send_queue.pop
191
- message_type = data["action"]
192
-
193
- if message_type == "end_of_transmission"
194
- # Because of the unpredictable sequencing between the test suite finishing
195
- # (EOT gets queued) and disconnections happening (retransmit results gets
196
- # queued), we don't want to send an EOT before any retransmits are sent.
197
- if @send_queue.length > 0
198
- @send_queue << data
199
- Buildkite::TestCollector.logger.debug("putting eot at back of queue")
200
- next
201
- end
202
- @eot_queued_mutex.synchronize do
203
- @eot_queued = false
204
- end
205
- end
206
-
207
- @connection.transmit({
208
- "identifier" => @channel,
209
- "command" => "message",
210
- "data" => data.to_json
211
- })
212
-
213
- if Buildkite::TestCollector.debug_enabled
214
- ids = if message_type == "record_results"
215
- data["results"].map { |result| result["id"] }
216
- end
217
- Buildkite::TestCollector.logger.debug("transmitted #{message_type} #{ids}")
218
- end
219
- end
220
- end
221
- end
222
-
223
- def pop_with_timeout(message_type)
224
- Timeout.timeout(30, Buildkite::TestCollector::TimeoutError, "Timeout: Waited 30 seconds for #{message_type}") do
225
- @establish_subscription_queue.pop
226
- end
227
- end
228
-
229
- def wait_for_welcome
230
- welcome = pop_with_timeout("welcome")
231
-
232
- if welcome && welcome != { "type" => "welcome" }
233
- raise InitialConnectionFailure.new("Wrong message received, expected a welcome, but received: #{welcome.inspect}")
234
- end
235
- end
236
-
237
- def wait_for_confirm
238
- confirm = pop_with_timeout("confirm")
239
-
240
- if confirm && confirm != { "type" => "confirm_subscription", "identifier" => @channel }
241
- raise InitialConnectionFailure.new("Wrong message received, expected a confirm, but received: #{confirm.inspect}")
242
- end
243
- end
244
-
245
- def queue_and_track_result(ident, result_as_hash)
246
- @idents_mutex.synchronize do
247
- @unconfirmed_idents[ident] = result_as_hash
248
-
249
- @send_queue << {
250
- "action" => "record_results",
251
- "results" => [result_as_hash]
252
- }
253
- end
254
- end
255
-
256
- def confirm_idents(idents)
257
- retransmit_required = @closing
258
-
259
- @idents_mutex.synchronize do
260
- # Remove received idents from unconfirmed_idents
261
- idents.each { |key| @unconfirmed_idents.delete(key) }
262
-
263
- Buildkite::TestCollector.logger.debug("received confirm for indentifiers: #{idents}")
264
-
265
- # This @empty ConditionVariable broadcasts every time that @unconfirmed_idents is
266
- # empty, which will happen about every 10mb of data as that's when the server
267
- # sends back confirmations.
268
- #
269
- # However, there aren't any threads waiting on this signal until after we
270
- # send the EOT message, so the prior broadcasts shouldn't do anything.
271
- if @unconfirmed_idents.empty?
272
- @empty.broadcast
273
-
274
- retransmit_required = false
275
-
276
- Buildkite::TestCollector.logger.debug("all identifiers have been confirmed")
277
- else
278
- Buildkite::TestCollector.logger.debug("still waiting on confirm for identifiers: #{@unconfirmed_idents.keys}")
279
- end
280
- end
281
-
282
- # If we're closing, any unconfirmed results need to be retransmitted.
283
- retransmit if retransmit_required
284
- end
285
-
286
- def send_eot
287
- @eot_queued_mutex.synchronize do
288
- return if @eot_queued
289
-
290
- @send_queue << {
291
- "action" => "end_of_transmission",
292
- "examples_count" => @examples_count.to_json
293
- }
294
- @eot_queued = true
295
-
296
- Buildkite::TestCollector.logger.debug("added EOT to send queue")
297
- end
298
- end
299
-
300
- def process_message(data)
301
- # Check we're getting the data we expect
302
- return unless data["identifier"] == @channel
303
-
304
- case
305
- when data["message"].key?("confirm")
306
- confirm_idents(data["message"]["confirm"])
307
- else
308
- # unhandled message
309
- Buildkite::TestCollector.logger.debug("received unhandled message #{data["message"]}")
310
- end
311
- end
312
-
313
- def retransmit
314
- @idents_mutex.synchronize do
315
- results = @unconfirmed_idents.values
316
-
317
- # queue the contents of the buffer, unless it's empty
318
- if results.any?
319
- @send_queue << {
320
- "action" => "record_results",
321
- "results" => results
322
- }
323
-
324
- Buildkite::TestCollector.logger.debug("queueing up retransmitted results #{@unconfirmed_idents.keys}")
325
- end
326
- end
327
-
328
- # if we were disconnected in the closing phase, then resend the EOT
329
- # message so the server can persist the last upload part
330
- send_eot if @closing
331
- end
332
- end
333
- end