postburner 0.9.0.rc.1 → 1.0.0.pre.2
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/README.md +1082 -507
- data/app/models/postburner/job.rb +163 -39
- 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/generators/postburner/install/install_generator.rb +10 -0
- data/lib/generators/postburner/install/templates/config/postburner.yml +142 -0
- data/lib/postburner/active_job/adapter.rb +176 -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/queue.rb +20 -6
- 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 +78 -7
- metadata +44 -11
|
@@ -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,176 @@
|
|
|
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
|
+
# Indicates whether the adapter supports enqueuing after transaction commit.
|
|
62
|
+
#
|
|
63
|
+
# Returns true because Postburner uses Beanstalkd (external queue),
|
|
64
|
+
# which means jobs are safely enqueued outside the database transaction.
|
|
65
|
+
# This allows Rails to automatically defer job enqueuing until after
|
|
66
|
+
# the current database transaction commits.
|
|
67
|
+
#
|
|
68
|
+
# @return [Boolean] Always returns true
|
|
69
|
+
#
|
|
70
|
+
def enqueue_after_transaction_commit?
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Checks if a job should be tracked in PostgreSQL.
|
|
77
|
+
#
|
|
78
|
+
# Detects tracking by checking if the job class includes the
|
|
79
|
+
# Postburner::Tracked module.
|
|
80
|
+
#
|
|
81
|
+
# @param job [ActiveJob::Base] The job instance
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean] true if tracked, false otherwise
|
|
84
|
+
#
|
|
85
|
+
def tracked?(job)
|
|
86
|
+
job.class.included_modules.include?(Postburner::Tracked)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Enqueues a tracked job (with PostgreSQL audit trail).
|
|
90
|
+
#
|
|
91
|
+
# Creates a Postburner::TrackedJob record, then queues minimal payload
|
|
92
|
+
# to Beanstalkd with just the job ID reference.
|
|
93
|
+
#
|
|
94
|
+
# @param job [ActiveJob::Base] The job instance
|
|
95
|
+
# @param timestamp [Time, nil] When to execute the job
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
#
|
|
99
|
+
def enqueue_tracked(job, timestamp)
|
|
100
|
+
# Create Postburner::TrackedJob record
|
|
101
|
+
tracked_job = Postburner::TrackedJob.create!(
|
|
102
|
+
args: Postburner::ActiveJob::Payload.serialize_for_tracked(job),
|
|
103
|
+
run_at: timestamp,
|
|
104
|
+
queued_at: Time.zone.now
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Calculate delay for Beanstalkd
|
|
108
|
+
delay = timestamp ? [(timestamp.to_f - Time.now.to_f).to_i, 0].max : 0
|
|
109
|
+
|
|
110
|
+
# Queue to Beanstalkd with minimal payload
|
|
111
|
+
Postburner.connected do |conn|
|
|
112
|
+
tube_name = expand_tube_name(job.queue_name)
|
|
113
|
+
|
|
114
|
+
# Get priority and TTR from class configuration or fall back to defaults
|
|
115
|
+
pri = job.class.respond_to?(:queue_priority) && job.class.queue_priority ||
|
|
116
|
+
Postburner.configuration.default_priority
|
|
117
|
+
ttr = job.class.respond_to?(:queue_ttr) && job.class.queue_ttr ||
|
|
118
|
+
Postburner.configuration.default_ttr
|
|
119
|
+
|
|
120
|
+
bkid = conn.tubes[tube_name].put(
|
|
121
|
+
Postburner::ActiveJob::Payload.tracked_payload(job, tracked_job.id),
|
|
122
|
+
pri: pri,
|
|
123
|
+
delay: delay,
|
|
124
|
+
ttr: ttr
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Update tracked_job with Beanstalkd ID
|
|
128
|
+
tracked_job.update_column(:bkid, bkid)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Enqueues a default job (Beanstalkd only, no PostgreSQL).
|
|
133
|
+
#
|
|
134
|
+
# Queues full job data to Beanstalkd for fast execution without
|
|
135
|
+
# PostgreSQL overhead.
|
|
136
|
+
#
|
|
137
|
+
# @param job [ActiveJob::Base] The job instance
|
|
138
|
+
# @param timestamp [Time, nil] When to execute the job
|
|
139
|
+
#
|
|
140
|
+
# @return [void]
|
|
141
|
+
#
|
|
142
|
+
def enqueue_default(job, timestamp)
|
|
143
|
+
delay = timestamp ? [(timestamp.to_f - Time.now.to_f).to_i, 0].max : 0
|
|
144
|
+
|
|
145
|
+
Postburner.connected do |conn|
|
|
146
|
+
tube_name = expand_tube_name(job.queue_name)
|
|
147
|
+
|
|
148
|
+
# Get priority and TTR from class configuration or fall back to defaults
|
|
149
|
+
pri = job.class.respond_to?(:queue_priority) && job.class.queue_priority ||
|
|
150
|
+
Postburner.configuration.default_priority
|
|
151
|
+
ttr = job.class.respond_to?(:queue_ttr) && job.class.queue_ttr ||
|
|
152
|
+
Postburner.configuration.default_ttr
|
|
153
|
+
|
|
154
|
+
conn.tubes[tube_name].put(
|
|
155
|
+
Postburner::ActiveJob::Payload.default_payload(job),
|
|
156
|
+
pri: pri,
|
|
157
|
+
delay: delay,
|
|
158
|
+
ttr: ttr
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Expands queue name to full tube name with environment prefix.
|
|
164
|
+
#
|
|
165
|
+
# Delegates to Postburner::Configuration for consistent tube naming.
|
|
166
|
+
#
|
|
167
|
+
# @param queue_name [String] Queue name from ActiveJob
|
|
168
|
+
#
|
|
169
|
+
# @return [String] Full tube name (e.g., 'postburner.production.critical')
|
|
170
|
+
#
|
|
171
|
+
def expand_tube_name(queue_name)
|
|
172
|
+
Postburner.configuration.expand_tube_name(queue_name)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
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
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
module ActiveJob
|
|
5
|
+
# Handles serialization/deserialization of ActiveJob payloads for Beanstalkd.
|
|
6
|
+
#
|
|
7
|
+
# Provides consistent payload format for both default and tracked jobs, with
|
|
8
|
+
# versioning support for future format changes.
|
|
9
|
+
#
|
|
10
|
+
# ## Payload Format (V1)
|
|
11
|
+
#
|
|
12
|
+
# Both default and tracked jobs store the same ActiveJob data in Beanstalkd.
|
|
13
|
+
# The only difference is tracked jobs also persist to PostgreSQL and include
|
|
14
|
+
# a `postburner_job_id` reference.
|
|
15
|
+
#
|
|
16
|
+
# @example Default job payload
|
|
17
|
+
# {
|
|
18
|
+
# v: 1,
|
|
19
|
+
# tracked: false,
|
|
20
|
+
# postburner_job_id: nil,
|
|
21
|
+
# job_class: "SendEmail",
|
|
22
|
+
# job_id: "abc-123",
|
|
23
|
+
# queue_name: "mailers",
|
|
24
|
+
# arguments: [[1, 2, 3]],
|
|
25
|
+
# retry_count: 0,
|
|
26
|
+
# ...
|
|
27
|
+
# }
|
|
28
|
+
#
|
|
29
|
+
# @example Tracked job payload
|
|
30
|
+
# {
|
|
31
|
+
# v: 1,
|
|
32
|
+
# tracked: true,
|
|
33
|
+
# postburner_job_id: 456, # References postburner_jobs.id
|
|
34
|
+
# job_class: "ProcessPayment",
|
|
35
|
+
# job_id: "def-456",
|
|
36
|
+
# queue_name: "critical",
|
|
37
|
+
# arguments: [[789]],
|
|
38
|
+
# retry_count: 0,
|
|
39
|
+
# ...
|
|
40
|
+
# }
|
|
41
|
+
#
|
|
42
|
+
class Payload
|
|
43
|
+
VERSION = 1
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# Generates payload structure for an ActiveJob.
|
|
47
|
+
#
|
|
48
|
+
# @param job [ActiveJob::Base] The ActiveJob instance
|
|
49
|
+
# @param tracked [Boolean] Whether this is a tracked job
|
|
50
|
+
# @param postburner_job_id [Integer, nil] Postburner::Job ID for tracked jobs
|
|
51
|
+
#
|
|
52
|
+
# @return [Hash] Payload hash
|
|
53
|
+
#
|
|
54
|
+
# @api private
|
|
55
|
+
#
|
|
56
|
+
def for_job(job, tracked: false, postburner_job_id: nil)
|
|
57
|
+
{
|
|
58
|
+
v: VERSION,
|
|
59
|
+
tracked: tracked,
|
|
60
|
+
postburner_job_id: postburner_job_id,
|
|
61
|
+
job_class: job.class.name,
|
|
62
|
+
job_id: job.job_id,
|
|
63
|
+
queue_name: job.queue_name,
|
|
64
|
+
priority: job.priority,
|
|
65
|
+
arguments: ::ActiveJob::Arguments.serialize(job.arguments),
|
|
66
|
+
executions: job.executions,
|
|
67
|
+
exception_executions: job.exception_executions || {},
|
|
68
|
+
locale: job.locale,
|
|
69
|
+
timezone: job.timezone,
|
|
70
|
+
enqueued_at: job.enqueued_at&.iso8601,
|
|
71
|
+
retry_count: 0 # For default job retry tracking
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Generates JSON payload for a default job.
|
|
76
|
+
#
|
|
77
|
+
# @param job [ActiveJob::Base] The ActiveJob instance
|
|
78
|
+
#
|
|
79
|
+
# @return [String] JSON-encoded payload
|
|
80
|
+
#
|
|
81
|
+
def default_payload(job)
|
|
82
|
+
JSON.generate(for_job(job, tracked: false))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Generates JSON payload for a tracked job.
|
|
86
|
+
#
|
|
87
|
+
# @param job [ActiveJob::Base] The ActiveJob instance
|
|
88
|
+
# @param postburner_job_id [Integer] Postburner::Job ID
|
|
89
|
+
#
|
|
90
|
+
# @return [String] JSON-encoded payload
|
|
91
|
+
#
|
|
92
|
+
def tracked_payload(job, postburner_job_id)
|
|
93
|
+
JSON.generate(for_job(job, tracked: true, postburner_job_id: postburner_job_id))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Serializes ActiveJob data for PostgreSQL storage (tracked jobs).
|
|
97
|
+
#
|
|
98
|
+
# Returns the same structure as for_job but as a plain Hash
|
|
99
|
+
# (not JSON string) for storage in Postburner::Job args column.
|
|
100
|
+
#
|
|
101
|
+
# @param job [ActiveJob::Base] The ActiveJob instance
|
|
102
|
+
#
|
|
103
|
+
# @return [Hash] Payload hash for PostgreSQL storage
|
|
104
|
+
#
|
|
105
|
+
def serialize_for_tracked(job)
|
|
106
|
+
for_job(job, tracked: true).stringify_keys
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Parses and validates a JSON payload from Beanstalkd.
|
|
110
|
+
#
|
|
111
|
+
# Handles version checking and returns the parsed data.
|
|
112
|
+
#
|
|
113
|
+
# @param json_string [String] JSON payload from Beanstalkd
|
|
114
|
+
#
|
|
115
|
+
# @return [Hash] Parsed payload with string keys
|
|
116
|
+
#
|
|
117
|
+
# @raise [ArgumentError] if payload version is unknown
|
|
118
|
+
#
|
|
119
|
+
def parse(json_string)
|
|
120
|
+
data = JSON.parse(json_string)
|
|
121
|
+
|
|
122
|
+
# Handle version
|
|
123
|
+
version = data['v'] || data[:v] || 1
|
|
124
|
+
|
|
125
|
+
case version
|
|
126
|
+
when 1
|
|
127
|
+
data.is_a?(Hash) ? data.stringify_keys : data
|
|
128
|
+
else
|
|
129
|
+
raise ArgumentError, "Unknown payload version: #{version}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Detects if a parsed payload is for a tracked job.
|
|
134
|
+
#
|
|
135
|
+
# @param payload [Hash] Parsed payload
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean] true if tracked, false otherwise
|
|
138
|
+
#
|
|
139
|
+
def tracked?(payload)
|
|
140
|
+
payload['tracked'] == true || payload[:tracked] == true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Detects if a parsed payload is for a legacy Postburner::Job.
|
|
144
|
+
#
|
|
145
|
+
# Legacy format: { "class" => "JobClassName", "args" => [job_id] }
|
|
146
|
+
#
|
|
147
|
+
# @param payload [Hash] Parsed payload
|
|
148
|
+
#
|
|
149
|
+
# @return [Boolean] true if legacy format
|
|
150
|
+
#
|
|
151
|
+
def legacy_format?(payload)
|
|
152
|
+
payload.key?('class') && payload.key?('args') && !payload.key?('v')
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
# Beanstalkd-specific configuration DSL for ActiveJob classes using Postburner.
|
|
5
|
+
#
|
|
6
|
+
# Provides consistent API with Postburner::Job for setting Beanstalkd queue
|
|
7
|
+
# priority and TTR (time-to-run). Include this module in your ActiveJob classes
|
|
8
|
+
# to use Beanstalkd-specific configuration.
|
|
9
|
+
#
|
|
10
|
+
# @note Automatically included when you include Postburner::Tracked
|
|
11
|
+
#
|
|
12
|
+
# @example Default job with Beanstalkd config
|
|
13
|
+
# class ProcessPayment < ApplicationJob
|
|
14
|
+
# include Postburner::Beanstalkd
|
|
15
|
+
#
|
|
16
|
+
# queue_as :critical
|
|
17
|
+
# queue_priority 0 # Highest priority
|
|
18
|
+
# queue_ttr 300 # 5 minutes to complete
|
|
19
|
+
#
|
|
20
|
+
# def perform(payment_id)
|
|
21
|
+
# # ...
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# @example With tracking (Beanstalkd automatically included)
|
|
26
|
+
# class ProcessPayment < ApplicationJob
|
|
27
|
+
# include Postburner::Tracked # Includes Beanstalkd automatically
|
|
28
|
+
#
|
|
29
|
+
# queue_priority 0
|
|
30
|
+
# queue_ttr 600
|
|
31
|
+
#
|
|
32
|
+
# def perform(payment_id)
|
|
33
|
+
# log "Processing payment"
|
|
34
|
+
# # ...
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
module Beanstalkd
|
|
39
|
+
extend ActiveSupport::Concern
|
|
40
|
+
|
|
41
|
+
included do
|
|
42
|
+
class_attribute :postburner_priority, default: nil
|
|
43
|
+
class_attribute :postburner_ttr, default: nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class_methods do
|
|
47
|
+
# Sets or returns the queue priority.
|
|
48
|
+
#
|
|
49
|
+
# Lower numbers = higher priority in Beanstalkd (0 is highest).
|
|
50
|
+
# Falls back to Postburner.configuration.default_priority if not set.
|
|
51
|
+
#
|
|
52
|
+
# @param pri [Integer, nil] Priority to set (0-4294967295), or nil to get current value
|
|
53
|
+
#
|
|
54
|
+
# @return [Integer, nil] Current priority when getting, nil when setting
|
|
55
|
+
#
|
|
56
|
+
# @example Set priority
|
|
57
|
+
# queue_priority 0 # Highest priority
|
|
58
|
+
#
|
|
59
|
+
# @example Get priority
|
|
60
|
+
# ProcessPayment.queue_priority # => 0
|
|
61
|
+
#
|
|
62
|
+
def queue_priority(pri = nil)
|
|
63
|
+
if pri
|
|
64
|
+
self.postburner_priority = pri
|
|
65
|
+
nil
|
|
66
|
+
else
|
|
67
|
+
postburner_priority
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Sets or returns the queue TTR (time to run).
|
|
72
|
+
#
|
|
73
|
+
# Number of seconds Beanstalkd will wait for job completion before
|
|
74
|
+
# making it available again. Falls back to Postburner.configuration.default_ttr
|
|
75
|
+
# if not set.
|
|
76
|
+
#
|
|
77
|
+
# @param ttr [Integer, nil] Timeout in seconds, or nil to get current value
|
|
78
|
+
#
|
|
79
|
+
# @return [Integer, nil] Current TTR when getting, nil when setting
|
|
80
|
+
#
|
|
81
|
+
# @example Set TTR
|
|
82
|
+
# queue_ttr 300 # 5 minutes
|
|
83
|
+
#
|
|
84
|
+
# @example Get TTR
|
|
85
|
+
# ProcessPayment.queue_ttr # => 300
|
|
86
|
+
#
|
|
87
|
+
def queue_ttr(ttr = nil)
|
|
88
|
+
if ttr
|
|
89
|
+
self.postburner_ttr = ttr
|
|
90
|
+
nil
|
|
91
|
+
else
|
|
92
|
+
postburner_ttr
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|