postburner 1.0.0.pre.16 → 1.0.0.pre.17

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: 8e032f7d37f1680fe34a1402f8eb9f0b3ca61aa9231f6d540c35c3a919e3f58d
4
- data.tar.gz: bd35338dd14bc499c4c7d9f57d30eff3902615cd72876f766f1e379f12baba77
3
+ metadata.gz: 05d87a7d1949eb64ef1918b4bb0f0cdeb24796097c370f90576b3a158d0be5a7
4
+ data.tar.gz: 857304a0b925574ab66d6ad215e6cbe3bdba8c109ecb9a34e96e8ab0ca40aac3
5
5
  SHA512:
6
- metadata.gz: f207c1d4dd840bee6b00ad71de9cc1b774d9b2376f7f34125fcc3a164220a340badc6b1ef17931d3dc09a0bb78a09c66b1f1f9ffdf1ead3e9737c44a39fe9a29
7
- data.tar.gz: e0212499c62223a87720462226660045585ac4bde21258eb54aeed9d85683c0c9652477a6cf5824bdffa7e30314efb1e1cbb9d146eae8ef3ade2202af73f2abe
6
+ metadata.gz: af92d06c7d796c0db2bb766986bc9b27ad46e9958103e11871538e3224b43a054d80fb50b7bb0b5f8d93de1a11331e10b10d26058c219047e15530f6a36a1b9e
7
+ data.tar.gz: 351f86f49998e5350b7b705a4f77f68a91c29340995dc5531b964bbdb40b6bb55f3c4202459cdc7453429c7e3b7ceaa4ca934628fc8503305f9630eb9c287142
data/README.md CHANGED
@@ -1,12 +1,8 @@
1
1
  # Postburner
2
2
 
3
- Fast Beanstalkd-backed job queue with **optional PostgreSQL records**.
3
+ Fast Beanstalkd-backed job queue with **optional PostgreSQL records via ActiveRecord**.
4
4
 
5
- Postburner provides dual-mode job execution:
6
- - **Fast jobs**: Fast execution via Beanstalkd
7
- - **Tracked jobs**: Audited jobs logs, timing, errors, and statistics
8
-
9
- Built for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
5
+ Built for the real world where you may want fast background processing for most jobs, but comprehensive auditing for critical operations.
10
6
 
11
7
  - **ActiveJob Adapter** - To use with Rails, ActionMailer, ActiveStorage
12
8
  - **Dual-mode execution** - Beanstalkd only or tracked (database backed)
