postburner 0.7.2 → 0.9.0.rc.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 -13
- data/README.md +441 -17
- data/Rakefile +1 -1
- data/app/concerns/postburner/callbacks.rb +286 -0
- data/app/models/postburner/job.rb +609 -44
- 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 +105 -0
- data/lib/postburner/strategies/test_queue.rb +128 -0
- data/lib/postburner/time_helpers.rb +75 -0
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner.rb +359 -0
- metadata +26 -8
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../time_helpers'
|
|
4
|
+
|
|
5
|
+
module Postburner
|
|
6
|
+
# Test queue strategy with automatic time travel for scheduled jobs.
|
|
7
|
+
#
|
|
8
|
+
# This strategy executes jobs inline/synchronously like {TestQueue}, but
|
|
9
|
+
# automatically uses time travel for scheduled jobs instead of raising
|
|
10
|
+
# exceptions. When a job has a future run_at, it travels to that time,
|
|
11
|
+
# executes the job, then returns to the present.
|
|
12
|
+
#
|
|
13
|
+
# @note Requires ActiveSupport::Testing::TimeHelpers (Rails testing framework)
|
|
14
|
+
#
|
|
15
|
+
# ## When to Use ImmediateTestQueue
|
|
16
|
+
#
|
|
17
|
+
# Choose this strategy for test environments where you want:
|
|
18
|
+
# - **Automatic time management:** No need to manually call `travel_to`
|
|
19
|
+
# - **Simple scheduled job testing:** Test delayed/scheduled jobs without boilerplate
|
|
20
|
+
# - **Fast tests:** Jobs execute immediately regardless of schedule
|
|
21
|
+
# - **No Beanstalkd:** Tests run without external dependencies
|
|
22
|
+
# - **Convenience over control:** Less explicit but easier to use than {TestQueue}
|
|
23
|
+
#
|
|
24
|
+
# This is ideal for integration/feature tests where you want to verify that
|
|
25
|
+
# scheduled jobs execute correctly without managing time travel yourself. The
|
|
26
|
+
# tradeoff is less explicit control over timing compared to {TestQueue}.
|
|
27
|
+
#
|
|
28
|
+
# ## Strategy Behavior
|
|
29
|
+
#
|
|
30
|
+
# - **Execution:** Synchronous/inline (no Beanstalkd required)
|
|
31
|
+
# - **Testing mode:** Returns true
|
|
32
|
+
# - **Premature execution:** Automatically travels to run_at and executes
|
|
33
|
+
# - **Beanstalkd:** Not used (bkid remains nil)
|
|
34
|
+
# - **Time travel:** Automatic using ActiveSupport::Testing::TimeHelpers
|
|
35
|
+
# - **Execution order:** Jobs may execute out of intended chronological order
|
|
36
|
+
#
|
|
37
|
+
# ## How Time Travel Works
|
|
38
|
+
#
|
|
39
|
+
# When a job with future run_at is queued:
|
|
40
|
+
# 1. Detects job.run_at > Time.zone.now
|
|
41
|
+
# 2. Calls `travel_to(job.run_at)` to stub global time
|
|
42
|
+
# 3. Executes job at the scheduled time (Time.zone.now == job.run_at)
|
|
43
|
+
# 4. Returns to present time after execution
|
|
44
|
+
# 5. Job timestamps reflect the scheduled time, not actual time
|
|
45
|
+
#
|
|
46
|
+
# @note Time is stubbed globally during execution - side effects may occur
|
|
47
|
+
# @note Multiple scheduled jobs execute in queue order, not scheduled order
|
|
48
|
+
#
|
|
49
|
+
# ## Usage
|
|
50
|
+
#
|
|
51
|
+
# @example Explicitly activate ImmediateTestQueue
|
|
52
|
+
# Postburner.inline_immediate_test_strategy!
|
|
53
|
+
# job = MyJob.create!(args: { user_id: 123 })
|
|
54
|
+
# job.queue!(delay: 1.hour)
|
|
55
|
+
# # Job executes immediately at scheduled time
|
|
56
|
+
# assert job.reload.processed_at
|
|
57
|
+
#
|
|
58
|
+
# @example Scheduled jobs execute automatically
|
|
59
|
+
# Postburner.inline_immediate_test_strategy!
|
|
60
|
+
# job = SendReminderEmail.create!(args: { user_id: 123 })
|
|
61
|
+
#
|
|
62
|
+
# # Job executes immediately despite 2-day delay
|
|
63
|
+
# job.queue!(delay: 2.days)
|
|
64
|
+
#
|
|
65
|
+
# # Timestamps reflect the scheduled time
|
|
66
|
+
# assert_equal 2.days.from_now.to_i, job.reload.run_at.to_i
|
|
67
|
+
# assert_not_nil job.processed_at
|
|
68
|
+
#
|
|
69
|
+
# @example Multiple jobs execute in queue order, not schedule order
|
|
70
|
+
# Postburner.inline_immediate_test_strategy!
|
|
71
|
+
#
|
|
72
|
+
# job1 = MyJob.create!(args: { id: 1 })
|
|
73
|
+
# job2 = MyJob.create!(args: { id: 2 })
|
|
74
|
+
#
|
|
75
|
+
# job1.queue!(delay: 2.days) # Executes first (queued first)
|
|
76
|
+
# job2.queue!(delay: 1.hour) # Executes second (queued second)
|
|
77
|
+
#
|
|
78
|
+
# # Both are processed, but job1 executed before job2
|
|
79
|
+
# # despite job2 having an earlier scheduled time
|
|
80
|
+
#
|
|
81
|
+
# @example Testing feature with scheduled job
|
|
82
|
+
# test "sends reminder email after 24 hours" do
|
|
83
|
+
# Postburner.inline_immediate_test_strategy!
|
|
84
|
+
#
|
|
85
|
+
# user = users(:john)
|
|
86
|
+
# reminder = ReminderJob.create!(args: { user_id: user.id })
|
|
87
|
+
# reminder.queue!(delay: 24.hours)
|
|
88
|
+
#
|
|
89
|
+
# # Job executes immediately at scheduled time
|
|
90
|
+
# assert reminder.reload.processed_at
|
|
91
|
+
# assert_emails 1
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# @see TestQueue Test strategy requiring explicit time travel
|
|
95
|
+
# @see NiceQueue Default production strategy
|
|
96
|
+
# @see Postburner.inline_immediate_test_strategy!
|
|
97
|
+
#
|
|
98
|
+
class ImmediateTestQueue < TestQueue
|
|
99
|
+
class << self
|
|
100
|
+
include TimeHelpers
|
|
101
|
+
|
|
102
|
+
# Executes job inline with automatic time travel for scheduled jobs.
|
|
103
|
+
#
|
|
104
|
+
# Called automatically via after_save_commit hook when {Postburner::Job#queue!}
|
|
105
|
+
# is invoked. If the job has a future run_at, travels to that time before
|
|
106
|
+
# execution. Otherwise executes immediately.
|
|
107
|
+
#
|
|
108
|
+
# @param job [Postburner::Job] The job to execute
|
|
109
|
+
# @param options [Hash] Unused in test mode
|
|
110
|
+
#
|
|
111
|
+
# @return [Hash] Status hash with :status => 'INLINE', :id => nil
|
|
112
|
+
#
|
|
113
|
+
# @raise [RuntimeError] if ActiveSupport::Testing::TimeHelpers not available
|
|
114
|
+
#
|
|
115
|
+
# @api private
|
|
116
|
+
#
|
|
117
|
+
def insert(job, options = {})
|
|
118
|
+
# If job has a future run_at, travel to that time for execution
|
|
119
|
+
if job.run_at && job.run_at > Time.zone.now
|
|
120
|
+
travel_to(job.run_at) do
|
|
121
|
+
job.perform!(job.args)
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
# No future run_at, execute normally
|
|
125
|
+
job.perform!(job.args)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Return format matching Backburner response (symbol keys)
|
|
129
|
+
{ status: 'INLINE', id: nil }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
# Default production queue strategy with graceful handling of premature execution.
|
|
5
|
+
#
|
|
6
|
+
# This is the recommended production strategy and the default for Postburner.
|
|
7
|
+
# Unlike the strict {Queue} strategy, NiceQueue automatically re-inserts jobs
|
|
8
|
+
# that are executed before their scheduled run_at time, ensuring they run at
|
|
9
|
+
# the correct time without raising exceptions.
|
|
10
|
+
#
|
|
11
|
+
# @note This is the DEFAULT strategy set on Postburner initialization
|
|
12
|
+
#
|
|
13
|
+
# ## When to Use NiceQueue
|
|
14
|
+
#
|
|
15
|
+
# Choose this strategy for production environments:
|
|
16
|
+
# - **Default choice:** This should be your go-to production strategy
|
|
17
|
+
# - **Graceful recovery:** Automatically handles scheduling edge cases
|
|
18
|
+
# - **Worker restarts:** Handles jobs picked up during worker restarts
|
|
19
|
+
# - **Clock drift:** Tolerates minor time synchronization issues
|
|
20
|
+
# - **Robust operation:** Prevents job failures due to timing issues
|
|
21
|
+
#
|
|
22
|
+
# This strategy provides the most forgiving and robust behavior for production
|
|
23
|
+
# systems where you want scheduled jobs to execute correctly even if workers
|
|
24
|
+
# pick them up slightly early.
|
|
25
|
+
#
|
|
26
|
+
# ## Strategy Behavior
|
|
27
|
+
#
|
|
28
|
+
# - **Execution:** Asynchronous via Beanstalkd workers
|
|
29
|
+
# - **Testing mode:** Returns false
|
|
30
|
+
# - **Premature execution:** Re-inserts job with calculated delay, logs the event
|
|
31
|
+
# - **Beanstalkd:** Required and used for all job queueing
|
|
32
|
+
#
|
|
33
|
+
# ## How Premature Execution Works
|
|
34
|
+
#
|
|
35
|
+
# When a job is executed before its run_at time:
|
|
36
|
+
# 1. Calculates remaining delay: `job.run_at - Time.zone.now`
|
|
37
|
+
# 2. Re-inserts job to Beanstalkd with calculated delay
|
|
38
|
+
# 3. Logs "PREMATURE; RE-INSERTED" message to job logs
|
|
39
|
+
# 4. Returns without executing the job
|
|
40
|
+
# 5. Job executes normally when picked up at the correct time
|
|
41
|
+
#
|
|
42
|
+
# ## Usage
|
|
43
|
+
#
|
|
44
|
+
# @example Default behavior (automatically set)
|
|
45
|
+
# # No configuration needed - NiceQueue is the default
|
|
46
|
+
# job = MyJob.create!(args: { user_id: 123 })
|
|
47
|
+
# job.queue!(delay: 1.hour)
|
|
48
|
+
#
|
|
49
|
+
# @example Explicitly activate NiceQueue
|
|
50
|
+
# Postburner.nice_async_strategy!
|
|
51
|
+
# job = MyJob.create!(args: {})
|
|
52
|
+
# job.queue!(at: Time.zone.now + 2.days)
|
|
53
|
+
#
|
|
54
|
+
# @example Premature execution is handled gracefully
|
|
55
|
+
# job = MyJob.create!(args: {})
|
|
56
|
+
# job.queue!(delay: 1.hour)
|
|
57
|
+
# # Worker picks up job 5 minutes early...
|
|
58
|
+
# # Job is automatically re-inserted with 55-minute delay
|
|
59
|
+
# # "PREMATURE; RE-INSERTED" logged to job.logs
|
|
60
|
+
#
|
|
61
|
+
# @see Queue Strict production strategy that raises on premature execution
|
|
62
|
+
# @see TestQueue Test strategy with inline execution
|
|
63
|
+
# @see Postburner.nice_async_strategy!
|
|
64
|
+
#
|
|
65
|
+
class NiceQueue < Queue
|
|
66
|
+
class << self
|
|
67
|
+
# Handles jobs executed before their scheduled run_at time.
|
|
68
|
+
#
|
|
69
|
+
# Unlike the parent {Queue} strategy, NiceQueue gracefully handles premature
|
|
70
|
+
# execution by re-inserting the job with the appropriate delay instead of
|
|
71
|
+
# raising an exception.
|
|
72
|
+
#
|
|
73
|
+
# @param job [Postburner::Job] The job being executed prematurely
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
#
|
|
77
|
+
# @note Logs "PREMATURE; RE-INSERTED" message to job's audit trail
|
|
78
|
+
#
|
|
79
|
+
def handle_premature_perform(job)
|
|
80
|
+
response = job.insert! delay: job.run_at - Time.zone.now
|
|
81
|
+
job.log! "PREMATURE; RE-INSERTED: #{response}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../time_helpers'
|
|
4
|
+
|
|
5
|
+
module Postburner
|
|
6
|
+
# Null queue strategy for creating jobs without queueing to Beanstalkd.
|
|
7
|
+
#
|
|
8
|
+
# This strategy creates job records in the database but does NOT queue them
|
|
9
|
+
# to Beanstalkd. Jobs can be manually executed later using {.handle_perform!},
|
|
10
|
+
# which includes automatic time travel for scheduled jobs.
|
|
11
|
+
#
|
|
12
|
+
# @note This is a test-mode strategy (returns true for testing)
|
|
13
|
+
#
|
|
14
|
+
# ## When to Use NullQueue
|
|
15
|
+
#
|
|
16
|
+
# Choose this strategy when you want:
|
|
17
|
+
# - **Deferred batch processing:** Create many jobs upfront, execute them manually later
|
|
18
|
+
# - **Conditional execution:** Queue jobs that may or may not execute based on runtime conditions
|
|
19
|
+
# - **Testing/debugging:** Inspect jobs before execution without auto-execution
|
|
20
|
+
# - **Manual control:** Explicitly control when jobs execute
|
|
21
|
+
# - **No Beanstalkd:** Create jobs without requiring Beanstalkd server
|
|
22
|
+
#
|
|
23
|
+
# ## Strategy Behavior
|
|
24
|
+
#
|
|
25
|
+
# - **Execution:** Manual via {.handle_perform!} call
|
|
26
|
+
# - **Testing mode:** Returns true
|
|
27
|
+
# - **Queueing:** Jobs created in database but NOT sent to Beanstalkd
|
|
28
|
+
# - **Beanstalkd:** Not used (bkid remains nil)
|
|
29
|
+
# - **Time travel:** Automatic for scheduled jobs during manual execution
|
|
30
|
+
#
|
|
31
|
+
# ## Usage
|
|
32
|
+
#
|
|
33
|
+
# @example Create jobs without queueing
|
|
34
|
+
# Postburner.null_strategy!
|
|
35
|
+
# job1 = MyJob.create!(args: { id: 1 })
|
|
36
|
+
# job2 = MyJob.create!(args: { id: 2 })
|
|
37
|
+
#
|
|
38
|
+
# job1.queue!
|
|
39
|
+
# job2.queue!(delay: 1.hour)
|
|
40
|
+
#
|
|
41
|
+
# # Jobs created but NOT queued to Beanstalkd
|
|
42
|
+
# job1.bkid # => nil
|
|
43
|
+
# job2.bkid # => nil
|
|
44
|
+
#
|
|
45
|
+
# @example Manually execute jobs later
|
|
46
|
+
# # Later, execute manually
|
|
47
|
+
# Postburner::Job.perform(job1.id) # Executes immediately
|
|
48
|
+
# Postburner::Job.perform(job2.id) # Time travels to scheduled time
|
|
49
|
+
#
|
|
50
|
+
# job1.reload.processed_at # => present
|
|
51
|
+
# job2.reload.processed_at # => present
|
|
52
|
+
#
|
|
53
|
+
# @example Batch processing pattern
|
|
54
|
+
# Postburner.null_strategy!
|
|
55
|
+
#
|
|
56
|
+
# # Create 1000 jobs
|
|
57
|
+
# jobs = 1000.times.map do |i|
|
|
58
|
+
# job = ProcessRecord.create!(args: { record_id: i })
|
|
59
|
+
# job.queue!
|
|
60
|
+
# job
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# # Execute in batches
|
|
64
|
+
# jobs.each_slice(100) do |batch|
|
|
65
|
+
# batch.each { |job| Postburner::Job.perform(job.id) }
|
|
66
|
+
# sleep 1 # Rate limiting
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# @see TestQueue Parent strategy class
|
|
70
|
+
# @see Postburner.null_strategy!
|
|
71
|
+
# @see TimeHelpers
|
|
72
|
+
#
|
|
73
|
+
class NullQueue < TestQueue
|
|
74
|
+
class << self
|
|
75
|
+
include TimeHelpers
|
|
76
|
+
|
|
77
|
+
# Does NOT insert job into Beanstalkd queue.
|
|
78
|
+
#
|
|
79
|
+
# Called automatically via after_save_commit hook when {Postburner::Job#queue!}
|
|
80
|
+
# is invoked. Unlike other strategies, this does NOT queue to Beanstalkd - it
|
|
81
|
+
# only creates the job record in the database.
|
|
82
|
+
#
|
|
83
|
+
# @param job [Postburner::Job] The job (not queued)
|
|
84
|
+
# @param options [Hash] Unused in null mode
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash] Status hash with :status => 'NULL', :id => nil
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
#
|
|
90
|
+
def insert(job, options = {})
|
|
91
|
+
# Return format matching Backburner response (symbol keys)
|
|
92
|
+
{ status: 'NULL', id: nil }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Executes a job with automatic time travel for scheduled jobs.
|
|
96
|
+
#
|
|
97
|
+
# Called by {Postburner::Job.perform} when NullQueue strategy is active.
|
|
98
|
+
# If the job has a future run_at, it automatically travels to that time
|
|
99
|
+
# before execution. Otherwise executes immediately.
|
|
100
|
+
#
|
|
101
|
+
# Users should call {Postburner::Job.perform} instead of this method directly.
|
|
102
|
+
#
|
|
103
|
+
# @param job [Postburner::Job] The job to execute
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
106
|
+
#
|
|
107
|
+
# @raise [RuntimeError] if ActiveSupport::Testing::TimeHelpers not available
|
|
108
|
+
#
|
|
109
|
+
# @example Execute via Job.perform (recommended)
|
|
110
|
+
# Postburner.null_strategy!
|
|
111
|
+
# job = MyJob.create!(args: {})
|
|
112
|
+
# job.queue!(delay: 1.hour)
|
|
113
|
+
# Postburner::Job.perform(job.id) # Delegates to this method
|
|
114
|
+
# job.reload.processed_at # => present
|
|
115
|
+
#
|
|
116
|
+
# @see Postburner::Job.perform
|
|
117
|
+
# @see TimeHelpers#travel_to
|
|
118
|
+
# @api private
|
|
119
|
+
#
|
|
120
|
+
def handle_perform!(job)
|
|
121
|
+
if job.run_at && job.run_at > Time.zone.now
|
|
122
|
+
travel_to(job.run_at) do
|
|
123
|
+
job.perform!(job.args)
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
# No future run_at, execute normally
|
|
127
|
+
job.perform!(job.args)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
# Base production queue strategy that queues jobs to Beanstalkd asynchronously.
|
|
5
|
+
#
|
|
6
|
+
# This is the strict production strategy that raises an exception if a job
|
|
7
|
+
# is executed before its scheduled run_at time. Unlike {NiceQueue}, it does
|
|
8
|
+
# not automatically re-insert premature jobs.
|
|
9
|
+
#
|
|
10
|
+
# @note This is NOT the default strategy - use {NiceQueue} for production
|
|
11
|
+
#
|
|
12
|
+
# ## When to Use Queue
|
|
13
|
+
#
|
|
14
|
+
# Choose this strategy when you want strict enforcement of job scheduling:
|
|
15
|
+
# - You want jobs to fail loudly if executed prematurely
|
|
16
|
+
# - You're debugging scheduling issues
|
|
17
|
+
# - You want to catch configuration errors in production
|
|
18
|
+
#
|
|
19
|
+
# For most production use cases, use {NiceQueue} instead, which handles
|
|
20
|
+
# premature execution gracefully by re-inserting with appropriate delay.
|
|
21
|
+
#
|
|
22
|
+
# ## Strategy Behavior
|
|
23
|
+
#
|
|
24
|
+
# - **Execution:** Asynchronous via Beanstalkd workers
|
|
25
|
+
# - **Testing mode:** Returns false
|
|
26
|
+
# - **Premature execution:** Raises {Postburner::Job::PrematurePerform} exception
|
|
27
|
+
# - **Beanstalkd:** Required and used for all job queueing
|
|
28
|
+
#
|
|
29
|
+
# ## Usage
|
|
30
|
+
#
|
|
31
|
+
# @example Activate Queue strategy
|
|
32
|
+
# Postburner.async_strategy!
|
|
33
|
+
# job = MyJob.create!(args: { user_id: 123 })
|
|
34
|
+
# job.queue!(delay: 1.hour)
|
|
35
|
+
#
|
|
36
|
+
# @example Premature execution raises exception
|
|
37
|
+
# Postburner.async_strategy!
|
|
38
|
+
# job = MyJob.create!(args: {})
|
|
39
|
+
# job.queue!(delay: 1.hour)
|
|
40
|
+
# # Worker picks up job too early...
|
|
41
|
+
# # => raises PrematurePerform: "Job has future run_at: ..."
|
|
42
|
+
#
|
|
43
|
+
# @see NiceQueue Default production strategy with graceful premature handling
|
|
44
|
+
# @see TestQueue Test strategy with inline execution
|
|
45
|
+
# @see Postburner.async_strategy!
|
|
46
|
+
#
|
|
47
|
+
class Queue
|
|
48
|
+
class << self
|
|
49
|
+
# Returns whether this strategy is for testing.
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] Always false for production strategies
|
|
52
|
+
#
|
|
53
|
+
def testing
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Inserts job into Beanstalkd queue asynchronously.
|
|
58
|
+
#
|
|
59
|
+
# Called automatically via after_save_commit hook when {Postburner::Job#queue!}
|
|
60
|
+
# is invoked. Enqueues the job to Beanstalkd and returns the response.
|
|
61
|
+
# The bkid is updated by {Postburner::Job#insert!} after this returns.
|
|
62
|
+
#
|
|
63
|
+
# @param job [Postburner::Job] The job to insert
|
|
64
|
+
# @param options [Hash] Beanstalkd options
|
|
65
|
+
# @option options [Integer] :delay Seconds to delay execution
|
|
66
|
+
# @option options [Integer] :pri Priority (lower = higher priority)
|
|
67
|
+
# @option options [Integer] :ttr Time-to-run before job times out
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] Backburner response with :id and :status
|
|
70
|
+
#
|
|
71
|
+
# @raise [Beaneater::NotConnected] if Beanstalkd connection fails
|
|
72
|
+
#
|
|
73
|
+
# @api private
|
|
74
|
+
#
|
|
75
|
+
def insert(job, options = {})
|
|
76
|
+
Postburner::Job.transaction do
|
|
77
|
+
Backburner::Worker.enqueue(
|
|
78
|
+
Postburner::Job,
|
|
79
|
+
job.id,
|
|
80
|
+
options
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def handle_perform!(job)
|
|
86
|
+
job.perform!(job.args)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Handles jobs executed before their scheduled run_at time.
|
|
90
|
+
#
|
|
91
|
+
# This strategy raises an exception to fail loudly on premature execution.
|
|
92
|
+
# Override in subclasses (like {NiceQueue}) to handle differently.
|
|
93
|
+
#
|
|
94
|
+
# @param job [Postburner::Job] The job being executed prematurely
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
97
|
+
#
|
|
98
|
+
# @raise [Postburner::Job::PrematurePerform] Always raises for this strategy
|
|
99
|
+
#
|
|
100
|
+
def handle_premature_perform(job)
|
|
101
|
+
raise Postburner::Job::PrematurePerform, "Job has future run_at: #{job.run_at}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -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
|
data/lib/postburner/version.rb
CHANGED