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 +4 -4
- data/README.md +85 -11
- data/app/concerns/postburner/execution.rb +38 -1
- data/app/concerns/postburner/properties.rb +105 -7
- data/app/concerns/postburner/statistics.rb +2 -2
- data/app/models/postburner/mailer.rb +143 -10
- data/lib/postburner/configuration.rb +9 -0
- data/lib/postburner/strategies/strict_queue.rb +1 -1
- data/lib/postburner/tube.rb +67 -6
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/worker.rb +120 -90
- data/lib/postburner.rb +14 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4af263bde4adb664e58df7f7045152e0fee7c29c953227aeb962b385b68de21d
|
|
4
|
+
data.tar.gz: 976c3a47d49bf83c26719e7b8adf17d05c827c0cd982855c5258307d703c4acf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
1463
|
-
| `retry_stopped.job.postburner` | When
|
|
1464
|
-
| `discard.job.postburner` | When default
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.
|
|
216
|
+
# job.tube_name # => 'postburner.development.critical'
|
|
187
217
|
#
|
|
188
|
-
def
|
|
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.
|
|
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:
|
|
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
|
-
#
|
|
2
|
+
# A tracked job for sending ActionMailer emails via Beanstalkd.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
-
#
|
|
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
|
|
146
|
+
# Build the Mail::Message without sending.
|
|
147
|
+
#
|
|
148
|
+
# Useful for testing or inspecting the email before delivery.
|
|
42
149
|
#
|
|
43
|
-
#
|
|
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
|
|
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
|
|
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.
|
|
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
|
data/lib/postburner/tube.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
29
|
-
#
|
|
80
|
+
# @example Get first page of jobs
|
|
81
|
+
# tube.jobs(20)
|
|
82
|
+
# # => [#<Beaneater::Job id=1>, #<Beaneater::Job id=5>, ...]
|
|
30
83
|
#
|
|
31
|
-
#
|
|
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
|
data/lib/postburner/version.rb
CHANGED
data/lib/postburner/worker.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
|
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
|
-
|
|
267
|
-
|
|
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
|
|
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
|
-
|
|
413
|
-
|
|
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
|
|
553
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
623
|
+
# Handles errors for Postburner::Job (including tracked ActiveJob).
|
|
607
624
|
#
|
|
608
|
-
#
|
|
609
|
-
#
|
|
610
|
-
#
|
|
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
|
-
# -
|
|
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
|
|
617
|
-
# @param payload [Hash] Parsed job body
|
|
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
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
650
|
+
logger.info "[Postburner] Burying #{job_class_name}##{job_id} for inspection (bkid: #{beanstalk_job.id})"
|
|
631
651
|
|
|
632
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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.
|
|
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
|
-
#
|
|
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]
|