buildkite-test_collector 1.5.0 → 2.1.0.pre

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: 90ace3070ba5267cd956e59f09db4c065556295c916a042abeb5e9f6d6d2ff06
4
- data.tar.gz: 8ea5a68cdeb8fe4cbab374e8f5769daf867e913433ab45cc8630cad0ce1033c4
3
+ metadata.gz: e85ba163eccc317f8e03168153e2018d6b3033b760c3479d7e569848dee8be64
4
+ data.tar.gz: 2ee8f7c0088bd8cf0eff5b94bff12eaf93a5eec5b34e55e25f4bc31d130dbb53
5
5
  SHA512:
6
- metadata.gz: c8d067d2b24e5baab884443ec5911a9f9642cd94c61329f214ed2fbea642f6324e094e7d8adb909057986b85339f2757dc54b6904aaa0272ef6e0c93bf63f097
7
- data.tar.gz: d16f6a95ae882f1fc2c24ed4838b6a9e4af7a5648c94f15788463cb3a1080357f2ae4b797ae80b66a2cdb110235d4d1900bd59133b350f6430705fdeaae23438
6
+ metadata.gz: 302e0420cf87e321faaaa4cf7843d322d1b9589a31411e4e7239c781f6176ab7d2a8ff2c0d3650912bccd0f863946d8815d7f0667e1cf132497b35410dcfa247
7
+ data.tar.gz: ff0a31165893a7368465be84a8ba8fbc6f863198a3b8d8965cee9941a29b785a6c547eb70cfa6c6e1283d4378970c0e217dfff87b657cc91837736ccf6c7a978
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
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
+
6
+ ## v2.0.0.pre
7
+
8
+ - Major change: RSpec plugin to use HTTP Upload API instead of websocket connection to send test data #174 #175 - @niceking
9
+ - `identifier` field removed from trace #176 - @amybiyuliu
10
+ - Only warn on EOF errors and also catch SSLErrors #160 - @gchan
11
+
3
12
  ## v1.5.0
4
13
 
5
14
  - Send `failure_expanded` from minitest #171 - @nprizal
data/Gemfile.lock CHANGED
@@ -1,23 +1,23 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- buildkite-test_collector (1.5.0)
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.2)
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)
15
15
  tzinfo (~> 2.0)
16
- concurrent-ruby (1.2.0)
16
+ concurrent-ruby (1.2.2)
17
17
  diff-lcs (1.4.4)
18
18
  i18n (1.12.0)
19
19
  concurrent-ruby (~> 1.0)
20
- minitest (5.17.0)
20
+ minitest (5.18.0)
21
21
  rake (13.0.6)
22
22
  rspec (3.10.0)
23
23
  rspec-core (~> 3.10.0)
@@ -47,4 +47,4 @@ DEPENDENCIES
47
47
  rspec-expectations (~> 3.10)
48
48
 
49
49
  BUNDLED WITH
50
- 2.2.22
50
+ 2.3.25
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,
@@ -28,6 +28,28 @@ module Buildkite::TestCollector
28
28
  http.request(contact)
29
29
  end
30
30
 
31
+ def post_json(data)
32
+ contact_uri = URI.parse(url)
33
+
34
+ http = Net::HTTP.new(contact_uri.host, contact_uri.port)
35
+ http.use_ssl = contact_uri.scheme == "https"
36
+
37
+ contact = Net::HTTP::Post.new(contact_uri.path, {
38
+ "Authorization" => authorization_header,
39
+ "Content-Type" => "application/json",
40
+ })
41
+
42
+ data_set = data.map(&:as_hash)
43
+
44
+ contact.body = {
45
+ run_env: Buildkite::TestCollector::CI.env,
46
+ format: "json",
47
+ data: data_set
48
+ }.to_json
49
+
50
+ http.request(contact)
51
+ end
52
+
31
53
  private
32
54
 
33
55
  attr :url
@@ -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
@@ -11,8 +11,6 @@ Buildkite::TestCollector.uploader = Buildkite::TestCollector::Uploader
11
11
  RSpec.configure do |config|
