postburner 1.0.0.pre.3 → 1.0.0.pre.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f7e4e766eb885d932fe5949dc935b1efcb927894117cc427b7582e6807942f6
4
- data.tar.gz: d9b2ed2f5fe0aa61cf0a28d1560f861619c3049aa0bb44fb43b6a6d662914ce8
3
+ metadata.gz: a279e41bee615d867b4392c53c9cfe9535c1652842f166f2f6cd33b3fc6215e2
4
+ data.tar.gz: f11ad9028e92679bc5c5fc3d62b75d8a3a163394bb130dac4bb9a1bcd33c7cfb
5
5
  SHA512:
6
- metadata.gz: 4cb2922ca8f02625680f73232e4cc15590deecd1e6e7f792fda4b547cbb3b300de44107a055247a72ab5b56d0706631323149151708780dd9d719140255973f4
7
- data.tar.gz: c749613be57e8ca5bf14671a2f8c4743e34e9839f4432305c12bfb35e34e4718b1319096ed7336fe5d083994b5da7047b706c55f3a1b735439be4bb6a28c4a55
6
+ metadata.gz: 70eeebf1007b699b58713d3a9fc107aabf118fe6c13c39d519e4b800416d9ea185bb96012be1e37a617ef7b4e52896e1387aa2f11e9b45077912bb154071f62f
7
+ data.tar.gz: 1e34ecbf3dfe03401f7280deca6939e056bceb21ed6d2303e87b4f5c53d5ea23f0d712814acc48a356b471dbd1754505a155e7830c304051bfb1faae2fe4fde1
data/README.md CHANGED
@@ -3,13 +3,55 @@
3
3
  Fast Beanstalkd-backed job queue with **optional PostgreSQL records** for ActiveJob.
4
4
 
5
5
  Postburner provides dual-mode job execution:
6
- - **Default jobs**: Fast execution via Beanstalkd only (no PostgreSQL overhead)
7
- - **Tracked jobs**: Full audit trail with logs, timing, errors, and statistics
6
+ - **Fast jobs**: Fast execution via Beanstalkd
7
+ - **Tracked jobs**: Audited jobs logs, timing, errors, and statistics
8
8
 
9
9
  Built for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
10
10
 
11
11
  Depends on Beanstalkd, ActiveRecord, ActiveJob, Postgres.
12
12
 
13
+ ```ruby
14
+ # Default job (fast, no PostgreSQL overhead)
15
+ class SendSms < ApplicationJob
16
+ def perform(user_id)
17
+ user = User.find(user_id)
18
+ TextMessage.welcome(
19
+ to: user.phone_number,
20
+ body: "Welcome to our app!"
21
+ ).deliver_now
22
+ end
23
+ end
24
+
25
+ # Default job with Beanstalkd configuration
26
+ class DoSomething < ApplicationJob
27
+ include Postburner::Beanstalkd # optional, allow access to beanstalkd
28
+
29
+ priority 5000 # 0=highest, 65536=default, can set per job
30
+ ttr 30 # 30 second timeout
31
+
32
+ def perform(user_id)
33
+ # Do something
34
+ end
35
+ end
36
+
37
+ # Tracked job (full audit trail, includes Beanstalkd automatically)
38
+ class ProcessPayment < ApplicationJob
39
+ include Postburner::Tracked # ← Opt-in to tracking (includes Beanstalkd)
40
+
41
+ priority 0 # Highest priority
42
+ ttr 600 # 10 minute timeout
43
+
44
+ def perform(payment_id)
45
+ log "Processing payment #{payment_id}"
46
+ Payment.find(payment_id).process!
47
+ log! "Payment processed successfully"
48
+ end
49
+ end
50
+
51
+ # Run worker
52
+ bundle exec postburner --worker default
53
+ ```
54
+
13
55
  ## Why
14
56
 
15
57
  Postburner supports Async, Queues, Delayed, Priorities, Timeouts, and Retries from the [Backend Matrix](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). But uniquely, priorities are per job, in addition to the class level. Timeouts are per job and class level as well, and can be extended dynamically.
