postburner 1.0.0.pre.13 → 1.0.0.pre.14

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.
@@ -4,13 +4,13 @@ module Postburner
4
4
  # Default production queue strategy with graceful handling of premature execution.
5
5
  #
6
6
  # This is the recommended production strategy and the default for Postburner.
7
- # Unlike the strict {Queue} strategy, NiceQueue automatically re-inserts jobs
7
+ # Unlike the strict {StrictQueue} strategy, DefaultQueue automatically re-inserts jobs
8
8
  # that are executed before their scheduled run_at time, ensuring they run at
9
9
  # the correct time without raising exceptions.
10
10
  #
11
11
  # @note This is the DEFAULT strategy set on Postburner initialization
12
12
  #
13
- # ## When to Use NiceQueue
13
+ # ## When to Use DefaultQueue
14
14
  #
15
15
  # Choose this strategy for production environments:
16
16
  # - **Default choice:** This should be your go-to production strategy
@@ -42,12 +42,12 @@ module Postburner
42
42
  # ## Usage
43
43
  #
44
44
  # @example Default behavior (automatically set)
45
- # # No configuration needed - NiceQueue is the default
45
+ # # No configuration needed - DefaultQueue is the default
46
46
  # job = MyJob.create!(args: { user_id: 123 })
47
47
  # job.queue!(delay: 1.hour)
48
48
  #
49
- # @example Explicitly activate NiceQueue
50
- # Postburner.nice_async_strategy!
49
+ # @example Explicitly activate DefaultQueue
50
+ # Postburner.default_strategy!
51
51
  # job = MyJob.create!(args: {})
52
52
  # job.queue!(at: Time.zone.now + 2.days)
53
53
  #
@@ -58,15 +58,15 @@ module Postburner
58
58
  # # Job is automatically re-inserted with 55-minute delay
59
59
  # # "PREMATURE; RE-INSERTED" logged to job.logs
60
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!
61
+ # @see StrictQueue Strict production strategy that raises on premature execution
62
+ # @see InlineTestQueue Test strategy with inline execution
63
+ # @see Postburner.default_strategy!
64
64
  #
65
- class NiceQueue < Queue
65
+ class DefaultQueue < StrictQueue
66
66
  class << self
67
67
  # Handles jobs executed before their scheduled run_at time.
68
68
  #
69
- # Unlike the parent {Queue} strategy, NiceQueue gracefully handles premature
69
+ # Unlike the parent {StrictQueue} strategy, DefaultQueue gracefully handles premature
70
70
  # execution by re-inserting the job with the appropriate delay instead of
71
71
  # raising an exception.
72
72
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postburner
4
- # Test queue strategy with strict scheduling enforcement.
4
+ # Inline test queue strategy with strict scheduling enforcement.
5
5
  #
6
6
  # This strategy executes jobs inline/synchronously without Beanstalkd, making
7
7
  # tests fast and predictable. It raises {Postburner::Job::PrematurePerform} if
@@ -9,7 +9,7 @@ module Postburner
9
9
  #
10
10
  # @note Auto-detected when Rails.env.test? and ActiveJob adapter is :test
11
11
  #
12
- # ## When to Use TestQueue
12
+ # ## When to Use InlineTestQueue
13
13
  #
14
14
  # Choose this strategy for test environments where you want:
15
15
  # - **Explicit time management:** Forces you to use `travel_to` for scheduled jobs
@@ -33,10 +33,10 @@ module Postburner
33
33
  #
34
34
  # @example Automatically activated in Rails test environment
35
35
  # # In test_helper.rb or rails_helper.rb
36
- # # TestQueue is automatically set if Rails.env.test?
36
+ # # InlineTestQueue is automatically set if Rails.env.test?
37
37
  # # and ActiveJob.queue_adapter == :test
38
38
  #
39
- # @example Explicitly activate TestQueue
39
+ # @example Explicitly activate InlineTestQueue
40
40
  # Postburner.inline_test_strategy!
41
41
  # job = MyJob.create!(args: { user_id: 123 })
42
42
  # job.queue!
@@ -70,11 +70,11 @@ module Postburner
70
70
  # end
71
71
  # end
72
72
  #
73
- # @see ImmediateTestQueue Test strategy with automatic time travel
73
+ # @see ImmediateInlineTestQueue Test strategy with automatic time travel
74
74
  # @see NiceQueue Default production strategy
75
75
  # @see Postburner.inline_test_strategy!
76
76
  #
77
- class TestQueue < Queue
77
+ class InlineTestQueue < Queue
78
78
  class << self