12
12
  config.before(:suite) do
13
13
  config.add_formatter Buildkite::TestCollector::RSpecPlugin::Reporter
14
-
15
- Buildkite::TestCollector.safe { Buildkite::TestCollector::Uploader.configure }
16
14
  end
17
15
 
18
16
  config.around(:each) do |example|
@@ -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
@@ -34,7 +34,6 @@ module Buildkite::TestCollector::MinitestPlugin
34
34
  id: id,
35
35
  scope: example.class.name,
36
36
  name: example.name,
37
- identifier: identifier,
38
37
  location: location,
39
38
  file_name: file_name,
40
39
  result: result,
@@ -51,7 +50,6 @@ module Buildkite::TestCollector::MinitestPlugin
51
50
  "#{file_name}:#{line_number}"
52
51
  end
53
52
  end
54
- alias_method :identifier, :location
55
53
 
56
54
  def file_name
57
55
  @file_name ||= File.join('./', source_location[0].delete_prefix(project_dir))
@@ -7,6 +7,7 @@ module Buildkite::TestCollector::RSpecPlugin
7
7
  attr_reader :output
8
8
 
9
9
  def initialize(output)
10
+ Buildkite::TestCollector.session = Buildkite::TestCollector::Session.new
10
11
  @output = output
11
12
  end
12
13
 
@@ -19,21 +20,13 @@ module Buildkite::TestCollector::RSpecPlugin
19
20
  if example.execution_result.status == :failed
20
21
  trace.failure_reason, trace.failure_expanded = failure_info(notification)
21
22
  end
22
- Buildkite::TestCollector.session&.write_result(trace)
23
+ Buildkite::TestCollector.session.add_example_to_send_queue(example.id)
23
24
  end
24
25
  end
25
26
 
26
- def dump_summary(notification)
27
- if Buildkite::TestCollector.session.present?
28
- examples_count = {
29
- examples: notification.examples.count,
30
- failed: notification.failed_examples.count,
31
- pending: notification.pending_examples.count,
32
- errors_outside_examples: notification.errors_outside_of_examples_count
33
- }
34
-
35
- Buildkite::TestCollector.session.close(examples_count)
36
- end
27
+ def dump_summary(_notification)
28
+ Buildkite::TestCollector.session.send_remaining_data
29
+ Buildkite::TestCollector.session.close
37
30
  end
38
31
 
39
32
  alias_method :example_passed, :handle_example
@@ -28,7 +28,6 @@ module Buildkite::TestCollector::RSpecPlugin
28
28
  id: id,
29
29
  scope: example.example_group.metadata[:full_description],
30
30
  name: example.description,
31
- identifier: example.id,
32
31
  location: example.location,
33
32
  file_name: file_name,
34
33
  result: result,
@@ -2,332 +2,56 @@
2
2
 
3
3
  module Buildkite::TestCollector
4
4
  class Session
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
5
+ UPLOAD_THREAD_TIMEOUT = 60
6
+ UPLOAD_SESSION_TIMEOUT = 60
7
+ UPLOAD_API_MAX_RESULTS = 5000
9
8
 
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
9
+ def initialize
10
+ @send_queue_ids = []
11
+ @upload_threads = []
57
12
  end
58
13
 
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
14
+ def add_example_to_send_queue(id)
15
+ @send_queue_ids << id
74
16
 
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
17
+ if @send_queue_ids.size >= Buildkite::TestCollector.batch_size
18
+ send_ids = @send_queue_ids.shift(Buildkite::TestCollector.batch_size)
19
+ upload_data(send_ids)
90
20
  end
91
- retransmit
92
21
  end
93
22
 
94
- def close(examples_count)
95
- @closing = true
96
- @examples_count = examples_count
97
- Buildkite::TestCollector.logger.debug("closing socket connection")
23
+ def send_remaining_data
24
+ return if @send_queue_ids.empty?
98
25
 
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")
26
+ upload_data(@send_queue_ids)
124
27
  end
