postburner 0.9.0.rc.1 → 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.
@@ -70,45 +70,13 @@ module Postburner
70
70
  # @attr_reader [Array<Hash>] errata Array of error details with timestamps
71
71
  # @attr_reader [Array<Hash>] logs Array of log entries with timestamps
72
72
  #
73
- # Module to override Backburner::Queue methods to return nil when setting values
74
- # This prevents return values from interfering with callback definitions
75
- module BackburnerQueueOverrides
76
- def queue(name=nil)
77
- result = super
78
- name.nil? ? result : nil
79
- end
80
-
81
- def queue_priority(pri=nil)
82
- result = super
83
- pri.nil? ? result : nil
84
- end
85
-
86
- def queue_respond_timeout(ttr=nil)
87
- result = super
88
- ttr.nil? ? result : nil
89
- end
90
-
91
- def queue_max_job_retries(retries=nil)
92
- result = super
93
- retries.nil? ? result : nil
94
- end
95
-
96
- def queue_retry_delay(delay=nil)
97
- result = super
98
- delay.nil? ? result : nil
99
- end
100
-
101
- def queue_retry_delay_proc(proc=nil)
102
- result = super
103
- proc.nil? ? result : nil
104
- end
105
- end
106
-
107
73
  class Job < ApplicationRecord
108
- include Backburner::Queue
109
- singleton_class.prepend BackburnerQueueOverrides
74
+ include QueueConfig
110
75
  include Callbacks
111
76
 
