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.
data/README.md CHANGED
@@ -1,771 +1,1346 @@
1
1
  # Postburner
2
- An ActiveRecord layer on top of [Backburner](https://github.com/nesquena/backburner) for inspecting and auditing the
3
- queue, especially for delayed jobs. It isn't meant to be outperform other queues, but be safe (and inspectable).
4
2
 
5
- It is meant to be complementary to [Backburner](https://github.com/nesquena/backburner). Use Backburner as the default
6
- ActiveJob processor for mailers, active storage, and the like. Use a
7
- `Postburner::Job` for things that you want to track. See [Comparison to Backburner](#comparison-to-backburner) for more.
3
+ Fast Beanstalkd-backed job queue with **optional PostgreSQL audit trail** for ActiveJob.
8
4
 
9
- Postburner meant to be a replacement/upgrade for [Que](https://github.com/que-rb/que).
10
- If you need something faster, check out Que - we love it! Postburner is built for a slightly different purpose: to be
11
- simple, safe, and inspectable. All of which Que has (or can be added), but are included by default with some architecture
12
- differences. See [Comparison to Que](#comparison-to-que) for more.
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
13
8
 
14
- ## Queue Strategies
9
+ Built for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
15
10
 
16
- Postburner uses different strategies to control how jobs are queued and executed:
11
+ ## Features
17
12
 
18
- | Strategy | When to Use | Behavior | Requires Beanstalkd |
19
- |----------|-------------|----------|---------------------|
20
- | **NiceQueue** (default) | Production | Async via Beanstalkd, gracefully re-queues premature jobs | Yes |
21
- | **Queue** (suggested) | Production | Async via Beanstalkd, raises error on premature execution | Yes |
22
- | **TestQueue** | Testing with explicit time control | Inline execution, raises error for scheduled jobs | No |
23
- | **ImmediateTestQueue** | Testing with automatic time travel | Inline execution, auto time-travels for scheduled jobs | No |
24
- | **NullQueue** | Batch processing / deferred execution | Jobs created but not queued, manual execution via `Postburner::Job.perform(id)` | No |
13
+ - **ActiveJob native** - Works seamlessly with Rails, ActionMailer, ActiveStorage
14
+ - **Dual-mode execution** - Default or tracked (database backed)
15
+ - **Rich audit trail** - Logs, timing, errors, retry tracking (tracked jobs only)
16
+ - **ActiveRecord** - Query jobs with ActiveRecord (Tracked jobs only)
17
+ - **Beanstalkd** - Fast, reliable queue separate from your database, peristent storage
18
+ - **Process isolation** - Forking workers with optional threading for throughput
19
+ - **Test-friendly** - Inline execution without Beanstalkd in tests
20
+
21
+ ## Quick Start
22
+
23
+ **Installation:**
24
+
25
+ ```bash
26
+ sudo apt-get install beanstalkd # OR brew install beanstalkd
27
+
28
+ # Start beanstalkd (in-memory only)
29
+ beanstalkd -l 127.0.0.1 -p 11300
30
+
31
+ # OR with persistence (recommended for production)
32
+ mkdir -p /var/lib/beanstalkd
33
+ beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
34
+ ```
25
35
 
26
36
  ```ruby
27
- # Switch strategies
28
- Postburner.nice_async_strategy! # Default production (NiceQueue)
29
- Postburner.async_strategy! # Strict production (Queue)
30
- Postburner.inline_test_strategy! # Testing (TestQueue)
31
- Postburner.inline_immediate_test_strategy! # Testing with time travel (ImmediateTestQueue)
32
- Postburner.null_strategy! # Deferred execution (NullQueue)
37
+ # Gemfile
38
+ gem 'postburner', '~> 1.0.0.pre.1'
33
39
  ```
34
40
 
35
- ## Usage
41
+ ```bash
42
+ bundle install
43
+ rails generate postburner:install
44
+ rails db:migrate
45
+ ```
46
+
47
+ **Configuration:**
36
48
 
37
49
  ```ruby
38
- class RunDonation < Postburner::Job
39
- queue 'critical'
40
- queue_priority 0 # 0 is highest priority
41
- queue_max_job_retries 0 # don't retry
50
+ # config/application.rb
51
+ config.active_job.queue_adapter = :postburner
52
+ ```
42
53
 
43
- def perform(args)
44
- # do long tasks here
45
- # also have access to self.args
54
+ ```yaml
55
+ # config/postburner.yml
56
+ production:
57
+ beanstalk_url: <%= ENV['BEANSTALK_URL'] || 'beanstalk://localhost:11300' %>
58
+ worker_type: simple
59
+ queues:
60
+ default: {}
61
+ critical: {}
62
+ mailers: {}
63
+ ```
64
+
65
+ **Usage:**
66
+
67
+ ```ruby
68
+ # Default job (fast, no PostgreSQL overhead)
69
+ class SendEmail < ApplicationJob
70
+ def perform(user_id)
71
+ UserMailer.welcome(user_id).deliver_now
46
72
  end
47
73
  end
48
74
 
49
- # RunDonation#create! is the `ActiveRecord` method, so it returns a model,
50
- # you can manipulate, add columns to the table to, reference with foreign
51
- # keys, etc.
52
- job = RunDonation.create!(args: {donation_id: 123})
53
- # NOTE Make sure use use an after_commit or after_save_commit, etc to avoid
54
- # any race conditions of Postburner trying to process before a required
55
- # database mutation is commited.
56
- job.queue!
57
- => {:status=>"INSERTED", :id=>"1139"}
75
+ # Default job with Beanstalkd configuration
76
+ class SendEmail < ApplicationJob
77
+ include Postburner::Beanstalkd
78
+
79
+ queue_priority 100 # Custom priority
80
+ queue_ttr 300 # 5 minute timeout
81
+
82
+ def perform(user_id)
83
+ UserMailer.welcome(user_id).deliver_now
84
+ end
85
+ end
86
+
87
+ # Tracked job (full audit trail, includes Beanstalkd automatically)
88
+ class ProcessPayment < ApplicationJob
89
+ include Postburner::Tracked # ← Opt-in to tracking (includes Beanstalkd)
58
90
 
59
- # queue for later with `:at`
60
- RunDonation.create!(args: {donation_id: 123}).queue! at: Time.zone.now + 2.days
61
- => {:status=>"INSERTED", :id=>"1140"}
91
+ queue_priority 0 # Highest priority
92
+ queue_ttr 600 # 10 minute timeout
93
+
94
+ def perform(payment_id)
95
+ log "Processing payment #{payment_id}"
96
+ Payment.find(payment_id).process!
97
+ log! "Payment processed successfully"
98
+ end
99
+ end
62
100
 
63
- # queue for later with `:delay`
64
- #
65
- # `:delay` takes priority over `:at`takes priority over `:at` because the
66
- # beanstalkd protocol uses uses `delay`
67
- RunDonation.create!(args: {donation_id: 123}).queue! delay: 1.hour
68
- => {:status=>"INSERTED", :id=>"1141"}
101
+ # Run worker
102
+ bin/postburner --config config/postburner.yml --env production
69
103
  ```
70
104
 
71
- ### Backburner Queue Configuration
105
+ ## Table of Contents
72
106
 
73
- Postburner jobs inherit from `Backburner::Queue`, giving you access to Backburner's queue configuration methods. These are **optional** - Backburner provides sensible defaults.
107
+ - [Why Beanstalkd?](#why-beanstalkd)
108
+ - [Installation](#installation)
109
+ - [Usage](#usage)
110
+ - [Standard Jobs](#standard-jobs)
111
+ - [Tracked Jobs](#tracked-jobs)
112
+ - [Postburner::Job](#postburnerjob)
113
+ - [Workers](#workers)
114
+ - [Configuration](#configuration)
115
+ - [Queue Strategies](#queue-strategies)
116
+ - [Testing](#testing)
117
+ - [Job Management](#job-management)
118
+ - [Beanstalkd Integration](#beanstalkd-integration)
119
+ - [Web UI](#web-ui)
74
120
 
75
- ```ruby
76
- class CriticalJob < Postburner::Job
77
- # Specify which Beanstalkd tube to use (default: 'backburner-jobs')
78
- queue 'critical'
121
+ ## Why Beanstalkd?
79
122
 
80
- # Set job priority - lower numbers = higher priority (default: 65536)
81
- # Most urgent priority is 0
82
- queue_priority 0
123
+ Beanstalkd is a simple, fast, and reliable queue system. It is a good choice for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
83
124
 
84
- # Set maximum retry attempts before burying (default: from Backburner config)
85
- queue_max_job_retries 3
125
+ The [protocol](https://github.com/beanstalkd/beanstalkd/blob/master/doc/protocol.txt) reads more like a README than a protocol. Check it out and you will instantly understand how it works.
86
126
 
87
- # Set job timeout in seconds (default: from Backburner config)
88
- queue_respond_timeout 300
127
+ ### Binlogs
89
128
 
90
- def perform(args)
91
- # your job logic
92
- end
93
- end
129
+ 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.
130
+
131
+ **Setup:**
132
+
133
+ ```bash
134
+ # Create binlog directory
135
+ sudo mkdir -p /var/lib/beanstalkd
136
+ sudo chown beanstalkd:beanstalkd /var/lib/beanstalkd # If running as service
137
+
138
+ # Start beanstalkd with binlog persistence
139
+ beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
140
+ ```
141
+
142
+ **Configuration options:**
143
+
144
+ ```bash
145
+ # Basic persistence
146
+ beanstalkd -b /var/lib/beanstalkd
147
+
148
+ # With custom binlog file size (default varies)
149
+ beanstalkd -b /var/lib/beanstalkd -s 10485760 # 10MB per binlog file
150
+
151
+ # Reduce fsync frequency for better performance (trades safety for speed)
152
+ beanstalkd -b /var/lib/beanstalkd -f 200 # fsync at most every 200ms (default: 50ms)
153
+
154
+ # Never fsync (maximum performance, higher risk of data loss on power failure)
155
+ beanstalkd -b /var/lib/beanstalkd -F
94
156
  ```
95
157
 
96
- **Common configurations:**
158
+ **Options:**
159
+ - `-b <dir>` - Enable binlog persistence in specified directory
160
+ - `-s <bytes>` - Maximum size of each binlog file (requires `-b`)
161
+ - `-f <ms>` - Call fsync at most once every N milliseconds (requires `-b`, default: 50ms)
162
+ - `-F` - Never call fsync (requires `-b`, maximum performance but higher data loss risk)
163
+
164
+ **Note:** The fsync options control the trade-off between performance and safety. A power failure could result in loss of jobs added within the fsync interval.
165
+
166
+ ### Priority
167
+
168
+ Priority controls the order in which jobs are processed from the queue. Beanstalkd always processes the highest priority (lowest number) jobs first.
169
+
170
+ **Priority Range:** 0 to 4,294,967,295 (unsigned 32-bit integer)
171
+ - `0` = Highest priority (processed first)
172
+ - `4,294,967,295` = Lowest priority (processed last)
173
+ - Default: `65536` (configurable in `config/postburner.yml`)
174
+
175
+ When a worker reserves a job, Beanstalkd returns the highest priority job available in the tube. Jobs with the same priority are processed in FIFO order.
176
+
177
+ **ActiveJob with Postburner::Beanstalkd:**
97
178
 
98
179
  ```ruby
99
- # High priority job with no retries
100
- class PaymentJob < Postburner::Job
101
- queue 'payments'
102
- queue_priority 0
103
- queue_max_job_retries 0 # Don't retry - handle failures manually
104
- end
180
+ class ProcessPayment < ApplicationJob
181
+ include Postburner::Beanstalkd
105
182
 
106
- # Background job with retries
107
- class EmailJob < Postburner::Job
108
- queue 'emails'
109
- queue_priority 1000 # Lower priority
110
- queue_max_job_retries 5 # Retry up to 5 times
183
+ queue_as :critical
184
+ queue_priority 0 # Highest priority - processed immediately
111
185
  end
112
186
 
113
- # Long-running job
114
- class DataExportJob < Postburner::Job
115
- queue 'exports'
116
- queue_respond_timeout 3600 # 1 hour timeout
187
+ class SendEmail < ApplicationJob
188
+ include Postburner::Beanstalkd
189
+
190
+ queue_as :mailers
191
+ queue_priority 1000 # Lower priority - processed after critical jobs
117
192
  end
118
193
  ```
119
194
 
120
- These methods come from [Backburner](https://github.com/nesquena/backburner#configure-backburner) and work seamlessly with Postburner. See Backburner's documentation for all available options.
121
-
122
- ### Mailers
195
+ **Postburner::Job:**
123
196
 
124
197
  ```ruby
125
- j = Postburner::Mailer.
126
- delivery(UserMailer, :welcome)
127
- .with(name: 'Freddy')
198
+ class CriticalJob < Postburner::Job
199
+ queue 'critical'
200
+ queue_priority 0 # Highest priority
128
201
 
129
- j.queue!
130
- => {:status=>"INSERTED", :id=>"1139"}
131
- ```
202
+ def perform(args)
203
+ # Critical business logic
204
+ end
205
+ end
132
206
 
133
- ### Job Management Methods
207
+ class BackgroundTask < Postburner::Job
208
+ queue 'default'
209
+ queue_priority 5000 # Lower priority
134
210
 
135
- Postburner provides several methods for managing jobs in Beanstalkd and the database:
211
+ def perform(args)
212
+ # Non-urgent background work
213
+ end
214
+ end
215
+ ```
136
216
 
137
- #### delete! - Remove from Beanstalkd only
217
+ **Recommended Priority Ranges:**
138
218
 
139
- Deletes the job from the Beanstalkd queue but **keeps the database record**. Use this when you want to cancel a queued job while maintaining its audit trail.
219
+ | Priority Range | Use Case | Examples |
220
+ |---------------|----------|----------|
221
+ | 0-256 | Critical business operations | Payments, transactions, real-time notifications |
222
+ | 256-4096 | High priority tasks | Password resets, order confirmations |
223
+ | 4096-65536 | Standard background jobs | Email sending, data exports (65536 is default) |
224
+ | 65536-262144 | Low priority maintenance | Cache warming, log cleanup |
225
+ | 262144+ | Deferred/bulk operations | Analytics, batch reports |
140
226
 
141
- ```ruby
142
- job = MyJob.create!(args: {})
143
- job.queue!(delay: 1.hour)
144
- job.bkid # => 12345
227
+ **Configuration:**
145
228
 
146
- # Remove from Beanstalkd but keep in database
147
- job.delete!
229
+ Set default priority in `config/postburner.yml`:
148
230
 
149
- job.reload
150
- job.bkid # => 12345 (still has bkid)
151
- job.persisted? # => true (still in database)
152
- # Job won't execute, but you can still inspect it
231
+ ```yaml
232
+ production:
233
+ default_priority: 65536 # Default for jobs without explicit priority
234
+ default_ttr: 300
153
235
  ```
154
236
 
155
- #### remove! - Soft delete (Beanstalkd + removed_at)
156
-
157
- Removes the job from Beanstalkd **and** sets `removed_at` timestamp. The database record is preserved for audit trails. This is the recommended way to cancel jobs.
237
+ **Dynamic Priority (Postburner::Job only):**
158
238
 
159
239
  ```ruby
160
- job = MyJob.create!(args: {})
161
- job.queue!(delay: 1.hour)
240
+ class ProcessOrder < Postburner::Job
241
+ queue 'orders'
242
+ queue_priority 100 # Default
162
243
 
163
- # Soft delete - removes from queue and marks as removed
164
- job.remove!
244
+ before_enqueue :adjust_priority
245
+
246
+ def perform(args)
247
+ order = Order.find(args['order_id'])
248
+ order.process!
249
+ end
165
250
 
166
- job.reload
167
- job.removed_at # => 2025-11-01 12:34:56 UTC
168
- job.persisted? # => true (still in database)
169
- # Job won't execute and is marked as removed
251
+ private
252
+
253
+ def adjust_priority
254
+ if args['urgent']
255
+ self.queue_priority = 0 # Override to highest priority
256
+ end
257
+ end
258
+ end
259
+
260
+ # Usage
261
+ ProcessOrder.create!(args: { 'order_id' => 123, 'urgent' => true }).queue!
170
262
  ```
171
263
 
172
- #### destroy / destroy! - Delete from both
264
+ ### Time to Run (TTR)
173
265
 
174
- Destroys the database record **and** removes from Beanstalkd via `before_destroy` callback. Use when you want to completely eliminate the job.
266
+ TTR (Time-to-Run) is the maximum duration in seconds that a worker has to process a reserved job before Beanstalkd automatically releases it back to the ready queue.
267
+
268
+ 1. Worker reserves a job from Beanstalkd
269
+ 2. TTR countdown begins immediately upon reservation
270
+ 3. If job completes within TTR, worker deletes/buries the job (success)
271
+ 4. If TTR expires before completion, Beanstalkd automatically releases the job back to ready queue
272
+ 5. Another worker can then reserve and process the same job
273
+
274
+ This mechanism protects against workers crashing or hanging—jobs won't be stuck indefinitely.
275
+
276
+ **TTR Range:** 1 to 4,294,967,295 seconds (unsigned 32-bit integer)
277
+ - Minimum: `1` second (Beanstalkd silently increases 0 to 1)
278
+ - Default: `300` seconds (5 minutes, configurable in `config/postburner.yml`)
279
+
280
+ **ActiveJob with Postburner::Beanstalkd:**
175
281
 
176
282
  ```ruby
177
- job = MyJob.create!(args: {})
178
- job.queue!(delay: 1.hour)
283
+ class ProcessPayment < ApplicationJob
284
+ include Postburner::Beanstalkd
179
285
 
180
- # Completely remove job from database and Beanstalkd
181
- job.destroy!
286
+ queue_as :critical
287
+ queue_priority 0
288
+ queue_ttr 300 # 5 minutes to complete
289
+ end
182
290
 
183
- # Job no longer exists in database or Beanstalkd
184
- ```
291
+ class QuickEmail < ApplicationJob
292
+ include Postburner::Beanstalkd
185
293
 
186
- #### kick! - Retry buried jobs
294
+ queue_as :mailers
295
+ queue_ttr 60 # 1 minute for fast email jobs
296
+ end
187
297
 
188
- Moves a buried job back into the ready queue. Buried jobs are those that failed too many times or were explicitly buried by Beanstalkd.
298
+ class LongRunningReport < ApplicationJob
299
+ include Postburner::Beanstalkd
189
300
 
190
- ```ruby
191
- # Job failed and was buried
192
- job.beanstalk_job.stats['state'] # => 'buried'
301
+ queue_as :reports
302
+ queue_ttr 3600 # 1 hour for complex reports
303
+ end
304
+ ```
193
305
 
194
- # Move it back to ready queue for retry
195
- job.kick!
306
+ **Postburner::Job:**
196
307
 
197
- job.beanstalk_job.stats['state'] # => 'ready'
308
+ ```ruby
309
+ class DataImport < Postburner::Job
310
+ queue 'imports'
311
+ queue_ttr 1800 # 30 minutes (equivalent to queue_ttr)
312
+
313
+ def perform(args)
314
+ # Long-running import logic
315
+ end
316
+ end
198
317
  ```
199
318
 
200
- **Summary:**
201
- - **`delete!`** - Removes from Beanstalkd, keeps full database record (keeps `bkid`)
202
- - **`remove!`** - Removes from Beanstalkd, marks `removed_at`, keeps database record (**recommended for canceling jobs**)
203
- - **`destroy!`** - Removes from both Beanstalkd and database (complete deletion)
204
- - **`kick!`** - Moves buried job back to ready queue (for retrying failed jobs)
319
+ **Extending TTR with `extend!` (Touch Command):**
205
320
 
206
- ### [Beaneater](https://github.com/beanstalkd/beaneater) and [beanstalkd](https://raw.githubusercontent.com/beanstalkd/beanstalkd/master/doc/protocol.txt) Integration
321
+ For jobs that may take longer than expected, you can extend the TTR dynamically using `extend!`. This calls Beanstalkd's `touch` command, which resets the TTR countdown.
207
322
 
208
- Access Beanstalkd directly for advanced queue operations:
323
+ **Available for:**
324
+ - `Postburner::Job` subclasses (always available via `bk.extend!`)
325
+ - `Postburner::Tracked` ActiveJob classes (includes `extend!` method)
209
326
 
210
327
  ```ruby
211
- # Get the beanstalkd job id
212
- job.bkid
213
- => 1104
328
+ class ProcessImport < ApplicationJob
329
+ include Postburner::Tracked # Includes Beanstalkd and enables extend!
214
330
 
215
- # Get the beaneater job object - call any beaneater methods
216
- job.beanstalk_job
331
+ queue_as :imports
332
+ queue_ttr 300 # 5 minutes initial TTR
217
333
 
218
- # Get beanstalkd stats for this job
219
- job.beanstalk_job.stats
220
- => {"id"=>1104, "tube"=>"critical", "state"=>"ready", "pri"=>0, ...}
221
-
222
- # Get a cached Backburner connection
223
- c = Postburner.connection
224
- c.beanstalk.tubes.to_a
225
- c.beanstalk.tubes.to_a.map{|t| c.tubes[t.name].peek(:buried)}
226
- c.beanstalk.tubes['ss.development.caching'].stats
227
- c.beanstalk.tubes['ss.development.caching'].peek(:buried).kick
228
- c.beanstalk.tubes['ss.development.caching'].kick(3)
229
- c.close
230
-
231
- # Automatically close connection (recommended)
232
- Postburner.connected do |connection|
233
- # do stuff with connection
234
- connection.tubes['my.tube'].stats
334
+ def perform(file_id)
335
+ file = ImportFile.find(file_id)
336
+
337
+ file.each_batch(size: 100) do |batch|
338
+ batch.each do |row|
339
+ process_row(row)
340
+ end
341
+
342
+ extend! # Reset TTR to 300 seconds from now
343
+ log "Processed batch, extended TTR"
344
+ end
345
+ end
235
346
  end
236
- # Connection automatically closed
237
347
  ```
238
348
 
239
- Read about the [beanstalkd protocol](https://raw.githubusercontent.com/beanstalkd/beanstalkd/master/doc/protocol.txt).
349
+ **For Postburner::Job (via `bk` accessor):**
240
350
 
241
- ### Basic model fields
242
351
  ```ruby
243
- # ActiveRecord primary key
244
- job.id
352
+ class LargeDataProcessor < Postburner::Job
353
+ queue 'processing'
354
+ queue_ttr 600 # 10 minutes
245
355
 
246
- # int id of the beankstalkd job
247
- job.bkid
356
+ def perform(args)
357
+ dataset = Dataset.find(args['dataset_id'])
248
358
 
249
- # string uuid
250
- job.sid
359
+ dataset.each_chunk do |chunk|
360
+ process_chunk(chunk)
251
361
 
252
- # ActiveRecord STI job subtype
253
- job.type
362
+ bk.extend! # Reset TTR via beanstalkd job accessor
363
+ log "Chunk processed, TTR extended"
364
+ end
365
+ end
366
+ end
367
+ ```
368
+
369
+ **`extend!` and Beanstalkd `touch`**
370
+
371
+ - Calls Beanstalkd's `touch` command on the reserved job
372
+ - Resets the TTR countdown to the original TTR value from the current moment
373
+ - Example: Job has `queue_ttr 300`. After 200 seconds, calling `extend!` gives you another 300 seconds (not just 100)
374
+ - Can be called multiple times during job execution
375
+ - Useful for iterative processing where each iteration is quick but total time is unpredictable
254
376
 
255
- # jsonb arguments for use in job
256
- job.args
377
+ **Configuration:**
257
378
 
258
- # time job should run - not intended to be changed
259
- # TODO run_at should be readonly after create
260
- job.run_at
379
+ Set default TTR in `config/postburner.yml`:
380
+
381
+ ```yaml
382
+ production:
383
+ default_priority: 65536
384
+ default_ttr: 300 # 5 minutes default for all jobs
261
385
  ```
262
386
 
263
- ### Job statistics
387
+ **Recommended TTR Values:**
388
+
389
+ | Job Type | TTR | Reasoning |
390
+ |----------|-----|-----------|
391
+ | Email sending | 60-120s | Network operations, should be fast |
392
+ | API calls | 30-60s | External services, fast or timeout |
393
+ | File processing | 600-1800s | Variable based on file size, use `extend!` |
394
+ | Reports | 1800-3600s | Complex queries and aggregations |
395
+ | Imports/Exports | 3600s+ | Large datasets, use `extend!` in loops |
396
+
397
+ **Best Practices:**
398
+
399
+ 1. **Set realistic TTR values** based on expected job duration plus buffer
400
+ 2. **Use `extend!` for iterative work** where total time is unpredictable but each iteration is bounded
401
+ 3. **Don't set TTR too high** - it delays recovery from crashed workers
402
+ 4. **Monitor TTR timeouts** - frequent timeouts indicate jobs need more time or optimization
403
+ 5. **For Default jobs** (ActiveJob without `Postburner::Beanstalkd`), include the module to set custom TTR
404
+
405
+ **What happens on TTR timeout:**
406
+
264
407
  ```ruby
265
- # when job was inserted into beankstalkd
266
- job.queued_at
408
+ # Job starts processing
409
+ job = ProcessImport.perform_later(file_id)
410
+
411
+ # Worker crashes or hangs after 200 seconds
412
+ # At 300 seconds (TTR), Beanstalkd automatically:
413
+ # 1. Releases job back to ready queue
414
+ # 2. Increments the job's reserve count
415
+ # 3. Makes job available for another worker
267
416
 
268
- # last time attempted
269
- job.attempting_at
417
+ # Another worker picks up the job and retries
418
+ # (ActiveJob retry_on/discard_on handlers still apply)
419
+ ```
270
420
 
271
- # last time processing started
272
- job.processing_at
421
+ ## Installation
273
422
 
274
- # when completed
275
- job.processed_at
423
+ ### 1. Install Beanstalkd
276
424
 
277
- # when removed, may be nil
278
- job.removed_at
425
+ First [install beanstalkd](https://beanstalkd.github.io/download.html):
279
426
 
280
- # lag in ms from run_at/queued_at to attempting_at
281
- job.lag
427
+ ```bash
428
+ sudo apt-get install beanstalkd
429
+ brew install beanstalkd # for macOS/Linux
282
430
 
283
- # duration of processing in ms
284
- job.duration
431
+ # Start beanstalkd (in-memory only)
432
+ beanstalkd -l 127.0.0.1 -p 11300
285
433
 
286
- # number of attempts
287
- job.attempt_count
434
+ # OR with persistence (recommended for production)
435
+ mkdir -p /var/lib/beanstalkd
436
+ beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
437
+ ```
288
438
 
289
- # number of errors (length of errata)
290
- job.error_count
439
+ ### 2. Add Gem
291
440
 
292
- # number of log entries (length of logs)
293
- job.log_count
441
+ ```ruby
442
+ # Gemfile
443
+ gem 'postburner', '~> 1.0.0.pre.1'
444
+ ```
294
445
 
295
- # array of attempting_at times
296
- job.attempts
446
+ ```bash
447
+ bundle install
448
+ ```
297
449
 
298
- # array of errors
299
- job.errata
450
+ ### 3. Install Migration
300
451
 
301
- # array of log messages
302
- job.logs
452
+ ```bash
453
+ rails generate postburner:install
454
+ rails db:migrate
303
455
  ```
304
456
 
305
- ### Job logging and exceptions
457
+ This creates the `postburner_jobs` table for tracked jobs.
306
458
 
307
- Optionally, you can:
308
- 1. Add log messages to the job during processing to `logs`
309
- 1. Add log your own exceptions to `errata`
459
+ ### 4. Configure ActiveJob
310
460
 
311
461
  ```ruby
312
- class RunDonation < Postburner::Job
313
- queue 'critical'
314
- queue_priority 0 # 0 is highest priority
315
- queue_max_job_retries 0 # don't retry
462
+ # config/application.rb
463
+ config.active_job.queue_adapter = :postburner
464
+ ```
316
465
 
317
- def perform(args)
318
- # log at task, defaults to `:info`, but `:debug`, `:warning`, `:error`
319
- log "Log bad condition...", level: :error
466
+ ### 5. Create Worker Configuration
320
467
 
321
- begin
322
- # danger
323
- rescue Exception => e
324
- log_exception e
325
- end
326
- end
327
- end
468
+ ```bash
469
+ cp config/postburner.yml.example config/postburner.yml
328
470
  ```
329
471
 
330
- ### Callbacks
472
+ Edit `config/postburner.yml` for your environment (see [Configuration](#configuration)).
473
+
474
+ ## Usage
331
475
 
332
- Postburner provides ActiveJob-style lifecycle callbacks for job execution:
476
+ ### Default Jobs
477
+
478
+ Default jobs execute quickly via Beanstalkd without PostgreSQL overhead. Perfect for emails, cache warming, notifications, etc.
333
479
 
334
480
  ```ruby
335
- class ProcessPayment < Postburner::Job
336
- queue 'critical'
481
+ class SendWelcomeEmail < ApplicationJob
482
+ queue_as :mailers
337
483
 
338
- before_enqueue :validate_payment
339
- after_enqueue :send_confirmation
484
+ def perform(user_id)
485
+ UserMailer.welcome(user_id).deliver_now
486
+ end
487
+ end
340
488
 
341
- before_attempt :log_attempt
342
- after_attempt :update_metrics
489
+ # Enqueue immediately
490
+ SendWelcomeEmail.perform_later(123)
343
491
 
344
- before_processing :acquire_lock
345
- after_processing :release_lock
492
+ # Enqueue with delay
493
+ SendWelcomeEmail.set(wait: 1.hour).perform_later(123)
346
494
 
347
- after_processed :send_receipt # Only runs on success
495
+ # Enqueue at specific time
496
+ SendWelcomeEmail.set(wait_until: Date.tomorrow.noon).perform_later(123)
497
+ ```
348
498
 
349
- def perform(args)
350
- payment = Payment.find(args['payment_id'])
351
- payment.process!
352
- end
499
+ **Overview:**
500
+ - Fast execution (no database writes)
501
+ - Low overhead
502
+ - Standard ActiveJob retry_on/discard_on support
503
+ - No audit trail
504
+ - No logging or statistics
353
505
 
354
- private
506
+ #### Configuring Beanstalkd Priority and TTR
355
507
 
356
- def validate_payment
357
- raise "Invalid payment" unless args['payment_id'].present?
358
- end
508
+ For default jobs that need custom Beanstalkd configuration, include `Postburner::Beanstalkd`:
359
509
 
360
- def send_confirmation
361
- PaymentMailer.queued(args['payment_id']).deliver_later
362
- end
510
+ ```ruby
511
+ class SendWelcomeEmail < ApplicationJob
512
+ include Postburner::Beanstalkd
363
513
 
364
- def send_receipt
365
- PaymentMailer.receipt(args['payment_id']).deliver_later
514
+ queue_as :mailers
515
+ queue_priority 100 # Lower = higher priority (0 is highest)
516
+ queue_ttr 300 # Time-to-run in seconds (5 minutes)
517
+
518
+ def perform(user_id)
519
+ UserMailer.welcome(user_id).deliver_now
366
520
  end
367
521
  end
368
522
  ```
369
523
 
370
- **Available callbacks:**
371
- - `before_enqueue`, `around_enqueue`, `after_enqueue` - Run when `queue!` is called
372
- - `before_attempt`, `around_attempt`, `after_attempt` - Run on every execution attempt (including retries)
373
- - `before_processing`, `around_processing`, `after_processing` - Run during job execution
374
- - `after_processed` - Only runs after successful completion (not on errors)
524
+ **Configuration options:**
525
+ - `queue_priority` - Beanstalkd priority (0-4294967295, lower = higher priority)
526
+ - `queue_ttr` - Time-to-run in seconds before job times out
527
+
528
+ Jobs without `Postburner::Beanstalkd` use defaults from `config/postburner.yml`:
529
+ - `default_priority: 65536`
530
+ - `default_ttr: 300`
375
531
 
376
- ### Optionally, mount the engine
532
+ ### Tracked Jobs
533
+
534
+ Tracked jobs store full execution details in PostgreSQL, providing comprehensive audit trails (i.e. logging, timing, errors, retry tracking) for critical operations.
535
+
536
+ **Note:** `Postburner::Tracked` automatically includes `Postburner::Beanstalkd`, giving you access to `queue_priority`, `queue_ttr`, `bk`, and `extend!`.
377
537
 
378
538
  ```ruby
379
- mount Postburner::Engine => "/postburner"
539
+ class ProcessPayment < ApplicationJob
540
+ include Postburner::Tracked # ← Opt-in to PostgreSQL tracking (includes Beanstalkd)
380
541
 
381
- # mount only for development inspection
382
- mount Postburner::Engine => "/postburner" if Rails.env.development?
383
- ```
542
+ queue_as :critical
543
+ queue_priority 0 # Highest priority (Beanstalkd included automatically)
544
+ queue_ttr 600 # 10 minute timeout
545
+ retry_on StandardError, wait: :exponentially_longer, attempts: 5
384
546
 
385
- [Open the controller](https://guides.rubyonrails.org/engines.html#overriding-models-and-controllers)
386
- to add your own authentication or changes - or just create your own routes, controllers, and views.
547
+ def perform(payment_id)
548
+ payment = Payment.find(payment_id)
387
549
 
388
- [Override the views](https://guides.rubyonrails.org/engines.html#overriding-views) to make them
389
- prettier - or follow the suggestion above and use your own.
550
+ log "Starting payment processing for $#{payment.amount}"
390
551
 
391
- ## Testing
552
+ begin
553
+ payment.charge!
554
+ log! "Payment charged successfully"
555
+ rescue PaymentError => e
556
+ log_exception!(e)
557
+ raise
558
+ end
559
+
560
+ log "Payment complete", level: :info
561
+ end
562
+ end
563
+ ```
392
564
 
393
- Postburner provides multiple testing strategies to make your tests fast and predictable without requiring Beanstalkd. Tests automatically use `TestQueue` when Rails test mode is detected.
565
+ **Tracking provides:**
566
+ - Complete execution history (queued_at, processing_at, processed_at)
567
+ - Custom logs with `log` and `log!`
568
+ - Exception tracking with `log_exception()` and `log_exception!()`
569
+ - Timing statistics (lag, duration)
570
+ - Retry attempts tracking
571
+ - Query with ActiveRecord
572
+ - Foreign key relationships
573
+ - Beanstalkd operations via `bk` accessor
574
+ - TTR extension via `extend!` method
394
575
 
395
- ### Rails and ActiveJob Test Configuration
576
+ **Query tracked jobs:**
396
577
 
397
- Rails provides several configuration options that affect how background jobs behave in test environments:
578
+ ```ruby
579
+ # Find by state
580
+ Postburner::TrackedJob.where(processed_at: nil)
581
+ Postburner::TrackedJob.where.not(removed_at: nil)
582
+
583
+ # Find with errors
584
+ Postburner::TrackedJob.where("error_count > 0")
585
+
586
+ # Statistics
587
+ Postburner::TrackedJob.average(:duration) # Average execution time
588
+ Postburner::TrackedJob.maximum(:lag) # Worst lag
589
+
590
+ # Inspect execution
591
+ job = Postburner::TrackedJob.last
592
+ job.logs # Array of log entries with timestamps
593
+ job.errata # Array of exceptions with backtraces
594
+ job.attempts # Array of attempt timestamps
595
+ job.duration # Execution time in milliseconds
596
+ job.lag # Queue lag in milliseconds
597
+ ```
398
598
 
399
- #### ActiveJob Queue Adapter
599
+ ### Direct Postburner::Job Usage
400
600
 
401
- The `config.active_job.queue_adapter` setting controls how ActiveJob queues jobs. Rails supports several built-in adapters:
601
+ Direct `Postburner::Job` subclasses are **always tracked**:
402
602
 
403
603
  ```ruby
404
- # config/environments/test.rb (or config/application.rb)
604
+ class RunDonation < Postburner::Job
605
+ queue 'critical'
606
+ queue_priority 0
607
+ queue_max_job_retries 0
405
608
 
406
- # :test adapter (Rails default for test environment)
407
- # - Jobs are NOT executed automatically
408
- # - Jobs accumulate in ActiveJob::Base.queue_adapter.enqueued_jobs
409
- # - You must explicitly call perform_enqueued_jobs to execute them
410
- config.active_job.queue_adapter = :test
609
+ def perform(args)
610
+ donation = Donation.find(args['donation_id'])
611
+ donation.process!
612
+ log "Processed donation #{donation.id}"
613
+ end
614
+ end
411
615
 
412
- # :inline adapter
413
- # - Jobs execute immediately in the same process
414
- # - No queueing, synchronous execution like Postburner's test strategies
415
- config.active_job.queue_adapter = :inline
616
+ # Create and enqueue
617
+ job = RunDonation.create!(args: { 'donation_id' => 123 })
618
+ job.queue!
416
619
 
417
- # :async adapter
418
- # - Jobs execute asynchronously in a thread pool
419
- # - Not recommended for tests due to race conditions
420
- config.active_job.queue_adapter = :async
620
+ # With delay
621
+ job.queue!(delay: 1.hour)
421
622
 
422
- # :backburner adapter (for production use with Postburner)
423
- # - Requires Beanstalkd to be running
424
- # - Jobs queued to Beanstalkd tubes
425
- config.active_job.queue_adapter = :backburner
623
+ # At specific time
624
+ job.queue!(at: 2.days.from_now)
426
625
  ```
427
626
 
428
- #### ActiveJob Test Helpers
627
+ #### Instance-Level Queue Configuration
429
628
 
430
- When using the `:test` adapter, Rails provides test helpers in `ActiveJob::TestHelper`:
629
+ Override queue priority and TTR per job instance for dynamic behavior:
431
630
 
432
631
  ```ruby
433
- # test/test_helper.rb
434
- require 'test_helper'
632
+ # Set priority during creation
633
+ job = RunDonation.create!(
634
+ args: { 'donation_id' => 123 },
635
+ queue_priority: 1500, # Override class-level priority
636
+ queue_ttr: 300 # Override class-level TTR
637
+ )
638
+ job.queue!
435
639
 
436
- class MyTest < ActiveSupport::TestCase
437
- include ActiveJob::TestHelper
640
+ # Set dynamically after creation
641
+ job = RunDonation.create!(args: { 'donation_id' => 456 })
642
+ job.queue_priority = 2000
643
+ job.queue_ttr = 300
644
+ job.queue!
438
645
 
439
- test "job is enqueued" do
440
- assert_enqueued_with(job: MyJob, args: [{user_id: 123}]) do
441
- MyJob.perform_later(user_id: 123)
442
- end
646
+ # Use in before_enqueue callback for conditional behavior
647
+ class ProcessOrder < Postburner::Job
648
+ queue 'orders'
649
+ queue_priority 100 # Default priority
650
+ queue_ttr 120 # Default TTR
651
+
652
+ before_enqueue :set_priority_based_on_urgency
653
+
654
+ def perform(args)
655
+ order = Order.find(args['order_id'])
656
+ order.process!
443
657
  end
444
658
 
445
- test "job is performed" do
446
- perform_enqueued_jobs do
447
- MyJob.perform_later(user_id: 123)
659
+ private
660
+
661
+ def set_priority_based_on_urgency
662
+ if args['express_shipping']
663
+ self.queue_priority = 0 # High priority for express orders
664
+ self.queue_ttr = 600 # Allow 10 minutes to complete
665
+ else
666
+ self.queue_priority = 1000 # Low priority for standard orders
667
+ self.queue_ttr = 120 # Standard 2 minute timeout
448
668
  end
449
- # Job has now executed
450
669
  end
451
670
  end
671
+
672
+ # Express order gets high priority automatically
673
+ ProcessOrder.create!(args: { 'order_id' => 789, 'express_shipping' => true }).queue!
452
674
  ```
453
675
 
454
- **Available helpers:**
455
- - `perform_enqueued_jobs` - Executes all enqueued jobs
456
- - `assert_enqueued_jobs` - Asserts number of jobs enqueued
457
- - `assert_enqueued_with` - Asserts specific job was enqueued with args
458
- - `assert_no_enqueued_jobs` - Asserts no jobs were enqueued
459
- - `assert_performed_jobs` - Asserts number of jobs performed
460
- - `queue_adapter.enqueued_jobs` - Array of enqueued jobs for inspection
676
+ **Overview:**
677
+ - Same as Tracked jobs
678
+ - Access the ActiveRecord Postburner::Job directly.
679
+
680
+ ## Workers
681
+
682
+ Postburner uses named worker configurations to support different deployment patterns. Each worker can have different fork/thread settings and process different queues, enabling flexible production deployments.
683
+
684
+ ### Named Workers Configuration
685
+
686
+ Configure multiple named workers with different concurrency profiles:
687
+
688
+ ```yaml
689
+ production:
690
+ beanstalk_url: <%= ENV['BEANSTALK_URL'] %>
691
+
692
+ workers:
693
+ # Heavy, memory-intensive jobs - more processes, fewer threads
694
+ imports:
695
+ default_forks: 4
696
+ default_threads: 1
697
+ default_gc_limit: 500
698
+ queues:
699
+ - imports
700
+ - data_processing
701
+
702
+ # General jobs - fewer processes, many threads
703
+ general:
704
+ default_forks: 2
705
+ default_threads: 100
706
+ default_gc_limit: 5000
707
+ queues:
708
+ - default
709
+ - mailers
710
+ - notifications
711
+ ```
461
712
 
462
- #### Other ActiveJob Configuration Options
713
+ ### Puma-Style Architecture
463
714
 
464
- ```ruby
465
- # config/application.rb or config/environments/test.rb
715
+ The worker supports two modes based on fork configuration:
716
+ - **`forks: 0`** - Single process with thread pools (development/staging)
717
+ - **`forks: 1+`** - Multiple processes with thread pools (production, Puma-style)
466
718
 
467
- # Set default queue name for all jobs
468
- config.active_job.queue_name_prefix = "myapp_#{Rails.env}"
719
+ This gives you a natural progression from simple single-threaded development to high-concurrency production.
469
720
 
470
- # Skip after_enqueue callbacks if before_enqueue halts
471
- config.active_job.skip_after_callbacks_if_terminated = true
721
+ ### Configuration Examples
472
722
 
473
- # Log arguments when jobs are enqueued (useful for debugging)
474
- config.active_job.log_arguments = true # default: true
723
+ **Development (single worker, single-threaded):**
724
+ ```yaml
725
+ development:
726
+ beanstalk_url: beanstalk://localhost:11300
475
727
 
476
- # Set queue priority (if adapter supports it)
477
- config.active_job.default_queue_name = 'default'
728
+ workers:
729
+ default:
730
+ # Defaults: forks=0, threads=1, gc_limit=nil
731
+ queues:
732
+ - default
733
+ - mailers
734
+ ```
478
735
 
479
- # Configure job serialization
480
- config.active_job.custom_serializers << MyCustomSerializer
736
+ **Staging (single worker, multi-threaded):**
737
+ ```yaml
738
+ staging:
739
+ beanstalk_url: beanstalk://localhost:11300
740
+
741
+ workers:
742
+ default:
743
+ default_threads: 10
744
+ default_gc_limit: 5000
745
+ queues:
746
+ - critical
747
+ - default
748
+ - mailers
481
749
  ```
482
750
 
483
- ### Postburner Test Modes vs ActiveJob Adapters
751
+ **Production (multiple workers with different profiles):**
752
+ ```yaml
753
+ production:
754
+ beanstalk_url: <%= ENV['BEANSTALK_URL'] %>
755
+
756
+ workers:
757
+ imports:
758
+ default_forks: 4 # 4 processes
759
+ default_threads: 1 # 1 thread per process = 4 concurrent jobs
760
+ default_gc_limit: 500
761
+ queues:
762
+ - imports
763
+ - data_processing
764
+
765
+ general:
766
+ default_forks: 2 # 2 processes
767
+ default_threads: 100 # 100 threads per process = 200 concurrent jobs
768
+ default_gc_limit: 5000
769
+ queues:
770
+ - default
771
+ - mailers
772
+ - notifications
773
+ ```
484
774
 
485
- **Key Difference:** Postburner jobs (subclasses of `Postburner::Job`) do NOT use ActiveJob's queue adapter. Postburner has its own queue strategies that control execution independently of ActiveJob settings.
775
+ ### Running Workers
486
776
 
487
- | Postburner Strategy | Execution | Use Case | ActiveJob Equivalent |
488
- |---------------------|-----------|----------|----------------------|
489
- | `TestQueue` | Inline, raises on scheduled | Explicit time control | `:inline` adapter |
490
- | `ImmediateTestQueue` | Inline with auto time-travel | Convenience testing | `:inline` adapter + `travel_to` |
491
- | `NullQueue` | Deferred, manual execution | Batch processing | `:test` adapter (manual `perform_enqueued_jobs`) |
492
- | `Queue` / `NiceQueue` | Async via Beanstalkd | Production | `:backburner` adapter |
777
+ **Single worker (auto-selected):**
778
+ ```bash
779
+ bin/postburner
780
+ ```
781
+ If only one worker is defined, it's automatically selected.
493
782
 
494
- **When to use what:**
495
- - **ActiveJob with `:test` adapter:** For testing standard ActiveJob classes (mailers, ActiveStorage, etc.)
496
- - **Postburner test strategies:** For testing `Postburner::Job` subclasses
497
- - Both can coexist in the same test suite with different behaviors
783
+ **Multiple workers (must specify):**
784
+ ```bash
785
+ bin/postburner --worker imports # Run the 'imports' worker
786
+ bin/postburner --worker general # Run the 'general' worker
787
+ ```
498
788
 
499
- ### Automatic Test Mode Detection
789
+ **Filter queues (override config):**
790
+ ```bash
791
+ bin/postburner --worker general --queues default,mailers # Only process specific queues
792
+ ```
500
793
 
501
- In Rails test environments, Postburner automatically switches to `TestQueue`:
794
+ ### Running Workers in Separate Processes
795
+
796
+ For production deployments, run different workers in separate OS processes for isolation and resource allocation:
797
+
798
+ **Docker Compose example:**
799
+ ```yaml
800
+ services:
801
+ # Process 1: Imports worker (forks=4, threads=1)
802
+ worker-imports:
803
+ build: .
804
+ command: bin/postburner --worker imports
805
+ environment:
806
+ RAILS_ENV: production
807
+ BEANSTALK_URL: beanstalk://beanstalkd:11300
808
+ depends_on:
809
+ - beanstalkd
810
+ - postgres
811
+
812
+ # Process 2: General worker (forks=2, threads=100)
813
+ worker-general:
814
+ build: .
815
+ command: bin/postburner --worker general
816
+ environment:
817
+ RAILS_ENV: production
818
+ BEANSTALK_URL: beanstalk://beanstalkd:11300
819
+ depends_on:
820
+ - beanstalkd
821
+ - postgres
822
+ ```
502
823
 
503
- ```ruby
504
- # Happens automatically when:
505
- # - Rails.env.test? is true
506
- # - ActiveJob::Base.queue_adapter_name == :test
824
+ **Procfile example (Heroku/Foreman):**
825
+ ```
826
+ worker_imports: bin/postburner --worker imports
827
+ worker_general: bin/postburner --worker general
828
+ ```
507
829
 
508
- # Verify you're in test mode
509
- Postburner.testing? # => true in tests
830
+ ### Worker Architecture
831
+
832
+ **Single Process Mode (forks: 0):**
833
+ ```
834
+ Main Process
835
+ ├─ Queue 'critical' Thread Pool (1 thread)
836
+ ├─ Queue 'default' Thread Pool (10 threads)
837
+ └─ Queue 'mailers' Thread Pool (5 threads)
510
838
  ```
511
839
 
512
- **Note:** Even though `TestQueue` is set automatically in Rails test environments, you can override it at any time to use a different test strategy:
840
+ **Multi-Process Mode (forks: 1+):**
841
+ ```
842
+ Parent Process
843
+ ├─ Fork 0 (queue: critical)
844
+ │ └─ Thread 1
845
+ ├─ Fork 0 (queue: default)
846
+ │ ├─ Thread 1-10
847
+ ├─ Fork 1 (queue: default) # Puma-style: multiple forks of same queue
848
+ │ ├─ Thread 1-10
849
+ ├─ Fork 2 (queue: default)
850
+ │ └─ Thread 1-10
851
+ ├─ Fork 3 (queue: default)
852
+ │ └─ Thread 1-10
853
+ │ └─ Total: 40 concurrent jobs for 'default' (4 forks × 10 threads)
854
+ └─ Fork 0 (queue: mailers)
855
+ └─ Thread 1-5
856
+ ```
513
857
 
514
- ```ruby
515
- # In test_helper.rb or specific tests
516
- Postburner.inline_immediate_test_strategy! # Use automatic time travel
517
- # OR
518
- Postburner.null_strategy! # Use deferred execution
858
+ ### Benefits
859
+
860
+ **Single Process Mode (forks: 0):**
861
+ - Simplest deployment
862
+ - Easy debugging
863
+ - Lower memory footprint
864
+ - Best for development and moderate load
865
+
866
+ **Multi-Process Mode (forks: 1+):**
867
+ - Horizontal scaling per queue
868
+ - Process isolation
869
+ - Automatic memory cleanup via GC limits
870
+ - Crash isolation (one fork down doesn't affect others)
871
+ - Best for production high-volume workloads
872
+
873
+ ### GC Limits
874
+
875
+ Set `default_gc_limit` per worker to automatically restart after processing N jobs.
876
+
877
+ - Worker processes N jobs
878
+ - Worker exits with code 99
879
+ - In single-process mode (forks: 0): process manager restarts the entire process
880
+ - In multi-process mode (forks: 1+): parent process automatically restarts just that fork
881
+
882
+ ```yaml
883
+ production:
884
+ workers:
885
+ imports:
886
+ default_forks: 4
887
+ default_threads: 1
888
+ default_gc_limit: 500 # Restart after 500 jobs (memory-intensive)
889
+ queues:
890
+ - imports
891
+ - data_processing
892
+
893
+ general:
894
+ default_forks: 2
895
+ default_threads: 100
896
+ default_gc_limit: 5000 # Restart after 5000 jobs
897
+ queues:
898
+ - default
899
+ - mailers
900
+ ```
519
901
 
520
- # The auto-detection only sets the initial strategy
521
- # You can change it anytime during tests
902
+ **Why?**
903
+ - Prevents memory bloat in long-running workers
904
+ - Automatic cleanup without manual intervention
905
+ - Works seamlessly with Docker and Kubernetes restarts
906
+
907
+ ## Configuration
908
+
909
+ ### YAML Configuration
910
+
911
+ ```yaml
912
+ # config/postburner.yml
913
+ default: &default
914
+ beanstalk_url: <%= ENV['BEANSTALK_URL'] || 'beanstalk://localhost:11300' %>
915
+
916
+ development:
917
+ <<: *default
918
+ # Defaults: forks: 0, threads: 1, gc_limit: nil
919
+ queues:
920
+ default: {}
921
+ mailers: {}
922
+
923
+ test:
924
+ <<: *default
925
+ # Defaults: forks: 0, threads: 1, gc_limit: nil
926
+ queues:
927
+ default: {}
928
+
929
+ staging:
930
+ <<: *default
931
+ default_threads: 10 # Multi-threaded, single process
932
+ default_gc_limit: 5000
933
+ queues:
934
+ default: {}
935
+ mailers: {}
936
+
937
+ production:
938
+ <<: *default
939
+ default_forks: 4 # Puma-style: multiple processes
940
+ default_threads: 10 # 10 threads per fork
941
+ default_gc_limit: 5000
942
+
943
+ queues:
944
+ critical:
945
+ forks: 1
946
+ threads: 1 # 1 concurrent job
947
+ gc_limit: 100
948
+
949
+ default:
950
+ forks: 4
951
+ threads: 10 # 40 total concurrent jobs (4 × 10)
952
+ gc_limit: 1000
953
+
954
+ mailers:
955
+ forks: 2
956
+ threads: 5 # 10 total email senders (2 × 5)
957
+ gc_limit: 500
522
958
  ```
523
959
 
524
- ### TestQueue: Explicit Time Control (Default for Tests)
960
+ ### Queue Configuration Methods
525
961
 
526
- The `TestQueue` strategy forces explicit time management, helping catch scheduling bugs:
962
+ For `Postburner::Job` subclasses (and backwards compatibility):
527
963
 
528
964
  ```ruby
529
- test "processes job immediately" do
530
- job = MyJob.create!(args: { user_id: 123 })
531
- job.queue!
532
- assert job.reload.processed_at
965
+ class CriticalJob < Postburner::Job
966
+ queue 'critical' # Beanstalkd tube name
967
+ queue_priority 0 # Lower = higher priority (0 is highest)
968
+ queue_ttr 300 # TTR (time-to-run) in seconds
969
+ queue_max_job_retries 3 # Max retry attempts
970
+ queue_retry_delay 5 # Fixed delay: 5 seconds between retries
971
+
972
+ def perform(args)
973
+ # ...
974
+ end
533
975
  end
976
+ ```
534
977
 
535
- test "processes scheduled job with time travel" do
536
- job = MyJob.create!(args: { user_id: 123 })
537
- job.queue!(delay: 2.hours)
978
+ **Exponential backoff with proc:**
538
979
 
539
- # Must use travel_to for scheduled jobs
540
- travel_to(job.run_at) do
541
- assert job.reload.processed_at
980
+ ```ruby
981
+ class BackgroundTask < Postburner::Job
982
+ queue 'default'
983
+ queue_max_job_retries 5
984
+ queue_retry_delay ->(retries) { 2 ** retries } # 2s, 4s, 8s, 16s, 32s
985
+
986
+ def perform(args)
987
+ # ...
542
988
  end
543
989
  end
544
990
  ```
545
991
 
546
- ### ImmediateTestQueue: Automatic Time Travel
992
+ ### ActiveJob Configuration
547
993
 
548
- For convenience in integration tests, `ImmediateTestQueue` automatically handles scheduled jobs:
994
+ For ActiveJob classes, use standard ActiveJob configuration:
549
995
 
550
996
  ```ruby
551
- test "scheduled jobs execute immediately" do
552
- Postburner.inline_immediate_test_strategy!
997
+ class MyJob < ApplicationJob
998
+ include Postburner::Tracked # Optional: for audit trail
553
999
 
554
- job = SendReminderEmail.create!(args: { user_id: 123 })
555
- job.queue!(delay: 24.hours)
1000
+ queue_as :default
556
1001
 
557
- # Automatically time-travels to scheduled time
558
- assert job.reload.processed_at
559
- assert_emails 1
1002
+ retry_on StandardError, wait: :exponentially_longer, attempts: 5
1003
+ retry_on CustomError, wait: 10.seconds
1004
+
1005
+ discard_on ActiveJob::DeserializationError
1006
+
1007
+ def perform(user_id)
1008
+ # ...
1009
+ end
560
1010
  end
561
1011
  ```
562
1012
 
563
- ### NullQueue: Deferred Execution
1013
+ ## Callbacks
564
1014
 
565
- Create jobs without queueing, then manually execute them:
1015
+ Postburner provides lifecycle callbacks:
566
1016
 
567
1017
  ```ruby
568
- test "batch processes jobs" do
569
- Postburner.null_strategy!
1018
+ class ProcessPayment < ApplicationJob
1019
+ include Postburner::Tracked
1020
+
1021
+ before_enqueue :validate_payment
1022
+ after_enqueue :send_confirmation
1023
+
1024
+ before_attempt :log_attempt # Runs on every retry
1025
+ after_attempt :update_metrics
1026
+
1027
+ before_processing :acquire_lock
1028
+ after_processing :release_lock
1029
+
1030
+ after_processed :send_receipt # Only on success
570
1031
 
571
- jobs = 5.times.map do |i|
572
- job = ProcessRecord.create!(args: { id: i })
573
- job.queue!
574
- job
1032
+ def perform(payment_id)
1033
+ # ...
575
1034
  end
576
1035
 
577
- # Manually execute when ready
578
- jobs.each { |job| Postburner::Job.perform(job.id) }
1036
+ private
579
1037
 
580
- assert jobs.all? { |j| j.reload.processed_at }
1038
+ def validate_payment
1039
+ raise "Invalid" unless payment_valid?
1040
+ end
1041
+
1042
+ def send_receipt
1043
+ PaymentMailer.receipt(arguments.first).deliver_later
1044
+ end
581
1045
  end
582
1046
  ```
583
1047
 
584
- ## Installation
1048
+ **Available callbacks:**
1049
+ - `before_enqueue`, `around_enqueue`, `after_enqueue` - When queued
1050
+ - `before_attempt`, `around_attempt`, `after_attempt` - Every execution (including retries)
1051
+ - `before_processing`, `around_processing`, `after_processing` - During execution
1052
+ - `after_processed` - Only after successful completion
585
1053
 
586
- First [install beanstalkd](https://beanstalkd.github.io/download.html). On Debian-based systems, that goes like this:
587
- ```bash
588
- sudo apt-get install beanstalkd
589
- ```
1054
+ ## Queue Strategies
1055
+
1056
+ Postburner uses different strategies to control job execution. These affect `Postburner::Job` subclasses (not ActiveJob classes).
1057
+
1058
+ | Strategy | When to Use | Behavior | Requires Beanstalkd |
1059
+ |----------|-------------|----------|---------------------|
1060
+ | **NiceQueue** (default) | Production | Async via Beanstalkd, gracefully re-queues premature jobs | Yes |
1061
+ | **Queue** | Production (strict) | Async via Beanstalkd, raises error on premature execution | Yes |
1062
+ | **TestQueue** | Testing with explicit time control | Inline execution, raises error for scheduled jobs | No |
1063
+ | **ImmediateTestQueue** | Testing with automatic time travel | Inline execution, auto time-travels for scheduled jobs | No |
1064
+ | **NullQueue** | Batch processing / deferred execution | Jobs created but not queued, manual execution | No |
590
1065
 
591
- Then add to your Gemfile.
592
1066
  ```ruby
593
- gem 'postburner'
1067
+ # Switch strategies
1068
+ Postburner.nice_async_strategy! # Default production (NiceQueue)
1069
+ Postburner.async_strategy! # Strict production (Queue)
1070
+ Postburner.inline_test_strategy! # Testing (TestQueue)
1071
+ Postburner.inline_immediate_test_strategy! # Testing with time travel
1072
+ Postburner.null_strategy! # Deferred execution
594
1073
  ```
595
1074
 
596
- Install...
597
- ```bash
598
- $ bundle
599
- ```
1075
+ **Note:** These strategies only affect `Postburner::Job` subclasses. ActiveJob classes execute according to the ActiveJob adapter configuration.
600
1076
 
601
- After bundling, install the migration.
602
- ```
603
- # install migration, possible to edit and add attributes or indexes as needed.
604
- $ bundle exec rails g postburner:install
1077
+ ## Testing
1078
+
1079
+ Postburner provides test-friendly execution modes that don't require Beanstalkd.
1080
+
1081
+ ### Automatic Test Mode
1082
+
1083
+ In Rails test environments, Postburner automatically uses inline execution:
1084
+
1085
+ ```ruby
1086
+ # test/test_helper.rb - automatic!
1087
+ Postburner.testing? # => true in tests
605
1088
  ```
606
1089
 
607
- Inspect `XXX_create_postburner_jobs.rb`. (The template is [here](-/blob/master/lib/generators/postburner/install/templates/migrations/create_postburner_jobs.rb.erb)).The required attributes are in there, but add more if you would like to use them,
608
- or do it in a separate migration. By default, several indexes are added
1090
+ ### Testing Default Jobs (ActiveJob)
1091
+
1092
+ Use standard ActiveJob test helpers:
609
1093
 
610
- Because `Postburner` is built on `Backburner`, add a `config/initializers/backburner.rb` with option found [here](https://github.com/nesquena/backburner#configuration).
611
1094
  ```ruby
612
- Backburner.configure do |config|
613
- config.beanstalk_url = "beanstalk://127.0.0.1"
614
- config.tube_namespace = "some.app.production"
615
- config.namespace_separator = "."
616
- config.on_error = lambda { |e| puts e }
617
- config.max_job_retries = 3 # default 0 retries
618
- config.retry_delay = 2 # default 5 seconds
619
- config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 3) }
620
- config.default_priority = 65536
621
- config.respond_timeout = 120
622
- config.default_worker = Backburner::Workers::Simple
623
- config.logger = Logger.new(STDOUT)
624
- config.primary_queue = "backburner-jobs"
625
- config.priority_labels = { :custom => 50, :useless => 1000 }
626
- config.reserve_timeout = nil
627
- config.job_serializer_proc = lambda { |body| JSON.dump(body) }
628
- config.job_parser_proc = lambda { |body| JSON.parse(body) }
1095
+ class MyTest < ActiveSupport::TestCase
1096
+ test "job executes" do
1097
+ SendEmail.perform_later(123)
1098
+ # Job executes immediately in test mode
1099
+ end
1100
+
1101
+ test "job with delay" do
1102
+ travel 1.hour do
1103
+ SendEmail.set(wait: 1.hour).perform_later(123)
1104
+ end
1105
+ end
629
1106
  end
630
1107
  ```
631
1108
 
632
- Finally, set `Backburner` for `ActiveJob`
633
- ```
634
- # config/application.rb
635
- config.active_job.queue_adapter = :backburner
636
- ```
1109
+ ### Testing Tracked Jobs
637
1110
 
638
- Postburner may later provide an adapter, but we recommend using `Postburner::Job` classes
639
- directly.
1111
+ Tracked jobs create database records you can inspect:
640
1112
 
641
- Add jobs to `app/jobs/`. There currently is no generator.
1113
+ ```ruby
1114
+ test "tracked job logs execution" do
1115
+ ProcessPayment.perform_later(456)
1116
+
1117
+ job = Postburner::TrackedJob.last
1118
+ assert job.processed_at.present?
1119
+ assert_includes job.logs.map { |l| l[1]['message'] }, "Processing payment 456"
1120
+ end
1121
+ ```
1122
+
1123
+ ### Testing Legacy Postburner::Job
642
1124
 
643
1125
  ```ruby
644
- # app/jobs/run_donation.rb
645
- class RunDonation < Postburner::Job
646
- queue 'critical'
647
- queue_priority 0 # 0 is highest priority
648
- queue_max_job_retries 0 # don't retry
1126
+ test "processes immediately" do
1127
+ job = RunDonation.create!(args: { 'donation_id' => 123 })
1128
+ job.queue!
1129
+
1130
+ assert job.reload.processed_at
1131
+ end
649
1132
 
650
- def process(args)
651
- # do long tasks here
652
- # also have access to self.args
1133
+ test "scheduled job with time travel" do
1134
+ job = RunDonation.create!(args: { 'donation_id' => 123 })
1135
+
1136
+ travel_to(2.hours.from_now) do
1137
+ job.queue!(delay: 2.hours)
1138
+ assert job.reload.processed_at
653
1139
  end
654
1140
  end
655
1141
  ```
656
1142
 
657
- ### Comparison to Backburner
1143
+ ## Job Management
658
1144
 
659
- Compared to plain [Backburner](https://github.com/nesquena/backburner),
660
- Postburner adds:
661
- - Database Jobs for inspection, linking, auditing, removal (and deletion)
662
- - Direct access to associated [Beanstalk](https://beanstalkd.github.io/) (via [beaneater](https://github.com/beanstalkd/beaneater))
663
- - Job Statistics (lag, attempts, logs, tracked errors)
664
- - Convenience methods to clear tubes, stats, and connections for Beanstalk.
1145
+ ### Canceling Jobs
665
1146
 
666
- Otherwise, Postburner tries to be a super simple layer on `Backburner::Queue`
667
- and `ActiveRecord`. Every tool with either of those are available in
668
- `Postburner::Job`s.
1147
+ **For Postburner::Job subclasses:**
669
1148
 
670
- Comes with a mountable interface that can be password protected with whatever
671
- authentication you use in your project.
1149
+ ```ruby
1150
+ job = RunDonation.create!(args: { 'donation_id' => 123 })
1151
+ job.queue!(delay: 1.hour)
672
1152
 
673
- ### Comparison to Que
1153
+ # Soft delete (recommended) - removes from queue, sets removed_at
1154
+ job.remove!
1155
+ job.removed_at # => 2025-11-19 12:34:56 UTC
1156
+ job.persisted? # => true (still in database for audit)
674
1157
 
675
- Postburner meant to be a replacement/upgrade for [Que](https://github.com/que-rb/que).
676
- However, if you need something faster and backed with ACID compliance, check out Que.
1158
+ # Hard delete - removes from both queue and database
1159
+ job.destroy!
677
1160
 
678
- Postburner has some additional features such as retained jobs after processing,
679
- stats, per job logging, etc.
1161
+ # Delete from Beanstalkd only (keeps database record)
1162
+ job.delete!
1163
+ ```
680
1164
 
681
- Postburner is meant to be simpler than `Que`. Que is incredible, but jobs should
682
- be simple so the logic and history can be transparent.
1165
+ **For tracked ActiveJob classes:**
683
1166
 
684
- ## Contributing
685
- Submit a pull request. Follow conventions of the project. Be nice.
1167
+ ```ruby
1168
+ # Find the TrackedJob record
1169
+ job = Postburner::TrackedJob.find(123)
1170
+ job.remove! # Cancel execution
1171
+ ```
686
1172
 
687
- ### V1 TODO
688
- - Basic tests
689
- - Add Authentication modules for engine mount.
1173
+ ### Retrying Buried Jobs
690
1174
 
691
- ### V1+ TODO
692
- - Install generator
693
- - Sub to backburner
694
- - Job generator
695
- - Build file in app/jobs
696
- - Inherit from Postburner::Job
697
- - Add before/after/around hooks
698
- - Add destroy, and remove actions on show page
699
- - Clear tubes.
700
- - Document how/when to use activerecord hooks
701
- - Document how/when to use backburner hooks
702
- - Document how/when to use postburner hooks
703
- - Add logging with Job.args in backburner logs
704
- - MAYBE - ActiveJob adapter
1175
+ Jobs that fail repeatedly get "buried" in Beanstalkd:
705
1176
 
1177
+ ```ruby
1178
+ job = Postburner::Job.find(123)
1179
+ job.kick! # Moves buried job back to ready queue
1180
+ ```
706
1181
 
707
- ### Running in Development
1182
+ ### Inspecting Job State
708
1183
 
709
- ```
710
- cd test/dummy
711
- bundle exec backburner
1184
+ ```ruby
1185
+ job = Postburner::TrackedJob.find(123)
1186
+
1187
+ # Timestamps
1188
+ job.queued_at # When queued to Beanstalkd
1189
+ job.run_at # Scheduled execution time
1190
+ job.attempting_at # First attempt started
1191
+ job.processing_at # Current/last processing started
1192
+ job.processed_at # Completed successfully
1193
+ job.removed_at # Soft deleted
1194
+
1195
+ # Statistics
1196
+ job.lag # Milliseconds between scheduled and actual execution
1197
+ job.duration # Milliseconds to execute
1198
+ job.attempt_count # Number of attempts
1199
+ job.error_count # Number of errors
1200
+
1201
+ # Audit trail
1202
+ job.logs # Array of log entries with timestamps
1203
+ job.errata # Array of exceptions with backtraces
1204
+ job.attempts # Array of attempt timestamps
712
1205
  ```
713
1206
 
714
- ### Releasing
1207
+ ## Beanstalkd Integration
715
1208
 
716
- 1. `gem bump -v minor -t`
717
- Where <minor> can be: major|minor|patch|pre|release or a version number
718
- 2. Edit `CHANGELOG.md` with details from `git log --oneline`
719
- 3. `git commit --amend`
720
- 4. `gem release -k nac -p`
721
- Where <nac> is an authorized key with push capabilities.
1209
+ Direct access to Beanstalkd for advanced operations:
722
1210
 
1211
+ ```ruby
1212
+ # Get Beanstalkd job ID
1213
+ job.bkid # => 12345
723
1214
 
724
- ## FAQ
1215
+ # Access Beaneater job object
1216
+ job.beanstalk_job.stats
1217
+ # => {"id"=>12345, "tube"=>"critical", "state"=>"ready", ...}
725
1218
 
726
- ### Can I save a Postburner::Job without enqueuing it?
1219
+ # Connection management
1220
+ Postburner.connected do |conn|
1221
+ conn.tubes.to_a # List all tubes
1222
+ conn.tubes['postburner.production.critical'].stats
1223
+ conn.tubes['postburner.production.critical'].kick(10) # Kick 10 buried jobs
1224
+ end
1225
+ ```
727
1226
 
728
- Yes! You can absolutely save a `Postburner::Job` without enqueuing it. Looking at the code, here's how it works:
729
1227
 
730
- #### Creating vs. Queueing
1228
+ ## Web UI
731
1229
 
732
- When you use `create!` or `save!`, the job is persisted to the database but **not** sent to Beanstalkd:
1230
+ Mount the inspection interface:
733
1231
 
734
1232
  ```ruby
735
- # Creates job in database but does NOT enqueue
736
- job = RunDonation.create!(args: {donation_id: 123})
737
- job.persisted? # => true
738
- job.queued_at # => nil
739
- job.bkid # => nil (no Beanstalkd ID)
1233
+ # config/routes.rb
1234
+ mount Postburner::Engine => "/postburner"
1235
+
1236
+ # Only in development
1237
+ mount Postburner::Engine => "/postburner" if Rails.env.development?
740
1238
  ```
741
1239
 
742
- To actually enqueue it, you must explicitly call `queue!`:
1240
+ Add your own authentication:
743
1241
 
744
1242
  ```ruby
745
- # Now it's enqueued to Beanstalkd
746
- job.queue!(delay: 1.hour)
747
- job.queued_at # => Time.zone.now
748
- job.bkid # => 12345 (Beanstalkd job ID)
1243
+ # config/routes.rb
1244
+ authenticate :user, ->(user) { user.admin? } do
1245
+ mount Postburner::Engine => "/postburner"
1246
+ end
1247
+ ```
1248
+
1249
+ ## Deployment
1250
+
1251
+ ### Docker
1252
+
1253
+ ```dockerfile
1254
+ # Dockerfile.worker
1255
+ FROM ruby:3.3
1256
+
1257
+ WORKDIR /app
1258
+ COPY . .
1259
+ RUN bundle install
1260
+
1261
+ CMD ["bundle", "exec", "postburner", "--config", "config/postburner.yml", "--env", "production"]
1262
+ ```
1263
+
1264
+ ```yaml
1265
+ # docker-compose.yml
1266
+ services:
1267
+ beanstalkd:
1268
+ image: schickling/beanstalkd
1269
+ command: ["-l", "0.0.0.0", "-p", "11300", "-b", "/var/lib/beanstalkd"]
1270
+ ports:
1271
+ - "11300:11300"
1272
+ volumes:
1273
+ - beanstalkd_data:/var/lib/beanstalkd
1274
+
1275
+ worker:
1276
+ build:
1277
+ context: .
1278
+ dockerfile: Dockerfile.worker
1279
+ depends_on:
1280
+ - beanstalkd
1281
+ - postgres
1282
+ environment:
1283
+ BEANSTALK_URL: beanstalk://beanstalkd:11300
1284
+ DATABASE_URL: postgres://postgres@postgres/myapp_production
1285
+
1286
+ volumes:
1287
+ beanstalkd_data:
749
1288
  ```
750
1289
 
751
- #### How it Works
1290
+ ## Migration from v0.x
752
1291
 
753
- The implementation uses a clever pattern (app/models/postburner/job.rb:86-724):
1292
+ Key changes in v1.0:
754
1293
 
755
- 1. **after_save_commit callback** (`insert_if_queued!` at line 86) runs on every save
756
- 2. **Conditional insertion** (line 722) - only inserts if `will_insert?` returns true
757
- 3. **Flag check** (lines 242-244) - `will_insert?` only returns true if `@_insert_options` is set
758
- 4. **Flag set** (line 199) - `@_insert_options` is only set inside the `queue!` method
1294
+ ### Removed
1295
+ - Backburner dependency
1296
+ - `config/initializers/backburner.rb`
1297
+ - Backburner worker (`bundle exec backburner`)
759
1298
 
760
- So the job is safely persisted without queueing until you explicitly call `queue!`.
1299
+ ### Added
1300
+ - ActiveJob adapter (first-class Rails integration)
1301
+ - Default jobs (Beanstalkd only, no PostgreSQL)
1302
+ - `bin/postburner` executable
1303
+ - `config/postburner.yml` configuration
1304
+ - Multiple worker types (Simple, Forking, ThreadsOnFork)
761
1305
 
762
- #### Use Cases
1306
+ ### Migration Steps
763
1307
 
764
- This is useful for:
765
- - Creating jobs conditionally queued based on business logic
766
- - Building jobs within transactions that might roll back
767
- - Testing job creation without running workers
768
- - Creating foreign key relationships before queueing
1308
+ 1. **Update Gemfile:**
1309
+ ```ruby
1310
+ gem 'postburner', '~> 1.0.0.pre.1'
1311
+ ```
1312
+
1313
+ 2. **Remove Backburner config:**
1314
+ ```bash
1315
+ rm config/initializers/backburner.rb
1316
+ ```
1317
+
1318
+ 3. **Create Postburner config:**
1319
+ ```bash
1320
+ cp config/postburner.yml.example config/postburner.yml
1321
+ ```
1322
+
1323
+ 4. **Update ActiveJob adapter:**
1324
+ ```ruby
1325
+ # config/application.rb
1326
+ config.active_job.queue_adapter = :postburner # was :backburner
1327
+ ```
1328
+
1329
+ 5. **Update worker command:**
1330
+ ```bash
1331
+ # Old
1332
+ bundle exec backburner
1333
+
1334
+ # New
1335
+ bundle exec postburner --config config/postburner.yml
1336
+ ```
1337
+
1338
+ 6. **Existing Jobs:** Direct `Postburner::Job` subclasses continue to work without changes!
1339
+
1340
+ ## Contributing
1341
+
1342
+ Submit a pull request. Follow project conventions. Be nice.
769
1343
 
770
1344
  ## License
1345
+
771
1346
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).