buildkite-test_collector 2.0.0.pre → 2.1.0.pre
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +2 -2
- data/README.md +0 -4
- data/lib/buildkite/test_collector/ci.rb +0 -1
- data/lib/buildkite/test_collector/library_hooks/minitest.rb +1 -1
- data/lib/buildkite/test_collector/minitest_plugin/reporter.rb +3 -11
- data/lib/buildkite/test_collector/uploader.rb +0 -34
- data/lib/buildkite/test_collector/version.rb +1 -1
- data/lib/buildkite/test_collector.rb +1 -37
- metadata +2 -5
- data/lib/buildkite/test_collector/logger.rb +0 -16
- data/lib/buildkite/test_collector/socket_connection.rb +0 -156
- data/lib/buildkite/test_collector/socket_session.rb +0 -333
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e85ba163eccc317f8e03168153e2018d6b3033b760c3479d7e569848dee8be64
|
4
|
+
data.tar.gz: 2ee8f7c0088bd8cf0eff5b94bff12eaf93a5eec5b34e55e25f4bc31d130dbb53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 302e0420cf87e321faaaa4cf7843d322d1b9589a31411e4e7239c781f6176ab7d2a8ff2c0d3650912bccd0f863946d8815d7f0667e1cf132497b35410dcfa247
|
7
|
+
data.tar.gz: ff0a31165893a7368465be84a8ba8fbc6f863198a3b8d8965cee9941a29b785a6c547eb70cfa6c6e1283d4378970c0e217dfff87b657cc91837736ccf6c7a978
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## v2.1.0.pre
|
4
|
+
- Minitest plugin to use HTTP Upload API instead of websocket connection to send test data #178 #179 - @niceking
|
5
|
+
|
3
6
|
## v2.0.0.pre
|
4
7
|
|
5
8
|
- Major change: RSpec plugin to use HTTP Upload API instead of websocket connection to send test data #174 #175 - @niceking
|
6
9
|
- `identifier` field removed from trace #176 - @amybiyuliu
|
10
|
+
- Only warn on EOF errors and also catch SSLErrors #160 - @gchan
|
7
11
|
|
8
12
|
## v1.5.0
|
9
13
|
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
buildkite-test_collector (2.
|
4
|
+
buildkite-test_collector (2.1.0.pre)
|
5
5
|
activesupport (>= 4.2)
|
6
6
|
websocket (~> 1.2)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
activesupport (7.0.4.
|
11
|
+
activesupport (7.0.4.3)
|
12
12
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
13
|
i18n (>= 1.6, < 2)
|
14
14
|
minitest (>= 5.1)
|
data/README.md
CHANGED
@@ -89,10 +89,6 @@ BUILDKITE_ANALYTICS_EXECUTION_NAME_PREFIX
|
|
89
89
|
BUILDKITE_ANALYTICS_EXECUTION_NAME_SUFFIX
|
90
90
|
```
|
91
91
|
|
92
|
-
## 🔍 Debugging
|
93
|
-
|
94
|
-
To enable debugging output, set the `BUILDKITE_ANALYTICS_DEBUG_ENABLED` environment variable to `true`.
|
95
|
-
|
96
92
|
## 🔜 Roadmap
|
97
93
|
|
98
94
|
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 💚
|
@@ -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,
|
@@ -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
|
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
|
-
|
25
|
-
|
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
|
@@ -22,15 +22,12 @@ require "active_support/notifications"
|
|
22
22
|
|
23
23
|
require_relative "test_collector/version"
|
24
24
|
require_relative "test_collector/error"
|
25
|
-
require_relative "test_collector/logger"
|
26
25
|
require_relative "test_collector/ci"
|
27
26
|
require_relative "test_collector/http_client"
|
28
27
|
require_relative "test_collector/uploader"
|
29
28
|
require_relative "test_collector/network"
|
30
29
|
require_relative "test_collector/object"
|
31
30
|
require_relative "test_collector/tracer"
|
32
|
-
require_relative "test_collector/socket_connection"
|
33
|
-
require_relative "test_collector/socket_session"
|
34
31
|
require_relative "test_collector/session"
|
35
32
|
|
36
33
|
module Buildkite
|
@@ -43,17 +40,15 @@ module Buildkite
|
|
43
40
|
attr_accessor :url
|
44
41
|
attr_accessor :uploader
|
45
42
|
attr_accessor :session
|
46
|
-
attr_accessor :debug_enabled
|
47
43
|
attr_accessor :tracing_enabled
|
48
44
|
attr_accessor :artifact_path
|
49
45
|
attr_accessor :env
|
50
46
|
attr_accessor :batch_size
|
51
47
|
end
|
52
48
|
|
53
|
-
def self.configure(hook:, token: nil, url: nil,
|
49
|
+
def self.configure(hook:, token: nil, url: nil, tracing_enabled: true, artifact_path: nil, env: {})
|
54
50
|
self.api_token = (token || ENV["BUILDKITE_ANALYTICS_TOKEN"])&.strip
|
55
51
|
self.url = url || DEFAULT_URL
|
56
|
-
self.debug_enabled = debug_enabled || !!(ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"])
|
57
52
|
self.tracing_enabled = tracing_enabled
|
58
53
|
self.artifact_path = artifact_path
|
59
54
|
self.env = env
|
@@ -74,30 +69,6 @@ module Buildkite
|
|
74
69
|
tracer&.leave
|
75
70
|
end
|
76
71
|
|
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
72
|
def self.enable_tracing!
|
102
73
|
return unless self.tracing_enabled
|
103
74
|
|
@@ -108,12 +79,5 @@ module Buildkite
|
|
108
79
|
Buildkite::TestCollector::Uploader.tracer&.backfill(:sql, finish - start, **{ query: payload[:sql] })
|
109
80
|
end
|
110
81
|
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
82
|
end
|
119
83
|
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.
|
4
|
+
version: 2.1.0.pre
|
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-
|
11
|
+
date: 2023-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -94,7 +94,6 @@ files:
|
|
94
94
|
- lib/buildkite/test_collector/http_client.rb
|
95
95
|
- lib/buildkite/test_collector/library_hooks/minitest.rb
|
96
96
|
- lib/buildkite/test_collector/library_hooks/rspec.rb
|
97
|
-
- lib/buildkite/test_collector/logger.rb
|
98
97
|
- lib/buildkite/test_collector/minitest_plugin.rb
|
99
98
|
- lib/buildkite/test_collector/minitest_plugin/reporter.rb
|
100
99
|
- lib/buildkite/test_collector/minitest_plugin/trace.rb
|
@@ -103,8 +102,6 @@ files:
|
|
103
102
|
- lib/buildkite/test_collector/rspec_plugin/reporter.rb
|
104
103
|
- lib/buildkite/test_collector/rspec_plugin/trace.rb
|
105
104
|
- lib/buildkite/test_collector/session.rb
|
106
|
-
- lib/buildkite/test_collector/socket_connection.rb
|
107
|
-
- lib/buildkite/test_collector/socket_session.rb
|
108
105
|
- lib/buildkite/test_collector/tracer.rb
|
109
106
|
- lib/buildkite/test_collector/uploader.rb
|
110
107
|
- lib/buildkite/test_collector/version.rb
|
@@ -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
|