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.
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Queue configuration module for Postburner::Job classes.
5
+ #
6
+ # Provides DSL methods for configuring queue behavior (name, priority, TTR, retries).
7
+ # Replaces Backburner::Queue with cleaner implementation that doesn't interfere
8
+ # with ActiveSupport::Callbacks.
9
+ #
10
+ # @example Basic usage
11
+ # class ProcessPayment < Postburner::Job
12
+ # queue 'critical'
13
+ # queue_priority 0
14
+ # queue_ttr 300
15
+ # queue_max_job_retries 3
16
+ #
17
+ # def perform(args)
18
+ # # ...
19
+ # end
20
+ # end
21
+ #
22
+ module QueueConfig
23
+ extend ActiveSupport::Concern
24
+
25
+ included do
26
+ class_attribute :postburner_queue_name, default: 'default'
27
+ class_attribute :postburner_priority, default: nil
28
+ class_attribute :postburner_ttr, default: nil
29
+ class_attribute :postburner_max_retries, default: nil
30
+ class_attribute :postburner_retry_delay, default: nil
31
+ end
32
+
33
+ class_methods do
34
+ # Sets or returns the queue name.
35
+ #
36
+ # @param name [String, Symbol, nil] Queue name to set, or nil to get current value
37
+ #
38
+ # @return [String, nil] Current queue name when getting, nil when setting
39
+ #
40
+ # @example Set queue
41
+ # queue 'critical'
42
+ #
43
+ # @example Get queue
44
+ # ProcessPayment.queue # => 'critical'
45
+ #
46
+ def queue(name = nil)
47
+ if name
48
+ self.postburner_queue_name = name.to_s
49
+ nil # Return nil to avoid callback interference
50
+ else
51
+ postburner_queue_name
52
+ end
53
+ end
54
+
55
+ # Sets or returns the queue priority.
56
+ #
57
+ # Lower numbers = higher priority in Beanstalkd.
58
+ #
59
+ # @param pri [Integer, nil] Priority to set (0-4294967295), or nil to get current value
60
+ #
61
+ # @return [Integer, nil] Current priority when getting, nil when setting
62
+ #
63
+ # @example Set priority
64
+ # queue_priority 0 # Highest priority
65
+ #
66
+ # @example Get priority
67
+ # ProcessPayment.queue_priority # => 0
68
+ #
69
+ def queue_priority(pri = nil)
70
+ if pri
71
+ self.postburner_priority = pri
72
+ nil
73
+ else
74
+ postburner_priority
75
+ end
76
+ end
77
+
78
+ # Sets or returns the queue TTR (time-to-run).
79
+ #
80
+ # Number of seconds Beanstalkd will wait for job completion before
81
+ # making it available again.
82
+ #
83
+ # @param ttr [Integer, nil] Timeout in seconds, or nil to get current value
84
+ #
85
+ # @return [Integer, nil] Current TTR when getting, nil when setting
86
+ #
87
+ # @example Set TTR
88
+ # queue_ttr 300 # 5 minutes
89
+ #
90
+ # @example Get TTR
91
+ # ProcessPayment.queue_ttr # => 300
92
+ #
93
+ def queue_ttr(ttr = nil)
94
+ if ttr
95
+ self.postburner_ttr = ttr
96
+ nil
97
+ else
98
+ postburner_ttr
99
+ end
100
+ end
101
+
102
+ # Sets or returns maximum number of job retries.
103
+ #
104
+ # @param retries [Integer, nil] Max retries, or nil to get current value
105
+ #
106
+ # @return [Integer, nil] Current max retries when getting, nil when setting
107
+ #
108
+ # @example Set max retries
109
+ # queue_max_job_retries 3
110
+ #
111
+ # @example Get max retries
112
+ # ProcessPayment.queue_max_job_retries # => 3
113
+ #
114
+ def queue_max_job_retries(retries = nil)
115
+ if retries
116
+ self.postburner_max_retries = retries
117
+ nil
118
+ else
119
+ postburner_max_retries
120
+ end
121
+ end
122
+
123
+ # Sets or returns the retry delay.
124
+ #
125
+ # Can accept either a fixed delay (Integer) or a proc for dynamic calculation.
126
+ # The proc receives the retry count and returns delay in seconds.
127
+ #
128
+ # @param delay [Integer, Proc, nil] Delay in seconds, proc, or nil to get current value
129
+ #
130
+ # @return [Integer, Proc, nil] Current delay when getting, nil when setting
131
+ #
132
+ # @example Set fixed retry delay
133
+ # queue_retry_delay 10
134
+ #
135
+ # @example Set exponential backoff with proc
136
+ # queue_retry_delay ->(retries) { 2 ** retries }
137
+ #
138
+ # @example Get retry delay
139
+ # ProcessPayment.queue_retry_delay # => 10 or #<Proc>
140
+ #
141
+ def queue_retry_delay(delay = nil)
142
+ if delay
143
+ self.postburner_retry_delay = delay
144
+ nil
145
+ else
146
+ postburner_retry_delay
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -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,119 @@
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] Response with :id (beanstalkd job id) and :status
70
+ #
71
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails
72
+ #
73
+ # @api private
74
+ #
75
+ def insert(job, options = {})
76
+ #debugger
77
+ Postburner::Job.transaction do
78
+ Postburner.connected do |conn|
79
+ tube_name = job.tube_name
80
+ data = { class: job.class.name, args: [job.id] }
81
+
82
+ # Get priority, TTR from job instance (respects instance overrides) or options
83
+ pri = options[:pri] || job.queue_priority || Postburner.configuration.default_priority
84
+ delay = options[:delay] || 0
85
+ ttr = options[:ttr] || job.queue_ttr || Postburner.configuration.default_ttr
86
+
87
+ response = conn.tubes[tube_name].put(
88
+ JSON.generate(data),
89
+ pri: pri,
90
+ delay: delay,
91
+ ttr: ttr
92
+ )
93
+
94
+ response
95
+ end
96
+ end
97
+ end
98
+
99
+ def handle_perform!(job)
100
+ job.perform!(job.args)
101
+ end
102
+
103
+ # Handles jobs executed before their scheduled run_at time.
104
+ #
105
+ # This strategy raises an exception to fail loudly on premature execution.
106
+ # Override in subclasses (like {NiceQueue}) to handle differently.
107
+ #
108
+ # @param job [Postburner::Job] The job being executed prematurely
109
+ #
110
+ # @return [void]
111
+ #
112
+ # @raise [Postburner::Job::PrematurePerform] Always raises for this strategy
113
+ #
114
+ def handle_premature_perform(job)
115
+ raise Postburner::Job::PrematurePerform, "Job has future run_at: #{job.run_at}"
116
+ end
117
+ end
118
+ end
119
+ end