postburner 1.0.0.pre.16 → 1.0.0.pre.18

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: 4af263bde4adb664e58df7f7045152e0fee7c29c953227aeb962b385b68de21d
4
+ data.tar.gz: 976c3a47d49bf83c26719e7b8adf17d05c827c0cd982855c5258307d703c4acf
5
5
  SHA512:
6
- metadata.gz: f207c1d4dd840bee6b00ad71de9cc1b774d9b2376f7f34125fcc3a164220a340badc6b1ef17931d3dc09a0bb78a09c66b1f1f9ffdf1ead3e9737c44a39fe9a29
7
- data.tar.gz: e0212499c62223a87720462226660045585ac4bde21258eb54aeed9d85683c0c9652477a6cf5824bdffa7e30314efb1e1cbb9d146eae8ef3ade2202af73f2abe
6
+ metadata.gz: 30e9379f4c7e05d18659f9864fb58c9e8a0f564575d323a9d3865b352ba3e0d08975bc9a3859d8044602f5c657c16bdd7dd32098944d07b6ad8007124f868a26
7
+ data.tar.gz: 1c0eb570bf338995bfa385a95c6d8ded11a937fb6155481b199e6a94f5992104d6ebc9524601056397f6251c89841d9148d7d45434e583d2e8f91e1e8f14aa66
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
@@ -273,6 +302,7 @@ end
273
302
  Jobs without `Postburner::Beanstalkd` use defaults from `config/postburner.yml`:
274
303
  - `default_priority: 65536`
275
304
  - `default_ttr: 300`
305
+ - `default_queue: default` (if not specified, defaults to `'default'`)
276
306
 
277
307
  #### Configuring Third-Party Jobs
278
308
 
@@ -1249,6 +1279,8 @@ production: # <- environment config
1249
1279
  # config/postburner.yml
1250
1280
  default: &default
1251
1281
  beanstalk_url: <%= ENV['BEANSTALK_URL'] || 'beanstalk://localhost:11300' %>
1282
+ # default_queue: default # Queue for jobs (defaults to 'default')
1283
+ default_mailer_queue: mailers # Queue for Postburner::Mailer (defaults to default_queue)
1252
1284
 
1253
1285
  development: # <- environment config
1254
1286
  <<: *default
@@ -1459,9 +1491,9 @@ Postburner emits ActiveSupport::Notifications events following Rails conventions
1459
1491
  |-------|------|--------------|
1460
1492
  | `perform_start.job.postburner` | Before job execution begins | `:job`, `:beanstalk_job_id` |
1461
1493
  | `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` |
1494
+ | `retry.job.postburner` | When Postburner::Job is retried | `:job`, `:beanstalk_job_id`, `:error`, `:wait`, `:attempt` |
1495
+ | `retry_stopped.job.postburner` | When Postburner::Job exhausts retries (buried) | `:job`, `:beanstalk_job_id`, `:error` |
1496
+ | `discard.job.postburner` | When default ActiveJob fails (discarded) | `:job`, `:beanstalk_job_id`, `:error` |
1465
1497
  | `enqueue.job.postburner` | When job is enqueued for immediate execution | `:job` |
1466
1498
  | `enqueue_at.job.postburner` | When job is enqueued with delay | `:job`, `:scheduled_at` |
1467
1499
 
@@ -1572,6 +1604,46 @@ end
1572
1604
 
