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