honeybadger 6.9.3 → 6.10.1

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: fe7e1dc021b03b75e886083cda940bd73ac4613427229e0b9a08c4e65badf548
4
- data.tar.gz: ea2e996262898454537f1e65e394d4d1cd93226420dab1d0d66ad8b408cac200
3
+ metadata.gz: bd51e9013ebffeda55792fca6ed7837bc8e9199aeb570e898bc74ea80df9cb66
4
+ data.tar.gz: 92787b0bfd6fa3401685a131f17f2b2d22a29e37db9781bc3ff9dd9afd442829
5
5
  SHA512:
6
- metadata.gz: 8ae65e4be79d6ed5761b54139488681f1d5a591f8a5f1b44185e17d4322120f2a280055704a1a56e5e0836bb12f2dcf2c91372f9e0c0c9b06a488e69089fd7ce
7
- data.tar.gz: 8ac9bb344ab71c34de08a8eefb3bb7dc2393d7c29d6f59f6cbe289d6c428a699cc679d599088d2d6c87f8e1a6de9069cc079f156b272385299e25b2e81361002
6
+ metadata.gz: 5abd942300193e9eb8a94dc5449d18b5f8dad03dcd21c67fa1b3aaa0ac39cfc3d13989add2757cff83a93faf33ca218a53745f91cfdecd4f95388641eb680c4d
7
+ data.tar.gz: 50afe4ea98c6181129622c0cf84aae411f80d6321331f964882b23eded3eefad0d0afa6b5994b1e3b801a4b512895ef02fa00fc8ba32078dcfad2ad2adf8a09d
data/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Change Log
2
2
 
3
3
 