@@ -23,6 +65,7 @@ Thus old-school [beanstalkd](https://beanstalkd.github.io/) is used with Postgre
23
65
  - Store the jobs outside of the database, but also persist them to disk for disaster recovery (beanstalkd binlogs)
24
66
  - Introspect the jobs either with ActiveRecord or Beanstalkd.
25
67
  - Only one worker type, that can be single/multi-process, with optional threading, and optional GC (Garbage Collection) limits (kill fork after processing N jobs).
68
+ - Easy testing.
26
69
 
27
70
  ## Features
28
71
 
@@ -36,35 +79,17 @@ Thus old-school [beanstalkd](https://beanstalkd.github.io/) is used with Postgre
36
79
 
37
80
  ## Quick Start
38
81
 
39
- ```bash
40
- sudo apt-get install beanstalkd # OR brew install beanstalkd
41
-
42
- # Start beanstalkd (in-memory only)
43
- beanstalkd -l 127.0.0.1 -p 11300
44
-
45
- # OR with persistence (recommended for production)
46
- mkdir -p /var/lib/beanstalkd
47
- beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
48
- ```
49
-
50
82
  ```ruby
51
83
  # Gemfile
52
- gem 'postburner', '~> 1.0.0.pre.2'
53
- ```
84
+ gem 'postburner', '~> 1.0.0.pre.3'
54
85
 
55
- ```bash
56
- rails generate postburner:install
57
- rails db:migrate
58
- ```
59
-
60
- ```ruby
61
86
  # config/application.rb
62
87
  config.active_job.queue_adapter = :postburner
63
88
  ```
64
89
 
65
90
  ```yaml
66
91
  # config/postburner.yml
67
- production: # <- environment config, i.e. defaults
92
+ development: # <- environment config, i.e. defaults
68
93
  beanstalk_url: <%= ENV['BEANSTALK_URL'] || 'beanstalk://localhost:11300' %>
69
94
  default_forks: 2
70
95
  default_threads: 10
@@ -91,48 +116,15 @@ production: # <- environment config, i.e. defaults
91
116
  - video
92
117
  ```
93
118
 
94
- **Usage:**
95
-
96
- ```ruby
97
- # Default job (fast, no PostgreSQL overhead)
98
- class SendSms < ApplicationJob
99
- def perform(user_id)
100
- user = User.find(user_id)
101
- TextMessage.welcome(
102
- to: user.phone_number,
103
- body: "Welcome to our app!"
104
- ).deliver_now
105
- end
106
- end
107
-
108
- # Default job with Beanstalkd configuration
109
- class DoSomething < ApplicationJob
110
- include Postburner::Beanstalkd # optional, allow access to beanstalkd
111
-
112
- priority 5000 # 0=highest, 65536=default, can set per job
113
- ttr 30 # 30 second timeout
114
-
115
- def perform(user_id)
116
- # Do something
117
- end
118
- end
119
-
120
- # Tracked job (full audit trail, includes Beanstalkd automatically)
121
- class ProcessPayment < ApplicationJob
122
- include Postburner::Tracked # ← Opt-in to tracking (includes Beanstalkd)
119
+ ```bash
120
+ sudo apt-get install beanstalkd # OR brew install beanstalkd
123
121
 
124
- priority 0 # Highest priority
125
- ttr 600 # 10 minute timeout
122
+ beanstalkd -l 127.0.0.1 -p 11300 # Start beanstalkd
126
123
 
127
- def perform(payment_id)
128
- log "Processing payment #{payment_id}"
129
- Payment.find(payment_id).process!
130
- log! "Payment processed successfully"
131
- end
132
- end
124
+ bundle exec rails generate postburner:install
125
+ bundle exec rails db:migrate
133
126
 
134
- # Run worker
135
- bundle exec postburner --worker default
127
+ bundle exec postburner # start
136
128
  ```
137
129
 
138
130
  ## Table of Contents
@@ -159,11 +151,14 @@ The [protocol](https://github.com/beanstalkd/beanstalkd/blob/master/doc/protocol
159
151
 
160
152
  Here is a picture of the typical job lifecycle:
161
153
 
154
+ ```
162
155
  put reserve delete
163
- -----> [READY] ---------> [RESERVED] --------> *poof*
156
+ -----> [READY] ---------> [RESERVED] --------> *poof*`
157
+ ```
164
158
 
165
159
  Here is a picture with more possibilities:
166
160
 
161
+ ```
167
162
  put with delay release with delay
168
163
  ----------------> [DELAYED] <------------.
169
164
  | |
@@ -182,13 +177,12 @@ Here is a picture with more possibilities:
182
177
  |
183
178
  | delete
184
179
  `--------> *poof*
180
+ ```
185
181
 
186
182
  ### Binlogs
187
183
 
188
184
  Beanstalkd lets you persist jobs to disk in case of a crash or restart. Just restart beanstalkd and the jobs will be back in the queue.
189
185
 
190
- **Setup:**
191
-
192
186
  ```bash
193
187
  # Create binlog directory
194
188
  sudo mkdir -p /var/lib/beanstalkd
@@ -198,7 +192,7 @@ sudo chown beanstalkd:beanstalkd /var/lib/beanstalkd # If running as service
198
192
  beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
199
193
  ```
200
194
 
201
- **Configuration options:**
195
+ **Other options:**
202
196
 
203
197
  ```bash
204
198
  # Basic persistence
@@ -214,7 +208,7 @@ beanstalkd -b /var/lib/beanstalkd -f 200 # fsync at most every 200ms (default:
214
208
  beanstalkd -b /var/lib/beanstalkd -F
215
209
  ```
216
210
 
217
- **Options:**
211
+ **Beanstalkd switches:**
218
212
  - `-b <dir>` - Enable binlog persistence in specified directory
219
213
  - `-s <bytes>` - Maximum size of each binlog file (requires `-b`)
220
214
  - `-f <ms>` - Call fsync at most once every N milliseconds (requires `-b`, default: 50ms)
@@ -1296,7 +1290,7 @@ Direct access to Beanstalkd for advanced operations:
1296
1290
  job.bkid # => 12345
1297
1291
 
1298
1292
  # Access Beaneater job object
1299
- job.beanstalk_job.stats
1293
+ job.bk.stats
1300
1294
  # => {"id"=>12345, "tube"=>"critical", "state"=>"ready", ...}
1301
1295
 
1302
1296
  # Connection management
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern providing Beanstalkd command methods for Postburner jobs.
5
+ #
6
+ # Provides methods for interacting with Beanstalkd queue operations such as
7
+ # deleting, kicking, and extending TTR. All methods handle connection retries
8
+ # and gracefully handle missing bkid (e.g., in test mode).
9
+ #
10
+ # @example Basic Beanstalkd operations
11
+ # class MyJob < Postburner::Job
12
+ # def perform(args)
13
+ # # ... work ...
14
+ # extend! # Extend TTR during long operation
15
+ # end
16
+ # end
17
+ #
18
+ # job.delete! # Remove from Beanstalkd
19
+ # job.kick! # Retry buried job
20
+ #
21
+ module Commands
22
+ extend ActiveSupport::Concern
23
+
24
+ # Kicks a buried job back into the ready queue in Beanstalkd.
25
+ #
26
+ # This is a Beanstalkd operation used to retry jobs that were buried due to
27
+ # repeated failures or explicit burial. Does not modify the ActiveRecord model.
28
+ #
29
+ # Automatically retries with fresh connection if Beanstalkd connection is stale.
30
+ #
31
+ # @return [void]
32
+ #
33
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails after retry
34
+ #
35
+ # @note Does nothing if job has no bkid (e.g., in test mode)
36
+ # @note Only works on buried jobs - see Beanstalkd documentation
37
+ #
38
+ # @example
39
+ # job.kick! # Moves buried job back to ready queue
40
+ #
41
+ # @see #delete!
42
+ # @see #bk
43
+ #
44
+ def kick!
45
+ return unless self.bk
46
+ begin
47
+ self.bk.kick
48
+ rescue Beaneater::NotConnected => e
49
+ self.bk!.kick
50
+ end
51
+ end
52
+
53
+ # Extends the job's time-to-run (TTR) in Beanstalkd.
54
+ #
55
+ # Calls touch on the Beanstalkd job, extending the TTR by the original
56
+ # TTR value. Use this during long-running operations to prevent the job
57
+ # from timing out.
58
+ #
59
+ # @return [void]
60
+ #
61
+ # @note Does nothing if job has no bkid (e.g., in test mode)
62
+ #
63
+ # @example Process large file line by line
64
+ # def perform(args)
65
+ # file = File.find(args['file_id'])
66
+ # file.each_line do |line|
67
+ # # ... process line ...
68
+ # extend! # Extend TTR to prevent timeout
69
+ # end
70
+ # end
71
+ #
72
+ # @see #bk
73
+ #
74
+ def extend!
75
+ return unless self.bk
76
+ begin
77
+ self.bk.touch
78
+ rescue Beaneater::NotConnected => e
79
+ self.bk!.touch
80
+ end
81
+ end
82
+
83
+ # Deletes the job from the Beanstalkd queue.
84
+ #
85
+ # This is a Beanstalkd operation that removes the job from the queue but
86
+ # does NOT destroy the ActiveRecord model. Use {#destroy} or {#remove!} to
87
+ # also update the database record.
88
+ #
89
+ # Automatically retries with fresh connection if Beanstalkd connection is stale.
90
+ # Called automatically by before_destroy callback when using {#destroy}.
91
+ #
92
+ # @return [void]
93
+ #
94
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails after retry
95
+ #
96
+ # @note Does nothing if job has no bkid (e.g., in test mode)
97
+ # @note Does not modify ActiveRecord model - only affects Beanstalkd
98
+ #
99
+ # @example
100
+ # job.delete! # Removes from Beanstalkd queue only
101
+ # job.reload # Job still exists in database
102
+ #
103
+ # @see #remove!
104
+ # @see #destroy
105
+ # @see #bk
106
+ #
107
+ def delete!
108
+ return unless self.bk
109
+ begin
110
+ self.bk.delete
111
+ rescue Beaneater::NotConnected => e
112
+ self.bk!.delete
113
+ end
114
+ end
115
+
116
+ # Soft-deletes the job by removing from Beanstalkd and setting removed_at timestamp.
117
+ #
118
+ # Unlike {#destroy}, this preserves the job record in the database for audit trails
119
+ # while removing it from the Beanstalkd queue and marking it as removed.
120
+ #
121
+ # @return [void]
122
+ #
123
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails
124
+ #
125
+ # @note Idempotent - does nothing if already removed
126
+ # @note Does not destroy ActiveRecord model - only soft deletes
127
+ #
128
+ # @example
129
+ # job.remove!
130
+ # job.removed_at # => 2025-10-31 12:34:56 UTC
131
+ # job.persisted? # => true (still in database)
132
+ #
133
+ # @see #delete!
134
+ # @see #destroy
135
+ #
136
+ def remove!
137
+ return if self.removed_at
138
+ self.delete!
139
+ self.update_column(:removed_at, Time.zone.now)
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern providing job execution methods for Postburner jobs.
5
+ #
6
+ # Handles the full job execution lifecycle including validation, callbacks,
7
+ # timing tracking, error handling, and state management. Provides both the
8
+ # class-level entry point for workers and the instance-level execution logic.
9
+ #
10
+ # @example Direct execution
11
+ # Postburner::Job.perform(job.id)
12
+ #
13
+ # @example Instance execution
14
+ # job.perform!(job.args)
15
+ #
16
+ module Execution
17
+ extend ActiveSupport::Concern
18
+
19
+ class_methods do
20
+ # Executes a job by ID, delegating to the current queue strategy.
21
+ #
22
+ # Loads the job from database by ID and delegates execution to the current
23
+ # queue strategy's {handle_perform!} method. This provides a unified API
24
+ # for job execution regardless of strategy (async, test, or null).
25
+ #
26
+ # Called automatically by Backburner workers in production. Can also be
27
+ # called manually for test/null strategies to trigger execution.
28
+ #
29
+ # @param id [Integer] Job ID to execute
30
+ # @param _ [Hash] Unused Backburner metadata parameter
31
+ #
32
+ # @return [void]
33
+ #
34
+ # @example Backburner automatic execution (production)
35
+ # # Jobs execute in tube: backburner.worker.queue.backburner-jobs
36
+ # # Backburner calls: Postburner::Job.perform(job_id)
37
+ #
38
+ # @example Manual execution with NullQueue
39
+ # Postburner.null_strategy!
40
+ # job = MyJob.create!(args: {})
41
+ # job.queue!(delay: 1.hour)
42
+ # Postburner::Job.perform(job.id) # Time travels and executes
43
+ #
44
+ # @note Strategy-aware: delegates to Postburner.queue_strategy.handle_perform!
45
+ # @note For NullQueue, automatically handles time travel for scheduled jobs
46
+ #
47
+ # @see #perform!
48
+ # @see Queue.handle_perform!
49
+ # @see NullQueue.handle_perform!
50
+ #
51
+ def perform(id, _={})
52
+ job = nil
53
+ begin
54
+ job = self.find(id)
55
+ rescue ActiveRecord::RecordNotFound => e
56
+ Rails.logger.warn <<-MSG
57
+ [Postburner::Job] [#{id}] Not Found.
58
+ MSG
59
+ end
60
+ #job.perform!(job.args)
61
+ Postburner.queue_strategy.handle_perform!(job)
62
+ end
63
+ end
64
+
65
+ # Executes the job with full lifecycle management and error handling.
66
+ #
67
+ # This is the main execution method called by Backburner workers or test strategies.
68
+ # Performs validation checks, executes callbacks, calls the subclass {#perform} method,
69
+ # tracks timing and statistics, and handles errors.
70
+ #
71
+ # Execution flow:
72
+ # 1. Runs attempt callbacks (fires on every retry)
73
+ # 2. Updates attempting metadata (attempting_at, attempts, lag)
74
+ # 3. Validates job state (queued, not processed, not removed, not premature)
75
+ # 4. Runs processing callbacks
76
+ # 5. Calls subclass {#perform} method
77
+ # 6. Runs processed callbacks (only on success)
78
+ # 7. Updates completion metadata (processed_at, duration)
79
+ # 8. Logs and tracks any exceptions
80
+ #
81
+ # @param args [Hash] Arguments to pass to {#perform} method
82
+ #
83
+ # @return [void]
84
+ #
85
+ # @raise [Exception] Any exception raised by {#perform} is logged and re-raised
86
+ #
87
+ # @note Does not execute if queued_at is nil, in the future, already processed, or removed
88
+ # @note Premature execution (before run_at) is delegated to queue strategy
89
+ #
90
+ # @see #perform
91
+ # @see Postburner.queue_strategy
92
+ # @see Callbacks
93
+ #
94
+ def perform!(args={})
95
+ run_callbacks :attempt do
96
+ self.attempting
97
+
98
+ self.update_columns(
99
+ attempting_at: self.attempting_at,
100
+ attempts: self.attempts,
101
+ attempt_count: self.attempts.length,
102
+ lag: self.lag,
103
+ processing_at: Time.zone.now,
104
+ )
105
+
106
+ begin
107
+ if self.queued_at.nil?
108
+ self.log! "Not Queued", level: :error
109
+ return
110
+ end
111
+
112
+ if self.queued_at > Time.zone.now
113
+ self.log! "Future Queued", level: :error
114
+ return
115
+ end
116
+
117
+ if self.processed_at.present?
118
+ self.log! "Already Processed", level: :error
119
+ self.delete!
120
+ return
121
+ end
122
+
123
+ if self.removed_at.present?
124
+ self.log! "Removed", level: :error
125
+ return
126
+ end
127
+
128
+ if self.run_at && self.run_at.to_i > Time.zone.now.to_i
129
+ Postburner.queue_strategy.handle_premature_perform(self)
130
+ return
131
+ end
132
+
133
+ self.log!("START (bkid #{self.bkid})")
134
+
135
+ run_callbacks :processing do
136
+ begin
137
+ self.perform(args)
138
+ rescue Exception => exception
139
+ self.persist_metadata!
140
+ self.log! '[Postburner] Exception raised during perform prevented completion.'
141
+ raise exception
142
+ end
143
+ end
144
+
145
+ self.log!("DONE (bkid #{self.bkid})")
146
+
147
+ begin
148
+ now = Time.zone.now
149
+ _duration = (now - self.processing_at) * 1000 rescue nil
150
+
151
+ run_callbacks :processed do
152
+ persist_metadata!(
153
+ processed_at: now,
154
+ duration: _duration,
155
+ )
156
+ end
157
+ rescue Exception => e
158
+ self.log_exception!(e)
159
+ self.log! '[Postburner] Could not set data after processing.'
160
+ # TODO README doesn't retry if Postburner is to blame
161
+ end
162
+
163
+ rescue Exception => exception
164
+ self.log_exception!(exception)
165
+ raise exception
166
+ end
167
+ end # run_callbacks :attempt
168
+
169
+ end
170
+
171
+ private
172
+
173
+ # Records an attempt and calculates execution lag.
174
+ #
175
+ # Appends current time to attempts array, sets attempting_at on first attempt,
176
+ # and calculates lag (delay between intended and actual execution time).
177
+ #
178
+ # @return [Time] Current timestamp
179
+ #
180
+ # @api private
181
+ #
182
+ def attempting
183
+ now = Time.zone.now
184
+ self.attempts << now
185
+ self.attempting_at ||= now
186
+ self.lag ||= (self.attempting_at - self.intended_at) * 1000 rescue nil
187
+ now
188
+ end
189
+ end
190
+ end