pgbus 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.bun-version +1 -0
  3. data/.claude/commands/architect.md +100 -0
  4. data/.claude/commands/github-review-comments.md +237 -0
  5. data/.claude/commands/lfg.md +271 -0
  6. data/.claude/commands/review-pr.md +69 -0
  7. data/.claude/commands/security.md +122 -0
  8. data/.claude/commands/tdd.md +148 -0
  9. data/.claude/rules/agents.md +49 -0
  10. data/.claude/rules/coding-style.md +91 -0
  11. data/.claude/rules/git-workflow.md +56 -0
  12. data/.claude/rules/performance.md +73 -0
  13. data/.claude/rules/testing.md +67 -0
  14. data/CHANGELOG.md +5 -0
  15. data/CLAUDE.md +80 -0
  16. data/CODE_OF_CONDUCT.md +10 -0
  17. data/LICENSE.txt +21 -0
  18. data/README.md +417 -0
  19. data/Rakefile +14 -0
  20. data/app/controllers/pgbus/api/stats_controller.rb +11 -0
  21. data/app/controllers/pgbus/application_controller.rb +35 -0
  22. data/app/controllers/pgbus/dashboard_controller.rb +27 -0
  23. data/app/controllers/pgbus/dead_letter_controller.rb +50 -0
  24. data/app/controllers/pgbus/events_controller.rb +23 -0
  25. data/app/controllers/pgbus/jobs_controller.rb +48 -0
  26. data/app/controllers/pgbus/processes_controller.rb +10 -0
  27. data/app/controllers/pgbus/queues_controller.rb +21 -0
  28. data/app/helpers/pgbus/application_helper.rb +69 -0
  29. data/app/views/layouts/pgbus/application.html.erb +76 -0
  30. data/app/views/pgbus/dashboard/_processes_table.html.erb +30 -0
  31. data/app/views/pgbus/dashboard/_queues_table.html.erb +39 -0
  32. data/app/views/pgbus/dashboard/_recent_failures.html.erb +33 -0
  33. data/app/views/pgbus/dashboard/_stats_cards.html.erb +28 -0
  34. data/app/views/pgbus/dashboard/show.html.erb +10 -0
  35. data/app/views/pgbus/dead_letter/_messages_table.html.erb +40 -0
  36. data/app/views/pgbus/dead_letter/index.html.erb +15 -0
  37. data/app/views/pgbus/dead_letter/show.html.erb +52 -0
  38. data/app/views/pgbus/events/index.html.erb +57 -0
  39. data/app/views/pgbus/events/show.html.erb +28 -0
  40. data/app/views/pgbus/jobs/_enqueued_table.html.erb +34 -0
  41. data/app/views/pgbus/jobs/_failed_table.html.erb +45 -0
  42. data/app/views/pgbus/jobs/index.html.erb +16 -0
  43. data/app/views/pgbus/jobs/show.html.erb +57 -0
  44. data/app/views/pgbus/processes/_processes_table.html.erb +37 -0
  45. data/app/views/pgbus/processes/index.html.erb +3 -0
  46. data/app/views/pgbus/queues/_queues_list.html.erb +41 -0
  47. data/app/views/pgbus/queues/index.html.erb +3 -0
  48. data/app/views/pgbus/queues/show.html.erb +49 -0
  49. data/bun.lock +18 -0
  50. data/config/routes.rb +45 -0
  51. data/docs/README.md +28 -0
  52. data/docs/switch_from_good_job.md +279 -0
  53. data/docs/switch_from_sidekiq.md +226 -0
  54. data/docs/switch_from_solid_queue.md +247 -0
  55. data/exe/pgbus +7 -0
  56. data/lib/generators/pgbus/install_generator.rb +56 -0
  57. data/lib/generators/pgbus/templates/migration.rb.erb +114 -0
  58. data/lib/generators/pgbus/templates/pgbus.yml.erb +74 -0
  59. data/lib/generators/pgbus/templates/pgbus_binstub.erb +7 -0
  60. data/lib/pgbus/active_job/adapter.rb +109 -0
  61. data/lib/pgbus/active_job/executor.rb +107 -0
  62. data/lib/pgbus/batch.rb +153 -0
  63. data/lib/pgbus/cli.rb +84 -0
  64. data/lib/pgbus/client.rb +162 -0
  65. data/lib/pgbus/concurrency/blocked_execution.rb +74 -0
  66. data/lib/pgbus/concurrency/semaphore.rb +66 -0
  67. data/lib/pgbus/concurrency.rb +65 -0
  68. data/lib/pgbus/config_loader.rb +27 -0
  69. data/lib/pgbus/configuration.rb +99 -0
  70. data/lib/pgbus/engine.rb +31 -0
  71. data/lib/pgbus/event.rb +31 -0
  72. data/lib/pgbus/event_bus/handler.rb +76 -0
  73. data/lib/pgbus/event_bus/publisher.rb +42 -0
  74. data/lib/pgbus/event_bus/registry.rb +54 -0
  75. data/lib/pgbus/event_bus/subscriber.rb +30 -0
  76. data/lib/pgbus/process/consumer.rb +113 -0
  77. data/lib/pgbus/process/dispatcher.rb +154 -0
  78. data/lib/pgbus/process/heartbeat.rb +71 -0
  79. data/lib/pgbus/process/signal_handler.rb +49 -0
  80. data/lib/pgbus/process/supervisor.rb +198 -0
  81. data/lib/pgbus/process/worker.rb +153 -0
  82. data/lib/pgbus/serializer.rb +43 -0
  83. data/lib/pgbus/version.rb +5 -0
  84. data/lib/pgbus/web/authentication.rb +24 -0
  85. data/lib/pgbus/web/data_source.rb +406 -0
  86. data/lib/pgbus.rb +49 -0
  87. data/package.json +9 -0
  88. data/sig/pgbus.rbs +4 -0
  89. metadata +198 -0