4
+ ## [6.10.1](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.10.0...v6.10.1) (2026-10-30)
5
+
6
+
7
+ ### Bug Fixes
8
+
9
+ * update bundled CA certificates ([#858](https://github.com/honeybadger-io/honeybadger-ruby/issues/858)) ([4f5489e](https://github.com/honeybadger-io/honeybadger-ruby/commit/4f5489ef806e3f9a8681063408cf4ec015d8aadf))
10
+
11
+ ### Changed
12
+
13
+ * The bundled CA certificates no longer include roots that Mozilla has removed. If you set `connection.host` to your own server and TLS verification fails after upgrading, set `connection.system_ssl_cert_chain: true` or `connection.ssl_ca_bundle_path` ([#858](https://github.com/honeybadger-io/honeybadger-ruby/issues/858))
14
+
15
+ ## [6.10.0](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.3...v6.10.0) (2026-10-20)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * retry notices and events on short Retry-After instead of dropping them ([#847](https://github.com/honeybadger-io/honeybadger-ruby/issues/847)) ([bb948b3](https://github.com/honeybadger-io/honeybadger-ruby/commit/bb948b309f2dcc6c01c052f8ea3519c5a68187ac))
21
+
22
+ ### Changed
23
+
24
+ * Default `events.max_queue_size` has been reduced from 100,000 to 10,000; raise it if your app sends large bursts of events ([#847](https://github.com/honeybadger-io/honeybadger-ruby/issues/847))
25
+
4
26
  ## [6.9.3](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.2...v6.9.3) (2026-09-29)
5
27
 
6
28
 
@@ -9,7 +9,10 @@ module Honeybadger
9
9
  class Response
10
10
  NOT_BLANK = /\S/
11
11
 
12
- attr_reader :code, :body, :message, :error
12
+ attr_reader :code, :body, :message, :error, :retry_after, :limit_reason
13
+
14
+ # Longest Retry-After, in seconds, accepted for a single retry delay.
15
+ MAX_RETRY_AFTER = 60
13
16
 
14
17
  FRIENDLY_ERRORS = {
15
18
  429 => "Your project is currently sending too many errors.\nThis issue should resolve itself once error traffic is reduced.".freeze,
@@ -36,6 +39,9 @@ module Honeybadger
36
39
  def initialize(*args)
37
40
  if (response = args.first).is_a?(Net::HTTPResponse)
38
41
  @code, @body, @message = response.code.to_i, response.body.to_s, response.message
42
+ @retry_after = parse_retry_after(response["Retry-After"])
43
+ # Why a 429 was sent, e.g. "overloaded".
44
+ @limit_reason = response["Honeybadger-Limit-Reason"]&.strip&.downcase
39
45
  else
40
46
  @code, @body, @message = args
41
47
  end
@@ -48,6 +54,26 @@ module Honeybadger
48
54
  @success
49
55
  end
50
56
 
57
+ # The server is briefly unavailable (502/503/504, or 429 with
58
+ # Limit-Reason "overloaded") and Retry-After is absent or at most
59
+ # MAX_RETRY_AFTER. Hold the payload and retry it. Any other 429 means
60
+ # the server is rejecting data.
61
+ def retryable?
62
+ return false if retry_after && retry_after > MAX_RETRY_AFTER
63
+ [502, 503, 504].include?(code) || (code == 429 && limit_reason == "overloaded")
64
+ end
65
+
66
+ # Seconds to drop queued data and reject new data, or nil if this isn't
67
+ # a rejection.
68
+ def reject_interval
69
+ return if retryable?
70
+ case code
71
+ when 429 then retry_after || 60
72
+ when 502, 503, 504 then retry_after
73
+ when 401, 403 then retry_after || 3600
74
+ end
75
+ end
76
+
51
77
  def error_message
52
78
  return message if code == :error
53
79
  return error if code == 403 && NOT_BLANK.match?(error)
@@ -60,6 +86,15 @@ module Honeybadger
60
86
 
61
87
  private
62
88
 
89
+ # Positive integer seconds only. Zero, negative, HTTP-date, and other
90
+ # values are treated as absent; Honeybadger only sends seconds.
91
+ def parse_retry_after(value)
92
+ return nil unless value
93
+ return nil unless /\A\d+\z/.match?(value.strip)
94
+ seconds = value.to_i
95
+ (seconds > 0) ? seconds : nil
96
+ end
97
+
63
98
  def parse_error(body)
64
99
  return unless NOT_BLANK.match?(body)
65
100
  obj = JSON.parse(body)
@@ -115,7 +115,7 @@ module Honeybadger
115
115
  },
116
116
  "events.max_queue_size": {
117
117
  description: "Maximum number of event for the event worker queue.",
118
- default: 100000,
118
+ default: 10000,
119
119
  type: Integer
120
120
  },
121
121
  "events.batch_size": {
@@ -16,25 +16,21 @@ module Honeybadger
16
16
 
17
17
  # Used to signal the worker to shutdown.
18
18
  SHUTDOWN = :__hb_worker_shutdown!
19
- FLUSH = :__hb_worker_flush!
20
19
  CHECK_TIMEOUT = :__hb_worker_check_timeout!
21
20
 
22
- # The base number for the exponential backoff formula when calculating the
23
- # throttle interval. `1.05 ** throttle` will reach an interval of 2 minutes
24
- # after around 100 429 responses from the server.
25
- BASE_THROTTLE = 1.05
21
+ # Number of times a batch is resent while the server is unavailable
22
+ # (6 attempts in total). Each batch gets its own retries.
23
+ MAX_RETRIES = 5
26
24
 
27
25
  # TODO: These could be configurable?
28
26
 
29
27
  def initialize(config)
30
28
  @config = config
31
- @throttle = 0
32
- @throttle_interval = 0
33
29
  @mutex = Mutex.new
34
- @marker = ConditionVariable.new
30
+ @flush_signal = ConditionVariable.new
35
31
  @queue = Queue.new
36
- @send_queue = Queue.new
37
32
  @shutdown = false
33
+ @flushes = 0
38
34
  @start_at = nil
39
35
  @pid = Process.pid
40
36
  @send_queue = []
@@ -67,13 +63,7 @@ module Honeybadger
67
63
  return true if force
68
64
  return true unless thread&.alive?
69
65
 
70
- if throttled?
71
- warn { sprintf("Unable to send %s event(s) to Honeybadger (currently throttled)", queue.size) } unless queue.empty?
72
- return true
73
- end
74
-
75
66
  info { sprintf("Waiting to send %s events(s) to Honeybadger", queue.size) } unless queue.empty?
76
- queue.push(FLUSH)
77
67
  queue.push(SHUTDOWN)
78
68
  !!thread.join
79
69
  ensure
@@ -81,13 +71,18 @@ module Honeybadger
81
71
  kill!
82
72
  end
83
73
 
84
- # Blocks until queue is processed up to this point in time.
74
+ # Blocks until queue is processed up to this point in time. Retries are
75
+ # skipped while a flush is pending, since the caller is waiting.
85
76
  def flush
86
77
  mutex.synchronize do
87
78
  if thread&.alive?
88
- queue.push(FLUSH)
89
- queue.push(marker)
90
- marker.wait(mutex)
79
+ @flushes += 1
80
+ queue.push(flush_signal)
81
+ begin
82
+ flush_signal.wait(mutex)
83
+ ensure
84
+ @flushes -= 1
85
+ end
91
86
  end
92
87
  end
93
88
  end
@@ -111,8 +106,8 @@ module Honeybadger
111
106
 
112
107
  private
113
108
 
114
- attr_reader :config, :queue, :pid, :mutex, :marker, :thread, :timeout_thread, :throttle,
115
- :throttle_interval, :start_at, :send_queue, :last_sent
109
+ attr_reader :config, :queue, :pid, :mutex, :flush_signal, :thread, :timeout_thread,
110
+ :start_at, :send_queue, :last_sent
116
111
 
117
112
  def_delegator :config, :backend
118
113
 
@@ -120,6 +115,10 @@ module Honeybadger
120
115
  mutex.synchronize { @shutdown }
121
116
  end
122
117
 
118
+ def flushing?
119
+ mutex.synchronize { @flushes > 0 }
120
+ end
121
+
123
122
  def suspended?
124
123
  mutex.synchronize { start_at && Time.now.to_i < start_at }
125
124
  end
@@ -130,10 +129,6 @@ module Honeybadger
130
129
  true
131
130
  end
132
131
 
133
- def throttled?
134
- mutex.synchronize { throttle > 0 }
135
- end
136
-
137
132
  def kill!
138
133
  d { "killing worker thread" }
139
134
 
@@ -150,10 +145,18 @@ module Honeybadger
150
145
  true
151
146
  end
152
147
 
148
+ # Drops queued and pending events and rejects new ones (push returns
149
+ # false) for +interval+ seconds. The thread restarts on the first push after that.
153
150
  def suspend(interval)
151
+ warn { sprintf("Dropping queued events and rejecting new ones for %ss", interval) }
154
152
  mutex.synchronize do
155
153
  @start_at = Time.now.to_i + interval
156
154
  queue.clear
155
+ send_queue.clear
156
+ if @dropped_events > 0
157
+ warn { sprintf("Dropped %s messages due to exceeding max queue size of %s", @dropped_events, config.events_max_queue_size) }
158
+ end
159
+ @dropped_events = 0
157
160
  end
158
161
 
159
162
  # Must be performed last since this may kill the current thread.
@@ -175,10 +178,13 @@ module Honeybadger
175
178
  end
176
179
  loop do
177
180
  case msg = queue.pop
178
- when SHUTDOWN then break
181
+ when SHUTDOWN
182
+ send_pending_batch
183
+ break
179
184
  when CHECK_TIMEOUT then check_timeout
180
- when FLUSH then flush_send_queue
181
- when ConditionVariable then signal_marker(msg)
185
+ when flush_signal
186
+ send_pending_batch
187
+ signal_flush_done
182
188
  else work(msg)
183
189
  end
184
190
  end
@@ -191,7 +197,7 @@ module Honeybadger
191
197
  sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
192
198
  }
193
199
  ensure
194
- release_marker
200
+ release_flush_waiters
195
201
  end
196
202
 
197
203
  def check_timeout
@@ -209,16 +215,35 @@ module Honeybadger
209
215
  end
210
216
 
211
217
  def send_batch
212
- send_now(mutex.synchronize { send_queue })
218
+ # batch is send_queue itself; clearing send_queue also clears batch.
219
+ batch = mutex.synchronize { send_queue }
220
+ response = send_to_backend(batch)
221
+ MAX_RETRIES.times do
222
+ break unless response.retryable?
223
+ break unless wait(response.retry_after || rand(5..15))
224
+ response = send_to_backend(batch)
225
+ end
226
+ handle_response(response)
227
+ if (interval = response.reject_interval)
228
+ suspend(interval) # clears the batch and terminates this thread
229
+ end
230
+
213
231
  mutex.synchronize do
214
232
  @last_sent = Time.now
215
- debug { sprintf("Sending %s events", send_queue.length) }
233
+ debug { sprintf("Finished attempting to send %s events", send_queue.length) }
216
234
  send_queue.clear
217
235
  if @dropped_events > 0
218
236
  warn { sprintf("Dropped %s messages due to exceeding max queue size of %s", @dropped_events, config.events_max_queue_size) }
219
237
  end
220
238
  @dropped_events = 0
221
239
  end
240
+
241
+ # Shutting down while the server is unavailable: stop without sending
242
+ # the rest of the queue.
243
+ if shutdown? && response.retryable?
244
+ warn { "Shutting down while the server is unavailable; dropping queued events" }
245
+ kill!
246
+ end
222
247
  end
223
248
 
224
249
  def check_and_send
@@ -228,7 +253,7 @@ module Honeybadger
228
253
  end
229
254
  end
230
255
 
231
- def flush_send_queue
256
+ def send_pending_batch
232
257
  return if mutex.synchronize { send_queue.empty? }
233
258
  send_batch
234
259
  rescue => e
@@ -241,14 +266,6 @@ module Honeybadger
241
266
  def work(msg)
242
267
  enqueue_msg(msg)
243
268
  check_and_send
244
-
245
- if shutdown? && throttled?
246
- warn { sprintf("Unable to send %s events(s) to Honeybadger (currently throttled)", queue.size) } if queue.size > 1
247
- kill!
248
- return
249
- end
250
-
251
- sleep(throttle_interval)
252
269
  rescue => e
253
270
  error {
254
271
  msg = "Error in worker thread class=%s message=%s\n\t%s"
@@ -261,49 +278,33 @@ module Honeybadger
261
278
  backend.event(msg)
262
279
  end
263
280
 
264
- def calc_throttle_interval
265
- ((BASE_THROTTLE**throttle) - 1).round(3)
266
- end
267
-
268
- def inc_throttle
269
- mutex.synchronize do
270
- @throttle += 1
271
- @throttle_interval = calc_throttle_interval
272
- throttle
273
- end
274
- end
275
-
276
- def dec_throttle
277
- mutex.synchronize do
278
- return nil if throttle == 0
279
- @throttle -= 1
280
- @throttle_interval = calc_throttle_interval
281
- throttle
281
+ # Sleeps in one-second steps so shutdown or a flush can interrupt.
282
+ # Returns false if one did, including during the final second; the
283
+ # batch in hand is then dropped.
284
+ def wait(seconds)
285
+ seconds.times do
286
+ return false if shutdown? || flushing?
287
+ sleep(1)
282
288
  end
289
+ !(shutdown? || flushing?)
283
290
  end
284
291
 
285
292
  def handle_response(response)
286
293
  d { sprintf("events_worker response code=%s message=%s", response.code, response.message.to_s.dump) }
287
294
 
288
295
  case response.code
289
- when 429, 503
290
- throttle = inc_throttle
291
- debug { sprintf("Insights Event send failed: project is sending too many events. code=%s throttle=%s interval=%s", response.code, throttle, throttle_interval) }
292
- suspend(3600)
293
- when 402
294
- warn { sprintf("Insights Event send failed: payment is required. code=%s", response.code) }
295
- suspend(3600)
296
- when 403
296
+ when 429, 502, 503, 504
297
+ if response.retryable?
298
+ warn { sprintf("Insights Event send failed: server unavailable, dropping this batch. code=%s", response.code) }
299
+ else
300
+ warn { sprintf("Insights Event send failed: server is rejecting data. code=%s reason=%s", response.code, response.limit_reason) }
301
+ end
302
+ when 401, 403
297
303
  warn { sprintf("Insights Event send failed: %s code=%s", response.error_message, response.code) }
298
- suspend(3600)
299
304
  when 413
300
305
  warn { sprintf("Insights Event send failed: Payload is too large. code=%s", response.code) }
301
306
  when 201
302
- if (throttle = dec_throttle)
303
- debug { sprintf("Success ⚡ Insights Event sent code=%s throttle=%s interval=%s", response.code, throttle, throttle_interval) }
304
- else
305
- debug { sprintf("Success ⚡ Insights Event sent code=%s", response.code) }
306
- end
307
+ debug { sprintf("Success ⚡ Insights Event sent code=%s", response.code) }
307
308
  when :stubbed
308
309
  debug { "Success ⚡ Development mode is enabled; This event will be sent after app is deployed." }
309
310
  when :error
@@ -313,17 +314,22 @@ module Honeybadger
313
314
  end
314
315
  end
315
316
 
316
- # Release the marker. Important to perform during cleanup when shutting
317
- # down, otherwise it could end up waiting indefinitely.
318
- def release_marker
319
- signal_marker(marker)
317
+ # Wake every waiting flush. Important to perform during cleanup when
318
+ # shutting down, otherwise they could end up waiting indefinitely.
319
+ def release_flush_waiters
320
+ if mutex.owned?
321
+ flush_signal.broadcast
322
+ else
323
+ mutex.synchronize { flush_signal.broadcast }
324
+ end
320
325
  end
321
326
 
322
- def signal_marker(marker)
327
+ # Wakes the flush waiting on this point in the queue.
328
+ def signal_flush_done
323
329
  if mutex.owned?
324
- marker.signal
330
+ flush_signal.signal
325
331
  else
326
- mutex.synchronize { marker.signal }
332
+ mutex.synchronize { flush_signal.signal }
327
333
  end
328
334
  end
329
335
  end
@@ -18,7 +18,7 @@ module Honeybadger
18
18
  @config = config
19
19
  @interval_seconds = 1
20
20
  @mutex = Mutex.new
21
- @marker = ConditionVariable.new
21
+ @flush_signal = ConditionVariable.new
22
22
  @queue = Queue.new
23
23
  @shutdown = false
24
24
  @start_at = nil
@@ -60,8 +60,8 @@ module Honeybadger
60
60
  def flush
61
61
  mutex.synchronize do
62
62
  if thread&.alive?
63
- queue.push(marker)
64
- marker.wait(mutex)
63
+ queue.push(flush_signal)
64
+ flush_signal.wait(mutex)
65
65
  end
66
66
  end
67
67
  end
@@ -84,7 +84,7 @@ module Honeybadger
84
84
 
85
85
  private
86
86
 
87
- attr_reader :config, :queue, :pid, :mutex, :marker, :thread, :interval_seconds, :start_at
87
+ attr_reader :config, :queue, :pid, :mutex, :flush_signal, :thread, :interval_seconds, :start_at
88
88
 
89
89
  def shutdown?
90
90
  mutex.synchronize { @shutdown }
@@ -127,7 +127,7 @@ module Honeybadger
127
127
  loop do
128
128
  case msg = queue.pop
129
129
  when SHUTDOWN then break
130
- when ConditionVariable then signal_marker(msg)
130
+ when flush_signal then signal_flush_done
131
131
  else work(msg)
132
132
  end
133
133
  end
@@ -140,7 +140,7 @@ module Honeybadger
140
140
  sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
141
141
  }
142
142
  ensure
143
- release_marker
143
+ release_flush_waiters
144
144
  end
145
145
 
146
146
  def work(msg)
@@ -160,17 +160,22 @@ module Honeybadger
160
160
  sleep(interval_seconds)
161
161
  end
162
162
 
163
- # Release the marker. Important to perform during cleanup when shutting
164
- # down, otherwise it could end up waiting indefinitely.
165
- def release_marker
166
- signal_marker(marker)
163
+ # Wake every waiting flush. Important to perform during cleanup when
164
+ # shutting down, otherwise they could end up waiting indefinitely.
165
+ def release_flush_waiters
166
+ if mutex.owned?
167
+ flush_signal.broadcast
168
+ else
169
+ mutex.synchronize { flush_signal.broadcast }
170
+ end
167
171
  end
168
172
 
169
- def signal_marker(marker)
173
+ # Wakes the flush waiting on this point in the queue.
174
+ def signal_flush_done
170
175
  if mutex.owned?
171
- marker.signal
176
+ flush_signal.signal
172
177
  else
173
- mutex.synchronize { marker.signal }
178
+ mutex.synchronize { flush_signal.signal }
174
179
  end
175
180
  end
176
181
  end
@@ -1,4 +1,4 @@
1
1
  module Honeybadger
2
2
  # The current String Honeybadger version.
3
- VERSION = "6.9.3".freeze
3
+ VERSION = "6.10.1".freeze
4
4
  end