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 +4 -4
- data/CHANGELOG.md +8 -1
- data/DESIGN.md +1 -11
- data/Gemfile.lock +2 -4
- data/README.md +3 -4
- data/buildkite-test_collector.gemspec +0 -1
- data/lib/buildkite/test_collector/ci.rb +0 -1
- data/lib/buildkite/test_collector/http_client.rb +0 -18
- 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 -40
- metadata +4 -21
- 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: 62d15c76f63986e5fbef7b57ef7319d4e13dbfe0224d7359f056c5be3d1a6016
|
4
|
+
data.tar.gz: 2e80852362308fb6a3b7e36f3ab2c9b7927386a446bead51d8d7dc799efef051
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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.
|
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
|
|
@@ -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
|
@@ -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,
|
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.
|
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-
|
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:
|
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
|