honeybadger 6.9.2 → 6.10.0

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: 3a87b6390eaeefa3027fa561efe5c289817d3bf7785d5e9f7ba0d56bf0937141
4
- data.tar.gz: a191e258298fbe566e6a7914fef8647f3b3dfb3c0ab83dfa0aaa1689ba4f30d7
3
+ metadata.gz: 0a8b90189275576b1cb4bca4f7ee77db0fe316415a03239cd61ce1585deddb42
4
+ data.tar.gz: b5c7cdbcfd8a655009872a9b3e02f7aec6917684172fc5b270c8cf559b46f52f
5
5
  SHA512:
6
- metadata.gz: dd5b1e3392c7d74312d66bf50a9a80ae7534526a717fbc68e851283bd0ff02f2f9d578bd6730b32364d4e9ca1dd1b3031e52191aa148c1da72634733c9a3a16b
7
- data.tar.gz: f3c484065df9a52ef468140d725681238cf8701a5bf819f80fd06e36b0f515eea94245d42ab9682c44801ede8036fe227b7b546deca7c90fb2fa7bd0cf28790e
6
+ metadata.gz: 666daade7949f2d486498eda33d6f74fb411a1745081e212aeb838b1cb9dd72ad9125d3631b6d0a6fe4e225ab251ead2a7d5c4e41fa00ba093435914f671861e
7
+ data.tar.gz: 7c24c28bf5184bb79d7c1e87ce7ba1f36ffff14863b8cc50582cf8b01cc5e07a220ae51f0f4d41df384ffc88dafcce797ee03a54eaf0df3563e6dab9396e0070
data/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Change Log
2
2
 
3
3
 
4
+ ## [6.10.0](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.3...v6.10.0) (2026-10-20)
5
+
6
+
7
+ ### Bug Fixes
8
+
9
+ * 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))
10
+
11
+ ### Changed
12
+
13
+ * 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))
14
+
15
+ ## [6.9.3](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.2...v6.9.3) (2026-09-29)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * propagate the Rails request ID to notices and Insights events ([#848](https://github.com/honeybadger-io/honeybadger-ruby/issues/848)) ([23edb08](https://github.com/honeybadger-io/honeybadger-ruby/commit/23edb08f4fda3b3f66fb697c5ec5502f39aeb127))
21
+
4
22
  ## [6.9.2](https://github.com/honeybadger-io/honeybadger-ruby/compare/v6.9.1...v6.9.2) (2026-09-15)
5
23
 
6
24
 
@@ -579,13 +579,18 @@ module Honeybadger
579
579
  # @api private
580
580
  def with_rack_env(rack_env, &block)
581
581
  context_manager.set_rack_env(rack_env)
582
- context_manager.set_request_id(rack_env["action_dispatch.request_id"] || SecureRandom.uuid)
582
+ context_manager.set_request_id(SecureRandom.uuid)
583
583
  yield
584
584
  ensure
585
585
  context_manager.set_rack_env(nil)
586
586
  context_manager.set_request_id(nil)
587
587
  end
588
588
 
589
+ # @api private
590
+ def set_request_id(request_id)
591
+ context_manager.set_request_id(request_id)
592
+ end
593
+
589
594
  # @api private
590
595
  attr_reader :worker, :events_worker, :metrics_worker
591
596
 
@@ -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
@@ -2,6 +2,7 @@ require "rails"
2
2
  require "yaml"
3
3
 
4
4
  require "honeybadger/ruby"
5
+ require "honeybadger/rack/request_id"
5
6
 
6
7
  module Honeybadger
7
8
  module Init
@@ -14,8 +15,14 @@ module Honeybadger
14
15
  initializer "honeybadger.install_middleware" do |app|
15
16
  honeybadger_config = Honeybadger::Agent.instance.config
16
17
 
17
- if honeybadger_config[:"exceptions.enabled"]
18
+ # ErrorNotifier also sets up the request context that Insights events
19
+ # read. Agent#notify does nothing when exceptions are disabled.
20
+ if honeybadger_config[:"exceptions.enabled"] || honeybadger_config[:"insights.enabled"]
18
21
  app.config.middleware.insert(0, Honeybadger::Rack::ErrorNotifier)
