postburner 1.0.0.rc.2 → 1.0.0.rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/README.md +67 -5
- data/app/controllers/postburner/jobs_controller.rb +30 -1
- data/app/models/postburner/job.rb +33 -0
- data/app/models/postburner/mailer.rb +14 -0
- data/app/models/postburner/orphaned_job.rb +107 -0
- data/app/models/postburner/schedule.rb +5 -0
- data/lib/generators/postburner/install/install_generator.rb +10 -4
- data/lib/generators/postburner/install/templates/migrations/create_postburner_jobs.rb.erb +17 -1
- data/lib/postburner/version.rb +1 -1
- data/lib/postburner.rb +39 -15
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc5a8c616b4bf393b03fdc5d2bef57eb03e1a9b9dc06fe592f89b3d1238c2606
|
|
4
|
+
data.tar.gz: 40fe3028ce331e7ecbc6facb5829b845272ce66c14578cdd12b79a4495de85ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e3095d5efd3761db840b44461f42f127269608a6070ead2e9b5751054e3875e11098d34ac9232237baa27350ec46fbf8413a80adde99feae0115bedd9b08e68
|
|
7
|
+
data.tar.gz: 3a38ad87ba8692bae49462dbf6ac8e0013cb64209200c44201a25e5cb5d2c73ea14719753f279da4ce7da016d1af581a446bec7ad97af035f4884d645c12fefe
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.0.0.rc.4 - 2026-06-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Postburner::OrphanedJob` — inert STI placeholder loaded when a row's `type` column references a deleted or renamed class. Prevents `ActiveRecord::SubclassNotFound` from crashing callers (e.g. the admin queued-jobs index). `perform` and `queue!`/`requeue!` raise `Postburner::Job::OrphanedJobError`. `readonly?` returns `true` to prevent Rails' `ensure_proper_type` from overwriting the original `type` string. `remove!` (soft-delete) and `destroy` still work. No migration required.
|
|
7
|
+
- `Postburner::Job::OrphanedJobError` — raised by `OrphanedJob#perform` and `#queue!`/`#requeue!`.
|
|
8
|
+
- `Postburner::Job#orphaned?` — predicate; always `false` on the base class, `true` on `OrphanedJob`.
|
|
9
|
+
- `Postburner::Job.find_sti_class` override — rescues `ActiveRecord::SubclassNotFound` and returns `Postburner::OrphanedJob` instead.
|
|
10
|
+
- Partial indexes on `postburner_jobs` (`idx_pbjobs_active_run_at`, `idx_pbjobs_processed`, `idx_pbjobs_unqueued`) tuned to the admin UI's default queries, keeping the jobs list fast on large tables.
|
|
11
|
+
- `JobsController#index` now accepts a `scope` param (`active`, `processed`, `unqueued`, `all`) and defaults to `active`, so the default page load matches the `idx_pbjobs_active_run_at` predicate.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `postburner_jobs.lag` is now `bigint` instead of `integer`, to avoid overflow on jobs that sit in the queue long enough to exceed the 32-bit range.
|
|
15
|
+
|
|
16
|
+
### Upgrade notes
|
|
17
|
+
- This is a **template mutation**, not an additive migration. Existing rc.x installs will need to either re-run `rails generate postburner:install` (and merge the migration by hand) or manually add an equivalent migration that creates the three partial indexes. Failing to do either is **non-catastrophic** — the admin UI will simply remain slow on large `postburner_jobs` tables until the indexes are in place.
|
|
18
|
+
- The `lag` column type change (`integer` → `bigint`) is also a template mutation. Existing installs should add a migration along the lines of `change_column :postburner_jobs, :lag, :bigint` to avoid overflow once lag values exceed the 32-bit range.
|
|
19
|
+
|
|
20
|
+
Example migration covering both the `lag` widening and the three partial indexes:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class UpgradePostburnerJobsForRc4 < ActiveRecord::Migration[7.2]
|
|
24
|
+
disable_ddl_transaction!
|
|
25
|
+
|
|
26
|
+
def up
|
|
27
|
+
# Widen lag to bigint to avoid 32-bit overflow on long-queued jobs.
|
|
28
|
+
# ~24.9 days -> ~292.5 million years
|
|
29
|
+
change_column :postburner_jobs, :lag, :bigint
|
|
30
|
+
|
|
31
|
+
# Partial indexes tuned to the admin UI's default queries.
|
|
32
|
+
add_index :postburner_jobs, :run_at,
|
|
33
|
+
name: 'idx_pbjobs_active_run_at',
|
|
34
|
+
where: '(processed_at IS NULL AND removed_at IS NULL AND queued_at IS NOT NULL)',
|
|
35
|
+
algorithm: :concurrently
|
|
36
|
+
|
|
37
|
+
add_index :postburner_jobs, :processed_at,
|
|
38
|
+
name: 'idx_pbjobs_processed',
|
|
39
|
+
order: { processed_at: :desc },
|
|
40
|
+
where: '(processed_at IS NOT NULL)',
|
|
41
|
+
algorithm: :concurrently
|
|
42
|
+
|
|
43
|
+
add_index :postburner_jobs, [:removed_at, :created_at],
|
|
44
|
+
name: 'idx_pbjobs_unqueued',
|
|
45
|
+
order: { created_at: :desc },
|
|
46
|
+
where: '(queued_at IS NULL AND removed_at IS NULL)',
|
|
47
|
+
algorithm: :concurrently
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def down
|
|
51
|
+
remove_index :postburner_jobs, name: 'idx_pbjobs_unqueued', algorithm: :concurrently
|
|
52
|
+
remove_index :postburner_jobs, name: 'idx_pbjobs_processed', algorithm: :concurrently
|
|
53
|
+
remove_index :postburner_jobs, name: 'idx_pbjobs_active_run_at', algorithm: :concurrently
|
|
54
|
+
|
|
55
|
+
change_column :postburner_jobs, :lag, :integer
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Notes:
|
|
61
|
+
- `disable_ddl_transaction!` + `algorithm: :concurrently` is recommended for production tables of any size — it builds the indexes without holding a long write lock. Drop both if you're on a small dev table and want a single transactional migration.
|
|
62
|
+
- `change_column ... :bigint` on PostgreSQL is an in-place type widening for `integer → bigint` and is fast (no full table rewrite), but it does take a brief `ACCESS EXCLUSIVE` lock — schedule accordingly on hot tables.
|
|
63
|
+
- Adjust `ActiveRecord::Migration[7.2]` to match your app's Rails version.
|
|
64
|
+
|
|
65
|
+
## v1.0.0.rc.3
|
|
66
|
+
|
|
67
|
+
## v1.0.0.rc.2
|
|
68
|
+
|
|
69
|
+
## v1.0.0.rc.1
|
|
70
|
+
|
|
3
71
|
## v0.7.0 - 2022-10-24
|
|
4
72
|
|
|
5
73
|
### Added
|
data/README.md
CHANGED
|
@@ -85,7 +85,7 @@ bundle exec rake postburner:work WORKER=default
|
|
|
85
85
|
|
|
86
86
|
## Table of Contents
|
|
87
87
|
|
|
88
|
-
- [Why](#why)
|
|
88
|
+
- [Why Postburner?](#why-postburner)
|
|
89
89
|
- [Quick Start](#quick-start)
|
|
90
90
|
- [Usage](#usage)
|
|
91
91
|
- [Standard Jobs](#standard-jobs)
|
|
@@ -106,7 +106,7 @@ bundle exec rake postburner:work WORKER=default
|
|
|
106
106
|
- [Installation](#installation)
|
|
107
107
|
- [Web UI - v2 Coming Soon](#web-ui)
|
|
108
108
|
|
|
109
|
-
## Why
|
|
109
|
+
## Why Postburner?
|
|
110
110
|
|
|
111
111
|
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.
|
|
112
112
|
|
|
@@ -927,6 +927,53 @@ test "tracked job logs execution" do
|
|
|
927
927
|
end
|
|
928
928
|
```
|
|
929
929
|
|
|
930
|
+
### Testing Emails
|
|
931
|
+
|
|
932
|
+
Standard ActiveJob test assertions (`assert_enqueued_emails`, `assert_enqueued_jobs`) work when using the `:test` ActiveJob adapter in your test environment, which is the Rails default:
|
|
933
|
+
|
|
934
|
+
```ruby
|
|
935
|
+
class EmailTest < ActiveSupport::TestCase
|
|
936
|
+
# assert_enqueued_emails works — Rails' :test adapter tracks the enqueue
|
|
937
|
+
test "welcome email is enqueued" do
|
|
938
|
+
assert_enqueued_emails 1 do
|
|
939
|
+
UserMailer.welcome(user).deliver_later
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# assert_emails works — the job executes inline, delivering the email
|
|
944
|
+
test "welcome email is sent" do
|
|
945
|
+
assert_emails 1 do
|
|
946
|
+
SendWelcomeEmail.perform_later(user.id)
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
**How this works:** `assert_enqueued_emails` and `assert_enqueued_jobs` are powered by Rails' `:test` ActiveJob adapter, which tracks enqueued jobs in an in-memory array. Postburner's queue strategies (`InlineTestQueue`, etc.) control `Postburner::Job` subclasses separately. The two systems coexist — ActiveJob assertions use the `:test` adapter, `Postburner::Job` assertions use database queries.
|
|
953
|
+
|
|
954
|
+
By default, Rails uses the `:test` adapter in test mode unless you override it. Since you set `:postburner` as your adapter in `config/application.rb`, you should set it back to `:test` for your test environment:
|
|
955
|
+
|
|
956
|
+
```ruby
|
|
957
|
+
# config/environments/test.rb
|
|
958
|
+
config.active_job.queue_adapter = :test
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
This is standard practice for any ActiveJob adapter (Sidekiq, SolidQueue, etc.) — the production adapter handles real execution, and the `:test` adapter enables Rails' built-in assertion helpers. Without this, `assert_enqueued_emails` will not work because Postburner's adapter executes jobs rather than tracking them in an array.
|
|
962
|
+
|
|
963
|
+
**`Postburner::Mailer`** bypasses ActiveJob entirely, so `assert_enqueued_emails` cannot see these jobs regardless of adapter. Assert on side effects instead:
|
|
964
|
+
|
|
965
|
+
```ruby
|
|
966
|
+
test "mailer job sends email" do
|
|
967
|
+
job = Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1)
|
|
968
|
+
|
|
969
|
+
assert_emails 1 do
|
|
970
|
+
job.queue! # Executes inline in test mode, delivering the email
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
assert job.reload.processed_at
|
|
974
|
+
end
|
|
975
|
+
```
|
|
976
|
+
|
|
930
977
|
### Switching Queue Strategies
|
|
931
978
|
|
|
932
979
|
For tests requiring specific queue behaviors, use `switch_queue_strategy!` and `restore_queue_strategy!`:
|
|
@@ -2031,13 +2078,15 @@ Postburner uses [Beaneater](https://github.com/beanstalkd/beaneater) as the Ruby
|
|
|
2031
2078
|
|
|
2032
2079
|
### Connection Methods
|
|
2033
2080
|
|
|
2081
|
+
Postburner uses **thread-local connections** — each thread gets its own Beanstalkd socket, matching the pattern used by worker threads. This is safe under multi-threaded servers like Puma.
|
|
2082
|
+
|
|
2034
2083
|
```ruby
|
|
2035
|
-
# Get
|
|
2084
|
+
# Get the thread-local Beaneater connection (returns Beaneater instance)
|
|
2036
2085
|
conn = Postburner.connection
|
|
2037
2086
|
conn.tubes.to_a # List all tubes
|
|
2038
2087
|
conn.beanstalk.stats # Server statistics
|
|
2039
2088
|
|
|
2040
|
-
# Block form - yields
|
|
2089
|
+
# Block form - yields thread-local connection
|
|
2041
2090
|
Postburner.connected do |conn|
|
|
2042
2091
|
conn.tubes.to_a # List all tubes
|
|
2043
2092
|
conn.tubes['postburner.production.critical'].stats
|
|
@@ -2045,6 +2094,19 @@ Postburner.connected do |conn|
|
|
|
2045
2094
|
end
|
|
2046
2095
|
```
|
|
2047
2096
|
|
|
2097
|
+
**Shutdown cleanup:**
|
|
2098
|
+
|
|
2099
|
+
On process shutdown, call `disconnect_all!` to close every thread's connection cleanly:
|
|
2100
|
+
|
|
2101
|
+
```ruby
|
|
2102
|
+
# config/puma.rb
|
|
2103
|
+
on_worker_shutdown do
|
|
2104
|
+
Postburner.disconnect_all!
|
|
2105
|
+
end
|
|
2106
|
+
```
|
|
2107
|
+
|
|
2108
|
+
This is optional — Ruby closes sockets on process exit — but gives you clean logs and explicit teardown.
|
|
2109
|
+
|
|
2048
2110
|
### Job-Level Access
|
|
2049
2111
|
|
|
2050
2112
|
```ruby
|
|
@@ -2248,7 +2310,7 @@ beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
|
|
|
2248
2310
|
|
|
2249
2311
|
```ruby
|
|
2250
2312
|
# Gemfile
|
|
2251
|
-
gem 'postburner', '~> 1.0.0.
|
|
2313
|
+
gem 'postburner', '~> 1.0.0.rc.2
|
|
2252
2314
|
```
|
|
2253
2315
|
|
|
2254
2316
|
```bash
|
|
@@ -2,12 +2,41 @@ require_dependency "postburner/application_controller"
|
|
|
2
2
|
|
|
3
3
|
module Postburner
|
|
4
4
|
class JobsController < ApplicationController
|
|
5
|
+
# Scopes align with the partial indexes defined in the install
|
|
6
|
+
# migration. Keeping the controller's default query matched to an
|
|
7
|
+
# index predicate keeps the admin UI fast on large tables.
|
|
8
|
+
SCOPES = %w[ active processed unqueued all ].freeze
|
|
9
|
+
DEFAULT_SCOPE = 'active'.freeze
|
|
10
|
+
|
|
5
11
|
def index
|
|
6
|
-
@
|
|
12
|
+
@scope = SCOPES.include?(params[:scope]) ? params[:scope] : DEFAULT_SCOPE
|
|
13
|
+
@jobs = scoped_jobs(@scope)
|
|
7
14
|
end
|
|
8
15
|
|
|
9
16
|
def show
|
|
10
17
|
@job = Job.find(params[:id])
|
|
11
18
|
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def scoped_jobs(scope)
|
|
23
|
+
case scope
|
|
24
|
+
when 'active'
|
|
25
|
+
# Uses idx_pbjobs_active_run_at
|
|
26
|
+
Job.where(processed_at: nil, removed_at: nil)
|
|
27
|
+
.where.not(queued_at: nil)
|
|
28
|
+
.order(run_at: :asc)
|
|
29
|
+
when 'processed'
|
|
30
|
+
# Uses idx_pbjobs_processed
|
|
31
|
+
Job.where.not(processed_at: nil)
|
|
32
|
+
.order(processed_at: :desc)
|
|
33
|
+
when 'unqueued'
|
|
34
|
+
# Uses idx_pbjobs_unqueued
|
|
35
|
+
Job.where(queued_at: nil, removed_at: nil)
|
|
36
|
+
.order(created_at: :desc)
|
|
37
|
+
else
|
|
38
|
+
Job.order(queued_at: :desc, created_at: :desc)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
12
41
|
end
|
|
13
42
|
end
|
|
@@ -88,6 +88,30 @@ module Postburner
|
|
|
88
88
|
|
|
89
89
|
validates :sid, presence: {strict: true}
|
|
90
90
|
|
|
91
|
+
# Resolves an STI type name to a class, falling back to OrphanedJob when the
|
|
92
|
+
# original class no longer exists. This tolerates rows whose `type` column
|
|
93
|
+
# references a deleted or renamed job class, preventing
|
|
94
|
+
# ActiveRecord::SubclassNotFound from crashing callers (e.g. the admin
|
|
95
|
+
# queued-jobs page).
|
|
96
|
+
#
|
|
97
|
+
# @param type_name [String] The value stored in the `type` column
|
|
98
|
+
# @return [Class] The resolved class, or {Postburner::OrphanedJob} if missing
|
|
99
|
+
#
|
|
100
|
+
def self.find_sti_class(type_name)
|
|
101
|
+
super
|
|
102
|
+
rescue ActiveRecord::SubclassNotFound
|
|
103
|
+
Postburner::OrphanedJob
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns whether this job instance is an orphaned placeholder.
|
|
107
|
+
#
|
|
108
|
+
# Always false on the base class. Overridden to return true in
|
|
109
|
+
# {Postburner::OrphanedJob}.
|
|
110
|
+
#
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
#
|
|
113
|
+
def orphaned? = false
|
|
114
|
+
|
|
91
115
|
# Abstract method to be implemented by subclasses.
|
|
92
116
|
#
|
|
93
117
|
# This method contains the actual work logic for the job. It is called
|
|
@@ -220,6 +244,15 @@ module Postburner
|
|
|
220
244
|
#
|
|
221
245
|
class AlreadyProcessed < StandardError; end
|
|
222
246
|
|
|
247
|
+
# Exception raised when attempting to perform or enqueue an orphaned job whose
|
|
248
|
+
# original class no longer exists in the application.
|
|
249
|
+
#
|
|
250
|
+
# @example
|
|
251
|
+
# job = Postburner::Job.find(id) # loads as OrphanedJob if class is gone
|
|
252
|
+
# job.perform # => raises OrphanedJobError
|
|
253
|
+
#
|
|
254
|
+
class OrphanedJobError < StandardError; end
|
|
255
|
+
|
|
223
256
|
# Exception raised when a job is executed before its scheduled run_at time.
|
|
224
257
|
#
|
|
225
258
|
# In production (Queue/NiceQueue), this is handled by re-inserting with delay.
|
|
@@ -50,6 +50,20 @@ module Postburner
|
|
|
50
50
|
# @example Queue to specific queue
|
|
51
51
|
# Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1).queue!(queue: 'bulk_mailers')
|
|
52
52
|
#
|
|
53
|
+
# == Testing
|
|
54
|
+
#
|
|
55
|
+
# Because Postburner::Mailer bypasses ActiveJob, Rails' +assert_enqueued_emails+
|
|
56
|
+
# will not detect these jobs. Use +assert_emails+ (which checks actual deliveries)
|
|
57
|
+
# or assert on the job record:
|
|
58
|
+
#
|
|
59
|
+
# # Assert email was delivered (inline in test mode)
|
|
60
|
+
# assert_emails 1 do
|
|
61
|
+
# Postburner::Mailer.delivery(UserMailer, :welcome).with(user_id: 1).queue!
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# # Assert job was processed
|
|
65
|
+
# assert job.reload.processed_at
|
|
66
|
+
#
|
|
53
67
|
# @see Postburner::Job Base class for tracked jobs
|
|
54
68
|
# @see Postburner::Configuration#default_mailer_queue Configuration option
|
|
55
69
|
class Mailer < Job
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postburner
|
|
4
|
+
# Inert STI placeholder loaded when a Postburner::Job row's `type` column
|
|
5
|
+
# references a class that no longer exists in the application.
|
|
6
|
+
#
|
|
7
|
+
# ## Why readonly?
|
|
8
|
+
#
|
|
9
|
+
# Rails' `ensure_proper_type` (called via `save`/`update`) would overwrite the
|
|
10
|
+
# original `type` value with `'Postburner::OrphanedJob'`, permanently destroying
|
|
11
|
+
# the record of which class was missing. `readonly?` returning true prevents all
|
|
12
|
+
# `save`/`update` paths and therefore blocks `ensure_proper_type` from running.
|
|
13
|
+
#
|
|
14
|
+
# Rails' `instantiate` reads the `type` column verbatim when loading records from
|
|
15
|
+
# the database and does NOT call `ensure_proper_type`, so the original class name
|
|
16
|
+
# is preserved on load. This class is only ever instantiated via the
|
|
17
|
+
# `find_sti_class` fallback in {Postburner::Job}, never created directly.
|
|
18
|
+
#
|
|
19
|
+
# ## Why remove! is overridden here
|
|
20
|
+
#
|
|
21
|
+
# In Rails 8.1+, `update_column` respects `readonly?` and raises
|
|
22
|
+
# `ActiveRecord::ReadOnlyRecord`. The base `Commands#remove!` calls
|
|
23
|
+
# `update_column(:removed_at, ...)` — so it would be blocked too. This subclass
|
|
24
|
+
# overrides `remove!` to use `self.class.where(id: self.id).update_all(...)` which
|
|
25
|
+
# operates at the relation level (bypasses both `readonly?` and all instance
|
|
26
|
+
# callbacks) while still soft-deleting the row. Admins can therefore safely
|
|
27
|
+
# soft-remove orphaned rows without restoring the missing class.
|
|
28
|
+
#
|
|
29
|
+
# ## Usage
|
|
30
|
+
#
|
|
31
|
+
# This class is loaded automatically by {Postburner::Job.find_sti_class} when a
|
|
32
|
+
# row's type is unresolvable. You should never instantiate it directly in
|
|
33
|
+
# application code.
|
|
34
|
+
#
|
|
35
|
+
# @example Typical lifecycle
|
|
36
|
+
# # A DB row exists with type: 'Flex::TriggerNoShowDeliveriesJob' (class deleted)
|
|
37
|
+
# job = Postburner::Job.find(42)
|
|
38
|
+
# job.class # => Postburner::OrphanedJob
|
|
39
|
+
# job.type # => 'Flex::TriggerNoShowDeliveriesJob'
|
|
40
|
+
# job.orphaned? # => true
|
|
41
|
+
# job.missing_class_name # => 'Flex::TriggerNoShowDeliveriesJob'
|
|
42
|
+
# job.perform # => raises Postburner::Job::OrphanedJobError
|
|
43
|
+
# job.remove! # soft-deletes fine despite readonly?
|
|
44
|
+
#
|
|
45
|
+
class OrphanedJob < Job
|
|
46
|
+
# Returns the original class name stored in the `type` column — the deleted
|
|
47
|
+
# or renamed class that this placeholder stands in for.
|
|
48
|
+
#
|
|
49
|
+
# @return [String]
|
|
50
|
+
#
|
|
51
|
+
def missing_class_name
|
|
52
|
+
self.type.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Always true; identifies this instance as an orphaned placeholder.
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
#
|
|
59
|
+
def orphaned? = true
|
|
60
|
+
|
|
61
|
+
# Raises {Postburner::Job::OrphanedJobError} because the original job class
|
|
62
|
+
# no longer exists and there is no implementation to run.
|
|
63
|
+
#
|
|
64
|
+
# @raise [Postburner::Job::OrphanedJobError] always
|
|
65
|
+
#
|
|
66
|
+
def perform(*)
|
|
67
|
+
raise Postburner::Job::OrphanedJobError,
|
|
68
|
+
"Cannot perform orphaned job ##{self.id}: class '#{self.missing_class_name}' no longer exists"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Raises {Postburner::Job::OrphanedJobError} because re-enqueuing a job whose
|
|
72
|
+
# class is gone would only produce another unperformable entry.
|
|
73
|
+
#
|
|
74
|
+
# @raise [Postburner::Job::OrphanedJobError] always
|
|
75
|
+
#
|
|
76
|
+
def queue!(*)
|
|
77
|
+
raise Postburner::Job::OrphanedJobError,
|
|
78
|
+
"Cannot enqueue orphaned job ##{self.id}: class '#{self.missing_class_name}' no longer exists"
|
|
79
|
+
end
|
|
80
|
+
alias_method :requeue!, :queue!
|
|
81
|
+
|
|
82
|
+
# Soft-deletes this orphaned row by setting `removed_at`, bypassing the
|
|
83
|
+
# `readonly?` guard via a relation-level `update_all` call (which does not go
|
|
84
|
+
# through instance save/update and therefore does not trigger
|
|
85
|
+
# `ensure_proper_type` either).
|
|
86
|
+
#
|
|
87
|
+
# Idempotent: does nothing if already removed.
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
90
|
+
#
|
|
91
|
+
def remove!
|
|
92
|
+
return if self.removed_at
|
|
93
|
+
|
|
94
|
+
self.delete!
|
|
95
|
+
now = Time.current
|
|
96
|
+
self.class.where(id: self.id).update_all(removed_at: now)
|
|
97
|
+
self.removed_at = now
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Prevents saves that would allow Rails' ensure_proper_type to overwrite the
|
|
101
|
+
# original `type` value with 'Postburner::OrphanedJob'.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] always true
|
|
104
|
+
#
|
|
105
|
+
def readonly? = true
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -366,6 +366,11 @@ module Postburner
|
|
|
366
366
|
|
|
367
367
|
execution.save!
|
|
368
368
|
|
|
369
|
+
# Reload so instrumented timestamps reflect persisted (microsecond)
|
|
370
|
+
# precision rather than the in-memory nanosecond values, matching what
|
|
371
|
+
# consumers see after a DB roundtrip (see #enqueue! reload below).
|
|
372
|
+
execution.reload
|
|
373
|
+
|
|
369
374
|
# Instrument execution creation
|
|
370
375
|
ActiveSupport::Notifications.instrument('create.schedule_execution.postburner', {
|
|
371
376
|
schedule: Postburner::Instrumentation.schedule_payload(self),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
1
3
|
class Postburner::InstallGenerator < Rails::Generators::Base
|
|
2
4
|
source_root File.expand_path('templates', __dir__)
|
|
3
5
|
include Rails::Generators::Migration
|
|
@@ -28,11 +30,15 @@ class Postburner::InstallGenerator < Rails::Generators::Base
|
|
|
28
30
|
_max = timestamps.max || timestamp[-2..-1].to_i
|
|
29
31
|
_next = _max + 1
|
|
30
32
|
raise "MISSING NEXT" if _next.blank?
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
# When seconds overflow past 99, roll forward by 1 minute and reset.
|
|
35
|
+
if _next > 99
|
|
36
|
+
rolled = Time.parse("#{stem}00 UTC") + 60
|
|
37
|
+
stem = rolled.strftime('%Y%m%d%H%M')
|
|
38
|
+
_next = 0
|
|
34
39
|
end
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
"#{stem}#{_next.to_s.rjust(2, '0')}"
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
private
|
|
@@ -13,7 +13,7 @@ class CreatePostburnerJobs < ActiveRecord::Migration<%= migration_version %>
|
|
|
13
13
|
t.datetime :processing_at
|
|
14
14
|
t.datetime :processed_at
|
|
15
15
|
t.datetime :removed_at
|
|
16
|
-
t.
|
|
16
|
+
t.bigint :lag
|
|
17
17
|
t.integer :duration
|
|
18
18
|
t.integer :attempt_count
|
|
19
19
|
t.integer :error_count
|
|
@@ -37,6 +37,22 @@ class CreatePostburnerJobs < ActiveRecord::Migration<%= migration_version %>
|
|
|
37
37
|
t.index [ :error_count ]
|
|
38
38
|
t.index [ :log_count ]
|
|
39
39
|
|
|
40
|
+
# Partial indexes tuned to the admin UI's default queries. Kept
|
|
41
|
+
# alongside the single-column indexes above: the single-column
|
|
42
|
+
# indexes still serve ad-hoc queries, while these partials keep
|
|
43
|
+
# the hot-path listing queries fast on large tables.
|
|
44
|
+
t.index :run_at,
|
|
45
|
+
name: 'idx_pbjobs_active_run_at',
|
|
46
|
+
where: 'processed_at IS NULL AND removed_at IS NULL AND queued_at IS NOT NULL'
|
|
47
|
+
t.index :processed_at,
|
|
48
|
+
name: 'idx_pbjobs_processed',
|
|
49
|
+
order: { processed_at: :desc },
|
|
50
|
+
where: 'processed_at IS NOT NULL'
|
|
51
|
+
t.index [ :removed_at, :created_at ],
|
|
52
|
+
name: 'idx_pbjobs_unqueued',
|
|
53
|
+
order: { created_at: :desc },
|
|
54
|
+
where: 'queued_at IS NULL AND removed_at IS NULL'
|
|
55
|
+
|
|
40
56
|
# Add these or more depending on what you want to search for.
|
|
41
57
|
#t.index [ :attempts ], using: :gin
|
|
42
58
|
#t.index [ :errata ], using: :gin
|
data/lib/postburner/version.rb
CHANGED
data/lib/postburner.rb
CHANGED
|
@@ -319,31 +319,31 @@ module Postburner
|
|
|
319
319
|
# conn = Postburner.connection
|
|
320
320
|
# conn.tubes['my.tube'].stats
|
|
321
321
|
#
|
|
322
|
-
# @note Connection is cached in
|
|
322
|
+
# @note Connection is cached in Thread.current[:postburner_connection] (thread-local)
|
|
323
323
|
# @note Automatically reconnects if connection is not active
|
|
324
324
|
#
|
|
325
325
|
# @see #connected
|
|
326
326
|
#
|
|
327
327
|
def self.connection
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
Thread.current[:postburner_connection] ||= Postburner::Connection.new
|
|
329
|
+
conn = Thread.current[:postburner_connection]
|
|
330
|
+
conn.reconnect! unless conn.connected?
|
|
331
|
+
conn
|
|
331
332
|
end
|
|
332
333
|
|
|
333
334
|
# Yields a Beanstalkd connection or returns cached connection.
|
|
334
335
|
#
|
|
335
|
-
# When called with a block, yields the connection
|
|
336
|
-
#
|
|
337
|
-
# cached connection.
|
|
336
|
+
# When called with a block, yields the thread-local connection.
|
|
337
|
+
# When called without a block, returns the thread-local connection.
|
|
338
338
|
#
|
|
339
339
|
# @overload connected
|
|
340
340
|
# Returns the cached Beanstalkd connection.
|
|
341
341
|
# @return [Postburner::Connection] Beanstalkd connection
|
|
342
342
|
#
|
|
343
343
|
# @overload connected {|conn| ... }
|
|
344
|
-
# Yields connection
|
|
344
|
+
# Yields connection for the duration of the block.
|
|
345
345
|
# @yieldparam conn [Postburner::Connection] Beanstalkd connection
|
|
346
|
-
# @return [
|
|
346
|
+
# @return [Object] return value of the block
|
|
347
347
|
#
|
|
348
348
|
# @example With block (recommended)
|
|
349
349
|
# Postburner.connected do |conn|
|
|
@@ -351,7 +351,6 @@ module Postburner
|
|
|
351
351
|
# puts tube.name
|
|
352
352
|
# end
|
|
353
353
|
# end
|
|
354
|
-
# # Connection automatically closed
|
|
355
354
|
#
|
|
356
355
|
# @example Without block
|
|
357
356
|
# conn = Postburner.connected
|
|
@@ -364,19 +363,44 @@ module Postburner
|
|
|
364
363
|
# end
|
|
365
364
|
#
|
|
366
365
|
# @see #connection
|
|
366
|
+
# @see #disconnect_all!
|
|
367
367
|
#
|
|
368
368
|
def self.connected
|
|
369
369
|
if block_given?
|
|
370
|
-
|
|
371
|
-
yield connection
|
|
372
|
-
ensure
|
|
373
|
-
connection.close if connection
|
|
374
|
-
end
|
|
370
|
+
yield connection
|
|
375
371
|
else
|
|
376
372
|
connection
|
|
377
373
|
end
|
|
378
374
|
end
|
|
379
375
|
|
|
376
|
+
# Closes and clears Beanstalkd connections on all known threads.
|
|
377
|
+
#
|
|
378
|
+
# Call this on worker shutdown to release sockets cleanly. Because connections
|
|
379
|
+
# are thread-local, a single `connection.close` call from the main thread would
|
|
380
|
+
# only close that thread's socket. This method iterates every live thread and
|
|
381
|
+
# closes each thread's connection, if any.
|
|
382
|
+
#
|
|
383
|
+
# @return [void]
|
|
384
|
+
#
|
|
385
|
+
# @example Puma worker shutdown (config/puma.rb)
|
|
386
|
+
# on_worker_shutdown do
|
|
387
|
+
# Postburner.disconnect_all!
|
|
388
|
+
# end
|
|
389
|
+
#
|
|
390
|
+
def self.disconnect_all!
|
|
391
|
+
Thread.list.each do |thread|
|
|
392
|
+
if (conn = thread[:postburner_connection])
|
|
393
|
+
begin
|
|
394
|
+
conn.close if conn.respond_to?(:close) && conn.connected?
|
|
395
|
+
rescue => e
|
|
396
|
+
warn "Postburner: failed to close connection on shutdown: #{e.message}" if $VERBOSE
|
|
397
|
+
ensure
|
|
398
|
+
thread[:postburner_connection] = nil
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
380
404
|
# Clears jobs from specified tubes or shows stats for all tubes.
|
|
381
405
|
#
|
|
382
406
|
# High-level method with formatted output. Delegates to Connection#clear_tubes!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: postburner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.rc.
|
|
4
|
+
version: 1.0.0.rc.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Smith
|
|
@@ -138,6 +138,7 @@ files:
|
|
|
138
138
|
- app/models/postburner/application_record.rb
|
|
139
139
|
- app/models/postburner/job.rb
|
|
140
140
|
- app/models/postburner/mailer.rb
|
|
141
|
+
- app/models/postburner/orphaned_job.rb
|
|
141
142
|
- app/models/postburner/schedule.rb
|
|
142
143
|
- app/models/postburner/schedule_execution.rb
|
|
143
144
|
- app/models/postburner/tracked_job.rb
|