@@ -0,0 +1,226 @@
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.
@@ -0,0 +1,247 @@
1
+ # Switch from SolidQueue to Pgbus
2
+
3
+ ## Overview
4
+
5
+ SolidQueue and Pgbus are both PostgreSQL-backed job processors with similar architectures: supervisor/worker process model, `FOR UPDATE SKIP LOCKED` for contention-free polling, and forked processes. The migration is straightforward since both are ActiveJob adapters.
6
+
7
+ **Key differences:** Pgbus adds LISTEN/NOTIFY for instant wake-up (SolidQueue only polls), dead letter queues, worker recycling, and an event bus. Pgbus uses PGMQ under the hood instead of custom tables.
8
+
9
+ **Effort estimate:** Low. Both are pure ActiveJob adapters, so your jobs work unchanged.
10
+
11
+ ## Step 1: Update dependencies
12
+
13
+ ```ruby
14
+ # Gemfile
15
+
16
+ # Remove
17
+ gem "solid_queue"
18
+ gem "mission_control-jobs" # if used
19
+
20
+ # Add
21
+ gem "pgbus"
22
+ ```
23
+
24
+ ```bash
25
+ bundle install
26
+ rails generate pgbus:install
27
+ rails db:migrate
28
+ ```
29
+
30
+ ## Step 2: Switch the adapter
31
+
32
+ ```ruby
33
+ # config/application.rb
34
+
35
+ # Before
36
+ config.active_job.queue_adapter = :solid_queue
37
+
38
+ # After
39
+ config.active_job.queue_adapter = :pgbus
40
+ ```
41
+
42
+ If you set the adapter per-environment or use `config.solid_queue.connects_to` for a separate queue database, update those too.
43
+
44
+ ## Step 3: Convert worker configuration
45
+
46
+ ```yaml
47
+ # Before: config/queue.yml (SolidQueue)
48
+ production:
49
+ dispatchers:
50
+ - polling_interval: 1
51
+ batch_size: 500
52
+ workers:
53
+ - queues: "critical"
54
+ threads: 5
55
+ processes: 2
56
+ polling_interval: 0.1
57
+ - queues: "default,low"
58
+ threads: 3
59
+ processes: 3
60
+ polling_interval: 1
61
+ ```
62
+
63
+ ```yaml
64
+ # After: config/pgbus.yml
65
+ production:
66
+ workers:
67
+ - queues: [critical]
68
+ threads: 5
69
+ - queues: [critical]
70
+ threads: 5
71
+ - queues: [default, low]
72
+ threads: 3
73
+ - queues: [default, low]
74
+ threads: 3
75
+ - queues: [default, low]
76
+ threads: 3
77
+ max_retries: 5
78
+ max_jobs_per_worker: 10000
79
+ max_memory_mb: 512
80
+ ```
81
+
82
+ > **Note:** SolidQueue's `processes: N` forks N identical workers. In Pgbus, list the same worker config N times, or let the supervisor handle it via configuration (one entry per process).
83
+
84
+ ### Configuration mapping
85
+
86
+ | SolidQueue | Pgbus | Notes |
87
+ |------------|-------|-------|
88
+ | `polling_interval` | `polling_interval` | Pgbus defaults to 0.1s; LISTEN/NOTIFY makes this a fallback only |
89
+ | `threads` | `threads` | Same concept |
90
+ | `processes` | Repeat worker entry | One entry per forked process |
91
+ | `dispatchers[].batch_size` | N/A | Pgbus dispatcher does maintenance, not dispatch |
92
+ | `queues: "a,b"` (string) | `queues: [a, b]` (array) | Different format |
93
+ | `queues: "*"` (wildcard) | List queues explicitly | PGMQ queues are explicit |
94
+
95
+ ## Step 4: Remove concurrency controls
96
+
97
+ SolidQueue's `limits_concurrency` is a SolidQueue-specific mixin. Remove it from your jobs:
98
+
99
+ ```ruby
100
+ # Before: SolidQueue concurrency control
101
+ class ProcessOrderJob < ApplicationJob
102
+ limits_concurrency to: 1,
103
+ key: ->(order) { order.account_id },
104
+ duration: 15.minutes,
105
+ on_conflict: :block
106
+
107
+ def perform(order)
108
+ # ...
109
+ end
110
+ end
111
+ ```
112
+
113
+ ```ruby
114
+ # After: Remove the SolidQueue mixin
115
+ class ProcessOrderJob < ApplicationJob
116
+ def perform(order)
117
+ # ...
118
+ end
119
+ end
120
+ ```
121
+
122
+ > Pgbus supports concurrency controls via `Pgbus::Concurrency`:
123
+ > ```ruby
124
+ > class ProcessOrderJob < ApplicationJob
125
+ > include Pgbus::Concurrency
126
+ > limits_concurrency to: 1,
127
+ > key: ->(order) { order.account_id },
128
+ > duration: 15.minutes,
129
+ > on_conflict: :block
130
+ > def perform(order)
131
+ > # ...
132
+ > end
133
+ > end
134
+ > ```
135
+
136
+ ## Step 5: Migrate recurring tasks
137
+
138
+ If you use SolidQueue's `config/recurring.yml`:
139
+
140
+ ```yaml
141
+ # Before: config/recurring.yml (SolidQueue)
142
+ production:
143
+ daily_cleanup:
144
+ class: CleanupJob
145
+ schedule: "every day at 2am"
146
+ queue: maintenance
147
+ hourly_sync:
148
+ class: SyncJob
149
+ schedule: "0 * * * *"
150
+ args: [42, "sync"]
151
+ ```
152
+
153
+ Pgbus does not yet have built-in recurring task support. Options:
154
+
155
+ 1. **Use the `whenever` gem** for cron-based scheduling:
156
+ ```ruby
157
+ # config/schedule.rb (whenever gem)
158
+ every 1.day, at: "2:00 am" do
159
+ runner "CleanupJob.perform_later"
160
+ end
161
+ every :hour do
162
+ runner "SyncJob.perform_later(42, 'sync')"
163
+ end
164
+ ```
165
+
166
+ 2. **Use system cron** directly:
167
+ ```cron
168
+ 0 2 * * * cd /app && bin/rails runner "CleanupJob.perform_later"
169
+ 0 * * * * cd /app && bin/rails runner "SyncJob.perform_later(42, 'sync')"
170
+ ```
171
+
172
+ 3. Wait for Pgbus recurring task support (planned).
173
+
174
+ ## Step 6: Replace the dashboard
175
+
176
+ ```ruby
177
+ # Before: config/routes.rb
178
+ mount MissionControl::Jobs::Engine, at: "/jobs"
179
+
180
+ # After:
181
+ mount Pgbus::Engine => "/pgbus"
182
+ ```
183
+
184
+ ## Step 7: Update process management
185
+
186
+ ```bash
187
+ # Before
188
+ bundle exec rake solid_queue:start
189
+
190
+ # After
191
+ bundle exec pgbus start
192
+ ```
193
+
194
+ ## Step 8: Clean up SolidQueue tables
195
+
196
+ After verifying Pgbus processes jobs correctly and SolidQueue's tables are drained:
197
+
198
+ ```ruby
199
+ class RemoveSolidQueue < ActiveRecord::Migration[7.1]
200
+ def up
201
+ drop_table :solid_queue_blocked_executions, if_exists: true
202
+ drop_table :solid_queue_claimed_executions, if_exists: true
203
+ drop_table :solid_queue_failed_executions, if_exists: true
204
+ drop_table :solid_queue_pauses, if_exists: true
205
+ drop_table :solid_queue_processes, if_exists: true
206
+ drop_table :solid_queue_ready_executions, if_exists: true
207
+ drop_table :solid_queue_recurring_executions, if_exists: true
208
+ drop_table :solid_queue_recurring_tasks, if_exists: true
209
+ drop_table :solid_queue_scheduled_executions, if_exists: true
210
+ drop_table :solid_queue_semaphores, if_exists: true
211
+ drop_table :solid_queue_jobs, if_exists: true
212
+ end
213
+ end
214
+ ```
215
+
216
+ ## What you gain
217
+
218
+ - **LISTEN/NOTIFY** -- SolidQueue only polls (100-150ms latency baseline). Pgbus wakes workers instantly via PostgreSQL LISTEN/NOTIFY, with polling as fallback only.
219
+ - **Dead letter queues** -- SolidQueue marks jobs as failed but keeps them in the same table. Pgbus routes failures to dedicated `_dlq` queues after `max_retries` for clear separation.
220
+ - **Worker recycling** -- SolidQueue workers run indefinitely. Pgbus recycles workers by job count, memory, or lifetime to prevent memory bloat.
221
+ - **Event bus** -- AMQP-style pub/sub with topic routing, not available in SolidQueue.
222
+ - **PGMQ under the hood** -- battle-tested message queue extension with visibility timeouts and atomic operations.
223
+
224
+ ## What you lose (for now)
225
+
226
+ | SolidQueue feature | Status in Pgbus |
227
+ |--------------------|-----------------|
228
+ | `limits_concurrency` | `Pgbus::Concurrency` with `limits_concurrency` DSL |
229
+ | `config/recurring.yml` | Planned |
230
+ | Queue pausing (`SolidQueue::Queue.pause`) | Planned |
231
+ | Separate queue database | Not planned (PGMQ lives in your primary DB) |
232
+ | Numeric job priorities | PGMQ reads in FIFO order per queue; use separate queues for priority |
233
+
234
+ ## Gotchas
235
+
236
+ 1. **PgBouncer**: If you run PgBouncer in transaction mode, LISTEN/NOTIFY won't work. Use session mode or direct connections for Pgbus worker processes. This also applies to PGMQ's `FOR UPDATE SKIP LOCKED`.
237
+
238
+ 2. **Separate queue database**: SolidQueue supports `connects_to` for a dedicated queue DB. Pgbus requires PGMQ in the same database it connects to. If you need isolation, use a separate PostgreSQL database with PGMQ installed and configure `Pgbus.configuration.database_url`.
239
+
240
+ 3. **Queue naming**: SolidQueue uses bare queue names (`default`). Pgbus prefixes all queues (`pgbus_default`). Your `queue_as :default` declarations work unchanged -- the prefix is applied automatically.
241
+
242
+ 4. **`ActionMailer::MailDeliveryJob`**: This can bypass the application-level adapter setting in some Rails versions. If mailer jobs don't appear in Pgbus, add to `ApplicationMailer`:
243
+ ```ruby
244
+ class ApplicationMailer < ActionMailer::Base
245
+ self.deliver_later_queue_name = :mailers
246
+ end
247
+ ```
data/exe/pgbus ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "pgbus"
5
+ require "pgbus/cli"
6
+
7
+ Pgbus::CLI.start(ARGV)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Pgbus
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Install Pgbus: create migration, config file, and binstub"
14
+
15
+ def create_migration
16
+ migration_template "migration.rb.erb", "db/migrate/install_pgbus.rb"
17
+ end
18
+
19
+ def create_config_file
20
+ template "pgbus.yml.erb", "config/pgbus.yml"
21
+ end
22
+
23
+ def create_binstub
24
+ template "pgbus_binstub.erb", "bin/pgbus"
25
+ chmod "bin/pgbus", 0o755
26
+ end
27
+
28
+ def configure_active_job
29
+ application_config = "config.active_job.queue_adapter = :pgbus"
30
+
31
+ return unless File.exist?(File.join(destination_root, "config", "application.rb"))
32
+
33
+ inject_into_file "config/application.rb",
34
+ "\n #{application_config}\n",
35
+ after: "class Application < Rails::Application\n"
36
+ end
37
+
38
+ def display_post_install
39
+ say ""
40
+ say "Pgbus installed successfully!", :green
41
+ say ""
42
+ say "Next steps:"
43
+ say " 1. Run: rails db:migrate"
44
+ say " 2. Edit config/pgbus.yml to configure workers"
45
+ say " 3. Start processing: bin/pgbus start"
46
+ say ""
47
+ end
48
+
49
+ private
50
+
51
+ def migration_version
52
+ "[#{ActiveRecord::Migration.current_version}]"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,114 @@
1
+ class InstallPgbus < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ # Install PGMQ extension
4
+ enable_extension "pgmq"
5
+
6
+ # Idempotent event processing deduplication
7
+ create_table :pgbus_processed_events do |t|
8
+ t.string :event_id, null: false
9
+ t.string :handler_class, null: false
10
+ t.datetime :processed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
11
+ end
12
+
13
+ add_index :pgbus_processed_events, [:event_id, :handler_class],
14
+ unique: true, name: "idx_pgbus_processed_events_unique"
15
+ add_index :pgbus_processed_events, :processed_at,
16
+ name: "idx_pgbus_processed_events_cleanup"
17
+
18
+ # Process registry (heartbeats, monitoring)
19
+ create_table :pgbus_processes do |t|
20
+ t.string :kind, null: false
21
+ t.string :hostname
22
+ t.integer :pid
23
+ t.jsonb :metadata, default: {}
24
+ t.datetime :last_heartbeat_at
25
+ t.timestamps
26
+ end
27
+
28
+ add_index :pgbus_processes, :last_heartbeat_at,
29
+ name: "idx_pgbus_processes_heartbeat"
30
+
31
+ # Failed events log (for dashboard visibility)
32
+ create_table :pgbus_failed_events do |t|
33
+ t.string :queue_name, null: false
34
+ t.bigint :msg_id
35
+ t.jsonb :payload
36
+ t.jsonb :headers
37
+ t.string :error_class
38
+ t.text :error_message
39
+ t.text :backtrace
40
+ t.integer :retry_count, default: 0
41
+ t.datetime :failed_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
42
+ end
43
+
44
+ add_index :pgbus_failed_events, :queue_name, name: "idx_pgbus_failed_events_queue"
45
+ add_index :pgbus_failed_events, :failed_at, name: "idx_pgbus_failed_events_time"
46
+
47
+ # Concurrency semaphores (counting locks for job concurrency limits)
48
+ create_table :pgbus_semaphores do |t|
49
+ t.string :key, null: false
50
+ t.integer :value, null: false, default: 0
51
+ t.integer :max_value, null: false, default: 1
52
+ t.datetime :expires_at, null: false
53
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
54
+ end
55
+
56
+ add_index :pgbus_semaphores, :key,
57
+ unique: true, name: "idx_pgbus_semaphores_key"
58
+ add_index :pgbus_semaphores, :expires_at,
59
+ name: "idx_pgbus_semaphores_expired"
60
+
61
+ # Blocked executions (jobs waiting for a concurrency semaphore)
62
+ create_table :pgbus_blocked_executions do |t|
63
+ t.string :concurrency_key, null: false
64
+ t.string :queue_name, null: false
65
+ t.jsonb :payload, null: false
66
+ t.integer :priority, null: false, default: 0
67
+ t.datetime :expires_at, null: false
68
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
69
+ end
70
+
71
+ add_index :pgbus_blocked_executions, [:concurrency_key, :priority, :created_at],
72
+ name: "idx_pgbus_blocked_release_order"
73
+
74
+ # Batch tracking (coordinated job groups with callbacks)
75
+ create_table :pgbus_batches do |t|
76
+ t.string :batch_id, null: false
77
+ t.string :description
78
+ t.string :on_finish_class
79
+ t.string :on_success_class
80
+ t.string :on_discard_class
81
+ t.jsonb :properties, default: {}
82
+ t.integer :total_jobs, null: false, default: 0
83
+ t.integer :completed_jobs, null: false, default: 0
84
+ t.integer :failed_jobs, null: false, default: 0
85
+ t.integer :discarded_jobs, null: false, default: 0
86
+ t.string :status, null: false, default: "pending"
87
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
88
+ t.datetime :finished_at
89
+ end
90
+
91
+ add_index :pgbus_batches, :batch_id,
92
+ unique: true, name: "idx_pgbus_batches_batch_id"
93
+ add_index :pgbus_batches, :status,
94
+ name: "idx_pgbus_batches_status"
95
+
96
+ # Create default queues via PGMQ
97
+ execute "SELECT pgmq.create('pgbus_default')"
98
+ execute "SELECT pgmq.create('pgbus_default_dlq')"
99
+ end
100
+
101
+ def down
102
+ execute "SELECT pgmq.drop_queue('pgbus_default_dlq')"
103
+ execute "SELECT pgmq.drop_queue('pgbus_default')"
104
+
105
+ drop_table :pgbus_batches
106
+ drop_table :pgbus_blocked_executions
107
+ drop_table :pgbus_semaphores
108
+ drop_table :pgbus_failed_events
109
+ drop_table :pgbus_processes
110
+ drop_table :pgbus_processed_events
111
+
112
+ disable_extension "pgmq"
113
+ end
114
+ end