postburner 1.0.0.pre.16 → 1.0.0.pre.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +80 -11
- data/app/concerns/postburner/execution.rb +38 -1
- data/app/concerns/postburner/properties.rb +96 -4
- data/lib/postburner/configuration.rb +4 -0
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/worker.rb +120 -90
- 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: 05d87a7d1949eb64ef1918b4bb0f0cdeb24796097c370f90576b3a158d0be5a7
|
|
4
|
+
data.tar.gz: 857304a0b925574ab66d6ad215e6cbe3bdba8c109ecb9a34e96e8ab0ca40aac3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af92d06c7d796c0db2bb766986bc9b27ad46e9958103e11871538e3224b43a054d80fb50b7bb0b5f8d93de1a11331e10b10d26058c219047e15530f6a36a1b9e
|
|
7
|
+
data.tar.gz: 351f86f49998e5350b7b705a4f77f68a91c29340995dc5531b964bbdb40b6bb55f3c4202459cdc7453429c7e3b7ceaa4ca934628fc8503305f9630eb9c287142
|
data/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# Postburner
|
|
2
2
|
|
|
3
|
-
Fast Beanstalkd-backed job queue with **optional PostgreSQL records**.
|
|
3
|
+
Fast Beanstalkd-backed job queue with **optional PostgreSQL records via ActiveRecord**.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **Fast jobs**: Fast execution via Beanstalkd
|
|
7
|
-
- **Tracked jobs**: Audited jobs logs, timing, errors, and statistics
|
|
8
|
-
|
|
9
|
-
Built for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
|
|
5
|
+
Built for the real world where you may want fast background processing for most jobs, but comprehensive auditing for critical operations.
|
|
10
6
|
|
|
11
7
|
- **ActiveJob Adapter** - To use with Rails, ActionMailer, ActiveStorage
|
|
12
8
|
- **Dual-mode execution** - Beanstalkd only or tracked (database backed)
|
|
@@ -104,6 +100,7 @@ bundle exec rake postburner:work WORKER=default
|
|
|
104
100
|
- [Configuration](#configuration)
|
|
105
101
|
- [Callbacks](#callbacks)
|
|
106
102
|
- [Instrumentation](#instrumentation)
|
|
103
|
+
- [Logging](#logging)
|
|
107
104
|
- [Why Beanstalkd?](#why-beanstalkd)
|
|
108
105
|
- [Beanstalkd Integration](#beanstalkd-integration)
|
|
109
106
|
- [Installation](#installation)
|
|
@@ -216,6 +213,38 @@ then queue it!
|
|
|
216
213
|
| **Set priority** | `.set(priority: 0)` | `job.queue!(priority: 0)` |
|
|
217
214
|
| **Set TTR** | `.set(ttr: 300)` | `job.queue!(ttr: 300)` |
|
|
218
215
|
|
|
216
|
+
|
|
217
|
+
| Retries | ActiveJob | Postburner::Job |
|
|
218
|
+
|---------|-----------|-----------------|
|
|
219
|
+
| **Default** | No retries (discarded) | No retries (buried) |
|
|
220
|
+
| **Disable retries** | `discard_on StandardError` | *(default behavior)* |
|
|
221
|
+
| **Enable retries** | `retry_on StandardError` | `max_retries 5` |
|
|
222
|
+
|
|
223
|
+
- **ActiveJob**: No automatic worker-level retries. Use ActiveJob's `retry_on`/`discard_on` for retry behavior. Failed jobs without retry configuration are discarded.
|
|
224
|
+
- **Postburner::Job**: No automatic retries by default. On failure, the job is buried in Beanstalkd for inspection. Use `max_retries` (1-32) to enable retries with exponential backoff (2^n seconds).
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# ActiveJob: Use retry_on for retries
|
|
228
|
+
class MyActiveJob < ApplicationJob
|
|
229
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 5
|
|
230
|
+
|
|
231
|
+
def perform(args)
|
|
232
|
+
# ...
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Postburner::Job: Use max_retries for retries
|
|
237
|
+
class MyJob < Postburner::Job
|
|
238
|
+
max_retries 5 # Exponential backoff: 1s, 2s, 4s, 8s, 16s
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Or use a fixed delay
|
|
242
|
+
class MyJob < Postburner::Job
|
|
243
|
+
max_retries 5
|
|
244
|
+
retry_delay 10 # 10 seconds between retries
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
219
248
|
## Usage
|
|
220
249
|
|
|
221
250
|
### Default Jobs
|
|
@@ -1459,9 +1488,9 @@ Postburner emits ActiveSupport::Notifications events following Rails conventions
|
|
|
1459
1488
|
|-------|------|--------------|
|
|
1460
1489
|
| `perform_start.job.postburner` | Before job execution begins | `:job`, `:beanstalk_job_id` |
|
|
1461
1490
|
| `perform.job.postburner` | Around job execution (includes duration) | `:job`, `:beanstalk_job_id` |
|
|
1462
|
-
| `retry.job.postburner` | When
|
|
1463
|
-
| `retry_stopped.job.postburner` | When
|
|
1464
|
-
| `discard.job.postburner` | When default
|
|
1491
|
+
| `retry.job.postburner` | When Postburner::Job is retried | `:job`, `:beanstalk_job_id`, `:error`, `:wait`, `:attempt` |
|
|
1492
|
+
| `retry_stopped.job.postburner` | When Postburner::Job exhausts retries (buried) | `:job`, `:beanstalk_job_id`, `:error` |
|
|
1493
|
+
| `discard.job.postburner` | When default ActiveJob fails (discarded) | `:job`, `:beanstalk_job_id`, `:error` |
|
|
1465
1494
|
| `enqueue.job.postburner` | When job is enqueued for immediate execution | `:job` |
|
|
1466
1495
|
| `enqueue_at.job.postburner` | When job is enqueued with delay | `:job`, `:scheduled_at` |
|
|
1467
1496
|
|
|
@@ -1572,6 +1601,46 @@ end
|
|
|
1572
1601
|
|
|
1573
1602
|
**Note:** These events complement (don't replace) ActiveJob's built-in instrumentation events like `enqueue.active_job` and `perform.active_job`.
|
|
1574
1603
|
|
|
1604
|
+
## Logging
|
|
1605
|
+
|
|
1606
|
+
### Configuration
|
|
1607
|
+
|
|
1608
|
+
Set the log level in your Rails environment configuration:
|
|
1609
|
+
|
|
1610
|
+
```ruby
|
|
1611
|
+
# config/environments/production.rb
|
|
1612
|
+
config.log_level = :info # Default, recommended for production
|
|
1613
|
+
|
|
1614
|
+
# config/environments/development.rb
|
|
1615
|
+
config.log_level = :debug # Verbose logging for development
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
Postburner uses `Rails.logger`, so standard Rails log level configuration applies.
|
|
1619
|
+
|
|
1620
|
+
### Debugging
|
|
1621
|
+
|
|
1622
|
+
Set the log level to `debug` for verbose logging:
|
|
1623
|
+
|
|
1624
|
+
```ruby
|
|
1625
|
+
config.log_level = :debug
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
### Custom Job Logging
|
|
1629
|
+
|
|
1630
|
+
```ruby
|
|
1631
|
+
class ProcessPayment < ApplicationJob
|
|
1632
|
+
include Postburner::Tracked
|
|
1633
|
+
|
|
1634
|
+
def perform(payment_id)
|
|
1635
|
+
log "Starting payment processing for $#{payment.amount}" # Stored in database
|
|
1636
|
+
payment.charge!
|
|
1637
|
+
log! "Payment charged successfully" # Saved immediately
|
|
1638
|
+
end
|
|
1639
|
+
end
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
These job-specific logs are stored in the database (for tracked jobs only) and are separate from Rails application logs, providing a complete audit trail for critical operations.
|
|
1643
|
+
|
|
1575
1644
|
## Why Beanstalkd?
|
|
1576
1645
|
|
|
1577
1646
|
Beanstalkd is a simple, fast, and reliable queue system. It is a good choice for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
|
|
@@ -1696,7 +1765,7 @@ class BackgroundTask < Postburner::Job
|
|
|
1696
1765
|
end
|
|
1697
1766
|
```
|
|
1698
1767
|
|
|
1699
|
-
**
|
|
1768
|
+
**Example Priority Ranges:**
|
|
1700
1769
|
|
|
1701
1770
|
| Priority Range | Use Case | Examples |
|
|
1702
1771
|
|---------------|----------|----------|
|
|
@@ -1712,7 +1781,7 @@ Set default priority in `config/postburner.yml`:
|
|
|
1712
1781
|
|
|
1713
1782
|
```yaml
|
|
1714
1783
|
production:
|
|
1715
|
-
default_priority: 65536 # Default
|
|
1784
|
+
default_priority: 65536 # Default without explicit priority set
|
|
1716
1785
|
default_ttr: 300
|
|
1717
1786
|
```
|
|
1718
1787
|
|
|
@@ -162,7 +162,7 @@ module Postburner
|
|
|
162
162
|
|
|
163
163
|
rescue Exception => exception
|
|
164
164
|
self.log_exception!(exception)
|
|
165
|
-
|
|
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,7 +202,7 @@ module Postburner
|
|
|
172
202
|
# job.queue_name # => 'urgent'
|
|
173
203
|
#
|
|
174
204
|
def queue_name
|
|
175
|
-
@queue_name || self.class.queue
|
|
205
|
+
@queue_name || self.class.queue || Postburner.configuration.default_queue_name
|
|
176
206
|
end
|
|
177
207
|
|
|
178
208
|
# Returns the full tube name with environment prefix.
|
|
@@ -222,5 +252,67 @@ module Postburner
|
|
|
222
252
|
@ttr || self.class.ttr
|
|
223
253
|
end
|
|
224
254
|
|
|
255
|
+
# Checks if this job should retry after a failure.
|
|
256
|
+
#
|
|
257
|
+
# Returns true if max_retries is configured and the current attempt count
|
|
258
|
+
# is less than max_retries.
|
|
259
|
+
#
|
|
260
|
+
# @return [Boolean] true if job should retry, false otherwise
|
|
261
|
+
#
|
|
262
|
+
# @example
|
|
263
|
+
# class MyJob < Postburner::Job
|
|
264
|
+
# max_retries 3
|
|
265
|
+
# end
|
|
266
|
+
#
|
|
267
|
+
# job = MyJob.create!(args: {})
|
|
268
|
+
# job.should_retry? # => true (attempt_count is 0)
|
|
269
|
+
# # After 3 failed attempts...
|
|
270
|
+
# job.should_retry? # => false
|
|
271
|
+
#
|
|
272
|
+
def should_retry?
|
|
273
|
+
max = self.class.max_retries || Postburner.configuration.default_max_retries
|
|
274
|
+
attempt_count.to_i < max.to_i
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Calculates the retry delay for the given attempt.
|
|
278
|
+
#
|
|
279
|
+
# Uses the class-level retry_delay configuration, which can be:
|
|
280
|
+
# - Integer: Fixed delay in seconds
|
|
281
|
+
# - Proc: Called with attempt number (0-based), returns delay in seconds
|
|
282
|
+
# - nil: Defaults to 5 seconds
|
|
283
|
+
#
|
|
284
|
+
# @param attempt [Integer] The attempt number (0-based: 0 for first retry)
|
|
285
|
+
#
|
|
286
|
+
# @return [Integer] Delay in seconds before next retry
|
|
287
|
+
#
|
|
288
|
+
# @example Fixed delay
|
|
289
|
+
# class MyJob < Postburner::Job
|
|
290
|
+
# max_retries 3
|
|
291
|
+
# retry_delay 10
|
|
292
|
+
# end
|
|
293
|
+
# job.retry_delay_for_attempt(0) # => 10
|
|
294
|
+
#
|
|
295
|
+
# @example Exponential backoff (2s, 4s, 8s...)
|
|
296
|
+
# class MyJob < Postburner::Job
|
|
297
|
+
# max_retries 5
|
|
298
|
+
# retry_delay ->(n) { 2 ** (n + 1) }
|
|
299
|
+
# end
|
|
300
|
+
# job.retry_delay_for_attempt(0) # => 2
|
|
301
|
+
# job.retry_delay_for_attempt(1) # => 4
|
|
302
|
+
# job.retry_delay_for_attempt(2) # => 8
|
|
303
|
+
#
|
|
304
|
+
def retry_delay_for_attempt(attempt)
|
|
305
|
+
delay_config = self.class.retry_delay || Postburner.configuration.default_retry_delay
|
|
306
|
+
|
|
307
|
+
case delay_config
|
|
308
|
+
when Proc
|
|
309
|
+
delay_config.call(attempt).to_i
|
|
310
|
+
when Integer
|
|
311
|
+
delay_config
|
|
312
|
+
else
|
|
313
|
+
2 ** attempt # Fallback to default exponential backoff
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
225
317
|
end
|
|
226
318
|
end
|
|
@@ -22,6 +22,7 @@ module Postburner
|
|
|
22
22
|
class Configuration
|
|
23
23
|
# Global settings
|
|
24
24
|
attr_accessor :beanstalk_url, :logger, :default_priority, :default_ttr
|
|
25
|
+
attr_accessor :default_queue_name, :default_max_retries, :default_retry_delay
|
|
25
26
|
attr_accessor :default_scheduler_interval, :default_scheduler_priority
|
|
26
27
|
attr_accessor :enqueue_options
|
|
27
28
|
|
|
@@ -52,6 +53,9 @@ module Postburner
|
|
|
52
53
|
@logger = options[:logger] || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT))
|
|
53
54
|
@default_priority = options[:default_priority] || 65536
|
|
54
55
|
@default_ttr = options[:default_ttr] || 300
|
|
56
|
+
@default_queue_name = options[:default_queue_name] || 'default'
|
|
57
|
+
@default_max_retries = options[:default_max_retries] || 0
|
|
58
|
+
@default_retry_delay = options[:default_retry_delay] || ->(n) { 2 ** n }
|
|
55
59
|
@default_scheduler_interval = options[:default_scheduler_interval] || 300
|
|
56
60
|
@default_scheduler_priority = options[:default_scheduler_priority] || 100
|
|
57
61
|
@enqueue_options = options[:enqueue_options]
|
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.
|