22
+ app.config.middleware.insert_after(::ActionDispatch::RequestId, Honeybadger::Rack::RequestId)
23
+ end
24
+
25
+ if honeybadger_config[:"exceptions.enabled"]
19
26
  app.config.middleware.insert_before(Honeybadger::Rack::ErrorNotifier, Honeybadger::Rack::UserInformer) if honeybadger_config[:"user_informer.enabled"]
20
27
  app.config.middleware.insert_before(Honeybadger::Rack::ErrorNotifier, Honeybadger::Rack::UserFeedback) if honeybadger_config[:"feedback.enabled"]
21
28
  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
@@ -0,0 +1,27 @@
1
+ require "honeybadger/ruby"
2
+
3
+ module Honeybadger
4
+ module Rack
5
+ # @api private
6
+ #
7
+ # Copies the request ID assigned by ActionDispatch::RequestId into the
8
+ # Honeybadger context. Must sit after ActionDispatch::RequestId in the
9
+ # middleware stack.
10
+ #
11
+ # No ensure/cleanup here: ErrorNotifier owns the request lifecycle and
12
+ # clears state after the stack unwinds. Clearing here would nil the ID
13
+ # before ErrorNotifier builds the notice on exception.
14
+ class RequestId
15
+ def initialize(app, agent = nil)
16
+ @app = app
17
+ @agent = agent if agent.is_a?(Agent)
18
+ end
19
+
20
+ def call(env)
21
+ request_id = env["action_dispatch.request_id"]
22
+ (@agent || Honeybadger::Agent.instance).set_request_id(request_id) if request_id
23
+ @app.call(env)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,4 +1,4 @@
1
1
  module Honeybadger
2
2
  # The current String Honeybadger version.
3
- VERSION = "6.9.2".freeze
3
+ VERSION = "6.10.0".freeze
4
4
  end
@@ -17,19 +17,17 @@ module Honeybadger
17
17
  # Used to signal the worker to shutdown.
18
18
  SHUTDOWN = :__hb_worker_shutdown!
19
19
 
20
- # The base number for the exponential backoff formula when calculating the
21
- # throttle interval. `1.05 ** throttle` will reach an interval of 2 minutes
22
- # after around 100 429 responses from the server.
23
- BASE_THROTTLE = 1.05
20
+ # Number of times a notice is resent while the server is unavailable
21
+ # (6 attempts in total). Each notice gets its own retries.
22
+ MAX_RETRIES = 5
24
23
 
25
24
  def initialize(config)
26
25
  @config = config
27
- @throttle = 0
28
- @throttle_interval = 0
29
26
  @mutex = Mutex.new
30
- @marker = ConditionVariable.new
27
+ @flush_signal = ConditionVariable.new
31
28
  @queue = Queue.new
32
29
  @shutdown = false
30
+ @flushes = 0
33
31
  @start_at = nil
34
32
  @pid = Process.pid
35
33
  end
@@ -45,6 +43,7 @@ module Honeybadger
45
43
  queue.push(msg)
46
44
  end
47
45
 
46
+ # Sends synchronously without retrying.
48
47
  def send_now(msg)
49
48
  response = notify_backend(msg)
50
49
  run_after_notify_hooks(msg, response)
@@ -61,11 +60,6 @@ module Honeybadger
61
60
  return true if force
62
61
  return true unless thread&.alive?
63
62
 
64
- if throttled?
65
- warn { sprintf("Unable to report %s error(s) to Honeybadger (currently throttled)", queue.size) } unless queue.empty?
66
- return true
67
- end
68
-
69
63
  info { sprintf("Waiting to report %s error(s) to Honeybadger", queue.size) } unless queue.empty?
70
64
 
71
65
  queue.push(SHUTDOWN)
@@ -75,12 +69,18 @@ module Honeybadger
75
69
  kill!
76
70
  end
77
71
 
78
- # Blocks until queue is processed up to this point in time.
72
+ # Blocks until queue is processed up to this point in time. Retries are
73
+ # skipped while a flush is pending, since the caller is waiting.
79
74
  def flush
80
75
  mutex.synchronize do
81
76
  if thread&.alive?
82
- queue.push(marker)
83
- marker.wait(mutex)
77
+ @flushes += 1
78
+ queue.push(flush_signal)
79
+ begin
80
+ flush_signal.wait(mutex)
81
+ ensure
82
+ @flushes -= 1
83
+ end
84
84
  end