79
79
  # Returns whether this strategy is for testing.
80
80
  #
@@ -121,8 +121,11 @@ module Postburner
121
121
  # @raise [Postburner::Job::PrematurePerform] Always raises with helpful message
122
122
  #
123
123
  def handle_premature_perform(job)
124
- raise Postburner::Job::PrematurePerform, "Job scheduled for #{job.run_at} (#{((job.run_at - Time.current) / 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."
124
+ raise Postburner::Job::PrematurePerform, "Job scheduled for #{job.run_at} (#{((job.run_at - Time.current) / 60).round(1)} minutes from now). Use `travel_to(job.run_at)` in your test, or set `Postburner.time_travel_test_strategy!` i.e. `Postburner::TimeTravelTestQueue` to execute scheduled jobs immediately."
125
125
  end
126
126
  end
127
127
  end
128
+
129
+ # Backward compatibility alias
130
+ TestQueue = InlineTestQueue
128
131
  end
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postburner
4
- # Base production queue strategy that queues jobs to Beanstalkd asynchronously.
4
+ # Strict production queue strategy that queues jobs to Beanstalkd asynchronously.
5
5
  #
6
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
7
+ # is executed before its scheduled run_at time. Unlike {DefaultQueue}, it does
8
8
  # not automatically re-insert premature jobs.
9
9
  #
10
- # @note This is NOT the default strategy - use {NiceQueue} for production
10
+ # @note This is NOT the default strategy - use {DefaultQueue} for production
11
11
  #
12
- # ## When to Use Queue
12
+ # ## When to Use StrictQueue
13
13
  #
14
14
  # Choose this strategy when you want strict enforcement of job scheduling:
15
15
  # - You want jobs to fail loudly if executed prematurely
16
16
  # - You're debugging scheduling issues
17
17
  # - You want to catch configuration errors in production
18
18
  #
19
- # For most production use cases, use {NiceQueue} instead, which handles
19
+ # For most production use cases, use {DefaultQueue} instead, which handles
20
20
  # premature execution gracefully by re-inserting with appropriate delay.
21
21
  #
22
22
  # ## Strategy Behavior
@@ -28,23 +28,23 @@ module Postburner
28
28
  #
29
29
  # ## Usage
30
30
  #
31
- # @example Activate Queue strategy
32
- # Postburner.async_strategy!
31
+ # @example Activate StrictQueue strategy
32
+ # Postburner.strict_strategy!
33
33
  # job = MyJob.create!(args: { user_id: 123 })
34
34
  # job.queue!(delay: 1.hour)
35
35
  #
36
36
  # @example Premature execution raises exception
37
- # Postburner.async_strategy!
37
+ # Postburner.strict_strategy!
38
38
  # job = MyJob.create!(args: {})
39
39
  # job.queue!(delay: 1.hour)
40
40
  # # Worker picks up job too early...
41
41
  # # => raises PrematurePerform: "Job has future run_at: ..."
42
42
  #
43
- # @see NiceQueue Default production strategy with graceful premature handling
44
- # @see TestQueue Test strategy with inline execution
45
- # @see Postburner.async_strategy!
43
+ # @see DefaultQueue Default production strategy with graceful premature handling
44
+ # @see InlineTestQueue Test strategy with inline execution
45
+ # @see Postburner.strict_strategy!
46
46
  #
47
- class Queue
47
+ class StrictQueue
48
48
  class << self
49
49
  # Returns whether this strategy is for testing.
50
50
  #
@@ -5,25 +5,25 @@ require_relative '../time_helpers'
5
5
  module Postburner
6
6
  # Test queue strategy with automatic time travel for scheduled jobs.
7
7
  #
8
- # This strategy executes jobs inline/synchronously like {TestQueue}, but
8
+ # This strategy executes jobs inline/synchronously like {InlineTestQueue}, but
9
9
  # automatically uses time travel for scheduled jobs instead of raising
10
10
  # exceptions. When a job has a future run_at, it travels to that time,
11
11
  # executes the job, then returns to the present.
12
12
  #
13
13
  # @note Requires ActiveSupport::Testing::TimeHelpers (Rails testing framework)
14
14
  #
15
- # ## When to Use ImmediateTestQueue
15
+ # ## When to Use TimeTravelTestQueue
16
16
  #
17
17
  # Choose this strategy for test environments where you want:
18
18
  # - **Automatic time management:** No need to manually call `travel_to`
19
19
  # - **Simple scheduled job testing:** Test delayed/scheduled jobs without boilerplate
