buildkite-test_collector 1.5.0 → 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 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