85
85
  end
86
86
  end
@@ -103,8 +103,7 @@ module Honeybadger
103
103
 
104
104
  private
105
105
 
106
- attr_reader :config, :queue, :pid, :mutex, :marker, :thread, :throttle,
107
- :throttle_interval, :start_at
106
+ attr_reader :config, :queue, :pid, :mutex, :flush_signal, :thread, :start_at
108
107
 
109
108
  def_delegator :config, :backend
110
109
 
@@ -112,6 +111,10 @@ module Honeybadger
112
111
  mutex.synchronize { @shutdown }
113
112
  end
114
113
 
114
+ def flushing?
115
+ mutex.synchronize { @flushes > 0 }
116
+ end
117
+
115
118
  def suspended?
116
119
  mutex.synchronize { start_at && Time.now.to_i < start_at }
117
120
  end
@@ -122,10 +125,6 @@ module Honeybadger
122
125
  true
123
126
  end
124
127
 
125
- def throttled?
126
- mutex.synchronize { throttle > 0 }
127
- end
128
-
129
128
  def kill!
130
129
  d { "killing worker thread" }
131
130
 
@@ -137,7 +136,10 @@ module Honeybadger
137
136
  true
138
137
  end
139
138
 
139
+ # Drops queued notices and rejects new ones (push returns false) for
140
+ # +interval+ seconds. The thread restarts on the first push after that.
140
141
  def suspend(interval)
142
+ warn { sprintf("Dropping queued errors and rejecting new ones for %ss", interval) }
141
143
  mutex.synchronize do
142
144
  @start_at = Time.now.to_i + interval
143
145
  queue.clear
@@ -153,7 +155,7 @@ module Honeybadger
153
155
  loop do
154
156
  case msg = queue.pop
155
157
  when SHUTDOWN then break
156
- when ConditionVariable then signal_marker(msg)
158
+ when flush_signal then signal_flush_done
157
159
  else work(msg)
158
160
  end
159
161
  end
@@ -166,19 +168,28 @@ module Honeybadger
166
168
  sprintf(msg, e.class, e.message.dump, Array(e.backtrace).join("\n\t"))
167
169
  }
168
170
  ensure
169
- release_marker
171
+ release_flush_waiters
170
172
  end
171
173
 
172
174
  def work(msg)
173
- send_now(msg)
175
+ response = notify_backend(msg)
176
+ MAX_RETRIES.times do
177
+ break unless response.retryable?
178
+ break unless wait(response.retry_after || rand(5..15))
179
+ response = notify_backend(msg)
180
+ end
181
+ run_after_notify_hooks(msg, response)
182
+ handle_response(msg, response)
183
+ if (interval = response.reject_interval)
184
+ suspend(interval) # terminates this thread
185
+ end
174
186
 
175
- if shutdown? && throttled?
176
- warn { sprintf("Unable to report %s error(s) to Honeybadger (currently throttled)", queue.size) } if queue.size > 1
187
+ # Shutting down while the server is unavailable: stop without sending
188
+ # the rest of the queue.
189
+ if shutdown? && response.retryable?
190
+ warn { "Shutting down while the server is unavailable; dropping queued errors" }
177
191
  kill!
178
- return
179
192
  end
180
-
181
- sleep(throttle_interval)
182
193
  rescue => e
183
194
  error {
184
195
  msg = "Error in worker thread class=%s message=%s\n\t%s"
@@ -206,49 +217,34 @@ module Honeybadger
206
217
  }
207
218
  end
208
219
 
209
- def calc_throttle_interval
210
- ((BASE_THROTTLE**throttle) - 1).round(3)
211
- end
212
-
213
- def inc_throttle
214
- mutex.synchronize do
215
- @throttle += 1
216
- @throttle_interval = calc_throttle_interval
217
- throttle
218
- end
219
- end
220
-
221
- def dec_throttle
222
- mutex.synchronize do
223
- return nil if throttle == 0
224
- @throttle -= 1
225
- @throttle_interval = calc_throttle_interval
226
- throttle
220
+ # Sleeps in one-second steps so shutdown or a flush can interrupt.
221
+ # Returns false if one did, including during the final second; the
222
+ # payload in hand is then dropped.
223
+ def wait(seconds)
224
+ seconds.times do
225
+ return false if shutdown? || flushing?
226
+ sleep(1)
227
227
  end