20
20
  # - **Fast tests:** Jobs execute immediately regardless of schedule
21
21
  # - **No Beanstalkd:** Tests run without external dependencies
22
- # - **Convenience over control:** Less explicit but easier to use than {TestQueue}
22
+ # - **Convenience over control:** Less explicit but easier to use than {InlineTestQueue}
23
23
  #
24
24
  # This is ideal for integration/feature tests where you want to verify that
25
25
  # scheduled jobs execute correctly without managing time travel yourself. The
26
- # tradeoff is less explicit control over timing compared to {TestQueue}.
26
+ # tradeoff is less explicit control over timing compared to {InlineTestQueue}.
27
27
  #
28
28
  # ## Strategy Behavior
29
29
  #
@@ -48,15 +48,15 @@ module Postburner
48
48
  #
49
49
  # ## Usage
50
50
  #
51
- # @example Explicitly activate ImmediateTestQueue
52
- # Postburner.inline_immediate_test_strategy!
51
+ # @example Explicitly activate TimeTravelTestQueue
52
+ # Postburner.time_travel_test_strategy!
53
53
  # job = MyJob.create!(args: { user_id: 123 })
54
54
  # job.queue!(delay: 1.hour)
55
55
  # # Job executes immediately at scheduled time
56
56
  # assert job.reload.processed_at
57
57
  #
58
58
  # @example Scheduled jobs execute automatically
59
- # Postburner.inline_immediate_test_strategy!
59
+ # Postburner.time_travel_test_strategy!
60
60
  # job = SendReminderEmail.create!(args: { user_id: 123 })
61
61
  #
62
62
  # # Job executes immediately despite 2-day delay
@@ -67,7 +67,7 @@ module Postburner
67
67
  # assert_not_nil job.processed_at
68
68
  #
69
69
  # @example Multiple jobs execute in queue order, not schedule order
70
- # Postburner.inline_immediate_test_strategy!
70
+ # Postburner.time_travel_test_strategy!
71
71
  #
72
72
  # job1 = MyJob.create!(args: { id: 1 })
73
73
  # job2 = MyJob.create!(args: { id: 2 })
@@ -80,7 +80,7 @@ module Postburner
80
80
  #
81
81
  # @example Testing feature with scheduled job
82
82
  # test "sends reminder email after 24 hours" do
83
- # Postburner.inline_immediate_test_strategy!
83
+ # Postburner.time_travel_test_strategy!
84
84
  #
85
85
  # user = users(:john)
86
86
  # reminder = ReminderJob.create!(args: { user_id: user.id })
@@ -91,11 +91,11 @@ module Postburner
91
91
  # assert_emails 1
92
92
  # end
93
93
  #
94
- # @see TestQueue Test strategy requiring explicit time travel
94
+ # @see InlineTestQueue Test strategy requiring explicit time travel
95
95
  # @see NiceQueue Default production strategy
96
- # @see Postburner.inline_immediate_test_strategy!
96
+ # @see Postburner.time_travel_test_strategy!
97
97
  #
98
- class ImmediateTestQueue < TestQueue
98
+ class TimeTravelTestQueue < InlineTestQueue
99
99
  class << self
100
100
  include TimeHelpers
101
101
 
@@ -147,4 +147,7 @@ module Postburner
147
147
  end
148
148
  end
149
149
  end