125
28
 
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
29
+ def close
30
+ # There are two thread joins here, because the inner join will wait up to
31
+ # UPLOAD_THREAD_TIMEOUT seconds PER thread that is uploading data, i.e.
32
+ # n_threads x UPLOAD_THREAD_TIMEOUT latency if Buildkite happens to be
33
+ # down. By wrapping that in an outer thread join with the
34
+ # UPLOAD_SESSION_TIMEOUT, we ensure that we only wait a max of
35
+ # UPLOAD_SESSION_TIMEOUT seconds before the session exits.
36
+ Thread.new do
37
+ @upload_threads.each { |t| t.join(UPLOAD_THREAD_TIMEOUT) }
38
+ end.join(UPLOAD_SESSION_TIMEOUT)
145
39
 
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
40
+ @upload_threads.each { |t| t&.kill }
156
41
  end
157
42
 
158
43
  private
159
44
 
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
45
+ def upload_data(ids)
46
+ data = Buildkite::TestCollector.uploader.traces.values_at(*ids).compact
185
47
 
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
48
+ # we do this in batches of UPLOAD_API_MAX_RESULTS in case the number of
49
+ # results exceeds this due to a bug, or user error in configuring the
50
+ # batch size
51
+ data.each_slice(UPLOAD_API_MAX_RESULTS) do |batch|
52
+ new_thread = Buildkite::TestCollector::Uploader.upload(batch)
53
+ @upload_threads << new_thread if new_thread
226
54
  end
227
55
  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
56
  end
333
57
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Buildkite::TestCollector
4
4
  class Uploader
5
+ MAX_UPLOAD_ATTEMPTS = 3
6
+
5
7
  def self.traces
6
8
  @traces ||= {}
7
9
  end
@@ -17,42 +19,33 @@ module Buildkite::TestCollector
17
19
  EOFError
18
20
  ]
19
21
 
20
- def self.configure
21
- Buildkite::TestCollector.logger.debug("hello from main thread")
22
-
23
- if Buildkite::TestCollector.api_token
24
- http = Buildkite::TestCollector::HTTPClient.new(Buildkite::TestCollector.url)
22
+ RETRYABLE_UPLOAD_ERRORS = [
23
+ Net::ReadTimeout,
24
+ Net::OpenTimeout,
25
+ OpenSSL::SSL::SSLError,
26
+ OpenSSL::SSL::SSLErrorWaitReadable,
27
+ EOFError
28
+ ]
25
29
 
26
- response = begin
27
- http.post
28
- rescue *Buildkite::TestCollector::Uploader::REQUEST_EXCEPTIONS => e
29
- Buildkite::TestCollector.logger.error "Buildkite Test Analytics: Error communicating with the server: #{e.message}"
30
- end
30
+ def self.tracer
31
+ Thread.current[:_buildkite_tracer]
32
+ end
31
33
 
32
- return unless response
34
+ def self.upload(data)
35
+ return false unless Buildkite::TestCollector.api_token
33
36
 
34
- case response.code
35
- when "401"
36
- Buildkite::TestCollector.logger.info "Buildkite Test Analytics: Invalid Suite API key. Please double check your Suite API key."
37
- when "200"
38
- json = JSON.parse(response.body)
37
+ http = Buildkite::TestCollector::HTTPClient.new(Buildkite::TestCollector.url)
39
38
 
40
- if (socket_url = json["cable"]) && (channel = json["channel"])
41
- Buildkite::TestCollector.session = Buildkite::TestCollector::Session.new(socket_url, http.authorization_header, channel)
39
+ Thread.new do
40
+ response = begin
41
+ upload_attempts ||= 0
42
+ http.post_json(data)
43
+ rescue *Buildkite::TestCollector::Uploader::RETRYABLE_UPLOAD_ERRORS => e
44
+ if (upload_attempts += 1) < MAX_UPLOAD_ATTEMPTS
45
+ retry
42
46
  end
43
- else
44
- request_id = response.to_hash["x-request-id"]
45
- 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}."
46
- end
47
- else
48
- if !!ENV["BUILDKITE_BUILD_ID"]
49
- Buildkite::TestCollector.logger.info "Buildkite Test Analytics: No Suite API key provided. You can get the API key from your Suite settings page."
50
47
  end