228
+ !(shutdown? || flushing?)
228
229
  end
229
230
 
230
231
  def handle_response(msg, response)
231
232
  d { sprintf("worker response id=%s code=%s message=%s", msg.id, response.code, response.message.to_s.dump) }
232
233
 
233
234
  case response.code
234
- when 429, 503
235
- throttle = inc_throttle
236
- warn { sprintf("Error report failed: project is sending too many errors. id=%s code=%s throttle=%s interval=%s", msg.id, response.code, throttle, throttle_interval) }
237
- when 402
238
- warn { sprintf("Error report failed: payment is required. id=%s code=%s", msg.id, response.code) }
239
- suspend(3600)
240
- when 403
235
+ when 429, 502, 503, 504
236
+ if response.retryable?
237
+ warn { sprintf("Error report failed: server unavailable, giving up on this error. id=%s code=%s", msg.id, response.code) }
238
+ else
239
+ warn { sprintf("Error report failed: server is rejecting data. id=%s code=%s reason=%s", msg.id, response.code, response.limit_reason) }
240
+ end
241
+ when 401, 403
241
242
  warn { sprintf("Error report failed: %s id=%s code=%s", response.error_message, msg.id, response.code) }
242
- suspend(3600)
243
243
  when 413
244
244
  warn { sprintf("Error report failed: Payload is too large. id=%s code=%s", msg.id, response.code) }
245
245
  when 201
246
246
  host = config.get(:"connection.ui_host")
247
- if (throttle = dec_throttle)
248
- info { sprintf("Success ⚡ https://%s/notice/%s id=%s code=%s throttle=%s interval=%s", host, msg.id, msg.id, response.code, throttle, throttle_interval) }
249
- else
250
- info { sprintf("Success ⚡ https://%s/notice/%s id=%s code=%s", host, msg.id, msg.id, response.code) }
251
- end
247
+ info { sprintf("Success ⚡ https://%s/notice/%s id=%s code=%s", host, msg.id, msg.id, response.code) }
252
248
  when :stubbed
253
249
  debug { sprintf("Success ⚡ Development mode is enabled; this error will be reported if it occurs after you deploy your app. id=%s", msg.id) }
254
250
  when :error
@@ -258,17 +254,22 @@ module Honeybadger
258
254
  end
259
255
  end
260
256
 
261
- # Release the marker. Important to perform during cleanup when shutting
262
- # down, otherwise it could end up waiting indefinitely.
263
- def release_marker
264
- signal_marker(marker)
257
+ # Wake every waiting flush. Important to perform during cleanup when
258
+ # shutting down, otherwise they could end up waiting indefinitely.
259
+ def release_flush_waiters
260
+ if mutex.owned?
261
+ flush_signal.broadcast
262
+ else
263
+ mutex.synchronize { flush_signal.broadcast }
264
+ end
265
265
  end
266
266
 
267
- def signal_marker(marker)
267
+ # Wakes the flush waiting on this point in the queue.
268
+ def signal_flush_done
268
269
  if mutex.owned?
269
- marker.signal
270
+ flush_signal.signal
270
271
  else
271
- mutex.synchronize { marker.signal }
272
+ mutex.synchronize { flush_signal.signal }
272
273
  end
273
274
  end
274
275
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeybadger
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.9.2
4
+ version: 6.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Honeybadger Industries LLC
@@ -131,6 +131,7 @@ files:
131
131
  - lib/honeybadger/plugins/thor.rb
132
132
  - lib/honeybadger/plugins/warden.rb
133
133
  - lib/honeybadger/rack/error_notifier.rb
134
+ - lib/honeybadger/rack/request_id.rb
134
135
  - lib/honeybadger/rack/user_feedback.rb
135
136
  - lib/honeybadger/rack/user_informer.rb
136
137
  - lib/honeybadger/registry.rb
@@ -217,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
218
  - !ruby/object:Gem::Version
218
219
  version: "0"
219
220
  requirements: []
220
- rubygems_version: 4.1.0.beta1
221
+ rubygems_version: 4.1.0.beta2
221
222
  specification_version: 4
222
223
  summary: "Full-stack error tracking, performance monitoring, logging, and more."
223
224
  test_files: []