150
+
151
+ # Backward compatibility alias
152
+ ImmediateTestQueue = TimeTravelTestQueue
150
153
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Test helper methods for switching queue strategies in tests.
5
+ #
6
+ # Provides instance methods for managing queue strategies in a test-safe,
7
+ # thread-safe manner. Use these methods to switch between strategies for
8
+ # individual tests or groups of tests.
9
+ #
10
+ # @example Using in setup/teardown
11
+ # class MyTest < ActiveSupport::TestCase
12
+ # include Postburner::TestHelpers
13
+ #
14
+ # def setup
15
+ # switch_queue_strategy!(:time_travel_test)
16
+ # end
17
+ #
18
+ # def teardown
19
+ # restore_queue_strategy!
20
+ # end
21
+ #
22
+ # def test_something
23
+ # job = MyJob.create!(args: {})
24
+ # job.queue!(delay: 1.hour)
25
+ # assert job.reload.processed_at
26
+ # end
27
+ # end
28
+ #
29
+ # @example Using block form
30
+ # class MyTest < ActiveSupport::TestCase
31
+ # include Postburner::TestHelpers
32
+ #
33
+ # def test_something
34
+ # use_queue_strategy(:time_travel_test) do
35
+ # job = MyJob.create!(args: {})
36
+ # job.queue!(delay: 1.hour)
37
+ # assert job.reload.processed_at
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ module TestHelpers
43
+ # Switch to a different queue strategy for testing.
44
+ #
45
+ # Stashes the current strategy in a thread-local variable so it can be
46
+ # restored later with {#restore_queue_strategy!}. This is thread-safe
47
+ # for parallel test execution.
48
+ #
49
+ # @param strategy [Class, Symbol] Strategy class or symbol name
50
+ # Symbols: :inline_test, :time_travel_test, :strict, :default, :null
51
+ #
52
+ # @return [Class] The newly activated strategy class
53
+ #
54
+ # @raise [ArgumentError] if strategy is invalid type or unknown symbol
55
+ #
56
+ # @example With class constant
57
+ # switch_queue_strategy!(Postburner::TimeTravelTestQueue)
58
+ #
59
+ # @example With symbol
60
+ # switch_queue_strategy!(:time_travel_test)
61
+ #
62
+ def switch_queue_strategy!(strategy)
63
+ Thread.current[:postburner_original_strategy] = Postburner.queue_strategy
64
+
65
+ Postburner.queue_strategy = case strategy
66
+ when Symbol
67
+ strategy_method = "#{strategy}_strategy!".to_sym
68
+ if Postburner.respond_to?(strategy_method, true)
69
+ Postburner.send(strategy_method)
70
+ Postburner.queue_strategy
71
+ else
72
+ raise ArgumentError, "Unknown strategy: #{strategy}. Available: inline_test, time_travel_test, strict, default, null"
73
+ end
74
+ when Class
75
+ strategy
76
+ else
77
+ raise ArgumentError, "Expected Class or Symbol, got #{strategy.class}"
78
+ end
79
+ end
80
+
81
+ # Restore the previously stashed queue strategy.
82
+ #
83
+ # Restores the strategy that was active when {#switch_queue_strategy!}
84
+ # was called. Clears the thread-local stash after restoring.
85
+ #
86
+ # @return [Class] The restored strategy class
87
+ #
88
+ # @raise [RuntimeError] if no strategy was stashed
89
+ #
90
+ # @example Typical usage in teardown
91
+ # def teardown
92
+ # restore_queue_strategy!
93
+ # end
94
+ #
95
+ def restore_queue_strategy!
96
+ original = Thread.current[:postburner_original_strategy]
97
+ if original.nil?
98
+ raise "No strategy to restore. Did you call switch_queue_strategy! first?"
99
+ end
100
+
101
+ Postburner.queue_strategy = original
102
+ Thread.current[:postburner_original_strategy] = nil
103
+ end
104
+
105
+ # Execute a block with a temporary queue strategy.
106
+ #
107
+ # Automatically switches to the specified strategy before the block
108
+ # and restores the original strategy after, even if an exception occurs.
109
+ #
110
+ # @param strategy [Class, Symbol] Strategy class or symbol name
111
+ # Symbols: :inline_test, :time_travel_test, :strict, :default, :null
112
+ #
113
+ # @yield Block to execute with the temporary strategy
114
+ #
115
+ # @return [Object] The return value of the block
116
+ #
117
+ # @raise [ArgumentError] if strategy is invalid type or unknown symbol
118
+ #
119
+ # @example
120
+ # use_queue_strategy(:time_travel_test) do
121
+ # job = MyJob.create!(args: {})
122
+ # job.queue!(delay: 1.hour)
123
+ # assert job.reload.processed_at
124
+ # end
125
+ #
126
+ def use_queue_strategy(strategy, &block)
127
+ switch_queue_strategy!(strategy)
128
+ yield
129
+ ensure
130
+ restore_queue_strategy!
131
+ end
132
+ end
133
+ end
@@ -1,3 +1,3 @@
1
1
  module Postburner
2
- VERSION = '1.0.0.pre.13'
2
+ VERSION = '1.0.0.pre.14'
3
3
  end
@@ -501,8 +501,8 @@ module Postburner
501
501
  # from Beanstalkd after successful completion.
502
502
  #
503
503
  # Instruments with ActiveSupport::Notifications:
504
- # - perform_start.postburner: Before job execution
505
- # - perform.postburner: Around job execution (includes duration)
504
+ # - perform_start.job.postburner: Before job execution
505
+ # - perform.job.postburner: Around job execution (includes duration)
506
506
  #
507
507
  # @param beanstalk_job [Beaneater::Job] Reserved job from Beanstalkd
508
508
  # @param payload [Hash] Parsed job body