1573
1605
  **Note:** These events complement (don't replace) ActiveJob's built-in instrumentation events like `enqueue.active_job` and `perform.active_job`.
1574
1606
 
1607
+ ## Logging
1608
+
1609
+ ### Configuration
1610
+
1611
+ Set the log level in your Rails environment configuration:
1612
+
1613
+ ```ruby
1614
+ # config/environments/production.rb
1615
+ config.log_level = :info # Default, recommended for production
1616
+
1617
+ # config/environments/development.rb
1618
+ config.log_level = :debug # Verbose logging for development
1619
+ ```
1620
+
1621
+ Postburner uses `Rails.logger`, so standard Rails log level configuration applies.
1622
+
1623
+ ### Debugging
1624
+
1625
+ Set the log level to `debug` for verbose logging:
1626
+
1627
+ ```ruby
1628
+ config.log_level = :debug
1629
+ ```
1630
+
1631
+ ### Custom Job Logging
1632
+
1633
+ ```ruby
1634
+ class ProcessPayment < ApplicationJob
1635
+ include Postburner::Tracked
1636
+
1637
+ def perform(payment_id)
1638
+ log "Starting payment processing for $#{payment.amount}" # Stored in database
1639
+ payment.charge!
1640
+ log! "Payment charged successfully" # Saved immediately
1641
+ end
1642
+ end
1643
+ ```
1644
+
1645
+ 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.
1646
+
1575
1647
  ## Why Beanstalkd?
1576
1648
 
1577
1649
  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 +1768,7 @@ class BackgroundTask < Postburner::Job
1696
1768
  end
1697
1769
  ```
1698
1770
 
1699
- **Recommended Priority Ranges:**
1771
+ **Example Priority Ranges:**
1700
1772
 
1701
1773
  | Priority Range | Use Case | Examples |
1702
1774
  |---------------|----------|----------|
@@ -1712,7 +1784,9 @@ Set default priority in `config/postburner.yml`:
1712
1784
 
1713
1785
  ```yaml
1714
1786
  production:
1715
- default_priority: 65536 # Default for jobs without explicit priority
1787
+ default_queue: default # Queue for jobs (defaults to 'default')
1788
+ default_mailer_queue: mailers # Queue for Postburner::Mailer (defaults to default_queue)
1789
+ default_priority: 65536 # Default without explicit priority set
1716
1790
  default_ttr: 300
1717
1791
  ```
1718
1792
 
@@ -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,10 +202,10 @@ 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
176
206
  end
177
207
 
178
- # Returns the full tube name with environment prefix.
208
+ # Returns the full Beanstalkd tube name with environment prefix.
179
209
  #
180
210
  # Expands the queue name to include the environment prefix
181
211
  # (e.g., 'critical' becomes 'postburner.development.critical').
@@ -183,12 +213,18 @@ module Postburner
183
213
  # @return [String] Full tube name with environment prefix
184
214
  #
185
215
  # @example
186
- # job.expanded_tube_name # => 'postburner.development.critical'
216
+ # job.tube_name # => 'postburner.development.critical'
187
217
  #
188
- def expanded_tube_name
218
+ def tube_name
189
219
  Postburner.configuration.expand_tube_name(queue_name)
190
220
  end
191
221
 
222
+ # @!method expanded_tube_name
223
+ # Alias for {#tube_name}.
224
+ # @return [String] Full tube name with environment prefix
225
+ # @see #tube_name
226
+ alias_method :expanded_tube_name, :tube_name
227
+
192
228
  # Returns the priority for this job instance.
193
229
  #
194
230
  # Checks instance-level override first, then falls back to class-level configuration.
@@ -222,5 +258,67 @@ module Postburner
222
258
  @ttr || self.class.ttr
223
259
  end
224
260
 
261
+ # Checks if this job should retry after a failure.
262
+ #
263
+ # Returns true if max_retries is configured and the current attempt count
264
+ # is less than max_retries.
265
+ #
266
+ # @return [Boolean] true if job should retry, false otherwise
267
+ #
268
+ # @example
269
+ # class MyJob < Postburner::Job
270
+ # max_retries 3
271
+ # end
272
+ #
273
+ # job = MyJob.create!(args: {})
274
+ # job.should_retry? # => true (attempt_count is 0)
275
+ # # After 3 failed attempts...
276
+ # job.should_retry? # => false
277
+ #
278
+ def should_retry?
279
+ max = self.class.max_retries || Postburner.configuration.default_max_retries
280
+ attempt_count.to_i < max.to_i
281
+ end
282
+
283
+ # Calculates the retry delay for the given attempt.
284
+ #
285
+ # Uses the class-level retry_delay configuration, which can be:
286
+ # - Integer: Fixed delay in seconds
287
+ # - Proc: Called with attempt number (0-based), returns delay in seconds
288
+ # - nil: Defaults to 5 seconds
289
+ #
290
+ # @param attempt [Integer] The attempt number (0-based: 0 for first retry)
291
+ #
292
+ # @return [Integer] Delay in seconds before next retry
293
+ #
294
+ # @example Fixed delay
295
+ # class MyJob < Postburner::Job
296
+ # max_retries 3
297
+ # retry_delay 10
298
+ # end
299
+ # job.retry_delay_for_attempt(0) # => 10
300
+ #
301
+ # @example Exponential backoff (2s, 4s, 8s...)
302
+ # class MyJob < Postburner::Job
303
+ # max_retries 5
304
+ # retry_delay ->(n) { 2 ** (n + 1) }
305
+ # end
306
+ # job.retry_delay_for_attempt(0) # => 2
307
+ # job.retry_delay_for_attempt(1) # => 4
308
+ # job.retry_delay_for_attempt(2) # => 8
309
+ #
310
+ def retry_delay_for_attempt(attempt)
311
+ delay_config = self.class.retry_delay || Postburner.configuration.default_retry_delay
312
+
313
+ case delay_config
314
+ when Proc
315
+ delay_config.call(attempt).to_i
316
+ when Integer
317
+ delay_config
318
+ else
319
+ 2 ** attempt # Fallback to default exponential backoff
320
+ end
321
+ end
322
+
225
323
  end
226
324
  end
@@ -73,13 +73,13 @@ module Postburner
73
73
  #
74
74
  def stats
75
75
  # Get configured watched tubes (expanded with environment prefix)
76
- watched = Postburner.watched_tube_names.include?(self.expanded_tube_name)
76
+ watched = Postburner.watched_tube_names.include?(self.tube_name)
77
77
 
78
78
  {
79
79
  id: self.id,
80
80
  bkid: self.bkid,
81
81
  queue: queue_name,
82
- tube: expanded_tube_name,
82
+ tube: tube_name,
83
83
  watched: watched,
84
84
  beanstalk: self.bk.stats.to_h.symbolize_keys,
85
85
  }
@@ -1,12 +1,90 @@
1
1
  module Postburner
2
- # Send a mailer, tracked.
2
+ # A tracked job for sending ActionMailer emails via Beanstalkd.
3
3
  #
4
- # Postburner::Mailer.deliver(UserMailer, :welcome).with(user_id: 1)
5
- # Postburner::Mailer.deliver(UserMailer, :welcome).with(user_id: 1).queue! at: Time.current + 1.day
6
- # Postburner::Mailer.deliver(UserMailer, :welcome).with(user_id: 1).queue! delay: 5.minutes
4
+ # Provides a chainable API similar to ActionMailer for building and
5
+ # queueing email deliveries with full audit trail support.
6
+ #
7
+ # == Queue Configuration
8
+ #
9
+ # By default, mailer jobs use the +default_queue+ (typically 'default').
10
+ # You can route mailers to a dedicated queue using several approaches:
11
+ #
12
+ # === Via configuration (recommended)
13
+ #
14
+ # Set +default_mailer_queue+ in your postburner.yml or configuration:
15
+ #
16
+ # # config/postburner.yml
17
+ # production:
18
+ # default_mailer_queue: mailers
19
+ #
20
+ # # Or programmatically
21
+ # Postburner.configure do |config|
22
+ # config.default_mailer_queue = 'mailers'
23
+ # end
24
+ #
25
+ # === Via queue! option (per-job override)
26
+ #
27
+ # Override the queue when queueing a specific job:
28
+ #
29
+ # Postburner::Mailer.delivery(UserMailer, :welcome)
30
+ # .with(user_id: 1)
31
+ # .queue!(queue: 'priority_mailers')
32
+ #
33
+ # === Via instance setter (per-job override)
34
+ #
35
+ # Set the queue on the job instance before queueing:
36
+ #
37
+ # job = Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1)
38
+ # job.queue_name = 'priority_mailers'
39
+ # job.queue!
40
+ #
41
+ # @example Basic delivery (queued immediately)
42
+ # Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1).queue!
43
+ #
44
+ # @example Scheduled delivery at a specific time
45
+ # Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1).queue!(at: Time.current + 1.day)
46
+ #
47
+ # @example Delayed delivery
48
+ # Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1).queue!(delay: 5.minutes)
49
+ #
50
+ # @example Queue to specific queue
51
+ # Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1).queue!(queue: 'bulk_mailers')
52
+ #
53
+ # @see Postburner::Job Base class for tracked jobs
54
+ # @see Postburner::Configuration#default_mailer_queue Configuration option
7
55
  class Mailer < Job
8
- #queue 'mailers'
9
56
 
57
+ # Returns the queue name for this mailer job.
58
+ #
59
+ # Checks in order: instance override, +default_mailer_queue+ config,
60
+ # then falls back to +default_queue+.
61
+ #
62
+ # @return [String] Queue name
63
+ #
64
+ # @example Default behavior (uses default_mailer_queue or default_queue)
65
+ # job = Postburner::Mailer.delivery(UserMailer, :welcome)
66
+ # job.queue_name # => 'mailers' (if configured) or 'default'
67
+ #
68
+ # @example Instance override
69
+ # job = Postburner::Mailer.delivery(UserMailer, :welcome)
70
+ # job.queue_name = 'priority_mailers'
71
+ # job.queue_name # => 'priority_mailers'
72
+ def queue_name
73
+ @queue_name ||
74
+ self.class.queue ||
75
+ Postburner.configuration.default_mailer_queue ||
76
+ Postburner.configuration.default_queue
77
+ end
78
+
79
+ # Build a new mailer job without persisting.
80
+ #
81
+ # @param mailer [Class] The ActionMailer class (e.g., UserMailer)
82
+ # @param action [Symbol, String] The mailer action/method name (e.g., :welcome)
83
+ # @return [Postburner::Mailer] An unsaved mailer job instance
84
+ #
85
+ # @example
86
+ # job = Postburner::Mailer.delivery(UserMailer, :welcome)
87
+ # job.with(user_id: 1).queue!
10
88
  def self.delivery(mailer, action)
11
89
  job = self.new(
12
90
  args: {
@@ -17,14 +95,33 @@ module Postburner
17
95
  job
18
96
  end
19
97
 
98
+ # Build and persist a new mailer job.
99
+ #
100
+ # @param mailer [Class] The ActionMailer class (e.g., UserMailer)
101
+ # @param action [Symbol, String] The mailer action/method name (e.g., :welcome)
102
+ # @return [Postburner::Mailer] A persisted mailer job instance
103
+ #
104
+ # @example
105
+ # job = Postburner::Mailer.delivery!(UserMailer, :password_reset)
106
+ # job.with!(token: reset_token)
20
107
  def self.delivery!(mailer, action)
21
108
  job = self.delivery(mailer, action)
22
109
  job.save!
23
110
  job
24
111
  end
25
112
 
26
- # Similar to ActionMailer #with - set the parameters
113
+ # Set the mailer parameters without persisting.
114
+ #
115
+ # Similar to ActionMailer's +#with+ method for parameterized mailers.
116
+ # Parameters are serialized using ActiveJob::Arguments for safe storage.
117
+ #
118
+ # @param params [Hash] Parameters to pass to the mailer
119
+ # @return [self] Returns self for method chaining
27
120
  #
121
+ # @example
122
+ # Postburner::Mailer.delivery(UserMailer, :welcome)
123
+ # .with(user_id: 1, locale: :en)
124
+ # .queue!
28
125
  def with(params={})
29
126
  self.args.merge!(
30
127
  'params' => ::ActiveJob::Arguments.serialize(params)
@@ -32,16 +129,31 @@ module Postburner
32
129
  self
33
130
  end
34
131
 
132
+ # Set the mailer parameters and persist immediately.
133
+ #
134
+ # @param params [Hash] Parameters to pass to the mailer
135
+ # @return [self] Returns self for method chaining
136
+ #
137
+ # @example
138
+ # job = Postburner::Mailer.delivery!(OrderMailer, :confirmation)
139
+ # job.with!(order_id: order.id)
35
140
  def with!(params={})
36
141
  self.with(params)
37
142
  self.save!
38
143
  self
39
144
  end
40
145
 
41
- # Build the mail but don't send.
146
+ # Build the Mail::Message without sending.
147
+ #
148
+ # Useful for testing or inspecting the email before delivery.
42
149
  #
43
- # Optional `args` argument for testing convenience.
150
+ # @return [Mail::Message] The assembled email message
44
151
  #
152
+ # @example Inspect email before sending
153
+ # job = Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1)
154
+ # mail = job.assemble
155
+ # puts mail.subject # => "Welcome!"
156
+ # puts mail.to # => ["user@example.com"]
45
157
  def assemble
46
158
  mail = self.mailer.with(self.params).send(self.action)
47
159
  mail
@@ -49,22 +161,43 @@ module Postburner
49
161
 
50
162
  # Get the mailer class.
51
163
  #
164
+ # @return [Class] The ActionMailer class
165
+ #
166
+ # @example
167
+ # job.mailer # => UserMailer
52
168
  def mailer
53
169
  self.args['mailer'].constantize
54
170
  end
55
171
 
56
- # Get the mailer action as a symbol.
172
+ # Get the mailer action name.
173
+ #
174
+ # @return [Symbol, nil] The mailer action as a symbol
57
175
  #
176
+ # @example
177
+ # job.action # => :welcome
58
178
  def action
59
179
  self.args['action']&.to_sym
60
180
  end
61
181
 
62
- # Get the deserialized params.
182
+ # Get the deserialized mailer parameters.
63
183
  #
184
+ # @return [Hash] The parameters passed via {#with}
185
+ #
186
+ # @example
187
+ # job.params # => { user_id: 1, locale: :en }
64
188
  def params
65
189
  ::ActiveJob::Arguments.deserialize(self.args['params']).to_h
66
190
  end
67
191
 
192
+ # Execute the mailer job.
193
+ #
194
+ # Called by the Postburner worker. Assembles and delivers the email,
195
+ # logging progress at each step.
196
+ #
197
+ # @param args [Hash] Job arguments (unused, params stored in {#args})
198
+ # @return [void]
199
+ #
200
+ # @api private
68
201
  def perform(args)
69
202
  self.log! "Building"
70
203
  mail = self.assemble
@@ -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, :default_mailer_queue, :default_max_retries, :default_retry_delay
25
26
  attr_accessor :default_scheduler_interval, :default_scheduler_priority
26
27
  attr_accessor :enqueue_options
27
28
 
@@ -33,6 +34,7 @@ module Postburner
33
34
  # @option options [Logger] :logger Logger instance (default: Rails.logger)
34
35
  # @option options [Integer] :default_priority Default job priority (default: 65536, lower = higher priority)
35
36
  # @option options [Integer] :default_ttr Default time-to-run in seconds (default: 300)
37
+ # @option options [String] :default_mailer_queue Queue name for Postburner::Mailer jobs (default: nil, uses default_queue)
36
38
  # @option options [Integer] :default_scheduler_interval Scheduler check interval in seconds (default: 300)
37
39
  # @option options [Integer] :default_scheduler_priority Scheduler job priority (default: 100)
38
40
  # @option options [Proc] :enqueue_options Proc that receives job and returns options hash
@@ -52,6 +54,10 @@ module Postburner
52
54
  @logger = options[:logger] || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT))
53
55
  @default_priority = options[:default_priority] || 65536
54
56
  @default_ttr = options[:default_ttr] || 300
57
+ @default_queue = options[:default_queue] || 'default'
58
+ @default_mailer_queue = options[:default_mailer_queue]
59
+ @default_max_retries = options[:default_max_retries] || 0
60
+ @default_retry_delay = options[:default_retry_delay] || ->(n) { 2 ** n }
55
61
  @default_scheduler_interval = options[:default_scheduler_interval] || 300
56
62
  @default_scheduler_priority = options[:default_scheduler_priority] || 100
57
63
  @enqueue_options = options[:enqueue_options]
@@ -93,6 +99,7 @@ module Postburner
93
99
  # default: &default
94
100
  # beanstalk_url: <%= ENV.fetch('BEANSTALK_URL', 'beanstalk://localhost:11300') %>
95
101
  # default_priority: 131072 # change default priority from 65536 to 131072
102
+ # default_mailer_queue: mailers # optional: route Postburner::Mailer to separate queue
96
103
  #
97
104
  # production: # <- environment config, i.e. defaults
98
105
  # <<: *default
@@ -162,6 +169,8 @@ module Postburner
162
169
  beanstalk_url: env_config['beanstalk_url'],
163
170
  default_priority: env_config['default_priority'],
164
171
  default_ttr: env_config['default_ttr'],
172
+ default_queue: env_config['default_queue'],
173
+ default_mailer_queue: env_config['default_mailer_queue'],
165
174
  default_scheduler_interval: env_config['scheduler_interval'],
166
175
  default_scheduler_priority: env_config['scheduler_priority'],
167
176
  worker_config: worker_config
@@ -76,7 +76,7 @@ module Postburner
76
76
  #debugger
77
77
  Postburner::Job.transaction do
78
78
  Postburner.connected do |conn|
79
- tube_name = job.expanded_tube_name
79
+ tube_name = job.tube_name
80
80
  data = { class: job.class.name, args: [job.id] }
81
81
 
82
82
  # Get priority, TTR from job instance (respects instance overrides) or options
@@ -1,35 +1,96 @@
1
1
  module Postburner
2
+ # A wrapper around Beaneater tubes for inspecting Beanstalkd queues.
3
+ #
4
+ # Provides methods to enumerate tubes and paginate through jobs,
5
+ # primarily used by the Postburner web UI for job inspection.
6
+ #
7
+ # @note Beanstalkd is not designed for job enumeration or searching.
8
+ # It is optimized for fast job reservation, not inspection. This class
9
+ # iterates through job IDs sequentially which is inherently inefficient.
10
+ # Use this only for debugging and development purposes, not for
11
+ # production workloads or high-volume introspection of queues.
12
+ #
13
+ # @example List all tubes
14
+ # tubes = Postburner::Tube.all
15
+ # tubes.each { |tube| puts tube.peek_ids }
16
+ #
17
+ # @example Paginate through jobs in a tube
18
+ # tube = Postburner::Tube.all.first
19
+ # first_page = tube.jobs(20)
20
+ # second_page = tube.jobs(20, after: first_page.last.id)
21
+ #
22
+ # @see Postburner.connection For accessing the Beaneater connection
2
23
  class Tube
24
+ # Initialize a new Tube wrapper.
25
+ #
26
+ # @param tube [Beaneater::Tube] The underlying Beaneater tube object
3
27
  def initialize(tube)
4
28
  @tube = tube
5
29
  end
6
30
 
7
31
  # Get all tubes as Postburner::Tube instances.
8
32
  #
33
+ # @return [Array<Postburner::Tube>] All tubes from the Beanstalkd server
34
+ #
35
+ # @example
36
+ # Postburner::Tube.all
37
+ # # => [#<Postburner::Tube>, #<Postburner::Tube>, ...]
9
38
  def self.all
10
39
  Postburner.connection.tubes.all.map { |tube| self.new(tube) }
11
40
  end
12
41
 
13
- # Get all peeked ids across all known tubes.
42
+ # Get all peeked job IDs across all known tubes.
43
+ #
44
+ # Useful for finding the minimum known job ID when starting pagination.
45
+ #
46
+ # @return [Array<Integer>] Sorted array of job IDs from all tubes
14
47
  #
48
+ # @example
49
+ # Postburner::Tube.peek_ids
50
+ # # => [1, 5, 12, 47, 89, 102]
15
51
  def self.peek_ids
16
52
  self.all.map(&:peek_ids).flatten.sort
17
53
  end
18
54
 
19
- # Get all peeked ids.
55
+ # Get peeked job IDs from this tube.
20
56
  #
57
+ # Peeks at buried, ready, and delayed states to find known job IDs.
58
+ #
59
+ # @return [Array<Integer>] Job IDs visible via peek operations
60
+ #
61
+ # @example
62
+ # tube = Postburner::Tube.all.first
63
+ # ids = tube.peek_ids # => [1, 5, 12]
21
64
  def peek_ids
22
65
  [ :buried, :ready, :delayed ].map { |type| @tube.peek(type) }.
23
66
  reject(&:nil?).map(&:id).map(&:to_i)
24
67
  end
25
68
 
26
- # Get paginated array of jobs.
69
+ # Get a paginated array of jobs from this tube.
70
+ #
71
+ # Efficiently retrieves jobs by starting from known IDs and iterating
72
+ # until the requested count is fulfilled. Supports cursor-based pagination
73
+ # via the +after+ parameter.
74
+ #
75
+ # @param count [Integer] Number of jobs to return (default: 20)
76
+ # @param limit [Integer] Maximum ID range to scan (default: 1000)
77
+ # @param after [Integer, nil] Return jobs after this ID for pagination
78
+ # @return [Array<Beaneater::Job>] Jobs from this tube
27
79
  #
28
- # Attempts to do this efficiently as possible, by peeking at known
29
- # ids and exiting when count has been fulfilled.
80
+ # @example Get first page of jobs
81
+ # tube.jobs(20)
82
+ # # => [#<Beaneater::Job id=1>, #<Beaneater::Job id=5>, ...]
30
83
  #
31
- # Just pass the last known id to after for the next batch.
84
+ # @example Access job properties
85
+ # jobs = tube.jobs(5)
86
+ # jobs.first.id # => 1
87
+ # jobs.first.body # => "{\"job_class\":\"MyJob\",...}"
88
+ # jobs.first.stats # => {"tube"=>"default", "state"=>"ready", ...}
32
89
  #
90
+ # @example Paginate through jobs
91
+ # page1 = tube.jobs(20)
92
+ # page2 = tube.jobs(20, after: page1.last.id)
93
+ # # => [#<Beaneater::Job id=25>, #<Beaneater::Job id=26>, ...]
33
94
  def jobs(count=20, limit: 1000, after: nil)
34
95
  # Note: beaneater transforms hyphenated beanstalkd stats to underscores
35
96
  stats = @tube.stats
@@ -1,3 +1,3 @@
1
1
  module Postburner
2
- VERSION = '1.0.0.pre.16'
2
+ VERSION = '1.0.0.pre.18'
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.
data/lib/postburner.rb CHANGED
@@ -405,6 +405,11 @@ module Postburner
405
405
  # Postburner.clear_jobs!(Postburner.scheduler_tube_name)
406
406
  # # Clears the scheduler watchdog tube
407
407
  #
408
+ # @example Clear watched tubes AND scheduler tube
409
+ # Postburner.clear_jobs!(Postburner.watched_tube_names + [Postburner.scheduler_tube_name])
410
+ # # Or use the convenience method:
411
+ # Postburner.clear_all!
412
+ #
408
413
  # @example Trying to clear unconfigured tube - RAISES ERROR
409
414
  # Postburner.clear_jobs!(['some-other-app-tube'])
410
415
  # # => ArgumentError: Cannot clear tubes not in configuration
@@ -413,6 +418,7 @@ module Postburner
413
418
  # result = Postburner.clear_jobs!(Postburner.watched_tube_names, silent: true)
414
419
  # result[:totals][:total] # => 42
415
420
  #
421
+ # @see #clear_all!
416
422
  # @see Connection#clear_tubes!
417
423
  #
418
424
  def self.clear_jobs!(tube_names = nil, silent: false)
@@ -431,21 +437,25 @@ module Postburner
431
437
  # Clears all configured tubes including scheduler.
432
438
  #
433
439
  # Convenience method that clears all watched tubes plus the scheduler tube
434
- # in a single call. This is the equivalent of:
440
+ # in a single call. Use this to completely reset Postburner's Beanstalkd state,
441
+ # such as during test setup or when recovering from a stuck state.
435
442
  #
443
+ # Equivalent to:
436
444
  # Postburner.clear_jobs!(Postburner.watched_tube_names + [Postburner.scheduler_tube_name])
437
445
  #
438
446
  # @param silent [Boolean] If true, suppress output to stdout (default: false)
439
447
  #
440
448
  # @return [Hash] Statistics and results (see Connection#clear_tubes!)
441
449
  #
442
- # @example Clear everything
450
+ # @example Clear everything (interactive/console use)
443
451
  # Postburner.clear_all!
444
452
  #
445
- # @example Silent mode
446
- # result = Postburner.clear_all!(silent: true)
453
+ # @example Silent mode (test setup or programmatic use)
454
+ # Postburner.clear_all!(silent: true)
447
455
  #
448
456
  # @see #clear_jobs!
457
+ # @see #watched_tube_names
458
+ # @see #scheduler_tube_name
449
459
  #
450
460
  def self.clear_all!(silent: false)
451
461
  all_tubes = watched_tube_names + [scheduler_tube_name]
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.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Smith