pgbus 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +37 -3
- data/Rakefile +98 -1
- data/app/controllers/pgbus/application_controller.rb +8 -0
- data/app/controllers/pgbus/recurring_tasks_controller.rb +36 -0
- data/app/helpers/pgbus/application_helper.rb +41 -0
- data/app/models/pgbus/application_record.rb +7 -0
- data/app/models/pgbus/batch_entry.rb +31 -0
- data/app/models/pgbus/blocked_execution.rb +40 -0
- data/app/models/pgbus/process_entry.rb +9 -0
- data/app/models/pgbus/processed_event.rb +9 -0
- data/app/models/pgbus/recurring_execution.rb +33 -0
- data/app/models/pgbus/recurring_task.rb +42 -0
- data/app/models/pgbus/semaphore.rb +29 -0
- data/app/views/layouts/pgbus/application.html.erb +1 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +9 -1
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +55 -18
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +46 -8
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +79 -0
- data/app/views/pgbus/recurring_tasks/index.html.erb +6 -0
- data/app/views/pgbus/recurring_tasks/show.html.erb +122 -0
- data/config/routes.rb +7 -0
- data/lib/active_job/queue_adapters/pgbus_adapter.rb +29 -0
- data/lib/generators/pgbus/add_recurring_generator.rb +56 -0
- data/lib/generators/pgbus/install_generator.rb +76 -2
- data/lib/generators/pgbus/templates/add_recurring_tables.rb.erb +31 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +72 -4
- data/lib/generators/pgbus/templates/recurring.yml.erb +40 -0
- data/lib/generators/pgbus/templates/upgrade_pgmq.rb.erb +30 -0
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +60 -0
- data/lib/pgbus/active_job/adapter.rb +3 -6
- data/lib/pgbus/active_job/executor.rb +26 -12
- data/lib/pgbus/batch.rb +65 -72
- data/lib/pgbus/cli.rb +11 -16
- data/lib/pgbus/client.rb +32 -15
- data/lib/pgbus/concurrency/blocked_execution.rb +32 -37
- data/lib/pgbus/concurrency/semaphore.rb +11 -39
- data/lib/pgbus/concurrency.rb +10 -2
- data/lib/pgbus/configuration.rb +48 -0
- data/lib/pgbus/engine.rb +19 -1
- data/lib/pgbus/event_bus/handler.rb +10 -23
- data/lib/pgbus/instrumentation.rb +29 -0
- data/lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql +2123 -0
- data/lib/pgbus/pgmq_schema.rb +159 -0
- data/lib/pgbus/process/consumer.rb +17 -9
- data/lib/pgbus/process/dispatcher.rb +33 -41
- data/lib/pgbus/process/heartbeat.rb +15 -23
- data/lib/pgbus/process/signal_handler.rb +23 -1
- data/lib/pgbus/process/supervisor.rb +79 -2
- data/lib/pgbus/process/worker.rb +42 -13
- data/lib/pgbus/recurring/already_recorded.rb +7 -0
- data/lib/pgbus/recurring/command_job.rb +28 -0
- data/lib/pgbus/recurring/config_loader.rb +35 -0
- data/lib/pgbus/recurring/schedule.rb +102 -0
- data/lib/pgbus/recurring/scheduler.rb +102 -0
- data/lib/pgbus/recurring/task.rb +111 -0
- data/lib/pgbus/serializer.rb +16 -6
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +217 -36
- data/lib/pgbus.rb +8 -0
- data/lib/tasks/pgbus_pgmq.rake +62 -0
- metadata +51 -24
- data/.bun-version +0 -1
- data/.claude/commands/architect.md +0 -100
- data/.claude/commands/github-review-comments.md +0 -237
- data/.claude/commands/lfg.md +0 -271
- data/.claude/commands/review-pr.md +0 -69
- data/.claude/commands/security.md +0 -122
- data/.claude/commands/tdd.md +0 -148
- data/.claude/rules/agents.md +0 -49
- data/.claude/rules/coding-style.md +0 -91
- data/.claude/rules/git-workflow.md +0 -56
- data/.claude/rules/performance.md +0 -73
- data/.claude/rules/testing.md +0 -67
- data/CLAUDE.md +0 -80
- data/CODE_OF_CONDUCT.md +0 -10
- data/bun.lock +0 -18
- data/docs/README.md +0 -28
- data/docs/switch_from_good_job.md +0 -279
- data/docs/switch_from_sidekiq.md +0 -226
- data/docs/switch_from_solid_queue.md +0 -247
- data/package.json +0 -9
- data/sig/pgbus.rbs +0 -4
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
# Switch from GoodJob to Pgbus
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
GoodJob and Pgbus are both PostgreSQL-native job processors with LISTEN/NOTIFY support. The biggest architectural difference: GoodJob uses advisory locks and a `good_jobs` table, while Pgbus uses PGMQ (a dedicated message queue extension) with visibility timeouts. Both are pure ActiveJob adapters.
|
|
6
|
-
|
|
7
|
-
**Effort estimate:** Low if you use standard ActiveJob. Medium if you rely on GoodJob's concurrency controls, batches, or cron.
|
|
8
|
-
|
|
9
|
-
## Step 1: Update dependencies
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
# Gemfile
|
|
13
|
-
|
|
14
|
-
# Remove
|
|
15
|
-
gem "good_job"
|
|
16
|
-
|
|
17
|
-
# Add
|
|
18
|
-
gem "pgbus"
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
bundle install
|
|
23
|
-
rails generate pgbus:install
|
|
24
|
-
rails db:migrate
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Step 2: Switch the adapter
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
# config/application.rb
|
|
31
|
-
|
|
32
|
-
# Before
|
|
33
|
-
config.active_job.queue_adapter = :good_job
|
|
34
|
-
|
|
35
|
-
# After
|
|
36
|
-
config.active_job.queue_adapter = :pgbus
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Remove GoodJob configuration:
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
# Before: Remove this block
|
|
43
|
-
config.good_job = {
|
|
44
|
-
execution_mode: :async,
|
|
45
|
-
queues: "critical:5;default:3;*:1",
|
|
46
|
-
max_threads: 5,
|
|
47
|
-
poll_interval: 10,
|
|
48
|
-
enable_cron: true,
|
|
49
|
-
enable_listen_notify: true,
|
|
50
|
-
# ...
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Step 3: Convert worker configuration
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
# Before: GoodJob config (in application.rb or environment files)
|
|
58
|
-
config.good_job = {
|
|
59
|
-
execution_mode: :external, # or :async
|
|
60
|
-
queues: "critical:5;default:3;low:1",
|
|
61
|
-
poll_interval: 10,
|
|
62
|
-
max_threads: 5,
|
|
63
|
-
shutdown_timeout: 30,
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
```yaml
|
|
68
|
-
# After: config/pgbus.yml
|
|
69
|
-
production:
|
|
70
|
-
workers:
|
|
71
|
-
- queues: [critical]
|
|
72
|
-
threads: 5
|
|
73
|
-
- queues: [default]
|
|
74
|
-
threads: 3
|
|
75
|
-
- queues: [low]
|
|
76
|
-
threads: 1
|
|
77
|
-
max_retries: 5
|
|
78
|
-
max_jobs_per_worker: 10000
|
|
79
|
-
max_memory_mb: 512
|
|
80
|
-
max_worker_lifetime: 3600
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Configuration mapping
|
|
84
|
-
|
|
85
|
-
| GoodJob | Pgbus | Notes |
|
|
86
|
-
|---------|-------|-------|
|
|
87
|
-
| `execution_mode: :external` | `bundle exec pgbus start` | Separate process (recommended for production) |
|
|
88
|
-
| `execution_mode: :async` | N/A | Pgbus always runs as separate processes |
|
|
89
|
-
| `queues: "critical:5;default:3"` | `workers:` array | One entry per worker process |
|
|
90
|
-
| `max_threads` | `threads` per worker | Per-worker, not global |
|
|
91
|
-
| `poll_interval` | `polling_interval` | Pgbus defaults to 0.1s; LISTEN/NOTIFY is primary |
|
|
92
|
-
| `enable_listen_notify` | `listen_notify` | Both support LISTEN/NOTIFY |
|
|
93
|
-
| `shutdown_timeout` | Handled by supervisor | Graceful shutdown on SIGTERM |
|
|
94
|
-
| `on_thread_error` | Configure via `Pgbus.logger` | Error reporting via logging |
|
|
95
|
-
| `preserve_job_records` | Always archived | PGMQ archives completed messages |
|
|
96
|
-
|
|
97
|
-
## Step 4: Remove concurrency controls
|
|
98
|
-
|
|
99
|
-
GoodJob's concurrency extensions are GoodJob-specific. Remove them:
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
# Before
|
|
103
|
-
class ProcessOrderJob < ApplicationJob
|
|
104
|
-
include GoodJob::ActiveJobExtensions::Concurrency
|
|
105
|
-
|
|
106
|
-
good_job_control_concurrency_with(
|
|
107
|
-
total_limit: 1,
|
|
108
|
-
enqueue_limit: 2,
|
|
109
|
-
perform_limit: 1,
|
|
110
|
-
enqueue_throttle: [10, 1.minute],
|
|
111
|
-
perform_throttle: [100, 1.hour],
|
|
112
|
-
key: -> { "ProcessOrder-#{arguments.first.id}" }
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
def perform(order)
|
|
116
|
-
# ...
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
```ruby
|
|
122
|
-
# After
|
|
123
|
-
class ProcessOrderJob < ApplicationJob
|
|
124
|
-
def perform(order)
|
|
125
|
-
# ...
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
> Pgbus supports concurrency controls via `Pgbus::Concurrency`:
|
|
131
|
-
> ```ruby
|
|
132
|
-
> class ProcessOrderJob < ApplicationJob
|
|
133
|
-
> include Pgbus::Concurrency
|
|
134
|
-
> limits_concurrency to: 1,
|
|
135
|
-
> key: -> { "ProcessOrder-#{arguments.first.id}" },
|
|
136
|
-
> duration: 15.minutes,
|
|
137
|
-
> on_conflict: :block
|
|
138
|
-
> def perform(order)
|
|
139
|
-
> # ...
|
|
140
|
-
> end
|
|
141
|
-
> end
|
|
142
|
-
> ```
|
|
143
|
-
|
|
144
|
-
## Step 5: Migrate batches
|
|
145
|
-
|
|
146
|
-
If you use `GoodJob::Batch`:
|
|
147
|
-
|
|
148
|
-
```ruby
|
|
149
|
-
# Before: GoodJob batches
|
|
150
|
-
GoodJob::Batch.enqueue(
|
|
151
|
-
on_finish: BatchCallbackJob,
|
|
152
|
-
on_success: SuccessNotifyJob,
|
|
153
|
-
description: "Import users"
|
|
154
|
-
) do
|
|
155
|
-
users.each { |u| ImportUserJob.perform_later(u) }
|
|
156
|
-
end
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Pgbus does not yet have batch support. Workaround using a coordinator job:
|
|
160
|
-
|
|
161
|
-
```ruby
|
|
162
|
-
# After: Coordinator pattern
|
|
163
|
-
class ImportUsersJob < ApplicationJob
|
|
164
|
-
def perform(user_ids)
|
|
165
|
-
user_ids.each { |id| ImportUserJob.perform_later(id) }
|
|
166
|
-
# Track completion via a counter in the database or Redis
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
## Step 6: Migrate cron / recurring jobs
|
|
172
|
-
|
|
173
|
-
```ruby
|
|
174
|
-
# Before: GoodJob cron
|
|
175
|
-
config.good_job.enable_cron = true
|
|
176
|
-
config.good_job.cron = {
|
|
177
|
-
daily_cleanup: {
|
|
178
|
-
cron: "0 2 * * *",
|
|
179
|
-
class: "CleanupJob",
|
|
180
|
-
set: { priority: -10, queue: "maintenance" },
|
|
181
|
-
},
|
|
182
|
-
hourly_sync: {
|
|
183
|
-
cron: "0 * * * *",
|
|
184
|
-
class: "SyncJob",
|
|
185
|
-
args: [42],
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Pgbus does not yet have built-in recurring task support. Options:
|
|
191
|
-
|
|
192
|
-
1. **Use the `whenever` gem**:
|
|
193
|
-
```ruby
|
|
194
|
-
# config/schedule.rb
|
|
195
|
-
every 1.day, at: "2:00 am" do
|
|
196
|
-
runner "CleanupJob.perform_later"
|
|
197
|
-
end
|
|
198
|
-
every :hour do
|
|
199
|
-
runner "SyncJob.perform_later(42)"
|
|
200
|
-
end
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
2. **Use system cron** directly:
|
|
204
|
-
```cron
|
|
205
|
-
0 2 * * * cd /app && bin/rails runner "CleanupJob.perform_later"
|
|
206
|
-
0 * * * * cd /app && bin/rails runner "SyncJob.perform_later(42)"
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
3. Wait for Pgbus recurring task support (planned).
|
|
210
|
-
|
|
211
|
-
## Step 7: Replace the dashboard
|
|
212
|
-
|
|
213
|
-
```ruby
|
|
214
|
-
# Before: config/routes.rb
|
|
215
|
-
mount GoodJob::Engine => "good_job"
|
|
216
|
-
|
|
217
|
-
# After:
|
|
218
|
-
mount Pgbus::Engine => "/pgbus"
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
## Step 8: Update process management
|
|
222
|
-
|
|
223
|
-
```bash
|
|
224
|
-
# Before (external mode)
|
|
225
|
-
bundle exec good_job start
|
|
226
|
-
|
|
227
|
-
# After
|
|
228
|
-
bundle exec pgbus start
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
If you ran GoodJob in `async` mode (in-process), note that Pgbus always runs as separate forked processes managed by a supervisor. Update your deployment accordingly -- you need `bundle exec pgbus start` as a separate process.
|
|
232
|
-
|
|
233
|
-
## Step 9: Clean up GoodJob tables
|
|
234
|
-
|
|
235
|
-
After verifying Pgbus is processing correctly and GoodJob's tables are drained:
|
|
236
|
-
|
|
237
|
-
```ruby
|
|
238
|
-
class RemoveGoodJob < ActiveRecord::Migration[7.1]
|
|
239
|
-
def up
|
|
240
|
-
drop_table :good_jobs, if_exists: true
|
|
241
|
-
drop_table :good_job_batches, if_exists: true
|
|
242
|
-
drop_table :good_job_executions, if_exists: true
|
|
243
|
-
drop_table :good_job_processes, if_exists: true
|
|
244
|
-
drop_table :good_job_settings, if_exists: true
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## What you gain
|
|
250
|
-
|
|
251
|
-
- **Dead letter queues** -- GoodJob retries in-place and marks jobs as discarded. Pgbus routes failures to dedicated `_dlq` queues for clear operational visibility.
|
|
252
|
-
- **Worker recycling** -- GoodJob workers run indefinitely. Pgbus recycles by job count, memory, or lifetime to prevent memory bloat.
|
|
253
|
-
- **Event bus** -- AMQP-style pub/sub with topic routing and idempotent handlers.
|
|
254
|
-
- **PGMQ** -- purpose-built message queue extension with atomic read/archive/delete, visibility timeouts, and `SKIP LOCKED` under the hood.
|
|
255
|
-
- **Supervisor/fork model** -- isolated worker processes. A memory leak or crash in one worker doesn't affect others.
|
|
256
|
-
|
|
257
|
-
## What you lose (for now)
|
|
258
|
-
|
|
259
|
-
| GoodJob feature | Status in Pgbus |
|
|
260
|
-
|-----------------|-----------------|
|
|
261
|
-
| Concurrency controls (`good_job_control_concurrency_with`) | `Pgbus::Concurrency` with `limits_concurrency` DSL |
|
|
262
|
-
| Throttling (`enqueue_throttle`, `perform_throttle`) | Planned |
|
|
263
|
-
| Batches (`GoodJob::Batch`) | `Pgbus::Batch` with on_finish/on_success/on_discard callbacks |
|
|
264
|
-
| Cron / recurring jobs | Planned |
|
|
265
|
-
| Async execution mode (in-process) | Not planned (forked processes only) |
|
|
266
|
-
| Capsules (isolated thread pools) | Workers are isolated by design (forked processes) |
|
|
267
|
-
| Advisory locks | Replaced by PGMQ visibility timeouts |
|
|
268
|
-
|
|
269
|
-
## Gotchas
|
|
270
|
-
|
|
271
|
-
1. **PgBouncer**: Both GoodJob and Pgbus use LISTEN/NOTIFY, which requires session-mode PgBouncer or direct connections. If you already had GoodJob working with PgBouncer, the same configuration applies.
|
|
272
|
-
|
|
273
|
-
2. **No async mode**: GoodJob can run in-process (`:async` mode) alongside your Rails app server. Pgbus requires a separate supervisor process. Make sure your deployment runs `bundle exec pgbus start` alongside your web server.
|
|
274
|
-
|
|
275
|
-
3. **Advisory locks vs. visibility timeouts**: GoodJob uses PostgreSQL advisory locks to claim jobs. Pgbus uses PGMQ's visibility timeout -- a claimed message becomes invisible for `visibility_timeout` seconds. If a worker crashes without archiving the message, it automatically becomes available again after the timeout expires. This is more resilient than advisory locks, which release on disconnect.
|
|
276
|
-
|
|
277
|
-
4. **Job record preservation**: GoodJob has `preserve_job_records` for keeping completed job records. PGMQ automatically archives completed messages to `a_pgbus_*` tables, which serve a similar purpose for debugging and auditing.
|
|
278
|
-
|
|
279
|
-
5. **Queue naming**: GoodJob uses bare queue names. Pgbus prefixes all queues (`pgbus_default`). Your `queue_as` declarations work unchanged -- the prefix is applied automatically.
|
data/docs/switch_from_sidekiq.md
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# Switch from Sidekiq to Pgbus
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Sidekiq uses Redis as its message broker. Pgbus uses PostgreSQL via PGMQ. Switching eliminates Redis from your infrastructure and gives you dead letter queues, worker recycling, and an event bus -- all backed by your existing database.
|
|
6
|
-
|
|
7
|
-
**Effort estimate:** Low if you use ActiveJob exclusively. Medium-high if you use native Sidekiq workers or Pro/Enterprise features.
|
|
8
|
-
|
|
9
|
-
## Step 1: Update dependencies
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
# Gemfile
|
|
13
|
-
|
|
14
|
-
# Remove
|
|
15
|
-
gem "sidekiq"
|
|
16
|
-
gem "sidekiq-cron" # if used
|
|
17
|
-
gem "sidekiq-unique-jobs" # if used
|
|
18
|
-
|
|
19
|
-
# Add
|
|
20
|
-
gem "pgbus"
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
bundle install
|
|
25
|
-
rails generate pgbus:install
|
|
26
|
-
rails db:migrate
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Step 2: Switch the adapter
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
# config/application.rb (or config/environments/production.rb)
|
|
33
|
-
|
|
34
|
-
# Before
|
|
35
|
-
config.active_job.queue_adapter = :sidekiq
|
|
36
|
-
|
|
37
|
-
# After
|
|
38
|
-
config.active_job.queue_adapter = :pgbus
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Step 3: Convert native Sidekiq workers
|
|
42
|
-
|
|
43
|
-
If all your jobs inherit from `ApplicationJob` (ActiveJob), skip this step -- they work unchanged.
|
|
44
|
-
|
|
45
|
-
If you have native Sidekiq workers using `include Sidekiq::Job`, convert them:
|
|
46
|
-
|
|
47
|
-
```ruby
|
|
48
|
-
# Before: Native Sidekiq worker
|
|
49
|
-
class HardWorker
|
|
50
|
-
include Sidekiq::Job
|
|
51
|
-
sidekiq_options queue: :critical, retry: 5
|
|
52
|
-
|
|
53
|
-
def perform(user_id, action)
|
|
54
|
-
user = User.find(user_id)
|
|
55
|
-
# ...
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Enqueue
|
|
60
|
-
HardWorker.perform_async(user.id, "activate")
|
|
61
|
-
HardWorker.perform_at(5.minutes.from_now, user.id, "activate")
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
# After: ActiveJob
|
|
66
|
-
class HardWorker < ApplicationJob
|
|
67
|
-
queue_as :critical
|
|
68
|
-
retry_on StandardError, wait: :polynomially_longer, attempts: 5
|
|
69
|
-
|
|
70
|
-
def perform(user_id, action)
|
|
71
|
-
user = User.find(user_id)
|
|
72
|
-
# ...
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Enqueue
|
|
77
|
-
HardWorker.perform_later(user.id, "activate")
|
|
78
|
-
HardWorker.set(wait: 5.minutes).perform_later(user.id, "activate")
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### API mapping
|
|
82
|
-
|
|
83
|
-
| Sidekiq | ActiveJob / Pgbus |
|
|
84
|
-
|---------|-------------------|
|
|
85
|
-
| `perform_async(args)` | `perform_later(args)` |
|
|
86
|
-
| `perform_at(time, args)` | `.set(wait_until: time).perform_later(args)` |
|
|
87
|
-
| `perform_in(duration, args)` | `.set(wait: duration).perform_later(args)` |
|
|
88
|
-
| `sidekiq_options queue: :name` | `queue_as :name` |
|
|
89
|
-
| `sidekiq_options retry: N` | `retry_on StandardError, attempts: N` |
|
|
90
|
-
| `sidekiq_retries_exhausted` | `discard_on` + `after_discard` callback |
|
|
91
|
-
|
|
92
|
-
## Step 4: Replace middleware with ActiveJob callbacks
|
|
93
|
-
|
|
94
|
-
Sidekiq middleware wraps job push (client) and execution (server) in a Rack-style chain. ActiveJob provides equivalent hooks.
|
|
95
|
-
|
|
96
|
-
```ruby
|
|
97
|
-
# Before: Sidekiq server middleware
|
|
98
|
-
class LoggingMiddleware
|
|
99
|
-
include Sidekiq::ServerMiddleware
|
|
100
|
-
def call(job_instance, job_payload, queue)
|
|
101
|
-
Rails.logger.info("Starting #{job_payload['class']}")
|
|
102
|
-
yield
|
|
103
|
-
Rails.logger.info("Finished #{job_payload['class']}")
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
# After: ActiveJob callback (add to ApplicationJob or per-job)
|
|
110
|
-
class ApplicationJob < ActiveJob::Base
|
|
111
|
-
around_perform do |job, block|
|
|
112
|
-
Rails.logger.info("Starting #{job.class.name}")
|
|
113
|
-
block.call
|
|
114
|
-
Rails.logger.info("Finished #{job.class.name}")
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### Callback mapping
|
|
120
|
-
|
|
121
|
-
| Sidekiq middleware | ActiveJob callback |
|
|
122
|
-
|--------------------|-------------------|
|
|
123
|
-
| Server middleware (before yield) | `before_perform` |
|
|
124
|
-
| Server middleware (around yield) | `around_perform` |
|
|
125
|
-
| Server middleware (after yield) | `after_perform` |
|
|
126
|
-
| Client middleware (before yield) | `before_enqueue` |
|
|
127
|
-
| Client middleware (around yield) | `around_enqueue` |
|
|
128
|
-
| Client middleware (after yield) | `after_enqueue` |
|
|
129
|
-
|
|
130
|
-
## Step 5: Configure workers
|
|
131
|
-
|
|
132
|
-
```yaml
|
|
133
|
-
# Before: config/sidekiq.yml
|
|
134
|
-
:concurrency: 10
|
|
135
|
-
:queues:
|
|
136
|
-
- [critical, 3]
|
|
137
|
-
- [default, 2]
|
|
138
|
-
- [low, 1]
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
```yaml
|
|
142
|
-
# After: config/pgbus.yml
|
|
143
|
-
production:
|
|
144
|
-
workers:
|
|
145
|
-
- queues: [critical]
|
|
146
|
-
threads: 5
|
|
147
|
-
- queues: [default, low]
|
|
148
|
-
threads: 10
|
|
149
|
-
max_jobs_per_worker: 10000
|
|
150
|
-
max_memory_mb: 512
|
|
151
|
-
max_worker_lifetime: 3600
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Step 6: Replace the dashboard
|
|
155
|
-
|
|
156
|
-
```ruby
|
|
157
|
-
# Before: config/routes.rb
|
|
158
|
-
require "sidekiq/web"
|
|
159
|
-
mount Sidekiq::Web => "/sidekiq"
|
|
160
|
-
|
|
161
|
-
# After:
|
|
162
|
-
mount Pgbus::Engine => "/pgbus"
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
If you used `Sidekiq::Web`'s authentication:
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
Pgbus.configure do |config|
|
|
169
|
-
config.web_auth = ->(request) {
|
|
170
|
-
request.env["warden"].user&.admin?
|
|
171
|
-
}
|
|
172
|
-
end
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Step 7: Update process management
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
# Before
|
|
179
|
-
bundle exec sidekiq
|
|
180
|
-
|
|
181
|
-
# After
|
|
182
|
-
bundle exec pgbus start
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Update your `Procfile`, systemd units, or container entrypoints accordingly.
|
|
186
|
-
|
|
187
|
-
## Step 8: Remove Redis
|
|
188
|
-
|
|
189
|
-
Once all Sidekiq jobs have drained and you've verified Pgbus is processing correctly, remove Redis from your infrastructure:
|
|
190
|
-
|
|
191
|
-
- Remove `REDIS_URL` / Sidekiq Redis config from environment
|
|
192
|
-
- Remove `Sidekiq.configure_server` / `Sidekiq.configure_client` blocks
|
|
193
|
-
- Remove Redis from `docker-compose.yml`, Terraform, etc.
|
|
194
|
-
|
|
195
|
-
## What you gain
|
|
196
|
-
|
|
197
|
-
- **No Redis dependency** -- one fewer service to operate
|
|
198
|
-
- **Dead letter queues** -- failed jobs route to `_dlq` queues after `max_retries`, visible in the dashboard
|
|
199
|
-
- **Worker recycling** -- memory, job count, and lifetime limits prevent the memory bloat that plagues long-running Sidekiq processes
|
|
200
|
-
- **Event bus** -- AMQP-style topic routing for pub/sub, built into the same infrastructure
|
|
201
|
-
- **LISTEN/NOTIFY** -- instant job wake-up without polling overhead
|
|
202
|
-
|
|
203
|
-
## What you lose (for now)
|
|
204
|
-
|
|
205
|
-
| Sidekiq feature | Status in Pgbus |
|
|
206
|
-
|-----------------|-----------------|
|
|
207
|
-
| Batches (Pro) | `Pgbus::Batch` with on_finish/on_success/on_discard callbacks |
|
|
208
|
-
| Rate limiting (Enterprise) | Planned |
|
|
209
|
-
| Concurrency controls (Enterprise) | `Pgbus::Concurrency` with `limits_concurrency` DSL |
|
|
210
|
-
| Unique jobs (Enterprise / `sidekiq-unique-jobs`) | Partial -- event bus has idempotency; job-level dedup planned |
|
|
211
|
-
| Cron / recurring jobs (`sidekiq-cron`) | Planned |
|
|
212
|
-
| Real-time metrics (Sidekiq Web) | Pgbus dashboard covers queue depth, failures, processes |
|
|
213
|
-
|
|
214
|
-
## Gotchas
|
|
215
|
-
|
|
216
|
-
1. **Argument serialization**: Sidekiq passes raw JSON; ActiveJob uses GlobalID for ActiveRecord objects. If you pass raw IDs (`user.id`), both work the same. If you pass AR objects (`user`), ActiveJob serializes via GlobalID automatically.
|
|
217
|
-
|
|
218
|
-
2. **`Sidekiq::Testing.fake!`**: Replace with `ActiveJob::TestHelper`:
|
|
219
|
-
```ruby
|
|
220
|
-
include ActiveJob::TestHelper
|
|
221
|
-
assert_enqueued_jobs 1 { MyJob.perform_later(args) }
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
3. **`Sidekiq.redis { |conn| ... }`**: If you use Sidekiq's Redis connection for custom caching or distributed locks, you'll need a separate solution (e.g., PostgreSQL advisory locks, `with_advisory_lock` gem).
|
|
225
|
-
|
|
226
|
-
4. **Job priorities**: Sidekiq uses queue weight ordering. Pgbus processes queues in the order listed in the worker config. Put higher-priority queues first.
|