postburner 1.0.0.pre.15 → 1.0.0.pre.16
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 +52 -5
- data/app/concerns/postburner/execution.rb +1 -1
- data/app/concerns/postburner/insertion.rb +22 -9
- data/app/concerns/postburner/properties.rb +7 -2
- data/app/models/postburner/job.rb +12 -3
- data/lib/postburner/scheduler.rb +60 -11
- data/lib/postburner/tube.rb +3 -5
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/worker.rb +31 -5
- data/lib/postburner.rb +9 -10
- 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: 8e032f7d37f1680fe34a1402f8eb9f0b3ca61aa9231f6d540c35c3a919e3f58d
|
|
4
|
+
data.tar.gz: bd35338dd14bc499c4c7d9f57d30eff3902615cd72876f766f1e379f12baba77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f207c1d4dd840bee6b00ad71de9cc1b774d9b2376f7f34125fcc3a164220a340badc6b1ef17931d3dc09a0bb78a09c66b1f1f9ffdf1ead3e9737c44a39fe9a29
|
|
7
|
+
data.tar.gz: e0212499c62223a87720462226660045585ac4bde21258eb54aeed9d85683c0c9652477a6cf5824bdffa7e30314efb1e1cbb9d146eae8ef3ade2202af73f2abe
|
data/README.md
CHANGED
|
@@ -97,6 +97,7 @@ bundle exec rake postburner:work WORKER=default
|
|
|
97
97
|
- [Postburner::Job](#postburnerjob)
|
|
98
98
|
- [Scheduler](#scheduler)
|
|
99
99
|
- [Job Management](#job-management)
|
|
100
|
+
- [Writing Jobs](#writing-jobs)
|
|
100
101
|
- [Queue Strategies](#queue-strategies)
|
|
101
102
|
- [Testing](#testing)
|
|
102
103
|
- [Workers](#workers)
|
|
@@ -177,16 +178,45 @@ config.active_job.queue_adapter = :postburner
|
|
|
177
178
|
#config.active_job.queue_name_prefix = Postburner.tube_prefix(Rails.env) # i.e. "postburner.#{Rails.env}"
|
|
178
179
|
config.action_mailer.deliver_later_queue_name = 'mailers' # gets prefixed by config.active_job.queue_name_prefix
|
|
179
180
|
|
|
180
|
-
|
|
181
181
|
bundle exec postburner # start with bin/postburner
|
|
182
182
|
bundle exec rake postburner:work # or with rake task
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
### Enqueueing Jobs
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
```ruby
|
|
188
|
+
# ActiveJob (standard Rails API)
|
|
189
|
+
SendEmailJob.perform_later(user_id) # Enqueue immediately
|
|
190
|
+
SendEmailJob.set(wait: 1.hour).perform_later(user_id) # Delay by duration
|
|
191
|
+
SendEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(user_id) # Run at specific time
|
|
192
|
+
SendEmailJob.set(queue: 'critical').perform_later(user_id) # Override queue
|
|
193
|
+
SendEmailJob.set(priority: 0).perform_later(user_id) # Override priority
|
|
194
|
+
|
|
195
|
+
# Postburner::Job (always tracked, full API)
|
|
196
|
+
job = ProcessPayment.create!(args: { 'payment_id' => 123 })
|
|
197
|
+
job.queue! # Enqueue immediately
|
|
198
|
+
job.queue!(delay: 1.hour) # Delay by duration
|
|
199
|
+
job.queue!(at: Date.tomorrow.noon) # Run at specific time
|
|
200
|
+
job.queue!(queue: 'critical') # Override queue
|
|
201
|
+
job.queue!(priority: 0, ttr: 600) # Set priority and TTR
|
|
202
|
+
```
|
|
188
203
|
|
|
189
|
-
|
|
204
|
+
### ActiveJob vs Postburner::Job TL;DR
|
|
205
|
+
|
|
206
|
+
`Postburner::Job` as simple subclass of `ActiveRecord`, so the normal
|
|
207
|
+
`ActiveRecord` API applies! Thus the workflow is to create an instance,
|
|
208
|
+
then queue it!
|
|
209
|
+
|
|
210
|
+
| Operation | ActiveJob | Postburner::Job |
|
|
211
|
+
|-----------|-----------|-----------------|
|
|
212
|
+
| **Enqueue immediately** | `MyJob.perform_later(args)` | `MyJob.create!(args: {}).queue!` |
|
|
213
|
+
| **Delay** | `.set(wait: 1.hour)` | `job.queue!(delay: 1.hour)` |
|
|
214
|
+
| **Run at** | `.set(wait_until: time)` | `job.queue!(at: time)` |
|
|
215
|
+
| **Set queue** | `.set(queue: 'critical')` | `job.queue!(queue: 'critical')` |
|
|
216
|
+
| **Set priority** | `.set(priority: 0)` | `job.queue!(priority: 0)` |
|
|
217
|
+
| **Set TTR** | `.set(ttr: 300)` | `job.queue!(ttr: 300)` |
|
|
218
|
+
|
|
219
|
+
## Usage
|
|
190
220
|
|
|
191
221
|
### Default Jobs
|
|
192
222
|
|
|
@@ -335,7 +365,7 @@ job.duration # Execution time in milliseconds
|
|
|
335
365
|
job.lag # Queue lag in milliseconds
|
|
336
366
|
```
|
|
337
367
|
|
|
338
|
-
###
|
|
368
|
+
### Postburner::Job Usage
|
|
339
369
|
|
|
340
370
|
Direct `Postburner::Job` subclasses are **always tracked**:
|
|
341
371
|
|
|
@@ -363,6 +393,15 @@ job.queue!(delay: 1.hour)
|
|
|
363
393
|
job.queue!(at: 2.days.from_now)
|
|
364
394
|
```
|
|
365
395
|
|
|
396
|
+
> **Note:** The `args` parameter in `perform(args)` is optional. It's a convenience accessor to `self.args`, which is stored in a JSONB column on the job record. You can omit the parameter and access args directly:
|
|
397
|
+
>
|
|
398
|
+
> ```ruby
|
|
399
|
+
> def perform
|
|
400
|
+
> payment = Payment.find(self.args['payment_id'])
|
|
401
|
+
> # ...
|
|
402
|
+
> end
|
|
403
|
+
> ```
|
|
404
|
+
|
|
366
405
|
#### Instance-Level Queue Configuration
|
|
367
406
|
|
|
368
407
|
Override queue priority and TTR per job instance for dynamic behavior:
|
|
@@ -775,6 +814,14 @@ job.errata # Array of exceptions with backtraces
|
|
|
775
814
|
job.attempts # Array of attempt timestamps
|
|
776
815
|
```
|
|
777
816
|
|
|
817
|
+
## Writing Jobs
|
|
818
|
+
|
|
819
|
+
Pay attention to the following when writing jobs:
|
|
820
|
+
|
|
821
|
+
**Job Idempotency:** Jobs should be designed to be idempotent and safely re-runnable. Like all job queues, Postburner provides at-least-once delivery—in rare errant cases outside of Postburner's control, a job may be executed more than once, i.e. network issues, etc.
|
|
822
|
+
|
|
823
|
+
**TTR (Time-to-Run):** If a job exceeds its TTR without completion, Beanstalkd releases it back to the queue while still running—causing duplicate execution. For long-running jobs, call `extend!` periodically to reset the TTR, or set a sufficiently high TTR value. You must include the `Postburner::Beanstalkd` or `Postburner::Tracked` module with `ActiveJob` to use `extend!`.
|
|
824
|
+
|
|
778
825
|
## Queue Strategies
|
|
779
826
|
|
|
780
827
|
Postburner uses different strategies to control job execution. These affect `Postburner::Job` subclasses (not ActiveJob classes).
|
|
@@ -134,7 +134,7 @@ module Postburner
|
|
|
134
134
|
|
|
135
135
|
run_callbacks :processing do
|
|
136
136
|
begin
|
|
137
|
-
self.perform(args)
|
|
137
|
+
method(:perform).arity == 0 ? self.perform : self.perform(args)
|
|
138
138
|
rescue Exception => exception
|
|
139
139
|
self.persist_metadata!
|
|
140
140
|
self.log! '[Postburner] Exception raised during perform prevented completion.'
|
|
@@ -37,10 +37,12 @@ module Postburner
|
|
|
37
37
|
# @param options [Hash] Queue options
|
|
38
38
|
# @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
|
|
39
39
|
# @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
|
|
40
|
-
# @option options [Integer] :
|
|
41
|
-
# @option options [Integer] :
|
|
40
|
+
# @option options [Integer] :priority Priority (0-4294967295, 0 = HIGHEST), sets instance attribute
|
|
41
|
+
# @option options [Integer] :pri Beanstalkd priority (pass-through, for backwards compatibility)
|
|
42
|
+
# @option options [Integer] :ttr Time-to-run in seconds (1-4294967295, 0 is silently changed to 1)
|
|
43
|
+
# @option options [String] :queue Queue name override
|
|
42
44
|
#
|
|
43
|
-
# @return [
|
|
45
|
+
# @return [true] on success (including if already queued)
|
|
44
46
|
#
|
|
45
47
|
# @raise [ActiveRecord::RecordInvalid] if job is not valid
|
|
46
48
|
# @raise [AlreadyProcessed] if job was already processed
|
|
@@ -57,17 +59,25 @@ module Postburner
|
|
|
57
59
|
# job.queue!(at: '2025-01-15 09:00:00'.in_time_zone)
|
|
58
60
|
# job.queue!(at: Time.parse('2025-01-15 09:00:00 EST'))
|
|
59
61
|
#
|
|
60
|
-
# @example Queue with priority
|
|
61
|
-
# job.queue!(
|
|
62
|
+
# @example Queue with priority and TTR
|
|
63
|
+
# job.queue!(priority: 0, ttr: 600)
|
|
64
|
+
#
|
|
65
|
+
# @example Queue to specific queue
|
|
66
|
+
# job.queue!(queue: 'critical', delay: 30.minutes)
|
|
62
67
|
#
|
|
63
68
|
# @see #requeue!
|
|
64
69
|
# @see Postburner.queue_strategy
|
|
65
70
|
#
|
|
66
71
|
def queue!(options={})
|
|
67
|
-
return if self.queued_at.present? && self.bkid.present?
|
|
72
|
+
return true if self.queued_at.present? && self.bkid.present?
|
|
68
73
|
raise ActiveRecord::RecordInvalid, "Can't queue unless valid." unless self.valid?
|
|
69
74
|
raise AlreadyProcessed, "Processed at #{self.processed_at}" if self.processed_at
|
|
70
75
|
|
|
76
|
+
# Extract and set instance-level overrides
|
|
77
|
+
self.priority = options.delete(:priority) if options.key?(:priority)
|
|
78
|
+
self.ttr = options.delete(:ttr) if options.key?(:ttr)
|
|
79
|
+
self.queue_name = options.delete(:queue) if options.key?(:queue)
|
|
80
|
+
|
|
71
81
|
at = options.delete(:at)
|
|
72
82
|
now = Time.current
|
|
73
83
|
|
|
@@ -86,6 +96,8 @@ module Postburner
|
|
|
86
96
|
run_callbacks :enqueue do
|
|
87
97
|
self.save!
|
|
88
98
|
end
|
|
99
|
+
|
|
100
|
+
true
|
|
89
101
|
end
|
|
90
102
|
|
|
91
103
|
# Re-queues an existing job by removing it from Beanstalkd and queueing again.
|
|
@@ -96,10 +108,11 @@ module Postburner
|
|
|
96
108
|
# @param options [Hash] Queue options (same as {#queue!})
|
|
97
109
|
# @option options [Time, ActiveSupport::Duration] :at Absolute time to run the job
|
|
98
110
|
# @option options [Integer, ActiveSupport::Duration] :delay Seconds to delay execution
|
|
99
|
-
# @option options [Integer] :
|
|
100
|
-
# @option options [Integer] :ttr Time-to-run in seconds
|
|
111
|
+
# @option options [Integer] :priority Priority (0-4294967295, lower = higher priority)
|
|
112
|
+
# @option options [Integer] :ttr Time-to-run in seconds (1-4294967295)
|
|
113
|
+
# @option options [String] :queue Queue name override
|
|
101
114
|
#
|
|
102
|
-
# @return [
|
|
115
|
+
# @return [true] on success
|
|
103
116
|
#
|
|
104
117
|
# @raise [ActiveRecord::RecordInvalid] if job is not valid
|
|
105
118
|
# @raise [Beaneater::NotConnected] if Beanstalkd connection fails
|
|
@@ -23,7 +23,7 @@ module Postburner
|
|
|
23
23
|
|
|
24
24
|
included do
|
|
25
25
|
# Instance-level queue configuration (overrides class-level defaults)
|
|
26
|
-
attr_writer :priority, :ttr
|
|
26
|
+
attr_writer :priority, :ttr, :queue_name
|
|
27
27
|
|
|
28
28
|
class_attribute :postburner_queue_name, default: 'default'
|
|
29
29
|
class_attribute :postburner_priority, default: nil
|
|
@@ -166,8 +166,13 @@ module Postburner
|
|
|
166
166
|
# job = MyJob.create!(args: {})
|
|
167
167
|
# job.queue_name # => 'critical'
|
|
168
168
|
#
|
|
169
|
+
# @example Instance-level override
|
|
170
|
+
# job = MyJob.create!(args: {})
|
|
171
|
+
# job.queue_name = 'urgent'
|
|
172
|
+
# job.queue_name # => 'urgent'
|
|
173
|
+
#
|
|
169
174
|
def queue_name
|
|
170
|
-
self.class.queue
|
|
175
|
+
@queue_name || self.class.queue
|
|
171
176
|
end
|
|
172
177
|
|
|
173
178
|
# Returns the full tube name with environment prefix.
|
|
@@ -96,13 +96,13 @@ module Postburner
|
|
|
96
96
|
#
|
|
97
97
|
# @abstract Subclasses must implement this method
|
|
98
98
|
#
|
|
99
|
-
# @param args [Hash] Job arguments from the args JSONB column
|
|
99
|
+
# @param args [Hash] Job arguments from the args JSONB column (optional)
|
|
100
100
|
#
|
|
101
101
|
# @return [void]
|
|
102
102
|
#
|
|
103
103
|
# @raise [NotImplementedError] if subclass does not implement this method
|
|
104
104
|
#
|
|
105
|
-
# @example
|
|
105
|
+
# @example With args parameter
|
|
106
106
|
# class ProcessPayment < Postburner::Job
|
|
107
107
|
# def perform(args)
|
|
108
108
|
# payment = Payment.find(args['payment_id'])
|
|
@@ -111,14 +111,23 @@ module Postburner
|
|
|
111
111
|
# end
|
|
112
112
|
# end
|
|
113
113
|
#
|
|
114
|
+
# @example Without args parameter (access via self.args)
|
|
115
|
+
# class CleanupJob < Postburner::Job
|
|
116
|
+
# def perform
|
|
117
|
+
# log "Cleaning up #{self.args['table']}"
|
|
118
|
+
# # self.args is always available
|
|
119
|
+
# end
|
|
120
|
+
# end
|
|
121
|
+
#
|
|
114
122
|
# @note Use {#log} or {#log!} within perform to add entries to the job's audit trail
|
|
115
123
|
# @note Exceptions will be caught, logged to errata, and re-raised
|
|
124
|
+
# @note Args are always accessible via self.args regardless of method signature
|
|
116
125
|
#
|
|
117
126
|
# @see #perform!
|
|
118
127
|
# @see #log
|
|
119
128
|
# @see #log_exception
|
|
120
129
|
#
|
|
121
|
-
def perform(args)
|
|
130
|
+
def perform(args=nil)
|
|
122
131
|
raise NotImplementedError, "Subclasses must implement the perform method"
|
|
123
132
|
end
|
|
124
133
|
|
data/lib/postburner/scheduler.rb
CHANGED
|
@@ -136,6 +136,16 @@ module Postburner
|
|
|
136
136
|
# scheduler.perform
|
|
137
137
|
#
|
|
138
138
|
def perform
|
|
139
|
+
# Self-deduplicate: if another watchdog exists in the queue, exit early
|
|
140
|
+
# and let that one handle scheduling. This naturally resolves duplicate
|
|
141
|
+
# watchdogs that can occur from race conditions.
|
|
142
|
+
@skip_requeue = false
|
|
143
|
+
if another_watchdog_queued?
|
|
144
|
+
logger.info "[Postburner::Scheduler] Another watchdog already queued, exiting to deduplicate"
|
|
145
|
+
@skip_requeue = true
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
139
149
|
logger.info "[Postburner::Scheduler] Starting scheduler run"
|
|
140
150
|
|
|
141
151
|
ActiveSupport::Notifications.instrument('perform_start.scheduler.postburner', {
|
|
@@ -161,15 +171,24 @@ module Postburner
|
|
|
161
171
|
|
|
162
172
|
# Use advisory lock to coordinate multiple workers
|
|
163
173
|
ActiveSupport::Notifications.instrument('perform.scheduler.postburner', payload) do
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
begin
|
|
175
|
+
lock_acquired = Postburner::AdvisoryLock.with_lock(AdvisoryLock::SCHEDULER_LOCK_KEY, blocking: false) do
|
|
176
|
+
process_all_schedules
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if lock_acquired
|
|
181
|
+
logger.info "[Postburner::Scheduler] Scheduler run complete"
|
|
182
|
+
else
|
|
183
|
+
logger.info "[Postburner::Scheduler] Could not acquire lock, skipping"
|
|
184
|
+
end
|
|
185
|
+
rescue ActiveRecord::ConnectionTimeoutError => e
|
|
186
|
+
# This can happen if the connection pool is exhausted
|
|
187
|
+
# Log cleanly and let the watchdog retry on next interval
|
|
188
|
+
logger.warn "[Postburner::Scheduler] Database connection pool exhausted. Skipping scheduler run (need advisory lock), will retry on next interval"
|
|
189
|
+
logger.debug "[Postburner::Scheduler] Check database.yml pool and max_connections i.e. pool >= needed connection count from web/job workers"
|
|
190
|
+
logger.debug "[Postburner::Scheduler] ActiveRecord Connection timeout: #{e.message}"
|
|
191
|
+
lock_acquired = false
|
|
173
192
|
end
|
|
174
193
|
|
|
175
194
|
# Update payload with final stats (mutates the hash subscribers receive)
|
|
@@ -180,8 +199,8 @@ module Postburner
|
|
|
180
199
|
payload[:orphans_enqueued] = @orphans_enqueued
|
|
181
200
|
end
|
|
182
201
|
ensure
|
|
183
|
-
#
|
|
184
|
-
requeue_watchdog
|
|
202
|
+
# Re-queue watchdog for next run (unless we're deduplicating)
|
|
203
|
+
requeue_watchdog unless @skip_requeue
|
|
185
204
|
end
|
|
186
205
|
|
|
187
206
|
# Class method to enqueue watchdog to Beanstalkd
|
|
@@ -321,6 +340,32 @@ module Postburner
|
|
|
321
340
|
|
|
322
341
|
private
|
|
323
342
|
|
|
343
|
+
# Check if another watchdog is already queued in Beanstalkd.
|
|
344
|
+
#
|
|
345
|
+
# Used for self-deduplication: if this watchdog sees another one queued,
|
|
346
|
+
# it exits early and lets that one handle scheduling. This resolves
|
|
347
|
+
# duplicate watchdogs that can occur from race conditions.
|
|
348
|
+
#
|
|
349
|
+
# @return [Boolean] true if another watchdog exists (ready or delayed)
|
|
350
|
+
#
|
|
351
|
+
# @api private
|
|
352
|
+
#
|
|
353
|
+
def another_watchdog_queued?
|
|
354
|
+
Postburner.connected do |conn|
|
|
355
|
+
tube_name = Postburner.scheduler_tube_name
|
|
356
|
+
tube = conn.beanstalk.tubes[tube_name]
|
|
357
|
+
stats = tube.stats
|
|
358
|
+
|
|
359
|
+
# Check for ready or delayed jobs (not counting reserved, which is us)
|
|
360
|
+
# Note: beaneater transforms hyphenated beanstalkd stats to underscores
|
|
361
|
+
queued_count = stats.current_jobs_ready.to_i + stats.current_jobs_delayed.to_i
|
|
362
|
+
queued_count > 0
|
|
363
|
+
end
|
|
364
|
+
rescue => e
|
|
365
|
+
logger.debug "[Postburner::Scheduler] Could not check for duplicate watchdogs: #{e.message}"
|
|
366
|
+
false # Assume no duplicate if we can't check
|
|
367
|
+
end
|
|
368
|
+
|
|
324
369
|
# Process all enabled schedules.
|
|
325
370
|
#
|
|
326
371
|
# Iterates through all enabled schedules and calls process_schedule for each.
|
|
@@ -472,6 +517,10 @@ module Postburner
|
|
|
472
517
|
|
|
473
518
|
self.class.enqueue_watchdog
|
|
474
519
|
end
|
|
520
|
+
rescue ActiveRecord::ConnectionTimeoutError => e
|
|
521
|
+
# Connection pool exhausted - workers will recreate watchdog on next timeout
|
|
522
|
+
logger.warn "[Postburner::Scheduler] Could not re-queue watchdog (connection pool exhausted), workers will recreate on timeout"
|
|
523
|
+
logger.debug "[Postburner::Scheduler] Connection timeout details: #{e.message}"
|
|
475
524
|
rescue => e
|
|
476
525
|
logger.error "[Postburner::Scheduler] Failed to re-queue watchdog: #{e.class} - #{e.message}"
|
|
477
526
|
# This is critical - if watchdog isn't re-queued, scheduling stops
|
data/lib/postburner/tube.rb
CHANGED
|
@@ -31,10 +31,9 @@ module Postburner
|
|
|
31
31
|
# Just pass the last known id to after for the next batch.
|
|
32
32
|
#
|
|
33
33
|
def jobs(count=20, limit: 1000, after: nil)
|
|
34
|
-
#
|
|
34
|
+
# Note: beaneater transforms hyphenated beanstalkd stats to underscores
|
|
35
35
|
stats = @tube.stats
|
|
36
|
-
|
|
37
|
-
tube_name = stats_hash['name']
|
|
36
|
+
tube_name = stats.name
|
|
38
37
|
|
|
39
38
|
jobs = Array.new
|
|
40
39
|
|
|
@@ -48,8 +47,7 @@ module Postburner
|
|
|
48
47
|
job = @tube.client.jobs.find(i)
|
|
49
48
|
if job
|
|
50
49
|
job_stats = job.stats
|
|
51
|
-
|
|
52
|
-
jobs << job if job_stats_hash['tube'] == tube_name
|
|
50
|
+
jobs << job if job_stats.tube == tube_name
|
|
53
51
|
end
|
|
54
52
|
break if jobs.length >= count
|
|
55
53
|
end
|
data/lib/postburner/version.rb
CHANGED
data/lib/postburner/worker.rb
CHANGED
|
@@ -549,9 +549,10 @@ module Postburner
|
|
|
549
549
|
|
|
550
550
|
# Handles job execution errors with retry logic.
|
|
551
551
|
#
|
|
552
|
-
# For tracked jobs and legacy Postburner::Job: Buries the job for inspection
|
|
553
|
-
# and emits retry_stopped.job.postburner event.
|
|
554
|
-
# For default ActiveJob: Applies exponential backoff retry with max 5 attempts
|
|
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.
|
|
554
|
+
# For default ActiveJob: Applies exponential backoff retry with max 5 attempts,
|
|
555
|
+
# reporting to Rails.error only on final discard.
|
|
555
556
|
#
|
|
556
557
|
# Instruments with ActiveSupport::Notifications:
|
|
557
558
|
# - retry_stopped.job.postburner: When tracked job is buried after failure
|
|
@@ -568,7 +569,22 @@ module Postburner
|
|
|
568
569
|
payload = JSON.parse(beanstalk_job.body)
|
|
569
570
|
|
|
570
571
|
if payload['tracked'] || Postburner::ActiveJob::Payload.legacy_format?(payload)
|
|
571
|
-
|
|
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
|
+
})
|
|
572
588
|
|
|
573
589
|
job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
|
|
574
590
|
ActiveSupport::Notifications.instrument('retry_stopped.job.postburner', {
|
|
@@ -590,7 +606,8 @@ module Postburner
|
|
|
590
606
|
# Handles retry logic for default jobs.
|
|
591
607
|
#
|
|
592
608
|
# Applies exponential backoff (2^retry_count seconds, max 1 hour).
|
|
593
|
-
# After 5 failed attempts, discards the job permanently
|
|
609
|
+
# After 5 failed attempts, discards the job permanently and reports
|
|
610
|
+
# to Rails.error for integration with error tracking services.
|
|
594
611
|
#
|
|
595
612
|
# Instruments with ActiveSupport::Notifications:
|
|
596
613
|
# - retry.job.postburner: When job is retried
|
|
@@ -634,6 +651,15 @@ module Postburner
|
|
|
634
651
|
|
|
635
652
|
logger.info "[Postburner] Retrying default job #{payload['job_id']}, attempt #{retry_count + 1} in #{delay}s"
|
|
636
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
|
+
|
|
637
663
|
ActiveSupport::Notifications.instrument('discard.job.postburner', {
|
|
638
664
|
job: job_payload,
|
|
639
665
|
beanstalk_job_id: beanstalk_job.id,
|
data/lib/postburner.rb
CHANGED
|
@@ -559,19 +559,18 @@ module Postburner
|
|
|
559
559
|
tubes_to_inspect.each do |tube|
|
|
560
560
|
begin
|
|
561
561
|
stats = tube.stats
|
|
562
|
-
#
|
|
563
|
-
stats_hash = stats.instance_variable_get(:@hash) || {}
|
|
562
|
+
# Note: beaneater transforms hyphenated beanstalkd stats to underscores
|
|
564
563
|
|
|
565
564
|
tube_data = {
|
|
566
565
|
name: tube.name,
|
|
567
|
-
ready:
|
|
568
|
-
delayed:
|
|
569
|
-
buried:
|
|
570
|
-
reserved:
|
|
571
|
-
total: (
|
|
572
|
-
(
|
|
573
|
-
(
|
|
574
|
-
(
|
|
566
|
+
ready: stats.current_jobs_ready || 0,
|
|
567
|
+
delayed: stats.current_jobs_delayed || 0,
|
|
568
|
+
buried: stats.current_jobs_buried || 0,
|
|
569
|
+
reserved: stats.current_jobs_reserved || 0,
|
|
570
|
+
total: (stats.current_jobs_ready || 0) +
|
|
571
|
+
(stats.current_jobs_delayed || 0) +
|
|
572
|
+
(stats.current_jobs_buried || 0) +
|
|
573
|
+
(stats.current_jobs_reserved || 0)
|
|
575
574
|
}
|
|
576
575
|
rescue Beaneater::NotFoundError
|
|
577
576
|
# Tube doesn't exist yet, skip it
|