postburner 0.8.0 → 1.0.0.pre.1
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/CHANGELOG.md +0 -22
- data/README.md +1219 -238
- data/Rakefile +1 -1
- data/app/concerns/postburner/callbacks.rb +286 -0
- data/app/models/postburner/job.rb +735 -46
- data/app/models/postburner/tracked_job.rb +83 -0
- data/bin/postburner +91 -0
- data/bin/rails +14 -0
- data/config/environment.rb +3 -0
- data/config/postburner.yml +22 -0
- data/config/postburner.yml.example +142 -0
- data/lib/postburner/active_job/adapter.rb +163 -0
- data/lib/postburner/active_job/execution.rb +109 -0
- data/lib/postburner/active_job/payload.rb +157 -0
- data/lib/postburner/beanstalkd.rb +97 -0
- data/lib/postburner/configuration.rb +202 -0
- data/lib/postburner/connection.rb +113 -0
- data/lib/postburner/engine.rb +1 -1
- data/lib/postburner/queue_config.rb +151 -0
- data/lib/postburner/strategies/immediate_test_queue.rb +133 -0
- data/lib/postburner/strategies/nice_queue.rb +85 -0
- data/lib/postburner/strategies/null_queue.rb +132 -0
- data/lib/postburner/strategies/queue.rb +119 -0
- data/lib/postburner/strategies/test_queue.rb +128 -0
- data/lib/postburner/time_helpers.rb +75 -0
- data/lib/postburner/tracked.rb +171 -0
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/workers/base.rb +210 -0
- data/lib/postburner/workers/worker.rb +480 -0
- data/lib/postburner.rb +433 -4
- metadata +66 -17
- data/lib/postburner/tube.rb +0 -53
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
# Test queue strategy with strict scheduling enforcement.
|
|
5
|
+
#
|
|
6
|
+
# This strategy executes jobs inline/synchronously without Beanstalkd, making
|
|
7
|
+
# tests fast and predictable. It raises {Postburner::Job::PrematurePerform} if
|
|
8
|
+
# a job has a future run_at, forcing you to use explicit time travel in tests.
|
|
9
|
+
#
|
|
10
|
+
# @note Auto-detected when Rails.env.test? and ActiveJob adapter is :test
|
|
11
|
+
#
|
|
12
|
+
# ## When to Use TestQueue
|
|
13
|
+
#
|
|
14
|
+
# Choose this strategy for test environments where you want:
|
|
15
|
+
# - **Explicit time management:** Forces you to use `travel_to` for scheduled jobs
|
|
16
|
+
# - **Strict testing:** Catches scheduling bugs by failing loudly
|
|
17
|
+
# - **Synchronous execution:** Jobs run immediately on `queue!` call
|
|
18
|
+
# - **No Beanstalkd:** Tests run without external dependencies
|
|
19
|
+
# - **Predictable timing:** Full control over when jobs execute
|
|
20
|
+
#
|
|
21
|
+
# This is ideal for unit/integration tests where you want to verify job
|
|
22
|
+
# scheduling logic and maintain explicit control over time progression.
|
|
23
|
+
#
|
|
24
|
+
# ## Strategy Behavior
|
|
25
|
+
#
|
|
26
|
+
# - **Execution:** Synchronous/inline (no Beanstalkd required)
|
|
27
|
+
# - **Testing mode:** Returns true
|
|
28
|
+
# - **Premature execution:** Raises {Postburner::Job::PrematurePerform} exception
|
|
29
|
+
# - **Beanstalkd:** Not used (bkid remains nil)
|
|
30
|
+
# - **Time travel:** Requires explicit `travel_to` for scheduled jobs
|
|
31
|
+
#
|
|
32
|
+
# ## Usage
|
|
33
|
+
#
|
|
34
|
+
# @example Automatically activated in Rails test environment
|
|
35
|
+
# # In test_helper.rb or rails_helper.rb
|
|
36
|
+
# # TestQueue is automatically set if Rails.env.test?
|
|
37
|
+
# # and ActiveJob.queue_adapter == :test
|
|
38
|
+
#
|
|
39
|
+
# @example Explicitly activate TestQueue
|
|
40
|
+
# Postburner.inline_test_strategy!
|
|
41
|
+
# job = MyJob.create!(args: { user_id: 123 })
|
|
42
|
+
# job.queue!
|
|
43
|
+
# # Job executes immediately and synchronously
|
|
44
|
+
# assert job.reload.processed_at
|
|
45
|
+
#
|
|
46
|
+
# @example Scheduled jobs require explicit time travel
|
|
47
|
+
# Postburner.inline_test_strategy!
|
|
48
|
+
# job = MyJob.create!(args: {})
|
|
49
|
+
#
|
|
50
|
+
# # This will raise PrematurePerform
|
|
51
|
+
# # job.queue!(delay: 1.hour)
|
|
52
|
+
#
|
|
53
|
+
# # Use time travel instead
|
|
54
|
+
# job.queue!(delay: 1.hour)
|
|
55
|
+
# travel_to(job.run_at) do
|
|
56
|
+
# # Job executes within the time travel block
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# @example Testing with scheduled jobs
|
|
60
|
+
# test "processes payment after delay" do
|
|
61
|
+
# Postburner.inline_test_strategy!
|
|
62
|
+
# job = ProcessPayment.create!(args: { payment_id: 123 })
|
|
63
|
+
#
|
|
64
|
+
# future_time = 2.hours.from_now
|
|
65
|
+
# job.queue!(at: future_time)
|
|
66
|
+
#
|
|
67
|
+
# travel_to(future_time) do
|
|
68
|
+
# # Job executes here
|
|
69
|
+
# assert job.reload.processed_at
|
|
70
|
+
# end
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# @see ImmediateTestQueue Test strategy with automatic time travel
|
|
74
|
+
# @see NiceQueue Default production strategy
|
|
75
|
+
# @see Postburner.inline_test_strategy!
|
|
76
|
+
#
|
|
77
|
+
class TestQueue < Queue
|
|
78
|
+
class << self
|
|
79
|
+
# Returns whether this strategy is for testing.
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean] Always true for test strategies
|
|
82
|
+
#
|
|
83
|
+
def testing
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Executes job inline/synchronously without Beanstalkd.
|
|
88
|
+
#
|
|
89
|
+
# Called automatically via after_save_commit hook when {Postburner::Job#queue!}
|
|
90
|
+
# is invoked. Executes the job immediately in the same process. If the job
|
|
91
|
+
# has a future run_at, raises {Postburner::Job::PrematurePerform} to force
|
|
92
|
+
# explicit time management with `travel_to`.
|
|
93
|
+
#
|
|
94
|
+
# @param job [Postburner::Job] The job to execute
|
|
95
|
+
# @param options [Hash] Unused in test mode
|
|
96
|
+
#
|
|
97
|
+
# @return [Hash] Status hash with :status => 'INLINE', :id => nil
|
|
98
|
+
#
|
|
99
|
+
# @raise [Postburner::Job::PrematurePerform] if job has future run_at
|
|
100
|
+
#
|
|
101
|
+
# @api private
|
|
102
|
+
#
|
|
103
|
+
def insert(job, options = {})
|
|
104
|
+
# Execute immediately in test mode
|
|
105
|
+
# Will raise PrematurePerform if run_at is in the future
|
|
106
|
+
job.perform!(job.args)
|
|
107
|
+
|
|
108
|
+
# Return format matching Backburner response (symbol keys)
|
|
109
|
+
{ status: 'INLINE', id: nil }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Handles jobs executed before their scheduled run_at time.
|
|
113
|
+
#
|
|
114
|
+
# This strategy raises an exception with a helpful error message,
|
|
115
|
+
# forcing developers to use explicit `travel_to` calls in tests.
|
|
116
|
+
#
|
|
117
|
+
# @param job [Postburner::Job] The job being executed prematurely
|
|
118
|
+
#
|
|
119
|
+
# @return [void]
|
|
120
|
+
#
|
|
121
|
+
# @raise [Postburner::Job::PrematurePerform] Always raises with helpful message
|
|
122
|
+
#
|
|
123
|
+
def handle_premature_perform(job)
|
|
124
|
+
raise Postburner::Job::PrematurePerform, "Job scheduled for #{job.run_at} (#{((job.run_at - Time.zone.now) / 60).round(1)} minutes from now). Use `travel_to(job.run_at)` in your test, or set `Postburner.inline_immediate_test_strategy!` to execute scheduled jobs immediately."
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
# Provides time travel utilities for testing without requiring test context.
|
|
5
|
+
#
|
|
6
|
+
# This module wraps ActiveSupport::Testing::TimeHelpers to enable time travel
|
|
7
|
+
# from non-test contexts (like class methods or service objects). It creates
|
|
8
|
+
# a helper object extended with Rails' TimeHelpers and delegates time travel
|
|
9
|
+
# operations to it.
|
|
10
|
+
#
|
|
11
|
+
# @note Requires ActiveSupport::Testing::TimeHelpers (Rails testing framework)
|
|
12
|
+
# @note Time travel affects global time via Time.stub - use only in test environments
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# @example Basic time travel
|
|
17
|
+
# include Postburner::TimeHelpers
|
|
18
|
+
#
|
|
19
|
+
# travel_to(2.days.from_now) do
|
|
20
|
+
# # Code here executes as if it's 2 days in the future
|
|
21
|
+
# Time.zone.now # => 2 days from now
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example In a class method
|
|
25
|
+
# class MyService
|
|
26
|
+
# extend Postburner::TimeHelpers
|
|
27
|
+
#
|
|
28
|
+
# def self.process_scheduled_task(time)
|
|
29
|
+
# travel_to(time) do
|
|
30
|
+
# # Execute task at specified time
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @see ActiveSupport::Testing::TimeHelpers
|
|
36
|
+
#
|
|
37
|
+
module TimeHelpers
|
|
38
|
+
# Travels to specified time for block execution using Rails time helpers.
|
|
39
|
+
#
|
|
40
|
+
# Creates a helper object extended with ActiveSupport::Testing::TimeHelpers
|
|
41
|
+
# and uses it to stub global time. This is necessary for time travel outside
|
|
42
|
+
# of test method contexts where TimeHelpers aren't available directly.
|
|
43
|
+
#
|
|
44
|
+
# The travel_to call stubs Time globally (using Time.stub), so all time-based
|
|
45
|
+
# operations within the block execute as if they're happening at the specified
|
|
46
|
+
# time. After the block completes, time returns to normal.
|
|
47
|
+
#
|
|
48
|
+
# @param time [Time, DateTime, ActiveSupport::TimeWithZone] The time to travel to
|
|
49
|
+
# @param block [Proc] Block to execute at the specified time
|
|
50
|
+
#
|
|
51
|
+
# @return [Object] The return value of the block
|
|
52
|
+
#
|
|
53
|
+
# @raise [RuntimeError] if ActiveSupport::Testing::TimeHelpers not available
|
|
54
|
+
#
|
|
55
|
+
# @example Travel to specific time
|
|
56
|
+
# travel_to(Time.zone.parse('2025-12-25 00:00:00')) do
|
|
57
|
+
# puts Time.zone.now # => 2025-12-25 00:00:00
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Travel relative to now
|
|
61
|
+
# travel_to(1.hour.from_now) do
|
|
62
|
+
# # Execute code as if 1 hour has passed
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
def travel_to(time, &block)
|
|
66
|
+
unless defined?(ActiveSupport::Testing::TimeHelpers)
|
|
67
|
+
raise "ActiveSupport::Testing::TimeHelpers not available. " \
|
|
68
|
+
"Postburner::TimeHelpers requires Rails testing helpers for time travel."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
helper = Object.new.extend(ActiveSupport::Testing::TimeHelpers)
|
|
72
|
+
helper.travel_to(time, &block)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
# Concern for ActiveJob classes to opt-in to PostgreSQL tracking.
|
|
5
|
+
#
|
|
6
|
+
# Simply include this module in your ActiveJob class to enable full audit
|
|
7
|
+
# trail persistence in PostgreSQL. Without this, jobs execute as "default" jobs
|
|
8
|
+
# (Beanstalkd only, no PostgreSQL overhead).
|
|
9
|
+
#
|
|
10
|
+
# @example Opt-in to tracking
|
|
11
|
+
# class ProcessPayment < ApplicationJob
|
|
12
|
+
# include Postburner::Tracked # ← Enables PostgreSQL audit trail
|
|
13
|
+
#
|
|
14
|
+
# def perform(payment_id)
|
|
15
|
+
# log "Processing payment #{payment_id}"
|
|
16
|
+
# # ... work ...
|
|
17
|
+
# log! "Payment processed successfully"
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Without tracking (default mode)
|
|
22
|
+
# class SendEmail < ApplicationJob
|
|
23
|
+
# # No Postburner::Tracked - executes as default job
|
|
24
|
+
#
|
|
25
|
+
# def perform(email)
|
|
26
|
+
# # Fast execution, no PostgreSQL overhead
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
module Tracked
|
|
31
|
+
extend ActiveSupport::Concern
|
|
32
|
+
|
|
33
|
+
included do
|
|
34
|
+
# Include Beanstalkd configuration DSL automatically
|
|
35
|
+
include Postburner::Beanstalkd
|
|
36
|
+
|
|
37
|
+
# Reference to Postburner::Job during execution (set by worker)
|
|
38
|
+
attr_accessor :postburner_job
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Appends a log message to the job's audit trail.
|
|
42
|
+
#
|
|
43
|
+
# Only available for tracked jobs. Logs are stored in the
|
|
44
|
+
# Postburner::Job record's `logs` JSONB array.
|
|
45
|
+
#
|
|
46
|
+
# @param message [String] Log message
|
|
47
|
+
# @param level [Symbol] Log level (:debug, :info, :warning, :error)
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# def perform(user_id)
|
|
53
|
+
# log "Starting user processing"
|
|
54
|
+
# # ... work ...
|
|
55
|
+
# log "Completed successfully", level: :info
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
def log(message, level: :info)
|
|
59
|
+
postburner_job&.log(message, level: level)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Appends a log message and immediately persists to database.
|
|
63
|
+
#
|
|
64
|
+
# Use this for important log messages that should be saved immediately
|
|
65
|
+
# rather than batched with other updates.
|
|
66
|
+
#
|
|
67
|
+
# @param message [String] Log message
|
|
68
|
+
# @param level [Symbol] Log level (:debug, :info, :warning, :error)
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# def perform(payment_id)
|
|
74
|
+
# log! "Critical: Processing high-value payment #{payment_id}"
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
def log!(message, level: :info)
|
|
78
|
+
postburner_job&.log!(message, level: level)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Tracks an exception in the job's errata array.
|
|
82
|
+
#
|
|
83
|
+
# Appends exception details (class, message, backtrace) to the
|
|
84
|
+
# in-memory errata array. Does NOT persist immediately.
|
|
85
|
+
#
|
|
86
|
+
# @param exception [Exception] The exception to track
|
|
87
|
+
#
|
|
88
|
+
# @return [void]
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# def perform(user_id)
|
|
92
|
+
# process_user(user_id)
|
|
93
|
+
# rescue => e
|
|
94
|
+
# log_exception(e)
|
|
95
|
+
# raise
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
def log_exception(exception)
|
|
99
|
+
postburner_job&.log_exception(exception)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Tracks an exception and immediately persists to database.
|
|
103
|
+
#
|
|
104
|
+
# @param exception [Exception] The exception to track
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# def perform(user_id)
|
|
110
|
+
# process_user(user_id)
|
|
111
|
+
# rescue CriticalError => e
|
|
112
|
+
# log_exception!(e)
|
|
113
|
+
# raise
|
|
114
|
+
# end
|
|
115
|
+
#
|
|
116
|
+
def log_exception!(exception)
|
|
117
|
+
postburner_job&.log_exception!(exception)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the Beanstalkd job object for direct queue operations.
|
|
121
|
+
#
|
|
122
|
+
# Provides access to the underlying Beaneater job object through the
|
|
123
|
+
# TrackedJob wrapper. Use this to perform Beanstalkd operations like
|
|
124
|
+
# touch, bury, release, etc.
|
|
125
|
+
#
|
|
126
|
+
# @return [Beaneater::Job, nil] Beanstalkd job object or nil if not available
|
|
127
|
+
#
|
|
128
|
+
# @example Extend TTR during long operation
|
|
129
|
+
# def perform(file_id)
|
|
130
|
+
# file = File.find(file_id)
|
|
131
|
+
# file.each_line do |line|
|
|
132
|
+
# # ... process line ...
|
|
133
|
+
# bk&.touch # Extend TTR
|
|
134
|
+
# end
|
|
135
|
+
# end
|
|
136
|
+
#
|
|
137
|
+
# @example Other Beanstalkd operations
|
|
138
|
+
# bk&.bury # Bury the job
|
|
139
|
+
# bk&.release(pri: 0) # Release with priority
|
|
140
|
+
# bk&.stats # Get job statistics
|
|
141
|
+
#
|
|
142
|
+
# @see #extend!
|
|
143
|
+
#
|
|
144
|
+
def bk
|
|
145
|
+
postburner_job&.bk
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Extends the job's time-to-run (TTR) in Beanstalkd.
|
|
149
|
+
#
|
|
150
|
+
# Convenience method that calls touch on the Beanstalkd job, extending
|
|
151
|
+
# the TTR by the original TTR value. Use this during long-running operations
|
|
152
|
+
# to prevent the job from timing out.
|
|
153
|
+
#
|
|
154
|
+
# @return [void]
|
|
155
|
+
#
|
|
156
|
+
# @example Process large file line by line
|
|
157
|
+
# def perform(file_id)
|
|
158
|
+
# file = File.find(file_id)
|
|
159
|
+
# file.each_line do |line|
|
|
160
|
+
# # ... process line ...
|
|
161
|
+
# extend! # Extend TTR to prevent timeout
|
|
162
|
+
# end
|
|
163
|
+
# end
|
|
164
|
+
#
|
|
165
|
+
# @see #bk
|
|
166
|
+
#
|
|
167
|
+
def extend!
|
|
168
|
+
postburner_job&.extend!
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
data/lib/postburner/version.rb
CHANGED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
module Workers
|
|
5
|
+
# Base worker class with shared functionality for all worker types.
|
|
6
|
+
#
|
|
7
|
+
# Provides common methods for signal handling, job execution, error handling,
|
|
8
|
+
# and retry logic. Subclasses implement the specific execution strategy
|
|
9
|
+
# (simple, forking, threads_on_fork).
|
|
10
|
+
#
|
|
11
|
+
class Base
|
|
12
|
+
attr_reader :config, :logger
|
|
13
|
+
|
|
14
|
+
# @param config [Postburner::Configuration] Worker configuration
|
|
15
|
+
#
|
|
16
|
+
def initialize(config)
|
|
17
|
+
@config = config
|
|
18
|
+
@logger = config.logger
|
|
19
|
+
@shutdown = false
|
|
20
|
+
setup_signal_handlers
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Starts the worker loop.
|
|
24
|
+
#
|
|
25
|
+
# Subclasses must implement this method to define their execution strategy.
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
#
|
|
29
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
30
|
+
#
|
|
31
|
+
def start
|
|
32
|
+
raise NotImplementedError, "Subclasses must implement #start"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Initiates graceful shutdown.
|
|
36
|
+
#
|
|
37
|
+
# Sets shutdown flag to stop processing new jobs. Current jobs
|
|
38
|
+
# are allowed to finish.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
#
|
|
42
|
+
def shutdown
|
|
43
|
+
@shutdown = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Checks if shutdown has been requested.
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean] true if shutdown requested, false otherwise
|
|
49
|
+
#
|
|
50
|
+
def shutdown?
|
|
51
|
+
@shutdown
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
# Sets up signal handlers for graceful shutdown.
|
|
57
|
+
#
|
|
58
|
+
# TERM and INT signals trigger graceful shutdown.
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
#
|
|
62
|
+
def setup_signal_handlers
|
|
63
|
+
Signal.trap('TERM') { shutdown }
|
|
64
|
+
Signal.trap('INT') { shutdown }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Expands queue name to full tube name with environment prefix.
|
|
68
|
+
#
|
|
69
|
+
# Delegates to Postburner::Configuration#expand_tube_name.
|
|
70
|
+
#
|
|
71
|
+
# @param queue_name [String] Base queue name
|
|
72
|
+
#
|
|
73
|
+
# @return [String] Full tube name (e.g., 'postburner.production.critical')
|
|
74
|
+
#
|
|
75
|
+
def expand_tube_name(queue_name)
|
|
76
|
+
config.expand_tube_name(queue_name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Executes a job from Beanstalkd.
|
|
80
|
+
#
|
|
81
|
+
# Delegates to Postburner::ActiveJob::Execution to handle default,
|
|
82
|
+
# tracked, and legacy job formats.
|
|
83
|
+
#
|
|
84
|
+
# @param beanstalk_job [Beaneater::Job] Job from Beanstalkd
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
#
|
|
88
|
+
def execute_job(beanstalk_job)
|
|
89
|
+
logger.info "[Postburner] Executing #{beanstalk_job.class.name} #{beanstalk_job.id}"
|
|
90
|
+
Postburner::ActiveJob::Execution.execute(beanstalk_job.body)
|
|
91
|
+
logger.info "[Postburner] Deleting #{beanstalk_job.class.name} #{beanstalk_job.id} (success)"
|
|
92
|
+
beanstalk_job.delete
|
|
93
|
+
rescue => e
|
|
94
|
+
handle_error(beanstalk_job, e)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Handles job execution errors with retry logic.
|
|
98
|
+
#
|
|
99
|
+
# Implements retry strategy:
|
|
100
|
+
# - Parses payload to determine job type
|
|
101
|
+
# - For default jobs: manages retry count, re-queues with backoff
|
|
102
|
+
# - For tracked jobs: buries job (Postburner::Job handles retries)
|
|
103
|
+
# - For legacy jobs: buries job
|
|
104
|
+
#
|
|
105
|
+
# @param beanstalk_job [Beaneater::Job] Job from Beanstalkd
|
|
106
|
+
# @param error [Exception] The exception that was raised
|
|
107
|
+
#
|
|
108
|
+
# @return [void]
|
|
109
|
+
#
|
|
110
|
+
def handle_error(beanstalk_job, error)
|
|
111
|
+
logger.error "[Postburner] Job failed: #{error.class} - #{error.message}"
|
|
112
|
+
logger.error error.backtrace.join("\n")
|
|
113
|
+
|
|
114
|
+
begin
|
|
115
|
+
payload = JSON.parse(beanstalk_job.body)
|
|
116
|
+
|
|
117
|
+
if payload['tracked'] || Postburner::ActiveJob::Payload.legacy_format?(payload)
|
|
118
|
+
# Tracked and legacy jobs: bury for inspection
|
|
119
|
+
# (Postburner::Job has its own retry logic)
|
|
120
|
+
logger.info "[Postburner] Burying tracked/legacy job for inspection"
|
|
121
|
+
beanstalk_job.bury
|
|
122
|
+
else
|
|
123
|
+
# Default job: handle retry logic
|
|
124
|
+
handle_default_retry(beanstalk_job, payload, error)
|
|
125
|
+
end
|
|
126
|
+
rescue => retry_error
|
|
127
|
+
logger.error "[Postburner] Error handling failure: #{retry_error.message}"
|
|
128
|
+
beanstalk_job.bury rescue nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Handles retry logic for default jobs.
|
|
133
|
+
#
|
|
134
|
+
# Checks ActiveJob's retry_on configuration, increments retry count,
|
|
135
|
+
# and re-queues with exponential backoff if retries remaining.
|
|
136
|
+
#
|
|
137
|
+
# @param beanstalk_job [Beaneater::Job] Job from Beanstalkd
|
|
138
|
+
# @param payload [Hash] Parsed job payload
|
|
139
|
+
# @param error [Exception] The exception that was raised
|
|
140
|
+
#
|
|
141
|
+
# @return [void]
|
|
142
|
+
#
|
|
143
|
+
def handle_default_retry(beanstalk_job, payload, error)
|
|
144
|
+
retry_count = payload['retry_count'] || 0
|
|
145
|
+
job_class = payload['job_class'].constantize
|
|
146
|
+
|
|
147
|
+
# Check if job class wants to retry this error
|
|
148
|
+
# (This is simplified - full implementation would check retry_on config)
|
|
149
|
+
max_retries = 5 # Default max retries
|
|
150
|
+
|
|
151
|
+
if retry_count < max_retries
|
|
152
|
+
# Increment retry count
|
|
153
|
+
payload['retry_count'] = retry_count + 1
|
|
154
|
+
payload['executions'] = (payload['executions'] || 0) + 1
|
|
155
|
+
|
|
156
|
+
# Calculate backoff delay (exponential: 1s, 2s, 4s, 8s, 16s...)
|
|
157
|
+
delay = calculate_backoff(retry_count)
|
|
158
|
+
|
|
159
|
+
# Delete old job and insert new one with updated payload
|
|
160
|
+
beanstalk_job.delete
|
|
161
|
+
|
|
162
|
+
Postburner.connected do |conn|
|
|
163
|
+
tube_name = expand_tube_name(payload['queue_name'])
|
|
164
|
+
conn.tubes[tube_name].put(
|
|
165
|
+
JSON.generate(payload),
|
|
166
|
+
pri: payload['priority'] || 0,
|
|
167
|
+
delay: delay,
|
|
168
|
+
ttr: 120
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
logger.info "[Postburner] Retrying default job #{payload['job_id']}, attempt #{retry_count + 1} in #{delay}s"
|
|
173
|
+
else
|
|
174
|
+
# Max retries exceeded
|
|
175
|
+
logger.error "[Postburner] Discarding default job #{payload['job_id']} after #{retry_count} retries"
|
|
176
|
+
beanstalk_job.delete
|
|
177
|
+
# TODO: Call after_discard callback if configured
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Calculates exponential backoff delay for retries.
|
|
182
|
+
#
|
|
183
|
+
# @param retry_count [Integer] Number of retries so far
|
|
184
|
+
#
|
|
185
|
+
# @return [Integer] Delay in seconds (capped at 1 hour)
|
|
186
|
+
#
|
|
187
|
+
def calculate_backoff(retry_count)
|
|
188
|
+
# Exponential backoff: 2^retry_count, capped at 3600 seconds (1 hour)
|
|
189
|
+
[2 ** retry_count, 3600].min
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Watches the configured queues in Beanstalkd.
|
|
193
|
+
#
|
|
194
|
+
# @param connection [Postburner::Connection] Beanstalkd connection
|
|
195
|
+
# @param queue_name [String, nil] Optional specific queue name to watch (watches all if nil)
|
|
196
|
+
#
|
|
197
|
+
# @return [void]
|
|
198
|
+
#
|
|
199
|
+
def watch_queues(connection, queue_name = nil)
|
|
200
|
+
if queue_name
|
|
201
|
+
tube_name = config.expand_tube_name(queue_name)
|
|
202
|
+
connection.beanstalk.tubes.watch!(tube_name)
|
|
203
|
+
else
|
|
204
|
+
tube_names = config.expanded_tube_names
|
|
205
|
+
connection.beanstalk.tubes.watch!(*tube_names)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|