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.
- checksums.yaml +4 -4
- data/README.md +961 -555
- data/app/concerns/postburner/commands.rb +1 -1
- data/app/concerns/postburner/execution.rb +11 -11
- data/app/concerns/postburner/insertion.rb +1 -1
- data/app/concerns/postburner/logging.rb +2 -2
- data/app/concerns/postburner/statistics.rb +1 -1
- data/app/models/postburner/job.rb +27 -4
- data/app/models/postburner/mailer.rb +1 -1
- data/app/models/postburner/schedule.rb +703 -0
- data/app/models/postburner/schedule_execution.rb +353 -0
- data/app/views/postburner/jobs/show.html.haml +3 -3
- data/lib/generators/postburner/install/install_generator.rb +1 -0
- data/lib/generators/postburner/install/templates/config/postburner.yml +15 -6
- data/lib/generators/postburner/install/templates/migrations/create_postburner_schedules.rb.erb +71 -0
- data/lib/postburner/active_job/adapter.rb +3 -3
- data/lib/postburner/active_job/payload.rb +5 -0
- data/lib/postburner/advisory_lock.rb +123 -0
- data/lib/postburner/configuration.rb +43 -7
- data/lib/postburner/connection.rb +7 -6
- data/lib/postburner/runner.rb +26 -3
- data/lib/postburner/scheduler.rb +427 -0
- data/lib/postburner/strategies/immediate_test_queue.rb +24 -7
- data/lib/postburner/strategies/nice_queue.rb +1 -1
- data/lib/postburner/strategies/null_queue.rb +2 -2
- data/lib/postburner/strategies/test_queue.rb +2 -2
- data/lib/postburner/time_helpers.rb +4 -2
- data/lib/postburner/tube.rb +9 -1
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner/worker.rb +684 -0
- data/lib/postburner.rb +32 -13
- metadata +7 -3
- data/lib/postburner/workers/base.rb +0 -205
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
##
|
|
190
|
+
## Usage
|
|
142
191
|
|
|
143
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
+
### Default Jobs
|
|
160
197
|
|
|
161
|
-
|
|
198
|
+
Default jobs execute quickly via Beanstalkd without PostgreSQL overhead. Perfect for emails, cache warming, notifications, etc.
|
|
162
199
|
|
|
163
|
-
|
|
200
|
+
```ruby
|
|
201
|
+
class SendWelcomeEmail < ApplicationJob
|
|
202
|
+
queue_as :mailers
|
|
164
203
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
204
|
+
def perform(user_id)
|
|
205
|
+
UserMailer.welcome(user_id).deliver_now
|
|
206
|
+
end
|
|
207
|
+
end
|
|
169
208
|
|
|
170
|
-
|
|
209
|
+
# Enqueue immediately
|
|
210
|
+
SendWelcomeEmail.perform_later(123)
|
|
171
211
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
+
#### Configuring Beanstalkd Priority and TTR
|
|
196
227
|
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
248
|
+
Jobs without `Postburner::Beanstalkd` use defaults from `config/postburner.yml`:
|
|
249
|
+
- `default_priority: 65536`
|
|
250
|
+
- `default_ttr: 300`
|
|
211
251
|
|
|
212
|
-
|
|
213
|
-
beanstalkd -b /var/lib/beanstalkd -s 10485760 # 10MB per binlog file
|
|
252
|
+
### Tracked Jobs
|
|
214
253
|
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
def perform(payment_id)
|
|
268
|
+
payment = Payment.find(payment_id)
|
|
231
269
|
|
|
232
|
-
|
|
270
|
+
log "Starting payment processing for $#{payment.amount}"
|
|
233
271
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
280
|
+
log "Payment complete", level: :info
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
240
284
|
|
|
241
|
-
**
|
|
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
|
-
|
|
245
|
-
|
|
299
|
+
# Find by state
|
|
300
|
+
Postburner::TrackedJob.where(processed_at: nil)
|
|
301
|
+
Postburner::TrackedJob.where.not(removed_at: nil)
|
|
246
302
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
end
|
|
303
|
+
# Find with errors
|
|
304
|
+
Postburner::TrackedJob.where("error_count > 0")
|
|
250
305
|
|
|
251
|
-
|
|
252
|
-
|
|
306
|
+
# Statistics
|
|
307
|
+
Postburner::TrackedJob.average(:duration) # Average execution time
|
|
308
|
+
Postburner::TrackedJob.maximum(:lag) # Worst lag
|
|
253
309
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
319
|
+
### Direct Postburner::Job Usage
|
|
320
|
+
|
|
321
|
+
Direct `Postburner::Job` subclasses are **always tracked**:
|
|
260
322
|
|
|
261
323
|
```ruby
|
|
262
|
-
class
|
|
324
|
+
class ProcessPayment < Postburner::Job
|
|
263
325
|
queue 'critical'
|
|
264
|
-
priority 0
|
|
326
|
+
priority 0
|
|
327
|
+
max_retries 0
|
|
265
328
|
|
|
266
329
|
def perform(args)
|
|
267
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
336
|
+
# Create and enqueue
|
|
337
|
+
job = ProcessPayment.create!(args: { 'payment_id' => 123 })
|
|
338
|
+
job.queue!
|
|
274
339
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
347
|
+
#### Instance-Level Queue Configuration
|
|
282
348
|
|
|
283
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
+
# Use in before_enqueue callback for conditional behavior
|
|
304
367
|
class ProcessOrder < Postburner::Job
|
|
305
368
|
queue 'orders'
|
|
306
|
-
priority 100
|
|
369
|
+
priority 100 # Default priority
|
|
370
|
+
ttr 120 # Default TTR
|
|
307
371
|
|
|
308
|
-
before_enqueue :
|
|
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
|
|
318
|
-
if args['
|
|
319
|
-
self.priority = 0
|
|
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
|
-
#
|
|
325
|
-
ProcessOrder.create!(args: { 'order_id' =>
|
|
392
|
+
# Express order gets high priority automatically
|
|
393
|
+
ProcessOrder.create!(args: { 'order_id' => 789, 'express_shipping' => true }).queue!
|
|
326
394
|
```
|
|
327
395
|
|
|
328
|
-
|
|
396
|
+
**Overview:**
|
|
397
|
+
- Same as Tracked jobs
|
|
398
|
+
- Access the ActiveRecord Postburner::Job directly.
|
|
329
399
|
|
|
330
|
-
|
|
400
|
+
### Scheduler
|
|
331
401
|
|
|
332
|
-
|
|
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
|
-
|
|
404
|
+
The scheduler uses **immediate enqueue** combined with a **watchdog safety net**:
|
|
339
405
|
|
|
340
|
-
|
|
341
|
-
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
include Postburner::Beanstalkd
|
|
435
|
+
The scheduler automatically creates executions and enqueues jobs at the scheduled times.
|
|
364
436
|
|
|
365
|
-
|
|
366
|
-
ttr 3600 # 1 hour for complex reports
|
|
367
|
-
end
|
|
368
|
-
```
|
|
437
|
+
### Configuration
|
|
369
438
|
|
|
370
|
-
|
|
439
|
+
Add scheduler settings to `config/postburner.yml`:
|
|
371
440
|
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
447
|
+
workers:
|
|
448
|
+
default:
|
|
449
|
+
queues:
|
|
450
|
+
- default
|
|
451
|
+
- mailers
|
|
381
452
|
```
|
|
382
453
|
|
|
383
|
-
**
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
batch.each do |row|
|
|
403
|
-
process_row(row)
|
|
404
|
-
end
|
|
467
|
+
#### Creating Schedules
|
|
405
468
|
|
|
406
|
-
|
|
407
|
-
log "Processed batch, extended TTR"
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
end
|
|
411
|
-
```
|
|
469
|
+
##### Anchor-Based Scheduling (Recommended)
|
|
412
470
|
|
|
413
|
-
|
|
471
|
+
Use an anchor time plus interval for predictable, drift-free scheduling:
|
|
414
472
|
|
|
415
473
|
```ruby
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
515
|
+
**Available interval units:** `seconds`, `minutes`, `hours`, `days`, `weeks`, `months`, `years`
|
|
434
516
|
|
|
435
|
-
-
|
|
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
|
-
|
|
519
|
+
For complex schedules, use cron expressions (requires the `fugit` gem):
|
|
442
520
|
|
|
443
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
539
|
+
#### Schedule Options
|
|
452
540
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
564
|
+
#### Catch-Up Policy
|
|
462
565
|
|
|
463
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
#
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
#
|
|
482
|
-
|
|
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
|
-
|
|
595
|
+
#### Managing Schedules
|
|
486
596
|
|
|
487
|
-
|
|
597
|
+
```ruby
|
|
598
|
+
# Find schedules
|
|
599
|
+
schedule = Postburner::Schedule.find_by(name: 'daily_cleanup')
|
|
600
|
+
Postburner::Schedule.enabled # All enabled schedules
|
|
488
601
|
|
|
489
|
-
|
|
602
|
+
# Disable temporarily
|
|
603
|
+
schedule.update!(enabled: false)
|
|
490
604
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
brew install beanstalkd # for macOS/Linux
|
|
605
|
+
# Change catch-up policy
|
|
606
|
+
schedule.update!(catch_up: true)
|
|
494
607
|
|
|
495
|
-
#
|
|
496
|
-
|
|
608
|
+
# Check next run time
|
|
609
|
+
schedule.next_run_at # => 2025-01-02 09:00:00 -0500
|
|
497
610
|
|
|
498
|
-
#
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
620
|
+
#### Starting Schedules
|
|
504
621
|
|
|
505
|
-
|
|
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
|
-
|
|
511
|
-
bundle install
|
|
512
|
-
```
|
|
624
|
+
**Option 1: Explicit start (immediate enqueue)**
|
|
513
625
|
|
|
514
|
-
|
|
626
|
+
Call `start!` to create and enqueue the first execution immediately to Beanstalkd:
|
|
515
627
|
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
640
|
+
The job is immediately in Beanstalkd's delayed queue and will run at the scheduled time.
|
|
522
641
|
|
|
523
|
-
|
|
642
|
+
**Option 2: Auto-bootstrap (eventual pickup)**
|
|
524
643
|
|
|
525
|
-
|
|
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
|
-
|
|
646
|
+
```ruby
|
|
647
|
+
schedule = Postburner::Schedule.create!(...)
|
|
648
|
+
schedule.started? # => false
|
|
531
649
|
|
|
532
|
-
|
|
533
|
-
|
|
650
|
+
# Later, scheduler runs and auto-bootstraps...
|
|
651
|
+
schedule.reload.started? # => true
|
|
534
652
|
```
|
|
535
653
|
|
|
536
|
-
|
|
654
|
+
Use `start!` when you need predictable timing. Skip it when eventual consistency is acceptable.
|
|
537
655
|
|
|
538
|
-
|
|
656
|
+
#### Schedule Executions
|
|
539
657
|
|
|
540
|
-
|
|
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
|
-
|
|
546
|
-
queue_as :mailers
|
|
661
|
+
execution = Postburner::ScheduleExecution.find(123)
|
|
547
662
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
554
|
-
|
|
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
|
-
|
|
557
|
-
SendWelcomeEmail.set(wait: 1.hour).perform_later(123)
|
|
678
|
+
#### Timezone Handling
|
|
558
679
|
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
+
### Job Management
|
|
571
697
|
|
|
572
|
-
|
|
698
|
+
#### Canceling Jobs
|
|
699
|
+
|
|
700
|
+
**For Postburner::Job subclasses:**
|
|
573
701
|
|
|
574
702
|
```ruby
|
|
575
|
-
|
|
576
|
-
|
|
703
|
+
job = ProcessPayment.create!(args: { 'payment_id' => 123 })
|
|
704
|
+
job.queue!(delay: 1.hour)
|
|
577
705
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
end
|
|
585
|
-
end
|
|
586
|
-
```
|
|
711
|
+
# Hard delete - removes from both queue and database
|
|
712
|
+
job.destroy!
|
|
587
713
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
714
|
+
# Delete from Beanstalkd only (keeps database record)
|
|
715
|
+
job.delete!
|
|
716
|
+
```
|
|
591
717
|
|
|
592
|
-
|
|
593
|
-
- `default_priority: 65536`
|
|
594
|
-
- `default_ttr: 300`
|
|
718
|
+
**For tracked ActiveJob classes:**
|
|
595
719
|
|
|
596
|
-
|
|
720
|
+
```ruby
|
|
721
|
+
# Find the TrackedJob record
|
|
722
|
+
job = Postburner::TrackedJob.find(123)
|
|
723
|
+
job.remove! # Cancel execution
|
|
724
|
+
```
|
|
597
725
|
|
|
598
|
-
|
|
726
|
+
#### Retrying Buried Jobs
|
|
599
727
|
|
|
600
|
-
|
|
728
|
+
Jobs that fail repeatedly get "buried" in Beanstalkd:
|
|
601
729
|
|
|
602
730
|
```ruby
|
|
603
|
-
|
|
604
|
-
|
|
731
|
+
job = Postburner::Job.find(123)
|
|
732
|
+
job.kick! # Moves buried job back to ready queue
|
|
733
|
+
```
|
|
605
734
|
|
|
606
|
-
|
|
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
|
-
|
|
612
|
-
|
|
737
|
+
```ruby
|
|
738
|
+
job = Postburner::TrackedJob.find(123)
|
|
613
739
|
|
|
614
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
644
|
-
Postburner
|
|
645
|
-
Postburner
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
Postburner::TrackedJob.average(:duration) # Average execution time
|
|
652
|
-
Postburner::TrackedJob.maximum(:lag) # Worst lag
|
|
783
|
+
## Testing
|
|
653
784
|
|
|
654
|
-
|
|
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
|
-
###
|
|
787
|
+
### Automatic Test Mode
|
|
664
788
|
|
|
665
|
-
|
|
789
|
+
In Rails test environments, Postburner automatically uses inline execution:
|
|
666
790
|
|
|
667
791
|
```ruby
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
max_retries 0
|
|
792
|
+
# test/test_helper.rb - automatic!
|
|
793
|
+
Postburner.testing? # => true in tests
|
|
794
|
+
```
|
|
672
795
|
|
|
673
|
-
|
|
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
|
-
|
|
681
|
-
job = RunDonation.create!(args: { 'donation_id' => 123 })
|
|
682
|
-
job.queue!
|
|
798
|
+
Use standard ActiveJob test helpers:
|
|
683
799
|
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
688
|
-
|
|
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
|
-
|
|
815
|
+
### Testing Tracked Jobs
|
|
692
816
|
|
|
693
|
-
|
|
817
|
+
Tracked jobs create database records you can inspect:
|
|
694
818
|
|
|
695
819
|
```ruby
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
705
|
-
job
|
|
706
|
-
job.
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
831
|
+
```ruby
|
|
832
|
+
test "processes immediately" do
|
|
833
|
+
job = ProcessPayment.create!(args: { 'payment_id' => 123 })
|
|
834
|
+
job.queue!
|
|
717
835
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
order.process!
|
|
721
|
-
end
|
|
836
|
+
assert job.reload.processed_at
|
|
837
|
+
end
|
|
722
838
|
|
|
723
|
-
|
|
839
|
+
test "scheduled job with time travel" do
|
|
840
|
+
job = ProcessPayment.create!(args: { 'payment_id' => 123 })
|
|
724
841
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
##
|
|
1349
|
+
## Instrumentation
|
|
1225
1350
|
|
|
1226
|
-
Postburner
|
|
1351
|
+
Postburner emits ActiveSupport::Notifications events following ActiveJob conventions. Use these for monitoring, logging, or alerting.
|
|
1227
1352
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
|
1231
|
-
|
|
1232
|
-
|
|
|
1233
|
-
|
|
|
1234
|
-
|
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
**
|
|
1582
|
+
**ActiveJob with Postburner::Beanstalkd:**
|
|
1246
1583
|
|
|
1247
|
-
|
|
1584
|
+
```ruby
|
|
1585
|
+
class ProcessPayment < ApplicationJob
|
|
1586
|
+
include Postburner::Beanstalkd # Or Postburner::Tracked
|
|
1248
1587
|
|
|
1249
|
-
|
|
1588
|
+
queue_as :critical
|
|
1589
|
+
priority 0
|
|
1590
|
+
ttr 300 # 5 minutes to complete
|
|
1591
|
+
end
|
|
1250
1592
|
|
|
1251
|
-
|
|
1593
|
+
class QuickEmail < ApplicationJob
|
|
1594
|
+
include Postburner::Beanstalkd # Or Postburner::Tracked
|
|
1252
1595
|
|
|
1253
|
-
|
|
1596
|
+
queue_as :mailers
|
|
1597
|
+
ttr 60 # 1 minute for fast email jobs
|
|
1598
|
+
end
|
|
1254
1599
|
|
|
1255
|
-
|
|
1256
|
-
#
|
|
1257
|
-
Postburner.testing? # => true in tests
|
|
1258
|
-
```
|
|
1600
|
+
class LongRunningReport < ApplicationJob
|
|
1601
|
+
include Postburner::Beanstalkd # Or Postburner::Tracked
|
|
1259
1602
|
|
|
1260
|
-
|
|
1603
|
+
queue_as :reports
|
|
1604
|
+
ttr 3600 # 1 hour for complex reports
|
|
1605
|
+
end
|
|
1606
|
+
```
|
|
1261
1607
|
|
|
1262
|
-
|
|
1608
|
+
**Postburner::Job:**
|
|
1263
1609
|
|
|
1264
1610
|
```ruby
|
|
1265
|
-
class
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
1621
|
+
**Extending TTR with `extend!` (Touch Command):**
|
|
1280
1622
|
|
|
1281
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1630
|
+
class ProcessImport < ApplicationJob
|
|
1631
|
+
include Postburner::Tracked # Includes Beanstalkd and enables extend!
|
|
1286
1632
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
1651
|
+
**For Postburner::Job (via `bk` accessor):**
|
|
1294
1652
|
|
|
1295
1653
|
```ruby
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1654
|
+
class LargeDataProcessor < Postburner::Job
|
|
1655
|
+
queue 'processing'
|
|
1656
|
+
ttr 600 # 10 minutes
|
|
1299
1657
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1658
|
+
def perform(args)
|
|
1659
|
+
dataset = Dataset.find(args['dataset_id'])
|
|
1302
1660
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1661
|
+
dataset.each_chunk do |chunk|
|
|
1662
|
+
process_chunk(chunk)
|
|
1305
1663
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
### Canceling Jobs
|
|
1316
|
-
|
|
1317
|
-
**For Postburner::Job subclasses:**
|
|
1671
|
+
**`extend!` and Beanstalkd `touch`**
|
|
1318
1672
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1329
|
-
job.destroy!
|
|
1681
|
+
Set default TTR in `config/postburner.yml`:
|
|
1330
1682
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1683
|
+
```yaml
|
|
1684
|
+
production:
|
|
1685
|
+
default_priority: 65536
|
|
1686
|
+
default_ttr: 300 # 5 minutes default for all jobs
|
|
1333
1687
|
```
|
|
1334
1688
|
|
|
1335
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
1699
|
+
**Best Practices:**
|
|
1346
1700
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1707
|
+
**What happens on TTR timeout:**
|
|
1353
1708
|
|
|
1354
1709
|
```ruby
|
|
1355
|
-
|
|
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
|
-
#
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
#
|
|
1372
|
-
|
|
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
|