77
+ # Instance-level queue configuration (overrides class-level defaults)
78
+ attr_writer :queue_priority, :queue_ttr
79
+
112
80
  LOG_LEVELS = [
113
81
  :debug,
114
82
  :info,
@@ -524,7 +492,7 @@ module Postburner
524
492
  # @return [Beaneater::Job, nil] Beanstalkd job object or nil if no bkid
525
493
  #
526
494
  # @example Direct Beanstalkd operations
527
- # bk_job = job.beanstalk_job
495
+ # bk_job = job.bk
528
496
  # bk_job.stats # Get job statistics
529
497
  # bk_job.bury # Bury the job
530
498
  # bk_job.release(pri: 0) # Release with priority
@@ -533,7 +501,7 @@ module Postburner
533
501
  # @see #delete!
534
502
  # @see #kick!
535
503
  #
536
- def beanstalk_job
504
+ def bk
537
505
  return unless self.bkid.present?
538
506
  return @_beanstalk_job if @_beanstalk_job
539
507
 
@@ -542,6 +510,8 @@ module Postburner
542
510
  @_beanstalk_job
543
511
  end
544
512
 
513
+ alias_method :beanstalk_job, :bk
514
+
545
515
  # Returns the Beanstalkd job object with cache invalidation.
546
516
  #
547
517
  # Same as {#beanstalk_job} but clears the cached instance variable first,
@@ -554,11 +524,163 @@ module Postburner
554
524
  #
555
525
  # @see #beanstalk_job
556
526
  #
557
- def beanstalk_job!
527
+ def bk!
558
528
  @_beanstalk_job = nil
559
529
  self.beanstalk_job
560
530
  end
561
531
 
532
+ alias_method :beanstalk_job!, :bk!
533
+
534
+ # Returns job statistics including Beanstalkd job state.
535
+ #
536
+ # Fetches current job state from Beanstalkd and returns combined statistics
537
+ # about the job's PostgreSQL record and its current Beanstalkd status.
538
+ #
539
+ # @return [Hash] Statistics hash with the following keys:
540
+ # - id: PostgreSQL job ID
541
+ # - bkid: Beanstalkd job ID
542
+ # - queue: Queue name configured for this job class (e.g., 'sleep-jobs')
543
+ # - tube: Derived tube name with environment prefix (e.g., 'postburner.development.sleep-jobs')
544
+ # - watched: Boolean indicating if tube is in configured watch list
545
+ # - beanstalk: Hash of Beanstalkd job statistics:
546
+ # - id: Beanstalkd job ID
547
+ # - tube: Tube name where job resides
548
+ # - state: Job state (ready, reserved, delayed, buried)
549
+ # - pri: Priority (0 = highest, 4294967295 = lowest)
550
+ # - age: Seconds since job was created
551
+ # - delay: Seconds remaining before job becomes ready (0 if ready now)
552
+ # - ttr: Time-to-run in seconds (time allowed for job processing)
553
+ # - time_left: Seconds remaining before job times out (0 if not reserved)
554
+ # - file: Binlog file number containing job
555
+ # - reserves: Number of times job has been reserved
556
+ # - timeouts: Number of times job has timed out during processing
557
+ # - releases: Number of times job has been released back to ready
558
+ # - buries: Number of times job has been buried
559
+ # - kicks: Number of times job has been kicked from buried/delayed
560
+ #
561
+ # @raise [Beaneater::NotFoundError] if job no longer exists in Beanstalkd
562
+ #
563
+ # @example
564
+ # job.stats
565
+ # # => {
566
+ # # id: 5,
567
+ # # bkid: 1,
568
+ # # queue: "sleep-jobs",
569
+ # # tube: "postburner.development.sleep-jobs",
570
+ # # watched: false,
571
+ # # beanstalk: {
572
+ # # id: 1,
573
+ # # tube: "postburner.development.sleep-jobs",
574
+ # # state: "ready",
575
+ # # pri: 50,
576
+ # # age: 1391,
577
+ # # delay: 0,
578
+ # # ttr: 120,
579
+ # # time_left: 0,
580
+ # # file: 0,
581
+ # # reserves: 0,
582
+ # # timeouts: 0,
583
+ # # releases: 0,
584
+ # # buries: 0,
585
+ # # kicks: 0
586
+ # # }
587
+ # # }
588
+ #
589
+ def stats
590
+ # Get configured watched tubes (expanded with environment prefix)
591
+ watched = Postburner.watched_tube_names.include?(self.tube_name)
592
+
593
+ {
594
+ id: self.id,
595
+ bkid: self.bkid,
596
+ queue: queue_name,
597
+ tube: tube_name,
598
+ watched: watched,
599
+ beanstalk: self.bk.stats.to_h.symbolize_keys,
600
+ }
601
+ end
602
+
603
+ alias_method :beanstalk_job_stats, :stats
604
+
605
+ # Returns the queue name for this job instance.
606
+ #
607
+ # Checks instance-level override first, then falls back to class-level configuration.
608
+ #
609
+ # @return [String] Queue name
610
+ #
611
+ # @example Class-level configuration
612
+ # class MyJob < Postburner::Job
613
+ # queue 'critical'
614
+ # end
615
+ # job = MyJob.create!(args: {})
616
+ # job.queue_name # => 'critical'
617
+ #
618
+ def queue_name
619
+ self.class.queue
620
+ end
621
+
622
+ # Returns the queue priority for this job instance.
623
+ #
624
+ # Checks instance-level override first, then falls back to class-level configuration.
625
+ #
626
+ # @return [Integer, nil] Priority (lower = higher priority)
627
+ #
628
+ # @example Instance-level override
629
+ # job = MyJob.create!(args: {}, queue_priority: 1500)
630
+ # job.queue_priority # => 1500
631
+ #
632
+ def queue_priority
633
+ @queue_priority || self.class.queue_priority
634
+ end
635
+
636
+ # Returns the queue TTR (time-to-run) for this job instance.
637
+ #
638
+ # Checks instance-level override first, then falls back to class-level configuration.
639
+ #
640
+ # @return [Integer, nil] TTR in seconds
641
+ #
642
+ # @example Instance-level override
643
+ # job = MyJob.create!(args: {}, queue_ttr: 600)
644
+ # job.queue_ttr # => 600
645
+ #
646
+ def queue_ttr
647
+ @queue_ttr || self.class.queue_ttr
648
+ end
649
+
650
+ def tube_name
651
+ Postburner.configuration.expand_tube_name(queue_name)
652
+ end
653
+
654
+ # Extends the job's time-to-run (TTR) in Beanstalkd.
655
+ #
656
+ # Calls touch on the Beanstalkd job, extending the TTR by the original
657
+ # TTR value. Use this during long-running operations to prevent the job
658
+ # from timing out.
659
+ #
660
+ # @return [void]
661
+ #
662
+ # @note Does nothing if job has no bkid (e.g., in test mode)
663
+ #
664
+ # @example Process large file line by line
665
+ # def perform(args)
666
+ # file = File.find(args['file_id'])
667
+ # file.each_line do |line|
668
+ # # ... process line ...
669
+ # extend! # Extend TTR to prevent timeout
670
+ # end
671
+ # end
672
+ #
673
+ # @see #bk
674
+ #
675
+ def extend!
676
+ return unless self.bk
677
+ begin
678
+ self.bk.touch
679
+ rescue Beaneater::NotConnected => e
680
+ self.bk!.touch
681
+ end
682
+ end
683
+
562
684
  # Tracks an exception in the job's errata array.
563
685
  #
564
686
  # Appends exception details to the in-memory errata array with timestamp,
@@ -797,6 +919,7 @@ module Postburner
797
919
  # @api private
798
920
  #
799
921
  def insert_if_queued!
922
+ #debugger
800
923
  return unless self.will_insert?
801
924
  insert!(@_insert_options)
802
925
  end
@@ -815,6 +938,7 @@ module Postburner
815
938
  #
816
939
  def insert!(options={})
817
940
  response = Postburner.queue_strategy.insert(self, options)
941
+ #debugger
818
942
 
819
943
  # Response must be a hash with an :id key (value can be nil)
820
944
  # Backburner returns symbol keys
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Job wrapper for executing tracked ActiveJob instances.
5
+ #
6
+ # TrackedJob is a Postburner::Job subclass that deserializes and executes
7
+ # ActiveJob instances that opted-in to PostgreSQL tracking via the
8
+ # `Postburner::Tracked` concern.
9
+ #
10
+ # When an ActiveJob with `tracked` is enqueued, the adapter:
11
+ # 1. Creates a TrackedJob record in PostgreSQL
12
+ # 2. Stores the ActiveJob data in the `args` JSONB column
13
+ # 3. Queues a minimal payload to Beanstalkd with the TrackedJob ID
14
+ #
15
+ # When the worker executes the job:
16
+ # 1. Loads the TrackedJob record
17
+ # 2. Deserializes the ActiveJob from `args`
18
+ # 3. Executes it with full audit trail (logs, timing, errors)
19
+ #
20
+ # @example
21
+ # # User's ActiveJob (opts in to tracking)
22
+ # class ProcessPayment < ApplicationJob
23
+ # include Postburner::Tracked
24
+ # tracked
25
+ #
26
+ # def perform(payment_id)
27
+ # log "Processing payment #{payment_id}"
28
+ # # ...
29
+ # end
30
+ # end
31
+ #
32
+ # # ActiveJob adapter creates TrackedJob
33
+ # ProcessPayment.perform_later(123)
34
+ # # => Creates TrackedJob record, queues to Beanstalkd
35
+ #
36
+ # # Worker executes
37
+ # Postburner::Job.perform(tracked_job.id)
38
+ # # => Deserializes ProcessPayment job and executes with audit trail
39
+ #
40
+ class TrackedJob < Job
41
+ # Executes the wrapped ActiveJob instance.
42
+ #
43
+ # Deserializes the ActiveJob from args, restores metadata, sets up
44
+ # the bidirectional link for logging, and executes the job.
45
+ #
46
+ # @param args [Hash] JSONB args containing serialized ActiveJob data
47
+ #
48
+ # @return [void]
49
+ #
50
+ # @raise [Exception] Any exception raised by the ActiveJob is logged and re-raised
51
+ #
52
+ def perform(args)
53
+ # Extract ActiveJob metadata from args
54
+ job_class = args['job_class'].constantize
55
+ arguments = ::ActiveJob::Arguments.deserialize(args['arguments'])
56
+
57
+ # Instantiate the ActiveJob
58
+ job = job_class.new(*arguments)
59
+
60
+ # Restore ActiveJob metadata
61
+ job.job_id = args['job_id']
62
+ job.queue_name = args['queue_name']
63
+ job.priority = args['priority']
64
+ job.executions = args['executions'] || 0
65
+ job.exception_executions = args['exception_executions'] || {}
66
+ job.locale = args['locale']
67
+ job.timezone = args['timezone']
68
+
69
+ if args['enqueued_at']
70
+ job.enqueued_at = Time.iso8601(args['enqueued_at'])
71
+ end
72
+
73
+ # Give the ActiveJob access to this Postburner::Job for logging
74
+ if job.respond_to?(:postburner_job=)
75
+ job.postburner_job = self
76
+ end
77
+
78
+ # Execute the job (ActiveJob handles retry_on/discard_on)
79
+ # Exceptions are caught by Postburner::Job#perform! and logged to errata
80
+ job.perform_now
81
+ end
82
+ end
83
+ end
data/bin/postburner ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Postburner worker executable
5
+ #
6
+ # Loads configuration from YAML and starts the appropriate worker type.
7
+ #
8
+ # Usage:
9
+ # bin/postburner [--config PATH] [--env ENVIRONMENT]
10
+ #
11
+ # Examples:
12
+ # bin/postburner
13
+ # bin/postburner --config config/postburner.yml --env production
14
+ #
15
+
16
+ require 'optparse'
17
+
18
+ # Parse command-line options
19
+ options = {
20
+ config: 'config/postburner.yml',
21
+ env: ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development',
22
+ worker: nil,
23
+ queues: nil
24
+ }
25
+
26
+ OptionParser.new do |opts|
27
+ opts.banner = "Usage: bin/postburner [options]"
28
+
29
+ opts.on('-c', '--config PATH', 'Path to YAML configuration file (default: config/postburner.yml)') do |path|
30
+ options[:config] = path
31
+ end
32
+
33
+ opts.on('-e', '--env ENVIRONMENT', 'Environment (default: RAILS_ENV or development)') do |env|
34
+ options[:env] = env
35
+ end
36
+
37
+ opts.on('-w', '--worker WORKER', 'Worker name from config (required if multiple workers defined)') do |worker|
38
+ options[:worker] = worker
39
+ end
40
+
41
+ opts.on('-q', '--queues QUEUES', 'Comma-separated list of queues to process (default: all configured queues)') do |queues|
42
+ options[:queues] = queues.split(',').map(&:strip)
43
+ end
44
+
45
+ opts.on('-h', '--help', 'Show this help message') do
46
+ puts opts
47
+ exit
48
+ end
49
+ end.parse!
50
+
51
+ # Load Rails environment from current directory
52
+ # This executable should be run from your Rails application root
53
+ ENV['RAILS_ENV'] ||= options[:env]
54
+ require File.expand_path('config/environment', Dir.pwd)
55
+
56
+ # Postburner is loaded automatically via the Rails engine when the gem is in your Gemfile
57
+
58
+ # Load configuration
59
+ config_path = File.expand_path(options[:config], Dir.pwd)
60
+
61
+ begin
62
+ config = Postburner::Configuration.load_yaml(config_path, options[:env], options[:worker])
63
+ rescue ArgumentError => e
64
+ Rails.logger.error "[Postburner] ERROR: #{e.message}"
65
+ exit 1
66
+ end
67
+
68
+ # Filter queues if --queues option provided
69
+ if options[:queues]
70
+ # Validate that all specified queues exist in config
71
+ invalid_queues = options[:queues] - config.queue_names
72
+ unless invalid_queues.empty?
73
+ config.logger.error "[Postburner] ERROR: Unknown queue(s): #{invalid_queues.join(', ')}"
74
+ config.logger.error "[Postburner] Available queues: #{config.queue_names.join(', ')}"
75
+ exit 1
76
+ end
77
+
78
+ # Filter config to only include specified queues
79
+ filtered_queues = config.queues.select { |name, _| options[:queues].include?(name.to_s) }
80
+ config.queues = filtered_queues
81
+ end
82
+
83
+ config.logger.info "[Postburner] Configuration: #{config_path}"
84
+ config.logger.info "[Postburner] Environment: #{options[:env]}"
85
+ config.logger.info "[Postburner] Worker: #{options[:worker] || '(auto-selected)'}" if options[:worker] || options[:queues].nil?
86
+ config.logger.info "[Postburner] Queues: #{config.queue_names.join(', ')}"
87
+ config.logger.info "[Postburner] Defaults: forks=#{config.default_forks}, threads=#{config.default_threads}, gc_limit=#{config.default_gc_limit || 'none'}"
88
+
89
+ # Create and start worker
90
+ worker = Postburner::Workers::Worker.new(config)
91
+ worker.start
data/bin/rails ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/postburner/engine', __dir__)
7
+ APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12
+
13
+ require "rails/all"
14
+ require "rails/engine/commands"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Leave me here for vim-rails to work in an engine.
@@ -0,0 +1,22 @@
1
+ development:
2
+ beanstalk_url: beanstalk://localhost:11300
3
+ worker_type: simple
4
+ queues:
5
+ default: {}
6
+
7
+ test:
8
+ beanstalk_url: beanstalk://localhost:11300
9
+ worker_type: simple
10
+ queues:
11
+ default: {}
12
+
13
+ production:
14
+ beanstalk_url: beanstalk://localhost:11300
15
+ worker_type: threads_on_fork
16
+ queues:
17
+ critical:
18
+ threads: 1
19
+ gc_limit: 100
20
+ default:
21
+ threads: 5
22
+ gc_limit: 500
@@ -0,0 +1,142 @@
1
+ # Postburner Configuration Example
2
+ #
3
+ # Copy this file to config/postburner.yml and customize for your environment.
4
+ #
5
+ # ## Named Workers Configuration
6
+ #
7
+ # Postburner uses named worker configurations to support different deployment patterns:
8
+ # - Single worker: bin/postburner (auto-selects the single worker)
9
+ # - Multiple workers: bin/postburner --worker <name> (must specify which worker)
10
+ #
11
+ # Each worker can have different fork/thread settings and process different queues.
12
+ # This enables running different queue groups in separate OS processes with distinct
13
+ # concurrency profiles.
14
+ #
15
+ # ## Puma-Style Architecture
16
+ #
17
+ # - **forks: 0** = Single process with thread pool (development/staging)
18
+ # - **forks: 1+** = Multiple processes with thread pools (production)
19
+ #
20
+ # Scale by adjusting forks and threads per worker:
21
+ # - Development: forks=0, threads=1 (single-threaded, easiest debugging)
22
+ # - Staging: forks=0, threads=10 (multi-threaded, moderate load)
23
+ # - Production: forks=4, threads=10 (40 concurrent jobs per queue)
24
+ #
25
+
26
+ default: &default
27
+ # Beanstalkd connection URL
28
+ # Override with ENV['BEANSTALK_URL'] if set
29
+ beanstalk_url: <%= ENV['BEANSTALK_URL'] || 'beanstalk://localhost:11300' %>
30
+
31
+ development:
32
+ <<: *default
33
+
34
+ workers:
35
+ default:
36
+ # Single-threaded, single process (simplest for debugging)
37
+ # Defaults: forks=0, threads=1, gc_limit=nil
38
+ queues:
39
+ - default
40
+ - mailers
41
+
42
+ test:
43
+ <<: *default
44
+
45
+ workers:
46
+ default:
47
+ # Test mode uses inline strategies automatically
48
+ # Defaults: forks=0, threads=1, gc_limit=nil
49
+ queues:
50
+ - default
51
+
52
+ staging:
53
+ <<: *default
54
+
55
+ workers:
56
+ default:
57
+ # Multi-threaded, single process (moderate concurrency)
58
+ default_threads: 10
59
+ default_gc_limit: 5000
60
+ queues:
61
+ - critical
62
+ - default
63
+ - mailers
64
+
65
+ production:
66
+ <<: *default
67
+
68
+ # Example 1: Single worker processing all queues with same settings
69
+ # Run: bin/postburner
70
+ #
71
+ # workers:
72
+ # default:
73
+ # default_forks: 4
74
+ # default_threads: 10
75
+ # default_gc_limit: 5000
76
+ # queues:
77
+ # - critical
78
+ # - default
79
+ # - mailers
80
+ # - imports
81
+
82
+ # Example 2: Multiple workers with different concurrency profiles
83
+ # Run separate processes:
84
+ # bin/postburner --worker imports (4 forks, 1 thread each)
85
+ # bin/postburner --worker general (2 forks, 100 threads each)
86
+ #
87
+ workers:
88
+ # Heavy, memory-intensive jobs - more processes, fewer threads
89
+ imports:
90
+ default_forks: 4
91
+ default_threads: 1
92
+ default_gc_limit: 500
93
+ queues:
94
+ - imports
95
+ - data_processing
96
+
97
+ # General jobs - fewer processes, many threads
98
+ general:
99
+ default_forks: 2
100
+ default_threads: 100
101
+ default_gc_limit: 5000
102
+ queues:
103
+ - default
104
+ - mailers
105
+ - notifications
106
+
107
+ # Example 3: Fine-grained control with multiple specialized workers
108
+ # Run separate processes:
109
+ # bin/postburner --worker critical
110
+ # bin/postburner --worker default
111
+ # bin/postburner --worker mailers
112
+ #
113
+ # workers:
114
+ # critical:
115
+ # default_forks: 1
116
+ # default_threads: 1
117
+ # default_gc_limit: 100
118
+ # queues:
119
+ # - critical
120
+ #
121
+ # default:
122
+ # default_forks: 4
123
+ # default_threads: 10
124
+ # default_gc_limit: 5000
125
+ # queues:
126
+ # - default
127
+ #
128
+ # mailers:
129
+ # default_forks: 2
130
+ # default_threads: 5
131
+ # default_gc_limit: 2000
132
+ # queues:
133
+ # - mailers
134
+
135
+ # Global Defaults (can be overridden per worker):
136
+ #
137
+ # default_queue: default # Default queue name (optional)
138
+ # default_priority: 65536 # Lower = higher priority (optional, 0 is highest)
139
+ # default_ttr: 300 # Time-to-run in seconds (optional)
140
+ # default_threads: 1 # Thread count per fork (optional, defaults to 1)
141
+ # default_forks: 0 # Fork count (optional, defaults to 0 = single process)
142
+ # default_gc_limit: nil # Exit after N jobs for restart (optional, nil = no limit)