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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +0 -22
- data/README.md +1219 -238
- data/Rakefile +1 -1
- data/app/concerns/postburner/callbacks.rb +286 -0
- data/app/models/postburner/job.rb +735 -46
- data/app/models/postburner/tracked_job.rb +83 -0
- data/bin/postburner +91 -0
- data/bin/rails +14 -0
- data/config/environment.rb +3 -0
- data/config/postburner.yml +22 -0
- data/config/postburner.yml.example +142 -0
- data/lib/postburner/active_job/adapter.rb +163 -0
- data/lib/postburner/active_job/execution.rb +109 -0
- data/lib/postburner/active_job/payload.rb +157 -0
- data/lib/postburner/beanstalkd.rb +97 -0
- data/lib/postburner/configuration.rb +202 -0
- data/lib/postburner/connection.rb +113 -0
- data/lib/postburner/engine.rb +1 -1
- data/lib/postburner/queue_config.rb +151 -0
- 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 +119 -0
- data/lib/postburner/strategies/test_queue.rb +128 -0
- data/lib/postburner/time_helpers.rb +75 -0
- data/lib/postburner/tracked.rb +171 -0
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/workers/base.rb +210 -0
- data/lib/postburner/workers/worker.rb +480 -0
- data/lib/postburner.rb +433 -4
- metadata +66 -17
- data/lib/postburner/tube.rb +0 -53
|
@@ -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"
|
data/config/environment.rb
CHANGED
|
@@ -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)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module QueueAdapters
|
|
5
|
+
# Postburner adapter for ActiveJob.
|
|
6
|
+
#
|
|
7
|
+
# Provides dual-mode job execution:
|
|
8
|
+
# - **Default mode**: Fast execution via Beanstalkd only
|
|
9
|
+
# - **Tracked mode** (opt-in): Full PostgreSQL audit trail
|
|
10
|
+
#
|
|
11
|
+
# ## Usage
|
|
12
|
+
#
|
|
13
|
+
# @example Configure in Rails
|
|
14
|
+
# # config/application.rb
|
|
15
|
+
# config.active_job.queue_adapter = :postburner
|
|
16
|
+
#
|
|
17
|
+
# @example Default job (fast)
|
|
18
|
+
# class SendEmail < ApplicationJob
|
|
19
|
+
# def perform(user_id)
|
|
20
|
+
# # No PostgreSQL overhead, executes quickly
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example Tracked job (opt-in for audit trail)
|
|
25
|
+
# class ProcessPayment < ApplicationJob
|
|
26
|
+
# include Postburner::Tracked
|
|
27
|
+
# tracked # ← Enables PostgreSQL tracking
|
|
28
|
+
#
|
|
29
|
+
# def perform(payment_id)
|
|
30
|
+
# log "Processing payment..."
|
|
31
|
+
# # Full audit trail: logs, timing, errors
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
class PostburnerAdapter
|
|
36
|
+
# Enqueues a job for immediate execution.
|
|
37
|
+
#
|
|
38
|
+
# @param job [ActiveJob::Base] The job to enqueue
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
#
|
|
42
|
+
def enqueue(job)
|
|
43
|
+
enqueue_at(job, nil)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Enqueues a job for execution at a specific time.
|
|
47
|
+
#
|
|
48
|
+
# @param job [ActiveJob::Base] The job to enqueue
|
|
49
|
+
# @param timestamp [Time, nil] When to execute the job (nil = immediate)
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
#
|
|
53
|
+
def enqueue_at(job, timestamp)
|
|
54
|
+
if tracked?(job)
|
|
55
|
+
enqueue_tracked(job, timestamp)
|
|
56
|
+
else
|
|
57
|
+
enqueue_default(job, timestamp)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Checks if a job should be tracked in PostgreSQL.
|
|
64
|
+
#
|
|
65
|
+
# Detects tracking by checking if the job class includes the
|
|
66
|
+
# Postburner::Tracked module.
|
|
67
|
+
#
|
|
68
|
+
# @param job [ActiveJob::Base] The job instance
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean] true if tracked, false otherwise
|
|
71
|
+
#
|
|
72
|
+
def tracked?(job)
|
|
73
|
+
job.class.included_modules.include?(Postburner::Tracked)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Enqueues a tracked job (with PostgreSQL audit trail).
|
|
77
|
+
#
|
|
78
|
+
# Creates a Postburner::TrackedJob record, then queues minimal payload
|
|
79
|
+
# to Beanstalkd with just the job ID reference.
|
|
80
|
+
#
|
|
81
|
+
# @param job [ActiveJob::Base] The job instance
|
|
82
|
+
# @param timestamp [Time, nil] When to execute the job
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
#
|
|
86
|
+
def enqueue_tracked(job, timestamp)
|
|
87
|
+
# Create Postburner::TrackedJob record
|
|
88
|
+
tracked_job = Postburner::TrackedJob.create!(
|
|
89
|
+
args: Postburner::ActiveJob::Payload.serialize_for_tracked(job),
|
|
90
|
+
run_at: timestamp,
|
|
91
|
+
queued_at: Time.zone.now
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Calculate delay for Beanstalkd
|
|
95
|
+
delay = timestamp ? [(timestamp.to_f - Time.now.to_f).to_i, 0].max : 0
|
|
96
|
+
|
|
97
|
+
# Queue to Beanstalkd with minimal payload
|
|
98
|
+
Postburner.connected do |conn|
|
|
99
|
+
tube_name = expand_tube_name(job.queue_name)
|
|
100
|
+
|
|
101
|
+
# Get priority and TTR from class configuration or fall back to defaults
|
|
102
|
+
pri = job.class.respond_to?(:queue_priority) && job.class.queue_priority ||
|
|
103
|
+
Postburner.configuration.default_priority
|
|
104
|
+
ttr = job.class.respond_to?(:queue_ttr) && job.class.queue_ttr ||
|
|
105
|
+
Postburner.configuration.default_ttr
|
|
106
|
+
|
|
107
|
+
bkid = conn.tubes[tube_name].put(
|
|
108
|
+
Postburner::ActiveJob::Payload.tracked_payload(job, tracked_job.id),
|
|
109
|
+
pri: pri,
|
|
110
|
+
delay: delay,
|
|
111
|
+
ttr: ttr
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Update tracked_job with Beanstalkd ID
|
|
115
|
+
tracked_job.update_column(:bkid, bkid)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Enqueues a default job (Beanstalkd only, no PostgreSQL).
|
|
120
|
+
#
|
|
121
|
+
# Queues full job data to Beanstalkd for fast execution without
|
|
122
|
+
# PostgreSQL overhead.
|
|
123
|
+
#
|
|
124
|
+
# @param job [ActiveJob::Base] The job instance
|
|
125
|
+
# @param timestamp [Time, nil] When to execute the job
|
|
126
|
+
#
|
|
127
|
+
# @return [void]
|
|
128
|
+
#
|
|
129
|
+
def enqueue_default(job, timestamp)
|
|
130
|
+
delay = timestamp ? [(timestamp.to_f - Time.now.to_f).to_i, 0].max : 0
|
|
131
|
+
|
|
132
|
+
Postburner.connected do |conn|
|
|
133
|
+
tube_name = expand_tube_name(job.queue_name)
|
|
134
|
+
|
|
135
|
+
# Get priority and TTR from class configuration or fall back to defaults
|
|
136
|
+
pri = job.class.respond_to?(:queue_priority) && job.class.queue_priority ||
|
|
137
|
+
Postburner.configuration.default_priority
|
|
138
|
+
ttr = job.class.respond_to?(:queue_ttr) && job.class.queue_ttr ||
|
|
139
|
+
Postburner.configuration.default_ttr
|
|
140
|
+
|
|
141
|
+
conn.tubes[tube_name].put(
|
|
142
|
+
Postburner::ActiveJob::Payload.default_payload(job),
|
|
143
|
+
pri: pri,
|
|
144
|
+
delay: delay,
|
|
145
|
+
ttr: ttr
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Expands queue name to full tube name with environment prefix.
|
|
151
|
+
#
|
|
152
|
+
# Delegates to Postburner::Configuration for consistent tube naming.
|
|
153
|
+
#
|
|
154
|
+
# @param queue_name [String] Queue name from ActiveJob
|
|
155
|
+
#
|
|
156
|
+
# @return [String] Full tube name (e.g., 'postburner.production.critical')
|
|
157
|
+
#
|
|
158
|
+
def expand_tube_name(queue_name)
|
|
159
|
+
Postburner.configuration.expand_tube_name(queue_name)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
module ActiveJob
|
|
5
|
+
# Handles execution of ActiveJob jobs from Beanstalkd payloads.
|
|
6
|
+
#
|
|
7
|
+
# Supports both default jobs (fast, no PostgreSQL) and tracked jobs
|
|
8
|
+
# (full audit trail). Also handles legacy Postburner::Job format
|
|
9
|
+
# for backward compatibility.
|
|
10
|
+
#
|
|
11
|
+
class Execution
|
|
12
|
+
class << self
|
|
13
|
+
# Executes a job from a Beanstalkd payload.
|
|
14
|
+
#
|
|
15
|
+
# Automatically detects payload type (default, tracked, or legacy)
|
|
16
|
+
# and routes to the appropriate execution path.
|
|
17
|
+
#
|
|
18
|
+
# @param payload_json [String] JSON payload from Beanstalkd
|
|
19
|
+
#
|
|
20
|
+
# @return [void]
|
|
21
|
+
#
|
|
22
|
+
# @raise [StandardError] if job execution fails
|
|
23
|
+
#
|
|
24
|
+
def execute(payload_json)
|
|
25
|
+
payload = Payload.parse(payload_json)
|
|
26
|
+
|
|
27
|
+
if Payload.legacy_format?(payload)
|
|
28
|
+
execute_legacy(payload)
|
|
29
|
+
elsif Payload.tracked?(payload)
|
|
30
|
+
execute_tracked(payload)
|
|
31
|
+
else
|
|
32
|
+
execute_default(payload)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Executes a tracked job (with PostgreSQL audit trail).
|
|
39
|
+
#
|
|
40
|
+
# Loads the Postburner::Job record and delegates to its perform! method,
|
|
41
|
+
# which handles all the audit trail logging, timing, and error tracking.
|
|
42
|
+
#
|
|
43
|
+
# @param payload [Hash] Parsed payload
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
#
|
|
47
|
+
def execute_tracked(payload)
|
|
48
|
+
job_id = payload['postburner_job_id']
|
|
49
|
+
|
|
50
|
+
unless job_id
|
|
51
|
+
raise ArgumentError, "Tracked job missing postburner_job_id"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Delegate to Postburner::Job.perform which handles the full lifecycle
|
|
55
|
+
Postburner::Job.perform(job_id)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Executes a default job (Beanstalkd only, no PostgreSQL).
|
|
59
|
+
#
|
|
60
|
+
# Deserializes the ActiveJob, restores its metadata, and executes it.
|
|
61
|
+
# Handles retries via ActiveJob's retry_on/discard_on mechanisms.
|
|
62
|
+
#
|
|
63
|
+
# @param payload [Hash] Parsed payload
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
#
|
|
67
|
+
def execute_default(payload)
|
|
68
|
+
job_class = payload['job_class'].constantize
|
|
69
|
+
arguments = ::ActiveJob::Arguments.deserialize(payload['arguments'])
|
|
70
|
+
|
|
71
|
+
# Instantiate the job
|
|
72
|
+
job = job_class.new(*arguments)
|
|
73
|
+
|
|
74
|
+
# Restore job metadata
|
|
75
|
+
job.job_id = payload['job_id']
|
|
76
|
+
job.queue_name = payload['queue_name']
|
|
77
|
+
job.priority = payload['priority']
|
|
78
|
+
job.executions = payload['executions'] || 0
|
|
79
|
+
job.exception_executions = payload['exception_executions'] || {}
|
|
80
|
+
job.locale = payload['locale']
|
|
81
|
+
job.timezone = payload['timezone']
|
|
82
|
+
|
|
83
|
+
if payload['enqueued_at']
|
|
84
|
+
job.enqueued_at = Time.iso8601(payload['enqueued_at'])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Execute the job (ActiveJob handles retry_on/discard_on)
|
|
88
|
+
job.perform_now
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Executes a legacy Postburner::Job (backward compatibility).
|
|
92
|
+
#
|
|
93
|
+
# Legacy format: { "class" => "JobClassName", "args" => [job_id] }
|
|
94
|
+
#
|
|
95
|
+
# @param payload [Hash] Parsed legacy payload
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
#
|
|
99
|
+
def execute_legacy(payload)
|
|
100
|
+
job_class = payload['class'].constantize
|
|
101
|
+
job_id = payload['args'].first
|
|
102
|
+
|
|
103
|
+
# Delegate to the class's perform method (Postburner::Job.perform)
|
|
104
|
+
job_class.perform(job_id)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|