@@ -104,6 +100,7 @@ bundle exec rake postburner:work WORKER=default
104
100
  - [Configuration](#configuration)
105
101
  - [Callbacks](#callbacks)
106
102
  - [Instrumentation](#instrumentation)
103
+ - [Logging](#logging)
107
104
  - [Why Beanstalkd?](#why-beanstalkd)
108
105
  - [Beanstalkd Integration](#beanstalkd-integration)
109
106
  - [Installation](#installation)
@@ -216,6 +213,38 @@ then queue it!
216
213
  | **Set priority** | `.set(priority: 0)` | `job.queue!(priority: 0)` |
217
214
  | **Set TTR** | `.set(ttr: 300)` | `job.queue!(ttr: 300)` |
218
215
 
216
+
217
+ | Retries | ActiveJob | Postburner::Job |
218
+ |---------|-----------|-----------------|
219
+ | **Default** | No retries (discarded) | No retries (buried) |
220
+ | **Disable retries** | `discard_on StandardError` | *(default behavior)* |
221
+ | **Enable retries** | `retry_on StandardError` | `max_retries 5` |
222
+
223
+ - **ActiveJob**: No automatic worker-level retries. Use ActiveJob's `retry_on`/`discard_on` for retry behavior. Failed jobs without retry configuration are discarded.
224
+ - **Postburner::Job**: No automatic retries by default. On failure, the job is buried in Beanstalkd for inspection. Use `max_retries` (1-32) to enable retries with exponential backoff (2^n seconds).
225
+
226
+ ```ruby
227
+ # ActiveJob: Use retry_on for retries
228
+ class MyActiveJob < ApplicationJob
229
+ retry_on StandardError, wait: :polynomially_longer, attempts: 5
230
+
231
+ def perform(args)
232
+ # ...
233
+ end
234
+ end
235
+
236
+ # Postburner::Job: Use max_retries for retries
237
+ class MyJob < Postburner::Job
238
+ max_retries 5 # Exponential backoff: 1s, 2s, 4s, 8s, 16s
239
+ end
240
+
241
+ # Or use a fixed delay
242
+ class MyJob < Postburner::Job
243
+ max_retries 5
244
+ retry_delay 10 # 10 seconds between retries
245
+ end
246
+ ```
247
+
219
248
  ## Usage
220
249
 
221
250
  ### Default Jobs
@@ -1459,9 +1488,9 @@ Postburner emits ActiveSupport::Notifications events following Rails conventions
1459
1488
  |-------|------|--------------|
1460
1489
  | `perform_start.job.postburner` | Before job execution begins | `:job`, `:beanstalk_job_id` |
1461
1490
  | `perform.job.postburner` | Around job execution (includes duration) | `:job`, `:beanstalk_job_id` |
1462
- | `retry.job.postburner` | When job is retried after error | `:job`, `:beanstalk_job_id`, `:error`, `:wait`, `:attempt` |
1463
- | `retry_stopped.job.postburner` | When tracked job exhausts retries | `:job`, `:beanstalk_job_id`, `:error` |
1464
- | `discard.job.postburner` | When default job exhausts retries | `:job`, `:beanstalk_job_id`, `:error` |
1491
+ | `retry.job.postburner` | When Postburner::Job is retried | `:job`, `:beanstalk_job_id`, `:error`, `:wait`, `:attempt` |
1492
+ | `retry_stopped.job.postburner` | When Postburner::Job exhausts retries (buried) | `:job`, `:beanstalk_job_id`, `:error` |
1493
+ | `discard.job.postburner` | When default ActiveJob fails (discarded) | `:job`, `:beanstalk_job_id`, `:error` |
1465
1494
  | `enqueue.job.postburner` | When job is enqueued for immediate execution | `:job` |
1466
1495
  | `enqueue_at.job.postburner` | When job is enqueued with delay | `:job`, `:scheduled_at` |
1467
1496
 
@@ -1572,6 +1601,46 @@ end
1572
1601
 
1573
1602
  **Note:** These events complement (don't replace) ActiveJob's built-in instrumentation events like `enqueue.active_job` and `perform.active_job`.
1574
1603
 
1604
+ ## Logging
1605
+
1606
+ ### Configuration
1607
+
1608
+ Set the log level in your Rails environment configuration:
1609
+
1610
+ ```ruby
1611
+ # config/environments/production.rb
1612
+ config.log_level = :info # Default, recommended for production
1613
+
1614
+ # config/environments/development.rb
1615
+ config.log_level = :debug # Verbose logging for development
1616
+ ```
1617
+
1618
+ Postburner uses `Rails.logger`, so standard Rails log level configuration applies.
1619
+
1620
+ ### Debugging
1621
+
1622
+ Set the log level to `debug` for verbose logging:
1623
+
1624
+ ```ruby
1625
+ config.log_level = :debug
1626
+ ```
1627
+
1628
+ ### Custom Job Logging
1629
+
1630
+ ```ruby
1631
+ class ProcessPayment < ApplicationJob
1632
+ include Postburner::Tracked
1633
+
1634
+ def perform(payment_id)
1635
+ log "Starting payment processing for $#{payment.amount}" # Stored in database
1636
+ payment.charge!
1637
+ log! "Payment charged successfully" # Saved immediately
1638
+ end
1639
+ end
1640
+ ```
1641
+
1642
+ These job-specific logs are stored in the database (for tracked jobs only) and are separate from Rails application logs, providing a complete audit trail for critical operations.
1643
+
1575
1644
  ## Why Beanstalkd?
1576
1645
 
1577
1646
  Beanstalkd is a simple, fast, and reliable queue system. It is a good choice for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
@@ -1696,7 +1765,7 @@ class BackgroundTask < Postburner::Job
1696
1765
  end
1697
1766
  ```
1698
1767
 
1699
- **Recommended Priority Ranges:**
1768
+ **Example Priority Ranges:**
1700
1769
 
1701
1770
  | Priority Range | Use Case | Examples |
1702
1771
  |---------------|----------|----------|
@@ -1712,7 +1781,7 @@ Set default priority in `config/postburner.yml`:
1712
1781
 
1713
1782
  ```yaml
1714
1783
  production:
1715
- default_priority: 65536 # Default for jobs without explicit priority
1784
+ default_priority: 65536 # Default without explicit priority set
1716
1785
  default_ttr: 300
1717
1786
  ```
1718
1787
 
@@ -162,7 +162,7 @@ module Postburner
162
162
 
163
163
  rescue Exception => exception
164
164
  self.log_exception!(exception)
165
- raise exception
165
+ handle_retry_or_raise(exception)
166
166
  end
167
167
  end # run_callbacks :attempt
168
168
 
@@ -170,6 +170,43 @@ module Postburner
170
170
 
171
171
  private
172
172
 
173
+ # Handles retry logic or re-raises exception.
174
+ #
175
+ # If job has retries configured and hasn't exceeded max_retries, requeues
176
+ # the job with the configured delay. Otherwise, re-raises the exception
177
+ # for the worker to handle (typically by burying).
178
+ #
179
+ # Instruments with ActiveSupport::Notifications:
180
+ # - retry.job.postburner: When job is requeued for retry
181
+ #
182
+ # @param exception [Exception] The exception that caused the failure
183
+ #
184
+ # @raise [Exception] Re-raises if no retries configured or max exceeded
185
+ #
186
+ # @api private
187
+ #
188
+ def handle_retry_or_raise(exception)
189
+ if should_retry?
190
+ delay = retry_delay_for_attempt(attempt_count - 1)
191
+
192
+ self.log!("RETRY: attempt #{attempt_count}/#{self.class.max_retries}, delay #{delay}s")
193
+
194
+ # Instrument retry event
195
+ job_payload = Postburner::Instrumentation.job_payload_from_model(self, beanstalk_job_id: self.bkid)
196
+ ActiveSupport::Notifications.instrument('retry.job.postburner', {
197
+ job: job_payload,
198
+ beanstalk_job_id: self.bkid,
199
+ error: exception,
200
+ wait: delay,
201
+ attempt: attempt_count
202
+ })
203
+
204
+ requeue!(delay: delay)
205
+ else
206
+ raise exception
207
+ end
208
+ end
209
+
173
210
  # Records an attempt and calculates execution lag.
174
211
  #
175
212
  # Appends current time to attempts array, sets attempting_at on first attempt,
@@ -6,6 +6,26 @@ module Postburner
6
6
  # Provides DSL methods for configuring queue behavior (name, priority, TTR, retries).
7
7
  # Defines configurable properties for job queue management.
8
8
  #
9
+ # == Retry Behavior
10
+ #
11
+ # By default, Postburner::Job does NOT retry failed jobs. When a job raises an
12
+ # exception, it is buried in Beanstalkd for inspection. This differs from default
13
+ # ActiveJob behavior (5 retries with 2^n second backoff, ~31s total).
14
+ #
15
+ # Use +max_retries+ to enable automatic retries:
16
+ #
17
+ # class MyJob < Postburner::Job
18
+ # max_retries 5 # Retry up to 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s)
19
+ # end
20
+ #
21
+ # The default +retry_delay+ is exponential backoff (2^n seconds), matching
22
+ # Postburner's default ActiveJob behavior. Override with a fixed delay or custom proc:
23
+ #
24
+ # retry_delay 10 # Fixed 10 second delay between retries
25
+ #
26
+ # # Match ActiveJob's :exponentially_longer (polynomial: (n+1)^4 + 2)
27
+ # retry_delay ->(n) { ((n + 1) ** 4) + 2 } # 3s, 18s, 83s, 258s, 627s
28
+ #
9
29
  # @example Basic usage
10
30
  # class ProcessPayment < Postburner::Job
11
31
  # queue 'critical'
@@ -25,7 +45,9 @@ module Postburner
25
45
  # Instance-level queue configuration (overrides class-level defaults)
26
46
  attr_writer :priority, :ttr, :queue_name
27
47
 
28
- class_attribute :postburner_queue_name, default: 'default'
48
+ # Class-level overrides (all default to nil).
49
+ # Global defaults are defined in Postburner::Configuration (lib/postburner/configuration.rb).
50
+ class_attribute :postburner_queue_name, default: nil
29
51
  class_attribute :postburner_priority, default: nil
30
52
  class_attribute :postburner_ttr, default: nil
31
53
  class_attribute :postburner_max_retries, default: nil
@@ -106,7 +128,13 @@ module Postburner
106
128
 
107
129
  # Sets or returns maximum number of job retries.
108
130
  #
109
- # @param retries [Integer, nil] Max retries, or nil to get current value
131
+ # By default, Postburner::Job does NOT retry (max_retries is nil).
132
+ # Failed jobs are buried in Beanstalkd for inspection.
133
+ # Set max_retries to enable automatic retries on failure.
134
+ #
135
+ # Values of nil or 0 disable retries. Maximum allowed value is 32.
136
+ #
137
+ # @param retries [Integer, nil] Max retries (0-32), or nil to get current value
110
138
  #
111
139
  # @return [Integer, nil] Current max retries when getting, nil when setting
112
140
  #
@@ -116,9 +144,11 @@ module Postburner
116
144
  # @example Get max retries
117
145
  # ProcessPayment.max_retries # => 3
118
146
  #
147
+ # @see retry_delay
148
+ #
119
149
  def max_retries(retries = nil)
120
150
  if retries
121
- self.postburner_max_retries = retries
151
+ self.postburner_max_retries = [[retries.to_i, 0].max, 32].min
122
152
  nil
123
153
  else
124
154
  postburner_max_retries
@@ -172,7 +202,7 @@ module Postburner
172
202
  # job.queue_name # => 'urgent'
173
203
  #
174
204
  def queue_name
175
- @queue_name || self.class.queue
205
+ @queue_name || self.class.queue || Postburner.configuration.default_queue_name
176
206
  end
177
207
 
178
208
  # Returns the full tube name with environment prefix.
@@ -222,5 +252,67 @@ module Postburner
222
252
  @ttr || self.class.ttr
223
253
  end
224
254
 
255
+ # Checks if this job should retry after a failure.
256
+ #
257
+ # Returns true if max_retries is configured and the current attempt count
258
+ # is less than max_retries.
259
+ #
260
+ # @return [Boolean] true if job should retry, false otherwise
261
+ #
262
+ # @example
263
+ # class MyJob < Postburner::Job
264
+ # max_retries 3
265
+ # end
266
+ #
267
+ # job = MyJob.create!(args: {})
268
+ # job.should_retry? # => true (attempt_count is 0)
269
+ # # After 3 failed attempts...
270
+ # job.should_retry? # => false
271
+ #
272
+ def should_retry?
273
+ max = self.class.max_retries || Postburner.configuration.default_max_retries
274
+ attempt_count.to_i < max.to_i
275
+ end
276
+
277
+ # Calculates the retry delay for the given attempt.
278
+ #
279
+ # Uses the class-level retry_delay configuration, which can be:
280
+ # - Integer: Fixed delay in seconds
281
+ # - Proc: Called with attempt number (0-based), returns delay in seconds
282
+ # - nil: Defaults to 5 seconds
283
+ #
284
+ # @param attempt [Integer] The attempt number (0-based: 0 for first retry)
285
+ #
286
+ # @return [Integer] Delay in seconds before next retry
287
+ #
288
+ # @example Fixed delay
289
+ # class MyJob < Postburner::Job
290
+ # max_retries 3
291
+ # retry_delay 10
292
+ # end
293
+ # job.retry_delay_for_attempt(0) # => 10
294
+ #
295
+ # @example Exponential backoff (2s, 4s, 8s...)
296
+ # class MyJob < Postburner::Job
297
+ # max_retries 5
298
+ # retry_delay ->(n) { 2 ** (n + 1) }
299
+ # end
300
+ # job.retry_delay_for_attempt(0) # => 2
301
+ # job.retry_delay_for_attempt(1) # => 4
302
+ # job.retry_delay_for_attempt(2) # => 8
303
+ #
304
+ def retry_delay_for_attempt(attempt)
305
+ delay_config = self.class.retry_delay || Postburner.configuration.default_retry_delay
306
+
307
+ case delay_config
308
+ when Proc
309
+ delay_config.call(attempt).to_i
310
+ when Integer
311
+ delay_config
312
+ else
313
+ 2 ** attempt # Fallback to default exponential backoff
314
+ end
315
+ end
316
+
225
317
  end
226
318
  end
@@ -22,6 +22,7 @@ module Postburner
22
22
  class Configuration
23
23
  # Global settings
24
24
  attr_accessor :beanstalk_url, :logger, :default_priority, :default_ttr
25
+ attr_accessor :default_queue_name, :default_max_retries, :default_retry_delay
25
26
  attr_accessor :default_scheduler_interval, :default_scheduler_priority
26
27
  attr_accessor :enqueue_options
27
28
 
@@ -52,6 +53,9 @@ module Postburner
52
53
  @logger = options[:logger] || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT))
53
54
  @default_priority = options[:default_priority] || 65536
54
55
  @default_ttr = options[:default_ttr] || 300
56
+ @default_queue_name = options[:default_queue_name] || 'default'
57
+ @default_max_retries = options[:default_max_retries] || 0
58
+ @default_retry_delay = options[:default_retry_delay] || ->(n) { 2 ** n }
55
59
  @default_scheduler_interval = options[:default_scheduler_interval] || 300
56
60
  @default_scheduler_priority = options[:default_scheduler_priority] || 100
57
61
  @enqueue_options = options[:enqueue_options]
@@ -1,3 +1,3 @@
1
1
  module Postburner
2
- VERSION = '1.0.0.pre.16'
2
+ VERSION = '1.0.0.pre.17'
3
3
  end
@@ -136,6 +136,28 @@ module Postburner
136
136
  @shutdown
137
137
  end
138
138
 
139
+ # Checks if this process has been orphaned (parent died).
140
+ #
141
+ # When the parent process dies, the kernel re-parents children to init (PID 1).
142
+ # Detecting this allows forked children to exit gracefully instead of running
143
+ # indefinitely as orphans.
144
+ #
145
+ # @return [Boolean] true if parent PID is 1, false otherwise
146
+ def orphaned?
147
+ Process.ppid == 1
148
+ end
149
+
150
+ # Calculates exponential backoff sleep duration for reconnection attempts.
151
+ #
152
+ # Uses exponential backoff starting at 1 second and doubling each attempt,
153
+ # capped at 32 seconds to prevent excessively long waits.
154
+ #
155
+ # @param attempts [Integer] Number of consecutive failed attempts (0-based)
156
+ # @return [Integer] Sleep duration in seconds (1, 2, 4, 8, 16, or 32)
157
+ def reconnect_backoff(attempts)
158
+ [2**attempts, 32].min
159
+ end
160
+
139
161
  private
140
162
 
141
163
  # Returns the worker configuration hash.
@@ -148,13 +170,17 @@ module Postburner
148
170
 
149
171
  # Sets up signal handlers for graceful shutdown.
150
172
  #
151
- # Traps TERM and INT signals to initiate graceful shutdown.
173
+ # Trapped signals:
174
+ # - TERM: Graceful termination request (systemd, kill, process managers)
175
+ # - INT: Interrupt from keyboard (Ctrl+C)
176
+ # - HUP: Hangup signal when controlling terminal dies (prevents orphaned children)
152
177
  #
153
178
  # @return [void]
154
179
  # @api private
155
180
  def setup_signal_handlers
156
181
  Signal.trap('TERM') { shutdown }
157
182
  Signal.trap('INT') { shutdown }
183
+ Signal.trap('HUP') { shutdown }
158
184
  end
159
185
 
160
186
  # Expands queue name to full tube name with environment prefix.
@@ -245,17 +271,19 @@ module Postburner
245
271
  def process_jobs
246
272
  connection = Postburner::Connection.new
247
273
  timeout = worker_config[:timeout]
274
+ reconnect_attempts = 0
248
275
 
249
276
  watch_queues(connection, config.queue_names)
250
277
 
251
278
  until shutdown? || (@gc_limit && @jobs_processed.value >= @gc_limit)
252
279
  begin
253
- job = connection.beanstalk.tubes.reserve(timeout: timeout)
280
+ job = connection.beanstalk.tubes.reserve(timeout)
254
281
 
255
282
  if job
256
283
  logger.debug "[Postburner::Worker] Thread #{Thread.current.object_id} reserved job #{job.id}"
257
284
  execute_job(job)
258
285
  @jobs_processed.increment
286
+ reconnect_attempts = 0 # Reset backoff on successful job execution
259
287
  else
260
288
  ensure_scheduler_watchdog!(connection)
261
289
  end
@@ -263,8 +291,10 @@ module Postburner
263
291
  ensure_scheduler_watchdog!(connection)
264
292
  next
265
293
  rescue Beaneater::NotConnected => e
266
- logger.error "[Postburner::Worker] Thread disconnected: #{e.message}"
267
- sleep 1
294
+ backoff = reconnect_backoff(reconnect_attempts)
295
+ logger.error "[Postburner::Worker] Thread disconnected: #{e.message}, reconnecting in #{backoff}s (attempt #{reconnect_attempts + 1})"
296
+ sleep backoff
297
+ reconnect_attempts += 1
268
298
  connection.reconnect!
269
299
  watch_queues(connection, config.queue_names)
270
300
  rescue => e
@@ -359,6 +389,11 @@ module Postburner
359
389
  end
360
390
 
361
391
  until shutdown? || (gc_limit && jobs_processed.value >= gc_limit)
392
+ if orphaned?
393
+ logger.error "[Postburner::Worker] Fork #{fork_num} detected parent died (orphaned), initiating shutdown"
394
+ shutdown
395
+ break
396
+ end
362
397
  sleep 0.5
363
398
  end
364
399
 
@@ -391,17 +426,19 @@ module Postburner
391
426
  def process_jobs_in_fork(fork_num, jobs_processed, gc_limit)
392
427
  connection = Postburner::Connection.new
393
428
  timeout = worker_config[:timeout]
429
+ reconnect_attempts = 0
394
430
 
395
431
  watch_queues(connection, config.queue_names)
396
432
 
397
433
  until shutdown? || (gc_limit && jobs_processed.value >= gc_limit)
398
434
  begin
399
- job = connection.beanstalk.tubes.reserve(timeout: timeout)
435
+ job = connection.beanstalk.tubes.reserve(timeout)
400
436
 
401
437
  if job
402
438
  logger.debug "[Postburner::Worker] Fork #{fork_num} thread #{Thread.current.object_id} reserved job #{job.id}"
403
439
  execute_job(job)
404
440
  jobs_processed.increment
441
+ reconnect_attempts = 0 # Reset backoff on successful job execution
405
442
  else
406
443
  ensure_scheduler_watchdog!(connection)
407
444
  end
@@ -409,8 +446,10 @@ module Postburner
409
446
  ensure_scheduler_watchdog!(connection)
410
447
  next
411
448
  rescue Beaneater::NotConnected => e
412
- logger.error "[Postburner::Worker] Thread disconnected: #{e.message}"
413
- sleep 1
449
+ backoff = reconnect_backoff(reconnect_attempts)
450
+ logger.error "[Postburner::Worker] Thread disconnected: #{e.message}, reconnecting in #{backoff}s (attempt #{reconnect_attempts + 1})"
451
+ sleep backoff
452
+ reconnect_attempts += 1
414
453
  connection.reconnect!
415
454
  watch_queues(connection, config.queue_names)
416
455
  rescue => e
@@ -549,13 +588,16 @@ module Postburner
549
588
 
550
589
  # Handles job execution errors with retry logic.
551
590
  #
552
- # For tracked jobs and legacy Postburner::Job: Buries the job for inspection,
553
- # reports to Rails.error, and emits retry_stopped.job.postburner event.
591
+ # For Postburner::Job: The job handles its own retries in perform!. If an
592
+ # exception bubbles up here, it means no retries configured or max exceeded.
593
+ # Buries the job for inspection, reports to Rails.error, and emits
594
+ # retry_stopped.job.postburner event.
595
+ #
554
596
  # For default ActiveJob: Applies exponential backoff retry with max 5 attempts,
555
597
  # reporting to Rails.error only on final discard.
556
598
  #
557
599
  # Instruments with ActiveSupport::Notifications:
558
- # - retry_stopped.job.postburner: When tracked job is buried after failure
600
+ # - retry_stopped.job.postburner: When Postburner::Job is buried after failure
559
601
  #
560
602
  # @param beanstalk_job [Beaneater::Job] Failed job
561
603
  # @param error [Exception] The error that caused the failure
@@ -563,39 +605,14 @@ module Postburner
563
605
  # @api private
564
606
  def handle_error(beanstalk_job, error)
565
607
  logger.error "[Postburner] Job failed: #{error.class} - #{error.message}"
566
- logger.error error.backtrace.join("\n")
567
608
 
568
609
  begin
569
610
  payload = JSON.parse(beanstalk_job.body)
570
611
 
571
612
  if payload['tracked'] || Postburner::ActiveJob::Payload.legacy_format?(payload)
572
- # Extract job ID from payload
573
- job_id = if Postburner::ActiveJob::Payload.legacy_format?(payload)
574
- payload['args']&.first
575
- else
576
- payload['postburner_job_id']
577
- end
578
-
579
- logger.info "[Postburner] Burying tracked/legacy job for inspection (bkid: #{beanstalk_job.id}, job_id: #{job_id})"
580
-
581
- # Report to Rails error reporter for integration with error tracking services
582
- Rails.error.report(error, handled: false, context: {
583
- job_class: payload['job_class'] || payload['class'],
584
- job_id: job_id,
585
- beanstalk_job_id: beanstalk_job.id,
586
- queue_name: payload['queue_name']
587
- })
588
-
589
- job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
590
- ActiveSupport::Notifications.instrument('retry_stopped.job.postburner', {
591
- job: job_payload,
592
- beanstalk_job_id: beanstalk_job.id,
593
- error: error
594
- })
595
-
596
- beanstalk_job.bury
613
+ handle_postburner_job_error(beanstalk_job, payload, error)
597
614
  else
598
- handle_default_retry(beanstalk_job, payload, error)
615
+ handle_default_job_error(beanstalk_job, payload, error)
599
616
  end
600
617
  rescue => retry_error
601
618
  logger.error "[Postburner] Error handling failure: #{retry_error.message}"
@@ -603,72 +620,85 @@ module Postburner
603
620
  end
604
621
  end
605
622
 
606
- # Handles retry logic for default jobs.
623
+ # Handles errors for Postburner::Job (including tracked ActiveJob).
607
624
  #
608
- # Applies exponential backoff (2^retry_count seconds, max 1 hour).
609
- # After 5 failed attempts, discards the job permanently and reports
610
- # to Rails.error for integration with error tracking services.
625
+ # The job handles its own retries in perform!. If the exception bubbles up
626
+ # here, it means either no retries are configured or max_retries was exceeded.
627
+ # Buries the job for inspection.
611
628
  #
612
629
  # Instruments with ActiveSupport::Notifications:
613
- # - retry.job.postburner: When job is retried
614
- # - discard.job.postburner: When job is discarded after max retries
630
+ # - retry_stopped.job.postburner: When job is buried
615
631
  #
616
- # @param beanstalk_job [Beaneater::Job] Failed job to retry
617
- # @param payload [Hash] Parsed job body (modified with retry_count)
632
+ # @param beanstalk_job [Beaneater::Job] Failed job
633
+ # @param payload [Hash] Parsed job body
618
634
  # @param error [Exception] The error that caused the failure
619
635
  # @return [void]
620
636
  # @api private
621
- def handle_default_retry(beanstalk_job, payload, error)
622
- retry_count = payload['retry_count'] || 0
623
- max_retries = 5
624
- job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
637
+ def handle_postburner_job_error(beanstalk_job, payload, error)
638
+ job_id = if Postburner::ActiveJob::Payload.legacy_format?(payload)
639
+ payload['args']&.first
640
+ else
641
+ payload['postburner_job_id']
642
+ end
643
+
644
+ job_class_name = payload['job_class'] || payload['class']
625
645
 
626
- if retry_count < max_retries
627
- payload['retry_count'] = retry_count + 1
628
- payload['executions'] = (payload['executions'] || 0) + 1
646
+ # Log the error with backtrace (Postburner::Job doesn't use ActiveJob's logging)
647
+ logger.error "[Postburner] #{job_class_name}##{job_id} failed: #{error.class} - #{error.message}"
648
+ logger.error error.backtrace.join("\n")
629
649
 
630
- delay = [2 ** retry_count, 3600].min
650
+ logger.info "[Postburner] Burying #{job_class_name}##{job_id} for inspection (bkid: #{beanstalk_job.id})"
631
651
 
632
- beanstalk_job.delete
652
+ # Report to Rails error reporter for integration with error tracking services
653
+ Rails.error.report(error, handled: false, context: {
654
+ job_class: job_class_name,
655
+ job_id: job_id,
656
+ beanstalk_job_id: beanstalk_job.id,
657
+ queue_name: payload['queue_name']
658
+ })
633
659
 
634
- Postburner.connected do |conn|
635
- tube_name = expand_tube_name(payload['queue_name'])
636
- conn.tubes[tube_name].put(
637
- JSON.generate(payload),
638
- pri: payload['priority'] || config.default_priority,
639
- delay: delay,
640
- ttr: payload['ttr'] || config.default_ttr
641
- )
642
- end
660
+ job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
661
+ ActiveSupport::Notifications.instrument('retry_stopped.job.postburner', {
662
+ job: job_payload,
663
+ beanstalk_job_id: beanstalk_job.id,
664
+ error: error
665
+ })
643
666
 
644
- ActiveSupport::Notifications.instrument('retry.job.postburner', {
645
- job: job_payload,
646
- beanstalk_job_id: beanstalk_job.id,
647
- error: error,
648
- wait: delay,
649
- attempt: retry_count + 1
650
- })
667
+ beanstalk_job.bury
668
+ end
651
669
 
652
- logger.info "[Postburner] Retrying default job #{payload['job_id']}, attempt #{retry_count + 1} in #{delay}s"
653
- else
654
- # Report to Rails error reporter for integration with error tracking services
655
- Rails.error.report(error, handled: false, context: {
656
- job_class: payload['job_class'],
657
- job_id: payload['job_id'],
658
- beanstalk_job_id: beanstalk_job.id,
659
- queue_name: payload['queue_name'],
660
- retry_count: retry_count
661
- })
662
-
663
- ActiveSupport::Notifications.instrument('discard.job.postburner', {
664
- job: job_payload,
665
- beanstalk_job_id: beanstalk_job.id,
666
- error: error
667
- })
668
-
669
- logger.error "[Postburner] Discarding default job #{payload['job_id']} after #{retry_count} retries"
670
- beanstalk_job.delete
671
- end
670
+ # Handles errors for default ActiveJob jobs.
671
+ #
672
+ # Discards the job and reports to Rails.error. No automatic retries -
673
+ # use ActiveJob's retry_on/discard_on for retry behavior.
674
+ #
675
+ # Instruments with ActiveSupport::Notifications:
676
+ # - discard.job.postburner: When job is discarded
677
+ #
678
+ # @param beanstalk_job [Beaneater::Job] Failed job
679
+ # @param payload [Hash] Parsed job body
680
+ # @param error [Exception] The error that caused the failure
681
+ # @return [void]
682
+ # @api private
683
+ def handle_default_job_error(beanstalk_job, payload, error)
684
+ job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
685
+
686
+ # Report to Rails error reporter for integration with error tracking services
687
+ Rails.error.report(error, handled: false, context: {
688
+ job_class: payload['job_class'],
689
+ job_id: payload['job_id'],
690
+ beanstalk_job_id: beanstalk_job.id,
691
+ queue_name: payload['queue_name']
692
+ })
693
+
694
+ ActiveSupport::Notifications.instrument('discard.job.postburner', {
695
+ job: job_payload,
696
+ beanstalk_job_id: beanstalk_job.id,
697
+ error: error
698
+ })
699
+
700
+ logger.error "[Postburner] Discarding #{payload['job_class']} (#{payload['job_id']})"
701
+ beanstalk_job.delete
672
702
  end
673
703
 
674
704
  # Watches all configured queues in Beanstalkd.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postburner
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.16
4
+ version: 1.0.0.pre.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Smith