@@ -511,13 +511,14 @@ module Postburner
511
511
  # @api private
512
512
  def execute_regular_job(beanstalk_job, payload)
513
513
  job_description = format_job_description(payload)
514
- instrument_payload = { payload: payload, beanstalk_job_id: beanstalk_job.id }
514
+ job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
515
+ instrument_payload = { job: job_payload, beanstalk_job_id: beanstalk_job.id }
515
516
 
516
517
  logger.info "[Postburner] Executing #{job_description} (bkid: #{beanstalk_job.id})"
517
518
 
518
- ActiveSupport::Notifications.instrument('perform_start.postburner', instrument_payload)
519
+ ActiveSupport::Notifications.instrument('perform_start.job.postburner', instrument_payload)
519
520
 
520
- ActiveSupport::Notifications.instrument('perform.postburner', instrument_payload) do
521
+ ActiveSupport::Notifications.instrument('perform.job.postburner', instrument_payload) do
521
522
  Postburner::ActiveJob::Execution.execute(beanstalk_job.body)
522
523
  end
523
524
 
@@ -547,9 +548,13 @@ module Postburner
547
548
 
548
549
  # Handles job execution errors with retry logic.
549
550
  #
550
- # For tracked jobs and legacy Postburner::Job: Buries the job for inspection.
551
+ # For tracked jobs and legacy Postburner::Job: Buries the job for inspection
552
+ # and emits retry_stopped.job.postburner event.
551
553
  # For default ActiveJob: Applies exponential backoff retry with max 5 attempts.
552
554
  #
555
+ # Instruments with ActiveSupport::Notifications:
556
+ # - retry_stopped.job.postburner: When tracked job is buried after failure
557
+ #
553
558
  # @param beanstalk_job [Beaneater::Job] Failed job
554
559
  # @param error [Exception] The error that caused the failure
555
560
  # @return [void]
@@ -563,6 +568,14 @@ module Postburner
563
568
 
564
569
  if payload['tracked'] || Postburner::ActiveJob::Payload.legacy_format?(payload)
565
570
  logger.info "[Postburner] Burying tracked/legacy job for inspection"
571
+
572
+ job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
573
+ ActiveSupport::Notifications.instrument('retry_stopped.job.postburner', {
574
+ job: job_payload,
575
+ beanstalk_job_id: beanstalk_job.id,
576
+ error: error
577
+ })
578
+
566
579
  beanstalk_job.bury
567
580
  else
568
581
  handle_default_retry(beanstalk_job, payload, error)
@@ -579,8 +592,8 @@ module Postburner
579
592
  # After 5 failed attempts, discards the job permanently.
580
593
  #
581
594
  # Instruments with ActiveSupport::Notifications:
582
- # - retry.postburner: When job is retried
583
- # - discard.postburner: When job is discarded after max retries
595
+ # - retry.job.postburner: When job is retried
596
+ # - discard.job.postburner: When job is discarded after max retries
584
597
  #
585
598
  # @param beanstalk_job [Beaneater::Job] Failed job to retry
586
599
  # @param payload [Hash] Parsed job body (modified with retry_count)
@@ -590,6 +603,7 @@ module Postburner
590
603
  def handle_default_retry(beanstalk_job, payload, error)
591
604
  retry_count = payload['retry_count'] || 0
592
605
  max_retries = 5
606
+ job_payload = Postburner::Instrumentation.job_payload_from_hash(payload, beanstalk_job_id: beanstalk_job.id)
593
607
 
594
608
  if retry_count < max_retries
595
609
  payload['retry_count'] = retry_count + 1
@@ -609,8 +623,8 @@ module Postburner
609
623
  )
610
624
  end
611
625
 
612
- ActiveSupport::Notifications.instrument('retry.postburner', {
613
- payload: payload,
626
+ ActiveSupport::Notifications.instrument('retry.job.postburner', {
627
+ job: job_payload,
614
628
  beanstalk_job_id: beanstalk_job.id,
615
629
  error: error,
616
630
  wait: delay,
@@ -619,8 +633,8 @@ module Postburner
619
633
 
620
634
  logger.info "[Postburner] Retrying default job #{payload['job_id']}, attempt #{retry_count + 1} in #{delay}s"
621
635
  else
622
- ActiveSupport::Notifications.instrument('discard.postburner', {
623
- payload: payload,
636
+ ActiveSupport::Notifications.instrument('discard.job.postburner', {
637
+ job: job_payload,
624
638
  beanstalk_job_id: beanstalk_job.id,
625
639
  error: error
626
640
  })