postburner 1.0.0.pre.11 → 1.0.0.pre.12

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +961 -555
  3. data/app/concerns/postburner/commands.rb +1 -1
  4. data/app/concerns/postburner/execution.rb +11 -11
  5. data/app/concerns/postburner/insertion.rb +1 -1
  6. data/app/concerns/postburner/logging.rb +2 -2
  7. data/app/concerns/postburner/statistics.rb +1 -1
  8. data/app/models/postburner/job.rb +27 -4
  9. data/app/models/postburner/mailer.rb +1 -1
  10. data/app/models/postburner/schedule.rb +703 -0
  11. data/app/models/postburner/schedule_execution.rb +353 -0
  12. data/app/views/postburner/jobs/show.html.haml +3 -3
  13. data/lib/generators/postburner/install/install_generator.rb +1 -0
  14. data/lib/generators/postburner/install/templates/config/postburner.yml +15 -6
  15. data/lib/generators/postburner/install/templates/migrations/create_postburner_schedules.rb.erb +71 -0
  16. data/lib/postburner/active_job/adapter.rb +3 -3
  17. data/lib/postburner/active_job/payload.rb +5 -0
  18. data/lib/postburner/advisory_lock.rb +123 -0
  19. data/lib/postburner/configuration.rb +43 -7
  20. data/lib/postburner/connection.rb +7 -6
  21. data/lib/postburner/runner.rb +26 -3
  22. data/lib/postburner/scheduler.rb +427 -0
  23. data/lib/postburner/strategies/immediate_test_queue.rb +24 -7
  24. data/lib/postburner/strategies/nice_queue.rb +1 -1
  25. data/lib/postburner/strategies/null_queue.rb +2 -2
  26. data/lib/postburner/strategies/test_queue.rb +2 -2
  27. data/lib/postburner/time_helpers.rb +4 -2
  28. data/lib/postburner/tube.rb +9 -1
  29. data/lib/postburner/version.rb +1 -1
  30. data/lib/postburner/worker.rb +684 -0
  31. data/lib/postburner.rb +32 -13
  32. metadata +7 -3
  33. data/lib/postburner/workers/base.rb +0 -205
  34. data/lib/postburner/workers/worker.rb +0 -396
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Postburner
2
2
 
3
- Fast Beanstalkd-backed job queue with **optional PostgreSQL records** for ActiveJob.
3
+ Fast Beanstalkd-backed job queue with **optional PostgreSQL records**.
4
+
5
+ Depends on Beanstalkd, Postgres, ActiveRecord, and ActiveJob (1).
4
6
 
5
7
  Postburner provides dual-mode job execution:
6
8
  - **Fast jobs**: Fast execution via Beanstalkd
@@ -8,11 +10,20 @@ Postburner provides dual-mode job execution:
8
10
 
9
11
  Built for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
10
12
 
11
- Depends on Beanstalkd, ActiveRecord, ActiveJob, Postgres.
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
+ - **Scheduler** - Schedule jobs at fixed intervals, cron expressions, and calendar-aware anchor points.
18
+ - **Beanstalkd** - Fast, reliable queue separate from your database, peristent storage
19
+ - **Process isolation** - Forking workers with optional threading for throughput
20
+ - **Test-friendly** - Inline execution without Beanstalkd in tests
21
+
22
+ (1) An ActiveJob adapter is provided for seamless integration with the default Rails stack, you can use Postburner without ActiveJob by using the `Postburner::Job` class directly.
12
23
 
13
24
  ```ruby
14
25
  # Default job (fast, no PostgreSQL overhead)
15
- class SendSms < ApplicationJob
26
+ class SendSmsJob < ApplicationJob # i.e. ActiveJob
16
27
  def perform(user_id)
17
28
  user = User.find(user_id)
18
29
  TextMessage.welcome(
@@ -23,7 +34,7 @@ class SendSms < ApplicationJob
23
34
  end
24
35
 
25
36
  # Default job with Beanstalkd configuration
26
- class DoSomething < ApplicationJob
37
+ class DoSomethingJob < ApplicationJob # i.e. ActiveJob
27
38
  include Postburner::Beanstalkd # optional, allow access to beanstalkd
28
39
 
29
40
  priority 5000 # 0=highest, 65536=default, can set per job
@@ -35,7 +46,7 @@ class DoSomething < ApplicationJob
35
46
  end
36
47
 
37
48
  # Tracked job (full audit trail, includes Beanstalkd automatically)
38
- class ProcessPayment < ApplicationJob
49
+ class ProcessPaymentJob < ApplicationJob # i.e. ActiveJob
39
50
  include Postburner::Tracked # ← Opt-in to tracking (includes Beanstalkd)
40
51
 
41
52
  priority 0 # Highest priority
@@ -48,6 +59,31 @@ class ProcessPayment < ApplicationJob
48
59
  end
49
60
  end
50
61
 
62
+ # Native Postburner::Job (always tracked, full api)
63
+ class RunReportJob < Postburner::Job
64
+ queue 'critical'
65
+ priority 0
66
+ max_retries 0
67
+
68
+ def perform(args)
69
+ report = Report.find(args['report_id'])
70
+ report.run!
71
+ log "Ran report #{report.id}"
72
+ end
73
+ end
74
+
75
+ # Scheduled job (fixed interval, anchor based)
76
+ Postburner::Schedule.create!(
77
+ name: 'daily_event_retention',
78
+ job_class: EventRetentionJob, # either Postburner::Job or ActiveJob
79
+ anchor: Time.zone.parse('2025-01-01 09:30:00'),
80
+ interval: 1,
81
+ interval_unit: 'days',
82
+ timezone: 'America/New_York',
83
+ catch_up: false,
84
+ args: { retention_days: 30 }
85
+ )
86
+
51
87
  # Run worker (bin/postburner)
52
88
  bundle exec postburner --worker default
53
89
 
@@ -55,36 +91,49 @@ bundle exec postburner --worker default
55
91
  bundle exec rake postburner:work WORKER=default
56
92
  ```
57
93
 