51
48
  end
52
49
  end
53
-
54
- def self.tracer
55
- Thread.current[:_buildkite_tracer]
56
- end
57
50
  end
58
51
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Buildkite
4
4
  module TestCollector
5
- VERSION = "1.5.0"
5
+ VERSION = "2.1.0.pre"
6
6
  NAME = "buildkite-test_collector"
7
7
  end
8
8
  end
@@ -22,39 +22,37 @@ 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
31
  require_relative "test_collector/session"
34
32
 
35
33
  module Buildkite
36
34
  module TestCollector
37
35
  DEFAULT_URL = "https://analytics-api.buildkite.com/v1/uploads"
36
+ DEFAULT_UPLOAD_BATCH_SIZE = 500
38
37
 
39
38
  class << self
40
39
  attr_accessor :api_token
41
40
  attr_accessor :url
42
41
  attr_accessor :uploader
43
42
  attr_accessor :session
44
- attr_accessor :debug_enabled
45
43
  attr_accessor :tracing_enabled
46
44
  attr_accessor :artifact_path
47
45
  attr_accessor :env
46
+ attr_accessor :batch_size
48
47
  end
49
48
 
50
- def self.configure(hook:, token: nil, url: nil, debug_enabled: false, tracing_enabled: true, artifact_path: nil, env: {})
49
+ def self.configure(hook:, token: nil, url: nil, tracing_enabled: true, artifact_path: nil, env: {})
51
50
  self.api_token = (token || ENV["BUILDKITE_ANALYTICS_TOKEN"])&.strip
52
51
  self.url = url || DEFAULT_URL
