postburner 0.8.0 → 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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Postburner
2
- VERSION = '0.8.0'
2
+ VERSION = '0.9.0.rc.1'
3
3
  end