94
+ ## Table of Contents
95
+
96
+ - [Why](#why)
97
+ - [Quick Start](#quick-start)
98
+ - [Usage](#usage)
99
+ - [Standard Jobs](#standard-jobs)
100
+ - [Tracked Jobs](#tracked-jobs)
101
+ - [Postburner::Job](#postburnerjob)
102
+ - [Scheduler](#scheduler)
103
+ - [Job Management](#job-management)
104
+ - [Queue Strategies](#queue-strategies)
105
+ - [Testing](#testing)
106
+ - [Workers](#workers)
107
+ - [Configuration](#configuration)
108
+ - [Callbacks](#callbacks)
109
+ - [Instrumentation](#instrumentation)
110
+ - [Why Beanstalkd?](#why-beanstalkd)
111
+ - [Beanstalkd Integration](#beanstalkd-integration)
112
+ - [Installation](#installation)
113
+ - [Deployment](#deployment)
114
+ - [Web UI](#web-ui)
115
+
58
116
  ## Why
59
117
 
60
- 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.
118
+ 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. Postburner also supports scheduling jobs at fixed intervals, cron expressions, and calendar-aware anchor points.
61
119
 
62
120
  Postburner is inspired by the [Backburner](https://github.com/nesquena/backburner) gem i.e. "burner", Postgres i.e. "Post", and the database backends (SolidQueue, Que, etc), and the in memory/redis backends (Sidekiq, Resque, etc). And puma's concurrency model.
63
121
 
64
- Thus old-school [beanstalkd](https://beanstalkd.github.io/) is used with PostgreSQL to cover all the cases we care about.
122
+ Postburner [beanstalkd](https://beanstalkd.github.io/) is used with PostgreSQL to cover all the cases we care about.
65
123
  - Fast when you want it (light, ephemeral jobs like delayed turbo_stream rendering, communications)
66
- - Tracked when you need it (critical operations like payments, and processes.
124
+ - Tracked when you need it critical operations like payments, and processes.
67
125
  - Able to record jobs before and after execution in PostgreSQL, with foreign keys and constraints.
68
126
  - Store the jobs outside of the database, but also persist them to disk for disaster recovery (beanstalkd binlogs)
127
+ - Schedule jobs at fixed intervals, cron expressions, and calendar-aware anchor points.
69
128
  - Introspect the jobs either with ActiveRecord or Beanstalkd.
70
129
  - 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).
71
130
  - Easy testing.
72
131
 
73
- ## Features
74
-
75
- - **ActiveJob native** - Works seamlessly with Rails, ActionMailer, ActiveStorage
76
- - **Dual-mode execution** - Default or tracked (database backed)
77
- - **Rich audit trail** - Logs, timing, errors, retry tracking (tracked jobs only)
78
- - **ActiveRecord** - Query jobs with ActiveRecord (Tracked jobs only)
79
- - **Beanstalkd** - Fast, reliable queue separate from your database, peristent storage
80
- - **Process isolation** - Forking workers with optional threading for throughput
81
- - **Test-friendly** - Inline execution without Beanstalkd in tests
82
-
83
132
  ## Quick Start
84
133
 
85
134
  ```ruby
86
135
  # Gemfile
87
- gem 'postburner', '~> 1.0.0.pre.3'
136
+ gem 'postburner', '~> 1.0.0.pre.11'
88
137
 
89
138
  # config/application.rb
90
139
  config.active_job.queue_adapter = :postburner
@@ -138,174 +187,189 @@ bundle exec postburner # start with bin/postburner
138
187
  bundle exec rake postburner:work # or with rake task
139
188
  ```
140
189
 
141
- ## Table of Contents
190
+ ## Usage
142
191
 
143
- - [Why Beanstalkd?](#why-beanstalkd)
144
- - [Installation](#installation)
145
- - [Usage](#usage)
146
- - [Standard Jobs](#standard-jobs)
147
- - [Tracked Jobs](#tracked-jobs)
148
- - [Postburner::Job](#postburnerjob)
149
- - [Workers](#workers)
150
- - [Configuration](#configuration)
151
- - [Queue Strategies](#queue-strategies)
152
- - [Testing](#testing)
153
- - [Job Management](#job-management)
154
- - [Beanstalkd Integration](#beanstalkd-integration)
155
- - [Web UI](#web-ui)
192
+ **Job Idempotency:** Jobs should be designed to be idempotent and safely re-runnable. Like all job queues, Postburner provides at-least-once delivery—in rare errant cases outside of Postburner's control, a job may be executed more than once, i.e. network issues, etc.
156
193
 
157
- ## Why Beanstalkd?
194
+ **TTR (Time-to-Run):** If a job exceeds its TTR without completion, Beanstalkd releases it back to the queue while still running—causing duplicate execution. For long-running jobs, call `extend!` periodically to reset the TTR, or set a sufficiently high TTR value. You must include the `Postburner::Beanstalkd` or `Postburner::Tracked` module with `ActiveJob` to use `extend!`.
158
195
 
159
- 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.
196
+ ### Default Jobs
160
197
 
161
- 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. Here is a help
198
+ Default jobs execute quickly via Beanstalkd without PostgreSQL overhead. Perfect for emails, cache warming, notifications, etc.
162
199
 
163
- Here is a picture of the typical job lifecycle:
200
+ ```ruby
201
+ class SendWelcomeEmail < ApplicationJob
202
+ queue_as :mailers
164
203
 
165
- ```
166
- put reserve delete
167
- -----> [READY] ---------> [RESERVED] --------> *poof*`
168
- ```
204
+ def perform(user_id)
205
+ UserMailer.welcome(user_id).deliver_now
206
+ end
207
+ end
169
208
 
170
- Here is a picture with more possibilities:
209
+ # Enqueue immediately
210
+ SendWelcomeEmail.perform_later(123)
171
211
 
172
- ```
173
- put with delay release with delay
174
- ----------------> [DELAYED] <------------.
175
- | |
176
- | (time passes) |
177
- | |
178
- put v reserve | delete
179
- -----------------> [READY] ---------> [RESERVED] --------> *poof*
180
- ^ ^ | |
181
- | \ release | |
182
- | `-------------' |
183
- | |
184
- | kick |
185
- | |
186
- | bury |
187
- [BURIED] <---------------'
188
- |
189
- | delete
190
- `--------> *poof*
212
+ # Enqueue with delay
213
+ SendWelcomeEmail.set(wait: 1.hour).perform_later(123)
214
+
215
+ # Enqueue at specific time
216
+ SendWelcomeEmail.set(wait_until: Date.tomorrow.noon).perform_later(123)
191
217
  ```
192
218
 
193
- ### Binlogs
219
+ **Overview:**
220
+ - Fast execution (no database writes)
221
+ - Low overhead
222
+ - Standard ActiveJob retry_on/discard_on support
223
+ - No audit trail
224
+ - No logging or statistics
194
225
 
195
- 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.
226
+ #### Configuring Beanstalkd Priority and TTR
196
227
 
197
- ```bash
198
- # Create binlog directory
199
- sudo mkdir -p /var/lib/beanstalkd
200
- sudo chown beanstalkd:beanstalkd /var/lib/beanstalkd # If running as service
228
+ For default jobs that need custom Beanstalkd configuration, include `Postburner::Beanstalkd`:
201
229
 
202
- # Start beanstalkd with binlog persistence
203
- beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
230
+ ```ruby
231
+ class SendWelcomeEmail < ApplicationJob
232
+ include Postburner::Beanstalkd
233
+
234
+ queue_as :mailers
235
+ priority 100 # Lower = higher priority (0 is highest)
236
+ ttr 300 # Time-to-run in seconds (5 minutes)
237
+
238
+ def perform(user_id)
239
+ UserMailer.welcome(user_id).deliver_now
240
+ end
241
+ end
204
242
  ```
205
243
 
206
- **Other options:**
244
+ **Configuration options:**
245
+ - `queue_priority` - Beanstalkd priority (0-4294967295, lower = higher priority)
246
+ - `queue_ttr` - Time-to-run in seconds before job times out
207
247
 
208
- ```bash
209
- # Basic persistence
210
- beanstalkd -b /var/lib/beanstalkd
248
+ Jobs without `Postburner::Beanstalkd` use defaults from `config/postburner.yml`:
249
+ - `default_priority: 65536`
250
+ - `default_ttr: 300`
211
251
 
212
- # With custom binlog file size (default varies)
213
- beanstalkd -b /var/lib/beanstalkd -s 10485760 # 10MB per binlog file
252
+ ### Tracked Jobs
214
253
 
215
- # Reduce fsync frequency for better performance (trades safety for speed)
216
- beanstalkd -b /var/lib/beanstalkd -f 200 # fsync at most every 200ms (default: 50ms)
254
+ Tracked jobs store full execution details in PostgreSQL, providing comprehensive audit trails (i.e. logging, timing, errors, retry tracking) for critical operations.
217
255
 
218
- # Never fsync (maximum performance, higher risk of data loss on power failure)
219
- beanstalkd -b /var/lib/beanstalkd -F
220
- ```
256
+ **Note:** `Postburner::Tracked` automatically includes `Postburner::Beanstalkd`, giving you access to `queue_priority`, `queue_ttr`, `bk`, and `extend!`.
221
257
 
222
- **Beanstalkd switches:**
223
- - `-b <dir>` - Enable binlog persistence in specified directory
224
- - `-s <bytes>` - Maximum size of each binlog file (requires `-b`)
225
- - `-f <ms>` - Call fsync at most once every N milliseconds (requires `-b`, default: 50ms)
226
- - `-F` - Never call fsync (requires `-b`, maximum performance but higher data loss risk)
258
+ ```ruby
259
+ class ProcessPayment < ApplicationJob
260
+ include Postburner::Tracked # Opt-in to PostgreSQL tracking (includes Beanstalkd)
227
261
 
228
- **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.
262
+ queue_as :critical
263
+ priority 0 # Highest priority (Beanstalkd included automatically)
264
+ ttr 600 # 10 minute timeout
265
+ retry_on StandardError, wait: :exponentially_longer, attempts: 5
229
266
 
230
- ### Priority
267
+ def perform(payment_id)
268
+ payment = Payment.find(payment_id)
231
269
 
232
- Priority controls the order in which jobs are processed from the queue. Beanstalkd always processes the highest priority (lowest number) jobs first.
270
+ log "Starting payment processing for $#{payment.amount}"
233
271
 
234
- **Priority Range:** 0 to 4,294,967,295 (unsigned 32-bit integer)
235
- - `0` = Highest priority (processed first)
236
- - `4,294,967,295` = Lowest priority (processed last)
237
- - Default: `65536` (configurable in `config/postburner.yml`)
272
+ begin
273
+ payment.charge!
274
+ log! "Payment charged successfully"
275
+ rescue PaymentError => e
276
+ log_exception!(e)
277
+ raise
278
+ end
238
279
 
239
- 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.
280
+ log "Payment complete", level: :info
281
+ end
282
+ end
283
+ ```
240
284
 
241
- **ActiveJob with Postburner::Beanstalkd:**
285
+ **Tracking provides:**
286
+ - Complete execution history (queued_at, processing_at, processed_at)
287
+ - Custom logs with `log` and `log!`
288
+ - Exception tracking with `log_exception()` and `log_exception!()`
289
+ - Timing statistics (lag, duration)
290
+ - Retry attempts tracking
291
+ - Query with ActiveRecord
292
+ - Foreign key relationships
293
+ - Beanstalkd operations via `bk` accessor
294
+ - TTR extension via `extend!` method
295
+
296
+ **Query tracked jobs:**
242
297
 
243
298
  ```ruby
244
- class ProcessPayment < ApplicationJob
245
- include Postburner::Beanstalkd
299
+ # Find by state
300
+ Postburner::TrackedJob.where(processed_at: nil)
301
+ Postburner::TrackedJob.where.not(removed_at: nil)
246
302
 
247
- queue_as :critical
248
- priority 0 # Highest priority - processed immediately
249
- end
303
+ # Find with errors
304
+ Postburner::TrackedJob.where("error_count > 0")
250
305
 
251
- class SendEmail < ApplicationJob
252
- include Postburner::Beanstalkd
306
+ # Statistics
307
+ Postburner::TrackedJob.average(:duration) # Average execution time
308
+ Postburner::TrackedJob.maximum(:lag) # Worst lag
253
309
 
254
- queue_as :mailers
255
- priority 1000 # Lower priority - processed after critical jobs
256
- end
310
+ # Inspect execution
311
+ job = Postburner::TrackedJob.last
312
+ job.logs # Array of log entries with timestamps
313
+ job.errata # Array of exceptions with backtraces
314
+ job.attempts # Array of attempt timestamps
315
+ job.duration # Execution time in milliseconds
316
+ job.lag # Queue lag in milliseconds
257
317
  ```
258
318
 
259
- **Postburner::Job:**
319
+ ### Direct Postburner::Job Usage
320
+
321
+ Direct `Postburner::Job` subclasses are **always tracked**:
260
322
 
261
323
  ```ruby
262
- class CriticalJob < Postburner::Job
324
+ class ProcessPayment < Postburner::Job
263
325
  queue 'critical'
264
- priority 0 # Highest priority
326
+ priority 0
327
+ max_retries 0
265
328
 
266
329
  def perform(args)
267
- # Critical business logic
330
+ payment = Payment.find(args['payment_id'])
331
+ payment.process!
332
+ log "Processed payment #{payment.id}"
268
333
  end
269
334
  end
270
335
 
271
- class BackgroundTask < Postburner::Job
272
- queue 'default'
273
- priority 5000 # Lower priority
336
+ # Create and enqueue
337
+ job = ProcessPayment.create!(args: { 'payment_id' => 123 })
338
+ job.queue!
274
339
 
275
- def perform(args)
276
- # Non-urgent background work
277
- end
278
- end
340
+ # With delay
341
+ job.queue!(delay: 1.hour)
342
+
343
+ # At specific time
344
+ job.queue!(at: 2.days.from_now)
279
345
  ```
280
346
 
281
- **Recommended Priority Ranges:**
347
+ #### Instance-Level Queue Configuration
282
348
 
283
- | Priority Range | Use Case | Examples |
284
- |---------------|----------|----------|
285
- | 0-256 | Critical business operations | Payments, transactions, real-time notifications |
286
- | 256-4096 | High priority tasks | Password resets, order confirmations |
287
- | 4096-65536 | Standard background jobs | Email sending, data exports (65536 is default) |
288
- | 65536-262144 | Low priority maintenance | Cache warming, log cleanup |
289
- | 262144+ | Deferred/bulk operations | Analytics, batch reports |
349
+ Override queue priority and TTR per job instance for dynamic behavior:
290
350
 
291
- **Configuration:**
292
-
293
- Set default priority in `config/postburner.yml`:
294
-
295
- ```yaml
296
- production:
297
- default_priority: 65536 # Default for jobs without explicit priority
298
- default_ttr: 300
299
- ```
351
+ ```ruby
352
+ # Set priority during creation
353
+ job = ProcessPayment.create!(
354
+ args: { 'payment_id' => 123 },
355
+ queue_priority: 1500, # Override class-level priority
356
+ queue_ttr: 300 # Override class-level TTR
357
+ )
358
+ job.queue!
300
359
 
301
- **Dynamic Priority (Postburner::Job only):**
360
+ # Set dynamically after creation
361
+ job = ProcessPayment.create!(args: { 'payment_id' => 456 })
362
+ job.priority = 2000
363
+ job.ttr = 300
364
+ job.queue!
302
365
 
303
- ```ruby
366
+ # Use in before_enqueue callback for conditional behavior
304
367
  class ProcessOrder < Postburner::Job
305
368
  queue 'orders'
306
- priority 100 # Default
369
+ priority 100 # Default priority
370
+ ttr 120 # Default TTR
307
371
 
308
- before_enqueue :adjust_priority
372
+ before_enqueue :set_priority_based_on_urgency
309
373
 
310
374
  def perform(args)
311
375
  order = Order.find(args['order_id'])
@@ -314,433 +378,474 @@ class ProcessOrder < Postburner::Job
314
378
 
315
379
  private
316
380
 
317
- def adjust_priority
318
- if args['urgent']
319
- self.priority = 0 # Override to highest priority
381
+ def set_priority_based_on_urgency
382
+ if args['express_shipping']
383
+ self.priority = 0 # High priority for express orders
384
+ self.ttr = 600 # Allow 10 minutes to complete
385
+ else
386
+ self.priority = 1000 # Low priority for standard orders
387
+ self.ttr = 120 # Standard 2 minute timeout
320
388
  end
321
389
  end
322
390
  end
323
391
 
324
- # Usage
325
- ProcessOrder.create!(args: { 'order_id' => 123, 'urgent' => true }).queue!
392
+ # Express order gets high priority automatically
393
+ ProcessOrder.create!(args: { 'order_id' => 789, 'express_shipping' => true }).queue!
326
394
  ```
327
395
 
328
- ### Time to Run (TTR)
396
+ **Overview:**
397
+ - Same as Tracked jobs
398
+ - Access the ActiveRecord Postburner::Job directly.
329
399
 
330
- 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.
400
+ ### Scheduler
331
401
 
332
- 1. Worker reserves a job from Beanstalkd
333
- 2. TTR countdown begins immediately upon reservation
334
- 3. If job completes within TTR, worker deletes/buries the job (success)
335
- 4. If TTR expires before completion, Beanstalkd automatically releases the job back to ready queue
336
- 5. Another worker can then reserve and process the same job
402
+ Postburner includes a lightweight, fixed-rate scheduler for recurring jobs. Perfect for daily reports, weekly cleanups, or any task that needs to run on a predictable schedule.
337
403
 
338
- This mechanism protects against workers crashing or hanging—jobs won't be stuck indefinitely.
404
+ The scheduler uses **immediate enqueue** combined with a **watchdog safety net**:
339
405
 
340
- **TTR Range:** 1 to 4,294,967,295 seconds (unsigned 32-bit integer)
341
- - Minimum: `1` second (Beanstalkd silently increases 0 to 1)
342
- - Default: `300` seconds (5 minutes, configurable in `config/postburner.yml`)
406
+ 1. When an execution is created, it's immediately enqueued to Beanstalkd's delayed queue with the appropriate delay until `run_at`
407
+ 2. For `Postburner::Job` and `ActiveJob` with `Postburner::Tracked` schedules the next execution when the current job runs - providing immediate pickup without waiting for the watchdog. Normal `ActiveJob` schedules need to rely on the watchdog to create the next execution, so set the `default_scheduler_interval` to pick up exections appropriately.
408
+ 3. A lightweight watchdog job in the `scheduler` tube acts as a safety net:
409
+ ```json
410
+ { "scheduler": true, "interval": 300 }
411
+ ```
412
+ 4. When a worker reserves the watchdog, it instantiates `Postburner::Scheduler` which:
413
+ - Acquires a PostgreSQL advisory lock for coordination
414
+ - Auto-bootstraps any unstarted schedules
415
+ - Ensures each schedule has a future execution queued
416
+ - Re-queues a new watchdog with delay for the next interval
343
417
 
344
- **ActiveJob with Postburner::Beanstalkd:**
418
+ NOTE: The watchdog is ephemeral data in Beanstalkd, not a database record. `Postburner::Scheduler` is the handler class that does the work. This design requires no dedicated scheduler process - existing workers handle everything.
345
419
 
346
420
  ```ruby
347
- class ProcessPayment < ApplicationJob
348
- include Postburner::Beanstalkd
349
-
350
- queue_as :critical
351
- priority 0
352
- ttr 300 # 5 minutes to complete
353
- end
354
-
355
- class QuickEmail < ApplicationJob
356
- include Postburner::Beanstalkd
421
+ # Create a schedule for a daily report at 9:30 AM
422
+ Postburner::Schedule.create!(
423
+ name: 'daily_metrics',
424
+ job_class: 'GenerateMetricsReportJob',
425
+ anchor: Time.zone.parse('2025-01-01 09:30:00'),
426
+ interval: 1,
427
+ interval_unit: 'days',
428
+ timezone: 'America/New_York',
429
+ args: { report_type: 'daily' } # Passed as keyword args to perform(report_type:)
430
+ )
431
+ ```
357
432
 
358
- queue_as :mailers
359
- ttr 60 # 1 minute for fast email jobs
360
- end
433
+ The `args` hash is passed to each job execution. For ActiveJob classes, hash args become keyword arguments (`perform(report_type:)`). For `Postburner::Job` subclasses, args are passed as a hash to `perform(args)`.
361
434
 
362
- class LongRunningReport < ApplicationJob
363
- include Postburner::Beanstalkd
435
+ The scheduler automatically creates executions and enqueues jobs at the scheduled times.
364
436
 
365
- queue_as :reports
366
- ttr 3600 # 1 hour for complex reports
367
- end
368
- ```
437
+ ### Configuration
369
438
 
370
- **Postburner::Job:**
439
+ Add scheduler settings to `config/postburner.yml`:
371
440
 
372
- ```ruby
373
- class DataImport < Postburner::Job
374
- queue 'imports'
375
- ttr 1800 # 30 minutes (equivalent to queue_ttr)
441
+ ```yaml
442
+ production:
443
+ beanstalk_url: <%= ENV['BEANSTALK_URL'] %>
444
+ default_scheduler_interval: 300 # Pickup new schedules every 5 minutes
445
+ default_scheduler_priority: 100 # Scheduler jobs run at priority 100
376
446
 
377
- def perform(args)
378
- # Long-running import logic
379
- end
380
- end
447
+ workers:
448
+ default:
449
+ queues:
450
+ - default
451
+ - mailers
381
452
  ```
382
453
 
383
- **Extending TTR with `extend!` (Touch Command):**
384
-
385
- 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.
386
-
387
- **Available for:**
388
- - `Postburner::Job` subclasses (always available via `bk.extend!`)
389
- - `Postburner::Tracked` ActiveJob classes (includes `extend!` method)
454
+ **Configuration options:**
455
+ - `default_scheduler_interval` - How often (in seconds) to check for due schedules (default: 300)
456
+ - `default_scheduler_priority` - Beanstalkd priority for watchdog jobs (default: 100)
390
457
 
391
- ```ruby
392
- class ProcessImport < ApplicationJob
393
- include Postburner::Tracked # Includes Beanstalkd and enables extend!
458
+ **Choosing an interval:** Since executions are enqueued immediately to Beanstalkd's delayed queue, the watchdog interval primarily affects:
459
+ - How quickly new schedules are auto-bootstrapped (if you don't call `start!`)
460
+ - Recovery time if an execution somehow fails to enqueue
461
+ - How often `last_audit_at` is updated for monitoring
394
462
 
395
- queue_as :imports
396
- ttr 300 # 5 minutes initial TTR
463
+ For most use cases, the default 300 seconds is appropriate. Lower values increase database queries without significant benefit since jobs are already queued in Beanstalkd.
397
464
 
398
- def perform(file_id)
399
- file = ImportFile.find(file_id)
465
+ **No manual setup required:** Workers automatically watch the `scheduler` tube and create the watchdog job if one doesn't exist. Just start your workers and create schedules - the rest happens automatically.
400
466
 
401
- file.each_batch(size: 100) do |batch|
402
- batch.each do |row|
403
- process_row(row)
404
- end
467
+ #### Creating Schedules
405
468
 
406
- extend! # Reset TTR to 300 seconds from now
407
- log "Processed batch, extended TTR"
408
- end
409
- end
410
- end
411
- ```
469
+ ##### Anchor-Based Scheduling (Recommended)
412
470
 
413
- **For Postburner::Job (via `bk` accessor):**
471
+ Use an anchor time plus interval for predictable, drift-free scheduling:
414
472
 
415
473
  ```ruby
416
- class LargeDataProcessor < Postburner::Job
417
- queue 'processing'
418
- ttr 600 # 10 minutes
474
+ # Every day at 9:00 AM Eastern
475
+ Postburner::Schedule.create!(
476
+ name: 'daily_cleanup',
477
+ job_class: 'CleanupJob',
478
+ anchor: Time.zone.parse('2025-01-01 09:00:00'),
479
+ interval: 1,
480
+ interval_unit: 'days',
481
+ timezone: 'America/New_York'
482
+ )
419
483
 
420
- def perform(args)
421
- dataset = Dataset.find(args['dataset_id'])
484
+ # Every 6 hours starting from midnight
485
+ Postburner::Schedule.create!(
486
+ name: 'sync_data',
487
+ job_class: 'DataSyncJob',
488
+ anchor: Time.zone.parse('2025-01-01 00:00:00'),
489
+ interval: 6,
490
+ interval_unit: 'hours',
491
+ timezone: 'UTC'
492
+ )
422
493
 
423
- dataset.each_chunk do |chunk|
424
- process_chunk(chunk)
494
+ # Weekly on Saturday at 2:00 AM
495
+ Postburner::Schedule.create!(
496
+ name: 'weekly_report',
497
+ job_class: 'WeeklyReportJob',
498
+ anchor: Time.zone.parse('2025-01-04 02:00:00'), # A Saturday
499
+ interval: 1,
500
+ interval_unit: 'weeks',
501
+ timezone: 'America/Los_Angeles'
502
+ )
425
503
 
426
- bk.extend! # Reset TTR via beanstalkd job accessor
427
- log "Chunk processed, TTR extended"
428
- end
429
- end
430
- end
504
+ # Monthly on the 1st at midnight
505
+ Postburner::Schedule.create!(
506
+ name: 'monthly_billing',
507
+ job_class: 'MonthlyBillingJob',
508
+ anchor: Time.zone.parse('2025-01-01 00:00:00'),
509
+ interval: 1,
510
+ interval_unit: 'months',
511
+ timezone: 'UTC'
512
+ )
431
513
  ```
432
514
 
433
- **`extend!` and Beanstalkd `touch`**
515
+ **Available interval units:** `seconds`, `minutes`, `hours`, `days`, `weeks`, `months`, `years`
434
516
 
435
- - Calls Beanstalkd's `touch` command on the reserved job
436
- - Resets the TTR countdown to the original TTR value from the current moment
437
- - Example: Job has `ttr 300`. After 200 seconds, calling `extend!` gives you another 300 seconds (not just 100)
438
- - Can be called multiple times during job execution
439
- - Useful for iterative processing where each iteration is quick but total time is unpredictable
517
+ ##### Cron-Based Scheduling
440
518
 
441
- **Configuration:**
519
+ For complex schedules, use cron expressions (requires the `fugit` gem):
442
520
 
443
- Set default TTR in `config/postburner.yml`:
521
+ ```ruby
522
+ # Every weekday at 8:00 AM
523
+ Postburner::Schedule.create!(
524
+ name: 'weekday_standup',
525
+ job_class: 'StandupReminderJob',
526
+ cron: '0 8 * * 1-5',
527
+ timezone: 'America/Chicago'
528
+ )
444
529
 
445
- ```yaml
446
- production:
447
- default_priority: 65536
448
- default_ttr: 300 # 5 minutes default for all jobs
530
+ # Every 15 minutes
531
+ Postburner::Schedule.create!(
532
+ name: 'health_check',
533
+ job_class: 'HealthCheckJob',
534
+ cron: '*/15 * * * *',
535
+ timezone: 'UTC'
536
+ )
449
537
  ```
450
538
 
451
- **Recommended TTR Values:**
539
+ #### Schedule Options
452
540
 
453
- | Job Type | TTR | Reasoning |
454
- |----------|-----|-----------|
455
- | Email sending | 60-120s | Network operations, should be fast |
456
- | API calls | 30-60s | External services, fast or timeout |
457
- | File processing | 600-1800s | Variable based on file size, use `extend!` |
458
- | Reports | 1800-3600s | Complex queries and aggregations |
459
- | Imports/Exports | 3600s+ | Large datasets, use `extend!` in loops |
541
+ ```ruby
542
+ Postburner::Schedule.create!(
543
+ name: 'important_job', # Required: unique identifier
544
+ job_class: 'MyJob', # Required: ActiveJob or Postburner::Job class name
545
+
546
+ # Scheduling (choose anchor+interval OR cron)
547
+ anchor: Time.zone.parse('...'), # Start time for interval calculation
548
+ interval: 1, # Number of interval units
549
+ interval_unit: 'days', # seconds/minutes/hours/days/weeks/months/years
550
+ # OR
551
+ cron: '0 9 * * *', # Cron expression (requires fugit gem)
552
+
553
+ # Optional
554
+ timezone: 'America/New_York', # Default: 'UTC'
555
+ args: { key: 'value' }, # Arguments passed to job
556
+ queue: 'critical', # Override default queue
557
+ priority: 0, # Override default priority
558
+ enabled: true, # Enable/disable schedule
559
+ catch_up: false, # Skip missed executions (default: false)
560
+ description: 'Daily metrics run' # Human-readable description
561
+ )
562
+ ```
460
563
 
461
- **Best Practices:**
564
+ #### Catch-Up Policy
462
565
 
463
- 1. **Set realistic TTR values** based on expected job duration plus buffer
464
- 2. **Use `extend!` for iterative work** where total time is unpredictable but each iteration is bounded
465
- 3. **Don't set TTR too high** - it delays recovery from crashed workers
466
- 4. **Monitor TTR timeouts** - frequent timeouts indicate jobs need more time or optimization
467
- 5. **For Default jobs** (ActiveJob without `Postburner::Beanstalkd`), include the module to set custom TTR
566
+ The `catch_up` option controls what happens when the worker is down and misses scheduled executions:
468
567
 
469
- **What happens on TTR timeout:**
568
+ - **`catch_up: false` (default)** - Skip missed executions, create next future execution only (health checks, status updates, cache warming, monitoring)
569
+ - **`catch_up: true`** - Run all missed executions when worker restarts (data processing pipelines, billing cycles, audit requirements, SLA-sensitive operations)
470
570
 
471
- ```ruby
472
- # Job starts processing
473
- job = ProcessImport.perform_later(file_id)
571
+ Notes:
572
+ 1) Previously created executions and enqueued jobs are not affected by the catch-up policy.
573
+ 2) If `catch_up` if off, then there the skipped executions are not created or tracked. It resumes from the next future execution.
474
574
 
475
- # Worker crashes or hangs after 200 seconds
476
- # At 300 seconds (TTR), Beanstalkd automatically:
477
- # 1. Releases job back to ready queue
478
- # 2. Increments the job's reserve count
479
- # 3. Makes job available for another worker
575
+ ```ruby
576
+ # Skip missed executions (default)
577
+ Postburner::Schedule.create!(
578
+ name: 'health_check',
579
+ job_class: 'HealthCheckJob',
580
+ interval: 1,
581
+ interval_unit: 'minutes',
582
+ catch_up: false # Worker down 9:00-9:05 → skips 9:01-9:04, creates 9:06
583
+ )
480
584
 
481
- # Another worker picks up the job and retries
482
- # (ActiveJob retry_on/discard_on handlers still apply)
585
+ # Run all missed executions
586
+ Postburner::Schedule.create!(
587
+ name: 'process_billing',
588
+ job_class: 'BillingJob',
589
+ interval: 1,
590
+ interval_unit: 'hours',
591
+ catch_up: true # Worker down 9:00-12:00 → runs 9:00, 10:00, 11:00, 12:00
592
+ )
483
593
  ```
484
594
 
485
- ## Installation
595
+ #### Managing Schedules
486
596
 
487
- ### 1. Install Beanstalkd
597
+ ```ruby
598
+ # Find schedules
599
+ schedule = Postburner::Schedule.find_by(name: 'daily_cleanup')
600
+ Postburner::Schedule.enabled # All enabled schedules
488
601
 
489
- First [install beanstalkd](https://beanstalkd.github.io/download.html):
602
+ # Disable temporarily
603
+ schedule.update!(enabled: false)
490
604
 
491
- ```bash
492
- sudo apt-get install beanstalkd
493
- brew install beanstalkd # for macOS/Linux
605
+ # Change catch-up policy
606
+ schedule.update!(catch_up: true)
494
607
 
495
- # Start beanstalkd (in-memory only)
496
- beanstalkd -l 127.0.0.1 -p 11300
608
+ # Check next run time
609
+ schedule.next_run_at # => 2025-01-02 09:00:00 -0500
497
610
 
498
- # OR with persistence (recommended for production)
499
- mkdir -p /var/lib/beanstalkd
500
- beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
611
+ # Preview upcoming runs
612
+ schedule.next_run_at_times(5) # Next 5 run times
613
+
614
+ # View executions
615
+ schedule.executions.pending
616
+ schedule.executions.completed
617
+ schedule.executions.failed
501
618
  ```
502
619
 
503
- ### 2. Add Gem
620
+ #### Starting Schedules
504
621
 
505
- ```ruby
506
- # Gemfile
507
- gem 'postburner', '~> 1.0.0.pre.1'
508
- ```
622
+ When you create a schedule, it won't run until the first execution is created. You have two options:
509
623
 
510
- ```bash
511
- bundle install
512
- ```
624
+ **Option 1: Explicit start (immediate enqueue)**
513
625
 
514
- ### 3. Install Migration
626
+ Call `start!` to create and enqueue the first execution immediately to Beanstalkd:
515
627
 
516
- ```bash
517
- rails generate postburner:install
518
- rails db:migrate
628
+ ```ruby
629
+ schedule = Postburner::Schedule.create!(
630
+ name: 'daily_report',
631
+ job_class: 'DailyReportJob',
632
+ anchor: Time.zone.parse('2025-01-01 09:00:00'),
633
+ interval: 1,
634
+ interval_unit: 'days'
635
+ )
636
+ schedule.start! # Creates execution AND enqueues to Beanstalkd
637
+ schedule.started? # => true
519
638
  ```
520
639
 
521
- This creates the `postburner_jobs` table for tracked jobs.
640
+ The job is immediately in Beanstalkd's delayed queue and will run at the scheduled time.
522
641
 
523
- ### 4. Configure ActiveJob
642
+ **Option 2: Auto-bootstrap (eventual pickup)**
524
643
 
525
- ```ruby
526
- # config/application.rb
527
- config.active_job.queue_adapter = :postburner
528
- ```
644
+ If you don't call `start!`, the scheduler watchdog will automatically bootstrap the schedule on its next run. This adds up to one `default_scheduler_interval` of delay before the first execution is enqueued:
529
645
 
530
- ### 5. Create Worker Configuration
646
+ ```ruby
647
+ schedule = Postburner::Schedule.create!(...)
648
+ schedule.started? # => false
531
649
 
532
- ```bash
533
- cp config/postburner.yml.example config/postburner.yml
650
+ # Later, scheduler runs and auto-bootstraps...
651
+ schedule.reload.started? # => true
534
652
  ```
535
653
 
536
- Edit `config/postburner.yml` for your environment (see [Configuration](#configuration)).
654
+ Use `start!` when you need predictable timing. Skip it when eventual consistency is acceptable.
537
655
 
538
- ## Usage
656
+ #### Schedule Executions
539
657
 
540
- ### Default Jobs
541
-
542
- Default jobs execute quickly via Beanstalkd without PostgreSQL overhead. Perfect for emails, cache warming, notifications, etc.
658
+ Each scheduled run creates an execution record for tracking:
543
659
 
544
660
  ```ruby
545
- class SendWelcomeEmail < ApplicationJob
546
- queue_as :mailers
661
+ execution = Postburner::ScheduleExecution.find(123)
547
662
 
548
- def perform(user_id)
549
- UserMailer.welcome(user_id).deliver_now
550
- end
551
- end
663
+ execution.status # pending, running, completed, failed, skipped
664
+ execution.run_at # Scheduled time
665
+ execution.enqueued_at # When job was queued
666
+ execution.completed_at # When job finished
667
+ execution.beanstalk_job_id # Beanstalkd job ID
668
+ execution.job_id # Postburner::Job ID (if using Postburner::Job)
669
+ ```
552
670
 
553
- # Enqueue immediately
554
- SendWelcomeEmail.perform_later(123)
671
+ **Execution lifecycle:**
672
+ 1. Execution created with `pending` status and immediately enqueued to Beanstalkd
673
+ 2. Status changes to `scheduled` once enqueued
674
+ 3. At `run_at` time, Beanstalkd releases job to worker
675
+ 4. For `Postburner::Job` (and `ActiveJob` with `Postburner::Tracked`) schedules: `before_attempt` callback creates next execution
676
+ 5. Watchdog periodically verifies future executions exist (safety net)
555
677
 
556
- # Enqueue with delay
557
- SendWelcomeEmail.set(wait: 1.hour).perform_later(123)
678
+ #### Timezone Handling
558
679
 
559
- # Enqueue at specific time
560
- SendWelcomeEmail.set(wait_until: Date.tomorrow.noon).perform_later(123)
680
+ Schedules respect timezones for consistent execution times regardless of server location:
681
+
682
+ ```ruby
683
+ # Runs at 9:00 AM New York time (handles DST automatically)
684
+ Postburner::Schedule.create!(
685
+ name: 'ny_morning_job',
686
+ job_class: 'MorningJob',
687
+ anchor: Time.zone.parse('2025-01-01 09:00:00'),
688
+ interval: 1,
689
+ interval_unit: 'days',
690
+ timezone: 'America/New_York'
691
+ )
561
692
  ```
562
693
 
563
- **Overview:**
564
- - Fast execution (no database writes)
565
- - Low overhead
566
- - Standard ActiveJob retry_on/discard_on support
567
- - No audit trail
568
- - No logging or statistics
694
+ The scheduler calculates next run times in the specified timezone, so jobs run at the expected local time even across daylight saving transitions.
569
695
 
570
- #### Configuring Beanstalkd Priority and TTR
696
+ ### Job Management
571
697
 
572
- For default jobs that need custom Beanstalkd configuration, include `Postburner::Beanstalkd`:
698
+ #### Canceling Jobs
699
+
700
+ **For Postburner::Job subclasses:**
573
701
 
574
702
  ```ruby
575
- class SendWelcomeEmail < ApplicationJob
576
- include Postburner::Beanstalkd
703
+ job = ProcessPayment.create!(args: { 'payment_id' => 123 })
704
+ job.queue!(delay: 1.hour)
577
705
 
578
- queue_as :mailers
579
- priority 100 # Lower = higher priority (0 is highest)
580
- ttr 300 # Time-to-run in seconds (5 minutes)
706
+ # Soft delete (recommended) - removes from queue, sets removed_at
707
+ job.remove!
708
+ job.removed_at # => 2025-11-19 12:34:56 UTC
709
+ job.persisted? # => true (still in database for audit)
581
710
 
582
- def perform(user_id)
583
- UserMailer.welcome(user_id).deliver_now
584
- end
585
- end
586
- ```
711
+ # Hard delete - removes from both queue and database
712
+ job.destroy!
587
713
 
588
- **Configuration options:**
589
- - `queue_priority` - Beanstalkd priority (0-4294967295, lower = higher priority)
590
- - `queue_ttr` - Time-to-run in seconds before job times out
714
+ # Delete from Beanstalkd only (keeps database record)
715
+ job.delete!
716
+ ```
591
717
 
592
- Jobs without `Postburner::Beanstalkd` use defaults from `config/postburner.yml`:
593
- - `default_priority: 65536`
594
- - `default_ttr: 300`
718
+ **For tracked ActiveJob classes:**
595
719
 
596
- ### Tracked Jobs
720
+ ```ruby
721
+ # Find the TrackedJob record
722
+ job = Postburner::TrackedJob.find(123)
723
+ job.remove! # Cancel execution
724
+ ```
597
725
 
598
- Tracked jobs store full execution details in PostgreSQL, providing comprehensive audit trails (i.e. logging, timing, errors, retry tracking) for critical operations.
726
+ #### Retrying Buried Jobs
599
727
 
600
- **Note:** `Postburner::Tracked` automatically includes `Postburner::Beanstalkd`, giving you access to `queue_priority`, `queue_ttr`, `bk`, and `extend!`.
728
+ Jobs that fail repeatedly get "buried" in Beanstalkd:
601
729
 
602
730
  ```ruby
603
- class ProcessPayment < ApplicationJob
604
- include Postburner::Tracked # Opt-in to PostgreSQL tracking (includes Beanstalkd)
731
+ job = Postburner::Job.find(123)
732
+ job.kick! # Moves buried job back to ready queue
733
+ ```
605
734
 
606
- queue_as :critical
607
- priority 0 # Highest priority (Beanstalkd included automatically)
608
- ttr 600 # 10 minute timeout
609
- retry_on StandardError, wait: :exponentially_longer, attempts: 5
735
+ #### Inspecting Job State
610
736
 
611
- def perform(payment_id)
612
- payment = Payment.find(payment_id)
737
+ ```ruby
738
+ job = Postburner::TrackedJob.find(123)
613
739
 
614
- log "Starting payment processing for $#{payment.amount}"
740
+ # Timestamps
741
+ job.queued_at # When queued to Beanstalkd
742
+ job.run_at # Scheduled execution time
743
+ job.attempting_at # First attempt started
744
+ job.processing_at # Current/last processing started
745
+ job.processed_at # Completed successfully
746
+ job.removed_at # Soft deleted
615
747
 
616
- begin
617
- payment.charge!
618
- log! "Payment charged successfully"
619
- rescue PaymentError => e
620
- log_exception!(e)
621
- raise
622
- end
748
+ # Statistics
749
+ job.lag # Milliseconds between scheduled and actual execution
750
+ job.duration # Milliseconds to execute
751
+ job.attempt_count # Number of attempts
752
+ job.error_count # Number of errors
623
753
 
624
- log "Payment complete", level: :info
625
- end
626
- end
754
+ # Audit trail
755
+ job.logs # Array of log entries with timestamps
756
+ job.errata # Array of exceptions with backtraces
757
+ job.attempts # Array of attempt timestamps
627
758
  ```
628
759
 
629
- **Tracking provides:**
630
- - Complete execution history (queued_at, processing_at, processed_at)
631
- - Custom logs with `log` and `log!`
632
- - Exception tracking with `log_exception()` and `log_exception!()`
633
- - Timing statistics (lag, duration)
634
- - Retry attempts tracking
635
- - Query with ActiveRecord
636
- - Foreign key relationships
637
- - Beanstalkd operations via `bk` accessor
638
- - TTR extension via `extend!` method
760
+ ## Queue Strategies
639
761
 
640
- **Query tracked jobs:**
762
+ Postburner uses different strategies to control job execution. These affect `Postburner::Job` subclasses (not ActiveJob classes).
763
+
764
+ | Strategy | When to Use | Behavior | Requires Beanstalkd |
765
+ |----------|-------------|----------|---------------------|
766
+ | **NiceQueue** (default) | Production | Async via Beanstalkd, gracefully re-queues premature jobs | Yes |
767
+ | **Queue** | Production (strict) | Async via Beanstalkd, raises error on premature execution | Yes |
768
+ | **TestQueue** | Testing with explicit time control | Inline execution, raises error for scheduled jobs | No |
769
+ | **ImmediateTestQueue** | Testing with automatic time travel | Inline execution, auto time-travels for scheduled jobs | No |
770
+ | **NullQueue** | Batch processing / deferred execution | Jobs created but not queued, manual execution | No |
641
771
 
642
772
  ```ruby
643
- # Find by state
644
- Postburner::TrackedJob.where(processed_at: nil)
645
- Postburner::TrackedJob.where.not(removed_at: nil)
773
+ # Switch strategies
774
+ Postburner.nice_async_strategy! # Default production (NiceQueue)
775
+ Postburner.async_strategy! # Strict production (Queue)
776
+ Postburner.inline_test_strategy! # Testing (TestQueue)
777
+ Postburner.inline_immediate_test_strategy! # Testing with time travel
778
+ Postburner.null_strategy! # Deferred execution
779
+ ```
646
780
 
647
- # Find with errors
648
- Postburner::TrackedJob.where("error_count > 0")
781
+ **Note:** These strategies only affect `Postburner::Job` subclasses. ActiveJob classes execute according to the ActiveJob adapter configuration.
649
782
 
650
- # Statistics
651
- Postburner::TrackedJob.average(:duration) # Average execution time
652
- Postburner::TrackedJob.maximum(:lag) # Worst lag
783
+ ## Testing
653
784
 
654
- # Inspect execution
655
- job = Postburner::TrackedJob.last
656
- job.logs # Array of log entries with timestamps
657
- job.errata # Array of exceptions with backtraces
658
- job.attempts # Array of attempt timestamps
659
- job.duration # Execution time in milliseconds
660
- job.lag # Queue lag in milliseconds
661
- ```
785
+ Postburner provides test-friendly execution modes that don't require Beanstalkd.
662
786
 
663
- ### Direct Postburner::Job Usage
787
+ ### Automatic Test Mode
664
788
 
665
- Direct `Postburner::Job` subclasses are **always tracked**:
789
+ In Rails test environments, Postburner automatically uses inline execution:
666
790
 
667
791
  ```ruby
668
- class RunDonation < Postburner::Job
669
- queue 'critical'
670
- priority 0
671
- max_retries 0
792
+ # test/test_helper.rb - automatic!
793
+ Postburner.testing? # => true in tests
794
+ ```
672
795
 
673
- def perform(args)
674
- donation = Donation.find(args['donation_id'])
675
- donation.process!
676
- log "Processed donation #{donation.id}"
677
- end
678
- end
796
+ ### Testing Default Jobs (ActiveJob)
679
797
 
680
- # Create and enqueue
681
- job = RunDonation.create!(args: { 'donation_id' => 123 })
682
- job.queue!
798
+ Use standard ActiveJob test helpers:
683
799
 
684
- # With delay
685
- job.queue!(delay: 1.hour)
800
+ ```ruby
801
+ class MyTest < ActiveSupport::TestCase
802
+ test "job executes" do
803
+ SendEmail.perform_later(123)
804
+ # Job executes immediately in test mode
805
+ end
686
806
 
687
- # At specific time
688
- job.queue!(at: 2.days.from_now)
807
+ test "job with delay" do
808
+ travel 1.hour do
809
+ SendEmail.set(wait: 1.hour).perform_later(123)
810
+ end
811
+ end
812
+ end
689
813
  ```
690
814
 
691
- #### Instance-Level Queue Configuration
815
+ ### Testing Tracked Jobs
692
816
 
693
- Override queue priority and TTR per job instance for dynamic behavior:
817
+ Tracked jobs create database records you can inspect:
694
818
 
695
819
  ```ruby
696
- # Set priority during creation
697
- job = RunDonation.create!(
698
- args: { 'donation_id' => 123 },
699
- queue_priority: 1500, # Override class-level priority
700
- queue_ttr: 300 # Override class-level TTR
701
- )
702
- job.queue!
820
+ test "tracked job logs execution" do
821
+ ProcessPayment.perform_later(456)
703
822
 
704
- # Set dynamically after creation
705
- job = RunDonation.create!(args: { 'donation_id' => 456 })
706
- job.priority = 2000
707
- job.ttr = 300
708
- job.queue!
823
+ job = Postburner::TrackedJob.last
824
+ assert job.processed_at.present?
825
+ assert_includes job.logs.map { |l| l[1]['message'] }, "Processing payment 456"
826
+ end
827
+ ```
709
828
 
710
- # Use in before_enqueue callback for conditional behavior
711
- class ProcessOrder < Postburner::Job
712
- queue 'orders'
713
- priority 100 # Default priority
714
- ttr 120 # Default TTR
829
+ ### Testing Legacy Postburner::Job
715
830
 
716
- before_enqueue :set_priority_based_on_urgency
831
+ ```ruby
832
+ test "processes immediately" do
833
+ job = ProcessPayment.create!(args: { 'payment_id' => 123 })
834
+ job.queue!
717
835
 
718
- def perform(args)
719
- order = Order.find(args['order_id'])
720
- order.process!
721
- end
836
+ assert job.reload.processed_at
837
+ end
722
838
 
723
- private
839
+ test "scheduled job with time travel" do
840
+ job = ProcessPayment.create!(args: { 'payment_id' => 123 })
724
841
 
725
- def set_priority_based_on_urgency
726
- if args['express_shipping']
727
- self.priority = 0 # High priority for express orders
728
- self.ttr = 600 # Allow 10 minutes to complete
729
- else
730
- self.priority = 1000 # Low priority for standard orders
731
- self.ttr = 120 # Standard 2 minute timeout
732
- end
842
+ travel_to(2.hours.from_now) do
843
+ job.queue!(delay: 2.hours)
844
+ assert job.reload.processed_at
733
845
  end
734
846
  end
735
-
736
- # Express order gets high priority automatically
737
- ProcessOrder.create!(args: { 'order_id' => 789, 'express_shipping' => true }).queue!
738
847
  ```
739
848
 
740
- **Overview:**
741
- - Same as Tracked jobs
742
- - Access the ActiveRecord Postburner::Job directly.
743
-
744
849
  ## Workers
745
850
 
746
851
  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.
@@ -985,6 +1090,26 @@ Parent Process
985
1090
  - Crash isolation (one fork down doesn't affect others)
986
1091
  - Best for production high-volume workloads
987
1092
 
1093
+ ### Shutdown Timeout
1094
+
1095
+ The `shutdown_timeout` controls how long workers wait for in-flight jobs to complete during graceful shutdown. This applies to thread pool termination and child process shutdown.
1096
+
1097
+ ```yaml
1098
+ production:
1099
+ default_ttr: 300 # Default TTR for jobs
1100
+ default_shutdown_timeout: 300 # Defaults to default_ttr if not specified
1101
+
1102
+ workers:
1103
+ imports:
1104
+ shutdown_timeout: 600 # Override: wait up to 10 minutes for long imports
1105
+ queues:
1106
+ - imports
1107
+ ```
1108
+
1109
+ **Best practice:** Set `shutdown_timeout` to match or exceed your longest-running job's TTR to avoid force-killing jobs during deployment.
1110
+
1111
+ **Connection model:** Each worker thread maintains its own Beanstalkd connection for thread safety.
1112
+
988
1113
  ### GC Limits
989
1114
 
990
1115
  Set `default_gc_limit` at environment level or `gc_limit` per worker to automatically restart after processing N jobs.
@@ -1221,157 +1346,378 @@ end
1221
1346
  - `before_processing`, `around_processing`, `after_processing` - During execution
1222
1347
  - `after_processed` - Only after successful completion
1223
1348
 
1224
- ## Queue Strategies
1349
+ ## Instrumentation
1225
1350
 
1226
- Postburner uses different strategies to control job execution. These affect `Postburner::Job` subclasses (not ActiveJob classes).
1351
+ Postburner emits ActiveSupport::Notifications events following ActiveJob conventions. Use these for monitoring, logging, or alerting.
1227
1352
 
1228
- | Strategy | When to Use | Behavior | Requires Beanstalkd |
1229
- |----------|-------------|----------|---------------------|
1230
- | **NiceQueue** (default) | Production | Async via Beanstalkd, gracefully re-queues premature jobs | Yes |
1231
- | **Queue** | Production (strict) | Async via Beanstalkd, raises error on premature execution | Yes |
1232
- | **TestQueue** | Testing with explicit time control | Inline execution, raises error for scheduled jobs | No |
1233
- | **ImmediateTestQueue** | Testing with automatic time travel | Inline execution, auto time-travels for scheduled jobs | No |
1234
- | **NullQueue** | Batch processing / deferred execution | Jobs created but not queued, manual execution | No |
1353
+ ### Available Events
1354
+
1355
+ | Event | When | Payload Keys |
1356
+ |-------|------|--------------|
1357
+ | `perform_start.postburner` | Before job execution | `:payload`, `:beanstalk_job_id` |
1358
+ | `perform.postburner` | Around job execution (includes duration) | `:payload`, `:beanstalk_job_id` |
1359
+ | `retry.postburner` | When default job is retried | `:payload`, `:beanstalk_job_id`, `:error`, `:wait`, `:attempt` |
1360
+ | `discard.postburner` | When default job exhausts retries | `:payload`, `:beanstalk_job_id`, `:error` |
1361
+
1362
+ ### Subscribing to Events
1363
+
1364
+ ```ruby
1365
+ # config/initializers/postburner_instrumentation.rb
1366
+
1367
+ # Log all job executions
1368
+ ActiveSupport::Notifications.subscribe('perform.postburner') do |name, start, finish, id, payload|
1369
+ duration = (finish - start) * 1000
1370
+ Rails.logger.info "[Postburner] #{payload[:payload]['job_class']} completed in #{duration.round(2)}ms"
1371
+ end
1372
+
1373
+ # Alert on discarded jobs
1374
+ ActiveSupport::Notifications.subscribe('discard.postburner') do |*args|
1375
+ payload = args.last
1376
+ Alerting.notify(
1377
+ "Job discarded after max retries",
1378
+ job_class: payload[:payload]['job_class'],
1379
+ error: payload[:error].message
1380
+ )
1381
+ end
1382
+
1383
+ # Track retry metrics
1384
+ ActiveSupport::Notifications.subscribe('retry.postburner') do |*args|
1385
+ payload = args.last
1386
+ StatsD.increment('postburner.retry', tags: [
1387
+ "job:#{payload[:payload]['job_class']}",
1388
+ "attempt:#{payload[:attempt]}"
1389
+ ])
1390
+ end
1391
+ ```
1392
+
1393
+ **Note:** These events are emitted by the worker for jobs processed through Beanstalkd. They complement (don't replace) ActiveJob's built-in instrumentation events.
1394
+
1395
+ ## Why Beanstalkd?
1396
+
1397
+ 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.
1398
+
1399
+ 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. Here is a help
1400
+
1401
+ Here is a picture of the typical job lifecycle:
1402
+
1403
+ ```
1404
+ put reserve delete
1405
+ -----> [READY] ---------> [RESERVED] --------> *poof*`
1406
+ ```
1407
+
1408
+ Here is a picture with more possibilities:
1409
+
1410
+ ```
1411
+ put with delay release with delay
1412
+ ----------------> [DELAYED] <------------.
1413
+ | |
1414
+ | (time passes) |
1415
+ | |
1416
+ put v reserve | delete
1417
+ -----------------> [READY] ---------> [RESERVED] --------> *poof*
1418
+ ^ ^ | |
1419
+ | \ release | |
1420
+ | `-------------' |
1421
+ | |
1422
+ | kick |
1423
+ | |
1424
+ | bury |
1425
+ [BURIED] <---------------'
1426
+ |
1427
+ | delete
1428
+ `--------> *poof*
1429
+ ```
1430
+
1431
+ ### Binlogs
1432
+
1433
+ 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.
1434
+
1435
+ ```bash
1436
+ # Create binlog directory
1437
+ sudo mkdir -p /var/lib/beanstalkd
1438
+ sudo chown beanstalkd:beanstalkd /var/lib/beanstalkd # If running as service
1439
+
1440
+ # Start beanstalkd with binlog persistence
1441
+ beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
1442
+ ```
1443
+
1444
+ **Other options:**
1445
+
1446
+ ```bash
1447
+ # Basic persistence
1448
+ beanstalkd -b /var/lib/beanstalkd
1449
+
1450
+ # With custom binlog file size (default varies)
1451
+ beanstalkd -b /var/lib/beanstalkd -s 10485760 # 10MB per binlog file
1452
+
1453
+ # Reduce fsync frequency for better performance (trades safety for speed)
1454
+ beanstalkd -b /var/lib/beanstalkd -f 200 # fsync at most every 200ms (default: 50ms)
1455
+
1456
+ # Never fsync (maximum performance, higher risk of data loss on power failure)
1457
+ beanstalkd -b /var/lib/beanstalkd -F
1458
+ ```
1459
+
1460
+ **Beanstalkd switches:**
1461
+ - `-b <dir>` - Enable binlog persistence in specified directory
1462
+ - `-s <bytes>` - Maximum size of each binlog file (requires `-b`)
1463
+ - `-f <ms>` - Call fsync at most once every N milliseconds (requires `-b`, default: 50ms)
1464
+ - `-F` - Never call fsync (requires `-b`, maximum performance but higher data loss risk)
1465
+
1466
+ **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.
1467
+
1468
+ ### Priority
1469
+
1470
+ Priority controls the order in which jobs are processed from the queue. Beanstalkd always processes the highest priority (lowest number) jobs first.
1471
+
1472
+ **Priority Range:** 0 to 4,294,967,295 (unsigned 32-bit integer)
1473
+ - `0` = Highest priority (processed first)
1474
+ - `4,294,967,295` = Lowest priority (processed last)
1475
+ - Default: `65536` (configurable in `config/postburner.yml`)
1476
+
1477
+ 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.
1478
+
1479
+ **ActiveJob with Postburner::Beanstalkd:**
1480
+
1481
+ ```ruby
1482
+ class ProcessPayment < ApplicationJob
1483
+ include Postburner::Beanstalkd
1484
+
1485
+ queue_as :critical
1486
+ priority 0 # Highest priority - processed immediately
1487
+ end
1488
+
1489
+ class SendEmail < ApplicationJob
1490
+ include Postburner::Beanstalkd
1491
+
1492
+ queue_as :mailers
1493
+ priority 1000 # Lower priority - processed after critical jobs
1494
+ end
1495
+ ```
1496
+
1497
+ **Postburner::Job:**
1498
+
1499
+ ```ruby
1500
+ class CriticalJob < Postburner::Job
1501
+ queue 'critical'
1502
+ priority 0 # Highest priority
1503
+
1504
+ def perform(args)
1505
+ # Critical business logic
1506
+ end
1507
+ end
1508
+
1509
+ class BackgroundTask < Postburner::Job
1510
+ queue 'default'
1511
+ priority 5000 # Lower priority
1512
+
1513
+ def perform(args)
1514
+ # Non-urgent background work
1515
+ end
1516
+ end
1517
+ ```
1518
+
1519
+ **Recommended Priority Ranges:**
1520
+
1521
+ | Priority Range | Use Case | Examples |
1522
+ |---------------|----------|----------|
1523
+ | 0-256 | Critical business operations | Payments, transactions, real-time notifications |
1524
+ | 256-4096 | High priority tasks | Password resets, order confirmations |
1525
+ | 4096-65536 | Standard background jobs | Email sending, data exports (65536 is default) |
1526
+ | 65536-262144 | Low priority maintenance | Cache warming, log cleanup |
1527
+ | 262144+ | Deferred/bulk operations | Analytics, batch reports |
1528
+
1529
+ **Configuration:**
1530
+
1531
+ Set default priority in `config/postburner.yml`:
1532
+
1533
+ ```yaml
1534
+ production:
1535
+ default_priority: 65536 # Default for jobs without explicit priority
1536
+ default_ttr: 300
1537
+ ```
1538
+
1539
+ **Dynamic Priority (Postburner::Job only):**
1540
+
1541
+ ```ruby
1542
+ class ProcessOrder < Postburner::Job
1543
+ queue 'orders'
1544
+ priority 100 # Default
1545
+
1546
+ before_enqueue :adjust_priority
1547
+
1548
+ def perform(args)
1549
+ order = Order.find(args['order_id'])
1550
+ order.process!
1551
+ end
1552
+
1553
+ private
1554
+
1555
+ def adjust_priority
1556
+ if args['urgent']
1557
+ self.priority = 0 # Override to highest priority
1558
+ end
1559
+ end
1560
+ end
1561
+
1562
+ # Usage
1563
+ ProcessOrder.create!(args: { 'order_id' => 123, 'urgent' => true }).queue!
1564
+ ```
1565
+
1566
+ ### Time to Run (TTR)
1567
+
1568
+ 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.
1569
+
1570
+ 1. Worker reserves a job from Beanstalkd
1571
+ 2. TTR countdown begins immediately upon reservation
1572
+ 3. If job completes within TTR, worker deletes/buries the job (success)
1573
+ 4. If TTR expires before completion, Beanstalkd automatically releases the job back to ready queue
1574
+ 5. Another worker can then reserve and process the same job
1575
+
1576
+ This mechanism protects against workers crashing or hanging—jobs won't be stuck indefinitely.
1235
1577
 
1236
- ```ruby
1237
- # Switch strategies
1238
- Postburner.nice_async_strategy! # Default production (NiceQueue)
1239
- Postburner.async_strategy! # Strict production (Queue)
1240
- Postburner.inline_test_strategy! # Testing (TestQueue)
1241
- Postburner.inline_immediate_test_strategy! # Testing with time travel
1242
- Postburner.null_strategy! # Deferred execution
1243
- ```
1578
+ **TTR Range:** 1 to 4,294,967,295 seconds (unsigned 32-bit integer)
1579
+ - Minimum: `1` second (Beanstalkd silently increases 0 to 1)
1580
+ - Default: `300` seconds (5 minutes, configurable in `config/postburner.yml`)
1244
1581
 
1245
- **Note:** These strategies only affect `Postburner::Job` subclasses. ActiveJob classes execute according to the ActiveJob adapter configuration.
1582
+ **ActiveJob with Postburner::Beanstalkd:**
1246
1583
 
1247
- ## Testing
1584
+ ```ruby
1585
+ class ProcessPayment < ApplicationJob
1586
+ include Postburner::Beanstalkd # Or Postburner::Tracked
1248
1587
 
1249
- Postburner provides test-friendly execution modes that don't require Beanstalkd.
1588
+ queue_as :critical
1589
+ priority 0
1590
+ ttr 300 # 5 minutes to complete
1591
+ end
1250
1592
 
1251
- ### Automatic Test Mode
1593
+ class QuickEmail < ApplicationJob
1594
+ include Postburner::Beanstalkd # Or Postburner::Tracked
1252
1595
 
1253
- In Rails test environments, Postburner automatically uses inline execution:
1596
+ queue_as :mailers
1597
+ ttr 60 # 1 minute for fast email jobs
1598
+ end
1254
1599
 
1255
- ```ruby
1256
- # test/test_helper.rb - automatic!
1257
- Postburner.testing? # => true in tests
1258
- ```
1600
+ class LongRunningReport < ApplicationJob
1601
+ include Postburner::Beanstalkd # Or Postburner::Tracked
1259
1602
 
1260
- ### Testing Default Jobs (ActiveJob)
1603
+ queue_as :reports
1604
+ ttr 3600 # 1 hour for complex reports
1605
+ end
1606
+ ```
1261
1607
 
1262
- Use standard ActiveJob test helpers:
1608
+ **Postburner::Job:**
1263
1609
 
1264
1610
  ```ruby
1265
- class MyTest < ActiveSupport::TestCase
1266
- test "job executes" do
1267
- SendEmail.perform_later(123)
1268
- # Job executes immediately in test mode
1269
- end
1611
+ class DataImport < Postburner::Job
1612
+ queue 'imports'
1613
+ ttr 1800 # 30 minutes (equivalent to queue_ttr)
1270
1614
 
1271
- test "job with delay" do
1272
- travel 1.hour do
1273
- SendEmail.set(wait: 1.hour).perform_later(123)
1274
- end
1615
+ def perform(args)
1616
+ # Long-running import logic
1275
1617
  end
1276
1618
  end
1277
1619
  ```
1278
1620
 
1279
- ### Testing Tracked Jobs
1621
+ **Extending TTR with `extend!` (Touch Command):**
1280
1622
 
1281
- Tracked jobs create database records you can inspect:
1623
+ 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.
1624
+
1625
+ **Available for:**
1626
+ - `Postburner::Job` subclasses (always available via `bk.extend!`)
1627
+ - `Postburner::Tracked` ActiveJob classes (includes `extend!` method)
1282
1628
 
1283
1629
  ```ruby
1284
- test "tracked job logs execution" do
1285
- ProcessPayment.perform_later(456)
1630
+ class ProcessImport < ApplicationJob
1631
+ include Postburner::Tracked # Includes Beanstalkd and enables extend!
1286
1632
 
1287
- job = Postburner::TrackedJob.last
1288
- assert job.processed_at.present?
1289
- assert_includes job.logs.map { |l| l[1]['message'] }, "Processing payment 456"
1633
+ queue_as :imports
1634
+ ttr 300 # 5 minutes initial TTR
1635
+
1636
+ def perform(file_id)
1637
+ file = ImportFile.find(file_id)
1638
+
1639
+ file.each_batch(size: 100) do |batch|
1640
+ batch.each do |row|
1641
+ process_row(row)
1642
+ end
1643
+
1644
+ extend! # Reset TTR to 300 seconds from now
1645
+ log "Processed batch, extended TTR"
1646
+ end
1647
+ end
1290
1648
  end
1291
1649
  ```
1292
1650
 
1293
- ### Testing Legacy Postburner::Job
1651
+ **For Postburner::Job (via `bk` accessor):**
1294
1652
 
1295
1653
  ```ruby
1296
- test "processes immediately" do
1297
- job = RunDonation.create!(args: { 'donation_id' => 123 })
1298
- job.queue!
1654
+ class LargeDataProcessor < Postburner::Job
1655
+ queue 'processing'
1656
+ ttr 600 # 10 minutes
1299
1657
 
1300
- assert job.reload.processed_at
1301
- end
1658
+ def perform(args)
1659
+ dataset = Dataset.find(args['dataset_id'])
1302
1660
 
1303
- test "scheduled job with time travel" do
1304
- job = RunDonation.create!(args: { 'donation_id' => 123 })
1661
+ dataset.each_chunk do |chunk|
1662
+ process_chunk(chunk)
1305
1663
 
1306
- travel_to(2.hours.from_now) do
1307
- job.queue!(delay: 2.hours)
1308
- assert job.reload.processed_at
1664
+ bk.extend! # Reset TTR via beanstalkd job accessor
1665
+ log "Chunk processed, TTR extended"
1666
+ end
1309
1667
  end
1310
1668
  end
1311
1669
  ```
1312
1670
 
1313
- ## Job Management
1314
-
1315
- ### Canceling Jobs
1316
-
1317
- **For Postburner::Job subclasses:**
1671
+ **`extend!` and Beanstalkd `touch`**
1318
1672
 
1319
- ```ruby
1320
- job = RunDonation.create!(args: { 'donation_id' => 123 })
1321
- job.queue!(delay: 1.hour)
1673
+ - Calls Beanstalkd's `touch` command on the reserved job
1674
+ - Resets the TTR countdown to the original TTR value from the current moment
1675
+ - Example: Job has `ttr 300`. After 200 seconds, calling `extend!` gives you another 300 seconds (not just 100)
1676
+ - Can be called multiple times during job execution
1677
+ - Useful for iterative processing where each iteration is quick but total time is unpredictable
1322
1678
 
1323
- # Soft delete (recommended) - removes from queue, sets removed_at
1324
- job.remove!
1325
- job.removed_at # => 2025-11-19 12:34:56 UTC
1326
- job.persisted? # => true (still in database for audit)
1679
+ **Configuration:**
1327
1680
 
1328
- # Hard delete - removes from both queue and database
1329
- job.destroy!
1681
+ Set default TTR in `config/postburner.yml`:
1330
1682
 
1331
- # Delete from Beanstalkd only (keeps database record)
1332
- job.delete!
1683
+ ```yaml
1684
+ production:
1685
+ default_priority: 65536
1686
+ default_ttr: 300 # 5 minutes default for all jobs
1333
1687
  ```
1334
1688
 
1335
- **For tracked ActiveJob classes:**
1336
-
1337
- ```ruby
1338
- # Find the TrackedJob record
1339
- job = Postburner::TrackedJob.find(123)
1340
- job.remove! # Cancel execution
1341
- ```
1689
+ **Recommended TTR Values:**
1342
1690
 
1343
- ### Retrying Buried Jobs
1691
+ | Job Type | TTR | Reasoning |
1692
+ |----------|-----|-----------|
1693
+ | Email sending | 60-120s | Network operations, should be fast |
1694
+ | API calls | 30-60s | External services, fast or timeout |
1695
+ | File processing | 600-1800s | Variable based on file size, use `extend!` |
1696
+ | Reports | 1800-3600s | Complex queries and aggregations |
1697
+ | Imports/Exports | 3600s+ | Large datasets, use `extend!` in loops |
1344
1698
 
1345
- Jobs that fail repeatedly get "buried" in Beanstalkd:
1699
+ **Best Practices:**
1346
1700
 
1347
- ```ruby
1348
- job = Postburner::Job.find(123)
1349
- job.kick! # Moves buried job back to ready queue
1350
- ```
1701
+ 1. **Set realistic TTR values** based on expected job duration plus buffer
1702
+ 2. **Use `extend!` for iterative work** where total time is unpredictable but each iteration is bounded
1703
+ 3. **Don't set TTR too high** - it delays recovery from crashed workers
1704
+ 4. **Monitor TTR timeouts** - frequent timeouts indicate jobs need more time or optimization
1705
+ 5. **For Default jobs** (ActiveJob without `Postburner::Beanstalkd`), include the module to set custom TTR
1351
1706
 
1352
- ### Inspecting Job State
1707
+ **What happens on TTR timeout:**
1353
1708
 
1354
1709
  ```ruby
1355
- job = Postburner::TrackedJob.find(123)
1356
-
1357
- # Timestamps
1358
- job.queued_at # When queued to Beanstalkd
1359
- job.run_at # Scheduled execution time
1360
- job.attempting_at # First attempt started
1361
- job.processing_at # Current/last processing started
1362
- job.processed_at # Completed successfully
1363
- job.removed_at # Soft deleted
1710
+ # Job starts processing
1711
+ job = ProcessImport.perform_later(file_id)
1364
1712
 
1365
- # Statistics
1366
- job.lag # Milliseconds between scheduled and actual execution
1367
- job.duration # Milliseconds to execute
1368
- job.attempt_count # Number of attempts
1369
- job.error_count # Number of errors
1713
+ # Worker crashes or hangs after 200 seconds
1714
+ # At 300 seconds (TTR), Beanstalkd automatically:
1715
+ # 1. Releases job back to ready queue
1716
+ # 2. Increments the job's reserve count
1717
+ # 3. Makes job available for another worker
1370
1718
 
1371
- # Audit trail
1372
- job.logs # Array of log entries with timestamps
1373
- job.errata # Array of exceptions with backtraces
1374
- job.attempts # Array of attempt timestamps
1719
+ # Another worker picks up the job and retries
1720
+ # (ActiveJob retry_on/discard_on handlers still apply)
1375
1721
  ```
1376
1722
 
1377
1723
  ## Beanstalkd Integration
@@ -1463,12 +1809,32 @@ result = Postburner.clear_jobs!(['postburner.production.default'])
1463
1809
 
1464
1810
  # Pretty-print JSON output
1465
1811
  Postburner.clear_jobs!(['postburner.production.default'], silent: false)
1812
+ # OR
1813
+ Postburner.clear_jobs!(['postburner.production.default'])
1466
1814
  # Outputs formatted JSON to stdout
1467
1815
 
1468
1816
  # Silent mode (no output, just return data)
1469
1817
  result = Postburner.clear_jobs!(['postburner.production.default'], silent: true)
1470
1818
  ```
1471
1819
 
1820
+ **Shortcut using watched_tube_names or scheduler_tube_name:**
1821
+
1822
+ Clear all configured tubes at once:
1823
+
1824
+ ```ruby
1825
+ Postburner.clear_jobs!(Postburner.watched_tube_names)
1826
+ # => { tubes: [...], totals: {...}, cleared: true }
1827
+ ```
1828
+
1829
+ Or clear scheduler tube, specifically:
1830
+
1831
+ ```ruby
1832
+ Postburner.clear_jobs!(Postburner.scheduler_tube_name)
1833
+ # => { tubes: [...], totals: {...}, cleared: true }
1834
+ ```
1835
+
1836
+ These shortcuts are useful for testing and development, but not recommended for production.
1837
+
1472
1838
  **Safety validation:**
1473
1839
 
1474
1840
  Only tubes defined in your loaded configuration can be cleared. This prevents mistakes in multi-tenant Beanstalkd environments:
@@ -1481,21 +1847,6 @@ Postburner.clear_jobs!(['postburner.production.other-app'])
1481
1847
  # Configured tubes: postburner.production.default, postburner.production.critical
1482
1848
  ```
1483
1849
 
1484
- **Shortcut using watched_tube_names:**
1485
-
1486
- Clear all configured tubes at once:
1487
-
1488
- ```ruby
1489
- # Get all tubes from current configuration
1490
- watched_tubes = Postburner.watched_tube_names
1491
- # => ["postburner.production.default", "postburner.production.critical", "postburner.production.mailers"]
1492
-
1493
- # Clear all configured tubes
1494
- Postburner.clear_jobs!(watched_tubes, silent: true)
1495
- # or
1496
- Postburner.clear_jobs!(Postburner.watched_tube_names, silent: true)
1497
- ```
1498
-
1499
1850
  **Low-level Connection API:**
1500
1851
 
1501
1852
  For programmatic use without output formatting, use `Connection#clear_tubes!`:
@@ -1511,7 +1862,7 @@ Postburner.connected do |conn|
1511
1862
  end
1512
1863
  ```
1513
1864
 
1514
- ## Web UI
1865
+ ## Web UI -- Dated
1515
1866
 
1516
1867
  Mount the inspection interface:
1517
1868
 
@@ -1532,8 +1883,63 @@ authenticate :user, ->(user) { user.admin? } do
1532
1883
  end
1533
1884
  ```
1534
1885
 
1886
+ ## Installation
1887
+
1888
+ ### 1. Install Beanstalkd
1889
+
1890
+ First [install beanstalkd](https://beanstalkd.github.io/download.html):
1891
+
1892
+ ```bash
1893
+ sudo apt-get install beanstalkd
1894
+ brew install beanstalkd # for macOS/Linux
1895
+
1896
+ # Start beanstalkd (in-memory only)
1897
+ beanstalkd -l 127.0.0.1 -p 11300
1898
+
1899
+ # OR with persistence (recommended for production)
1900
+ mkdir -p /var/lib/beanstalkd
1901
+ beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
1902
+ ```
1903
+
1904
+ ### 2. Add Gem
1905
+
1906
+ ```ruby
1907
+ # Gemfile
1908
+ gem 'postburner', '~> 1.0.0.pre.1'
1909
+ ```
1910
+
1911
+ ```bash
1912
+ bundle install
1913
+ ```
1914
+
1915
+ ### 3. Install Migration
1916
+
1917
+ ```bash
1918
+ rails generate postburner:install
1919
+ rails db:migrate
1920
+ ```
1921
+
1922
+ This creates the `postburner_jobs` table for tracked jobs.
1923
+
1924
+ ### 4. Configure ActiveJob
1925
+
1926
+ ```ruby
1927
+ # config/application.rb
1928
+ config.active_job.queue_adapter = :postburner
1929
+ ```
1930
+
1931
+ ### 5. Create Worker Configuration
1932
+
1933
+ ```bash
1934
+ cp config/postburner.yml.example config/postburner.yml
1935
+ ```
1936
+
1937
+ Edit `config/postburner.yml` for your environment (see [Configuration](#configuration)).
1938
+
1535
1939
  ## Deployment
1536
1940
 
1941
+ Some recipies that might be useful or instructive:
1942
+
1537
1943
  ### Docker
1538
1944
 
1539
1945
  ```dockerfile