53
- self.debug_enabled = debug_enabled || !!(ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"])
54
52
  self.tracing_enabled = tracing_enabled
55
53
  self.artifact_path = artifact_path
56
54
  self.env = env
57
-
55
+ self.batch_size = ENV.fetch("BUILDKITE_ANALYTICS_UPLOAD_BATCH_SIZE") { DEFAULT_UPLOAD_BATCH_SIZE }.to_i
58
56
  self.hook_into(hook)
59
57
  end
60
58
 
@@ -71,30 +69,6 @@ module Buildkite
71
69
  tracer&.leave
72
70
  end
73
71
 
74
- def self.log_formatter
75
- @log_formatter ||= Buildkite::TestCollector::Logger::Formatter.new
76
- end
77
-
78
- def self.log_formatter=(log_formatter)
79
- @log_formatter = log_formatter
80
- logger.formatter = log_formatter
81
- end
82
-
83
- def self.logger=(logger)
84
- @logger = logger
85
- end
86
-
87
- def self.logger
88
- return @logger if defined?(@logger)
89
-
90
- debug_mode = ENV.fetch("BUILDKITE_ANALYTICS_DEBUG_ENABLED") do
91
- $DEBUG
92
- end
93
-
94
- level = !!debug_mode ? ::Logger::DEBUG : ::Logger::WARN
95
- @logger ||= Buildkite::TestCollector::Logger.new($stderr, level: level)
96
- end
97
-
98
72
  def self.enable_tracing!
99
73
  return unless self.tracing_enabled
100
74
 
@@ -105,12 +79,5 @@ module Buildkite
105
79
  Buildkite::TestCollector::Uploader.tracer&.backfill(:sql, finish - start, **{ query: payload[:sql] })
106
80
  end
107
81
  end
108
-
109
- def self.safe(&block)
110
- block.call
111
- rescue StandardError => e
112
- logger.error("Buildkite::TestCollector received exception: #{e}")
113
- logger.error("Backtrace:\n#{e.backtrace.join("\n")}")
114
- end
115
82
  end
116
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: 1.5.0
4
+ version: 2.1.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Buildkite
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-29 00:00:00.000000000 Z
11
+ date: 2023-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,7 +66,7 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.10'
69
- description:
69
+ description:
70
70
  email:
71
71
  - support+analytics@buildkite.com
72
72
  executables: []
@@ -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,7 +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
105
  - lib/buildkite/test_collector/tracer.rb
108
106
  - lib/buildkite/test_collector/uploader.rb
109
107
  - lib/buildkite/test_collector/version.rb
@@ -114,7 +112,7 @@ licenses:
114
112
  metadata:
115
113
  homepage_uri: https://github.com/buildkite/test-collector-ruby
116
114
  source_code_uri: https://github.com/buildkite/test-collector-ruby
117
- post_install_message:
115
+ post_install_message:
118
116
  rdoc_options: []
119
117
  require_paths:
120
118
  - lib
@@ -125,12 +123,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
123
  version: 2.3.0
126
124
  required_rubygems_version: !ruby/object:Gem::Requirement
127
125
  requirements:
128
- - - ">="
126
+ - - ">"
129
127
  - !ruby/object:Gem::Version
130
- version: '0'
128
+ version: 1.3.1
131
129
  requirements: []
132
- rubygems_version: 3.4.1
133
- signing_key:
130
+ rubygems_version: 3.1.6
131
+ signing_key:
134
132
  specification_version: 4
135
133
  summary: Track test executions and report to Buildkite Test Analytics
136
134
  test_files: []
@@ -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,153 +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::Session::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 => e
79
- Buildkite::TestCollector.logger.warn("#{e}")
80
- if @socket
81
- Buildkite::TestCollector.logger.error("attempting disconnected flow")
82
- @session.disconnected(self)
83
- disconnect
84
- end
85
- rescue Errno::ECONNRESET, Errno::ETIMEDOUT => e
86
- Buildkite::TestCollector.logger.error("#{e}")
87
- if @socket
88
- Buildkite::TestCollector.logger.error("attempting disconnected flow")
89
- @session.disconnected(self)
90
- disconnect
91
- end
92
- rescue IOError
93
- # This is fine to ignore
94
- Buildkite::TestCollector.logger.error("IOError")
95
- rescue IndexError
96
- # I don't like that we're doing this but I think it's the best of the options
97
- #
98
- # This relates to this issue https://github.com/ruby/openssl/issues/452
99
- # A fix for it has been released but the repercussions of overriding
100
- # the OpenSSL version in the stdlib seem worse than catching this error here.
101
- Buildkite::TestCollector.logger.error("IndexError")
102
- if @socket
103
- Buildkite::TestCollector.logger.error("attempting disconnected flow")
104
- @session.disconnected(self)
105
- disconnect
106
- end
107
- end
108
- end
109
-
110
- def transmit(data, type: :text)
111
- # this line prevents us from calling disconnect twice
112
- return if @socket.nil?
113
-
114
- raw_data = data.to_json
115
- frame = WebSocket::Frame::Outgoing::Client.new(data: raw_data, type: :text, version: @version)
116
- @socket.write(frame.to_s)
117
- rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError => e
118
- return unless @socket
119
- return if type == :close
120
- Buildkite::TestCollector.logger.error("got #{e}, attempting disconnected flow")
121
- @session.disconnected(self)
122
- disconnect
123
- rescue IndexError
124
- # I don't like that we're doing this but I think it's the best of the options
125
- #
126
- # This relates to this issue https://github.com/ruby/openssl/issues/452
127
- # A fix for it has been released but the repercussions of overriding
128
- # the OpenSSL version in the stdlib seem worse than catching this error here.
129
- Buildkite::TestCollector.logger.error("IndexError")
130
- if @socket
131
- Buildkite::TestCollector.logger.error("attempting disconnected flow")
132
- @session.disconnected(self)
133
- disconnect
134
- end
135
- end
136
-
137
- def close
138
- Buildkite::TestCollector.logger.debug("socket close")
139
- transmit(nil, type: :close)
140
- disconnect
141
- end
142
-
143
- private
144
-
145
- def disconnect
146
- Buildkite::TestCollector.logger.debug("socket disconnect")
147
- socket = @socket
148
- @socket = nil
149
- socket&.close
150
- @read_thread&.join unless @read_thread == Thread.current
151
- end
152
- end
153
- end