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.
@@ -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
@@ -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