pgbus 0.11.0 → 0.11.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/CHANGELOG.md +3 -0
- data/README.md +19 -0
- data/lib/generators/pgbus/add_failed_events_index_generator.rb +1 -1
- data/lib/generators/pgbus/add_job_stats_generator.rb +1 -1
- data/lib/generators/pgbus/add_job_stats_latency_generator.rb +1 -1
- data/lib/generators/pgbus/add_job_stats_queue_index_generator.rb +1 -1
- data/lib/generators/pgbus/add_outbox_generator.rb +1 -1
- data/lib/generators/pgbus/add_presence_generator.rb +1 -1
- data/lib/generators/pgbus/add_queue_states_generator.rb +1 -1
- data/lib/generators/pgbus/add_recurring_generator.rb +1 -1
- data/lib/generators/pgbus/add_stream_queues_generator.rb +1 -1
- data/lib/generators/pgbus/add_stream_stats_generator.rb +1 -1
- data/lib/generators/pgbus/add_uniqueness_keys_generator.rb +1 -1
- data/lib/generators/pgbus/install_generator.rb +5 -2
- data/lib/generators/pgbus/migrate_job_locks_generator.rb +1 -1
- data/lib/generators/pgbus/migration_path.rb +73 -7
- data/lib/generators/pgbus/tune_autovacuum_generator.rb +1 -1
- data/lib/generators/pgbus/tune_fillfactor_generator.rb +1 -1
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +1 -1
- data/lib/pgbus/cli.rb +24 -0
- data/lib/pgbus/configuration.rb +44 -2
- data/lib/pgbus/doctor.rb +74 -15
- data/lib/pgbus/process/supervisor.rb +35 -0
- data/lib/pgbus/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 727dd6a7515414a2a426857ea19cc8cf33b8c5ca2311ecde116bf643114e8d85
|
|
4
|
+
data.tar.gz: c68f0e3c246b554059160e901e96d61f80c5a5a9aa1716d6e4787f84cfdbe655
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f515f94c6e585a038ab468ca30dc49a284c9666ddb0116998532ce2afdf602ac8309972fbe79606ad055b73bd95da418210778cf80a9e3062288855d67a3b746
|
|
7
|
+
data.tar.gz: dd748949d2330236bfbc46b94c04ff907268e067830a2c4b91b3ff6b811324b2383d3c46bc950add9511c98355b39b39d9f3a58c03a3fed361838c866b24bf1c
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- **`config.doctor_on_boot` (+ `pgbus start --doctor` / `--doctor-strict`) — run the doctor preflight inside the booting supervisor, one Rails boot instead of two (issue #347).** The recommended deploy preflight was `bin/pgbus doctor || true` in the container entrypoint before `bin/pgbus start` — but both commands `require config/environment`, so the job container boots the full Rails app **twice** on every deploy (cost scaling with app boot time), and running doctor pre-supervisor makes the `Process liveness` check false-fail (no workers exist yet), which is exactly why the entrypoint has to swallow the exit code with `|| true` — losing any chance to gate on a genuinely-fatal finding. Now `config.doctor_on_boot = :report` (or `pgbus start --doctor`) runs the checks in the already-booted supervisor, after the DB is verified reachable and queues are bootstrapped but **before any worker is forked**: one boot, and the worker-dependent `Process liveness` check is skipped (it has no workers to observe yet and would false-fail on stale prior-generation rows during a redeploy). `config.doctor_on_boot = :strict` (or `--doctor-strict`) additionally **refuses to boot** — raising before forking anything, so the `ensure shutdown` path tears down the heartbeat/health server and no child starts — but *only* on a genuinely-fatal, non-transient check: a `Configuration` failure (a real config bug) or an **absent** PGMQ schema. It deliberately does **not** abort on `Queues`/`Database` failures — those are the transients the lenient queue bootstrap is built to ride out (children crash-and-backoff until the DB recovers), and `verify_connection!` already gated a hard-down DB moments earlier; making them strict-fatal would take down a whole fleet's cold boot in lockstep on a momentarily-saturated primary. Default `nil`/`false` = off (byte-identical to today; the supervisor constructs no `Doctor` and does zero extra work). The standalone `pgbus doctor` command and its exit-code semantics are unchanged. Refs #347.
|
|
13
14
|
- **`Pgbus::Streams::PhlexHelpers` — a Phlex-includable `pgbus_stream_from`, and `Pgbus.stream_name_budget` (issue #334).** Two small helpers that stop consuming apps reimplementing pgbus glue. (1) `pgbus_stream_from` is a Rails view helper, so a Phlex component couldn't call it without hand-registering an output-helper macro (phlex-rails ships no `Phlex::Rails::Helpers::PgbusStreamFrom`). `include Pgbus::Streams::PhlexHelpers` now bridges it exactly like phlex-rails' own `TurboStreamFrom`. phlex-rails stays an **optional** dependency — the module references its `HelperMacros` and is loaded on demand (`require "pgbus/streams/phlex_helpers"`), never eagerly. (2) `Pgbus.stream_name_budget` exposes the maximum stream-name length (the pgmq queue-name cap minus the queue prefix) so apps size/truncate stream identifiers up front instead of hand-computing `MAX_QUEUE_NAME_LENGTH - queue_prefix.length - 1` at every call site. Refs #334.
|
|
14
15
|
- **`EventBus::Registry#setup_all!(safe: true)` — a boot/rake-safe way to eagerly set up event subscribers (issue #334).** Calling `setup_all!` opens a PGMQ connection per subscriber (to create its queue + bind its topic), which is wrong during a `db:`/schema rake task (a live connection blocks `DROP DATABASE`) and crashes boot if the database isn't up yet — so apps wrapped it in a hand-rolled multi-class rescue plus a rake-task skip. `setup_all!(safe: true)` bakes that in: it skips entirely in a schema/asset rake context and swallows a connection error with a warning instead of raising. The default `setup_all!` is unchanged (raises). Refs #334.
|
|
15
16
|
- **Connection-pooler safety (`config.require_primary`, `config.connection_guc_mode`) — pgbus is now correct-and-loud behind a transaction/read-write-splitting pooler instead of silently stalling (issue #332).** Three linked fixes. (1) **Primary affinity:** a read/write-splitting pooler (pgdog/pgcat) can route pgmq's VOLATILE `read`/`archive`/`delete` to a read replica, where workers read nothing (`read_ct=0`) and jobs stop with a **healthy heartbeat** — the supervisor watchdog keys on loop advancement, not read progress, so nothing surfaced the stall. The job read path had zero primary-affinity defense (only the LISTEN/streamer paths validated the primary via `PrimaryValidator`). Setting `config.require_primary = true` (default **false** — a single-primary deployment is byte-identical) makes `verify_connection!` reject a connection that lands on a replica (`pg_is_in_recovery() => t`) at supervisor boot, raising a clear `ConfigurationError` instead of forking children that read nothing. (2) **GUC forwarding:** pgbus's AR-config extraction dropped database.yml's `variables:` block, so `client_min_messages` (and any GUC) never reached pgmq's dedicated raw connections (NOTICE flooding that three consuming apps hand-patched via `connection_params`). GUCs are now forwarded, on both the AR-extracted and explicit-`connection_params` paths, by `config.connection_guc_mode`: `:options` (default) bakes them into the libpq `options` STARTUP param (byte-compatible with today for the read-timeout `statement_timeout`, now also carrying `variables:`), while `:session` applies them via post-connect `SET` on a fresh connection instead — for a transaction-mode PgBouncer that **rejects** the `options` startup param with a FATAL. `pgbus doctor` gains a ninth check ("Primary affinity") that warns (never fails) when the job connection is currently on a replica, naming the direct-port remediation. DB-gated integration tests prove both GUC modes round-trip a forwarded GUC and that `require_primary` verifies on a real primary. Refs #332.
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
|
|
33
34
|
### Fixed
|
|
34
35
|
|
|
36
|
+
- **Migration generators now route to the pgbus database when `connects_to` is configured, even without an explicit `--database` (issue #344).** `rails g pgbus:add_stream_queues` (and every sibling generator via `Generators::MigrationPath`) keyed the separate-database decision purely on the `--database` flag. An app that had already set `config.connects_to = { database: { writing: :pgbus } }` (so its pgbus migrations live in `db/pgbus_migrate/` per database.yml's `migrations_paths:`) still got the migration written to the **primary** DB's `db/migrate/` on a bare invocation — where it then ran against the wrong database — and the "Next steps" output told the operator to run `rails db:migrate` instead of `rails db:migrate:pgbus`. `MigrationPath` now auto-detects the target database from `connects_to` (via the existing `DatabaseTargetDetector`: runtime config → initializer scan → application.rb scan) when `--database` is absent; an explicit `--database` still wins. The detected name drives both the migration path (resolving `migrations_paths` for that database, falling back to the `db/pgbus_migrate` convention) and the `db:migrate:<name>` suffix in every generator's post-install output. An app with no separate database is unaffected. Refs #344.
|
|
37
|
+
- **`extract_ar_connection_hash` no longer forces `host: "localhost"` / `port: 5432` on a socket-based (host-less) database.yml — pgmq's connections now match ActiveRecord's on Unix-socket dev setups (issue #343).** A local database.yml with no `host:` is a Unix-socket connection: ActiveRecord connects via libpq's default socket (`PGHOST` / the default socket dir), but pgbus's AR-config extraction defaulted the absent `host`/`port` to TCP `localhost:5432`, silently diverging from AR and pointing pgmq's dedicated raw connections at a *different* server (or nothing) on any machine where the socket dir isn't `localhost`. Apps on socket-based dev DBs therefore couldn't use the AR-extraction path at all and had to pin an explicit `connection_params` Hash whose only real job was *not* defaulting `host` (cosmos carried an `after_initialize` block for exactly this). The two fallbacks are dropped: `host`/`port` now pass through as-is and `.compact` removes them when absent, so libpq applies its own socket defaults and matches AR. A config that *does* set `host`/`port` is byte-identical to before. Refs #343.
|
|
35
38
|
- **`config.log_format` no longer breaks `ActiveSupport::TaggedLogging` — tags are preserved instead of dropped, and `logger.tagged` no longer raises (issue #334).** TaggedLogging works by extending the logger's formatter instance with a module that prepends the current tags. Installing a fresh pgbus formatter (`LogFormatter::JSON`/`Text`) stripped that extension, so `logger.tagged("req-42") { ... }` dropped the tag — and, worse, raised `NoMethodError: undefined method 'tagged'` on the pgbus formatter (a deploy-breaker several apps hit, then worked around with a dedicated `$stdout` logger). `log_format=` now re-extends the replacement formatter with `ActiveSupport::TaggedLogging::Formatter` when the previous one carried it, so tagging keeps working. Refs #334.
|
|
36
39
|
- **The "Dashboard is accessible without authentication" warning is no longer a false positive when a gating `base_controller_class` is set (issue #334).** The warning fired whenever `web_auth` was nil, ignoring that a non-default `base_controller_class` (e.g. an `AdminController` with its own `before_action`) already gates the dashboard — so apps set a redundant `web_auth` lambda purely to silence the log line. The warning now stays quiet when `base_controller_class` is anything other than the default `::ActionController::Base`. Refs #334.
|
|
37
40
|
- **`retry_on` on an `:until_executed` job no longer dead-letters the job instead of retrying (issue #333).** The uniqueness key is released only on success or DLQ, but ActiveJob's `retry_on` re-enqueues from *inside* `perform_now` (after incrementing `executions`), while the executor still holds the key. That retry re-enqueue hit the job's own still-held key, was rejected as a duplicate (`JobNotUnique` under `on_conflict: :reject`), and the original message dead-lettered — so a job with both `ensures_uniqueness :until_executed` and `retry_on` lost its retry and DLQ'd on the first transient failure. A retry re-enqueue (`executions > 0`) is now recognized as the same logical job re-acquiring its own key and is allowed through; the existing key row correctly stays held until the job finally succeeds or dead-letters. Cross-job uniqueness (a genuine fresh duplicate, `executions == 0`) is still rejected. Proven end-to-end with a DB-gated integration test (fail-once-then-succeed runs to success exactly once, no `JobNotUnique`, no DLQ, key cleaned up). Refs #333.
|
data/README.md
CHANGED
|
@@ -1872,6 +1872,25 @@ rake pgbus:doctor # identical checks, for a Rake-based deploy pipeline
|
|
|
1872
1872
|
|
|
1873
1873
|
The report ends with a resolved-config summary (queue prefix, pool size, roles, capsules) with any password redacted from `database_url` / `connection_params`. Warnings do not fail the exit code — only a `:fail` result does.
|
|
1874
1874
|
|
|
1875
|
+
##### Doctor preflight at boot (single Rails boot)
|
|
1876
|
+
|
|
1877
|
+
Running `pgbus doctor || true` in a container entrypoint before `pgbus start` boots the full Rails app **twice** on every deploy, and the pre-supervisor `Process liveness` check false-fails (no workers exist yet) — which is why the `|| true` is needed, swallowing any genuinely-fatal finding too. Instead, run the preflight *inside* the booting supervisor:
|
|
1878
|
+
|
|
1879
|
+
```bash
|
|
1880
|
+
pgbus start --doctor # run the preflight, log the report, always boot
|
|
1881
|
+
pgbus start --doctor-strict # refuse to boot on a fatal check (exit non-zero, fork nothing)
|
|
1882
|
+
```
|
|
1883
|
+
|
|
1884
|
+
or set it in the initializer:
|
|
1885
|
+
|
|
1886
|
+
```ruby
|
|
1887
|
+
Pgbus.configure do |c|
|
|
1888
|
+
c.doctor_on_boot = :report # or :strict; nil/false (default) is off
|
|
1889
|
+
end
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
The boot preflight runs after the DB is verified reachable and queues are bootstrapped, but **before any worker is forked** — one Rails boot, and the `Process liveness` check is skipped (nothing to observe yet, so it can't false-fail). `:strict` aborts the boot only on a genuinely-fatal, non-transient check — a `Configuration` failure or an **absent** PGMQ schema. A transient DB blip that the lenient queue bootstrap is designed to ride out (surfacing as a `Queues` or `Database` failure) is reported but never aborts, so a fleet-wide cold boot against a momentarily-saturated primary doesn't fail every pod in lockstep.
|
|
1893
|
+
|
|
1875
1894
|
#### pgbus dlq
|
|
1876
1895
|
|
|
1877
1896
|
Dead-letter inspect/drain operations from a headless deployment or incident runbook, routed through `Web::DataSource` so retry/discard semantics are identical to the dashboard (origin-queue re-enqueue, transactional produce+delete, lock release on discard) with zero raw SQL and no direct PGMQ calls:
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus failed events unique index added!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Failed jobs will now be tracked in the dashboard"
|
|
34
34
|
say ""
|
|
35
35
|
end
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus job stats table installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Stats collection is enabled by default"
|
|
34
34
|
say " 3. View insights at /pgbus/insights"
|
|
35
35
|
say ""
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus job stats latency columns installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Queue latency and retry metrics are now tracked automatically"
|
|
34
34
|
say " 3. View latency insights at /pgbus/insights"
|
|
35
35
|
say ""
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus job stats queue index added!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. The Insights 'latency by queue' aggregation will now use the index"
|
|
34
34
|
say " instead of sequentially scanning pgbus_job_stats. Install this on"
|
|
35
35
|
say " heavy-traffic deployments with a large job stats retention window."
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus outbox installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Enable in config: config.outbox_enabled = true"
|
|
34
34
|
say " 3. Restart pgbus: bin/pgbus start"
|
|
35
35
|
say ""
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus presence installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Use in your code:"
|
|
34
34
|
say " Pgbus.stream(@room).presence.join(member_id: current_user.id.to_s)"
|
|
35
35
|
say " Pgbus.stream(@room).presence.members"
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus queue states table installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Restart pgbus: bin/pgbus start"
|
|
34
34
|
say ""
|
|
35
35
|
end
|
|
@@ -33,7 +33,7 @@ module Pgbus
|
|
|
33
33
|
say "Pgbus recurring jobs installed!", :green
|
|
34
34
|
say ""
|
|
35
35
|
say "Next steps:"
|
|
36
|
-
say " 1. Run: rails db:migrate#{
|
|
36
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
37
37
|
say " 2. Edit config/recurring.yml to define your recurring tasks"
|
|
38
38
|
say " 3. Restart pgbus: bin/pgbus start"
|
|
39
39
|
say ""
|
|
@@ -35,7 +35,7 @@ module Pgbus
|
|
|
35
35
|
say "streams register themselves on their next broadcast after migrating."
|
|
36
36
|
say ""
|
|
37
37
|
say "Next steps:"
|
|
38
|
-
say " 1. Run: rails db:migrate#{
|
|
38
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
39
39
|
say " 2. Restart pgbus: bin/pgbus start"
|
|
40
40
|
say ""
|
|
41
41
|
end
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus stream stats table installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Opt in by setting `config.streams_stats_enabled = true` in your"
|
|
34
34
|
say " pgbus initializer (disabled by default — stream event volume can"
|
|
35
35
|
say " be high, so stats recording is off unless you ask for it)."
|
|
@@ -29,7 +29,7 @@ module Pgbus
|
|
|
29
29
|
say "Pgbus uniqueness keys table installed!", :green
|
|
30
30
|
say ""
|
|
31
31
|
say "Next steps:"
|
|
32
|
-
say " 1. Run: rails db:migrate#{
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
33
|
say " 2. Add `ensures_uniqueness` to your job classes (DSL is auto-included)"
|
|
34
34
|
say " 3. Restart pgbus: bin/pgbus start"
|
|
35
35
|
say ""
|
|
@@ -99,7 +99,7 @@ module Pgbus
|
|
|
99
99
|
|
|
100
100
|
say ""
|
|
101
101
|
say "Next steps:"
|
|
102
|
-
say " 1. Run: rails db:migrate#{
|
|
102
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
103
103
|
say " 2. Edit config/initializers/pgbus.rb to configure workers"
|
|
104
104
|
say " 3. Start processing: bin/pgbus start"
|
|
105
105
|
say ""
|
|
@@ -115,8 +115,11 @@ module Pgbus
|
|
|
115
115
|
options[:pgmq_schema_mode]
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# An explicit --database wins; otherwise fall back to a database already
|
|
119
|
+
# configured via connects_to so a bare re-install targets the right DB
|
|
120
|
+
# and its setup hints/output name it (issue #344).
|
|
118
121
|
def database_name
|
|
119
|
-
|
|
122
|
+
effective_database_name
|
|
120
123
|
end
|
|
121
124
|
end
|
|
122
125
|
end
|
|
@@ -34,7 +34,7 @@ module Pgbus
|
|
|
34
34
|
say " 3. Drop the old pgbus_job_locks table (8 columns, 3 indexes)"
|
|
35
35
|
say ""
|
|
36
36
|
say "Next steps:"
|
|
37
|
-
say " 1. Run: rails db:migrate#{
|
|
37
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
38
38
|
say " 2. Restart pgbus: bin/pgbus start"
|
|
39
39
|
say ""
|
|
40
40
|
end
|
|
@@ -1,27 +1,93 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "pgbus/generators/database_target_detector"
|
|
4
|
+
|
|
3
5
|
module Pgbus
|
|
4
6
|
module Generators
|
|
5
7
|
# Shared migration path logic for all pgbus generators.
|
|
6
8
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
9
|
+
# A separate pgbus database can be selected two ways:
|
|
10
|
+
#
|
|
11
|
+
# 1. --database=pgbus explicitly on the generator invocation, or
|
|
12
|
+
# 2. the host app has already configured Pgbus with a dedicated database
|
|
13
|
+
# (config.connects_to = { database: { writing: :pgbus } }).
|
|
10
14
|
#
|
|
11
|
-
#
|
|
15
|
+
# When either applies, migrations go to the separate-database path (e.g.
|
|
16
|
+
# db/pgbus_migrate) and the post-install output names the db:migrate:<name>
|
|
17
|
+
# task. When neither applies, everything falls back to db/migrate.
|
|
18
|
+
#
|
|
19
|
+
# Auto-detecting from connects_to (case 2) fixes issue #344: a bare
|
|
20
|
+
# `rails g pgbus:add_*` in an app configured for a separate database used to
|
|
21
|
+
# silently write to db/migrate and run against the WRONG database.
|
|
12
22
|
module MigrationPath
|
|
13
23
|
private
|
|
14
24
|
|
|
15
25
|
def pgbus_migrate_path
|
|
16
|
-
|
|
26
|
+
return "db/migrate" unless separate_database?
|
|
27
|
+
|
|
28
|
+
# --database was passed: Rails' db_migrate_path reads options[:database]
|
|
29
|
+
# and resolves migrations_paths from database.yml. When the database was
|
|
30
|
+
# instead auto-detected from connects_to, options[:database] is nil, so
|
|
31
|
+
# db_migrate_path can't see it — resolve the path for the detected
|
|
32
|
+
# database ourselves.
|
|
33
|
+
if options[:database].present?
|
|
17
34
|
db_migrate_path
|
|
18
35
|
else
|
|
19
|
-
|
|
36
|
+
resolve_detected_migrate_path
|
|
20
37
|
end
|
|
21
38
|
end
|
|
22
39
|
|
|
23
40
|
def separate_database?
|
|
24
|
-
|
|
41
|
+
effective_database_name.present?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The database name that migrations should target: an explicit --database
|
|
45
|
+
# wins; otherwise auto-detect from the app's connects_to configuration.
|
|
46
|
+
def effective_database_name
|
|
47
|
+
return @effective_database_name if defined?(@effective_database_name)
|
|
48
|
+
|
|
49
|
+
@effective_database_name =
|
|
50
|
+
if options[:database].present?
|
|
51
|
+
options[:database]
|
|
52
|
+
else
|
|
53
|
+
DatabaseTargetDetector.new(destination_root: destination_root).detect
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The `:name` suffix appended to `rails db:migrate` in post-install
|
|
58
|
+
# output. Empty string for single-database mode so the line reads a plain
|
|
59
|
+
# `rails db:migrate`.
|
|
60
|
+
def migrate_command_suffix
|
|
61
|
+
separate_database? ? ":#{effective_database_name}" : ""
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Migrations path for an auto-detected separate database. Rails' own
|
|
65
|
+
# db_migrate_path is keyed on options[:database], which is nil here, so
|
|
66
|
+
# look up migrations_paths for the detected database directly and fall
|
|
67
|
+
# back to the db/pgbus_migrate convention if it isn't in database.yml.
|
|
68
|
+
def resolve_detected_migrate_path
|
|
69
|
+
migrations_path_for(effective_database_name) || "db/pgbus_migrate"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def migrations_path_for(database_name)
|
|
73
|
+
return nil unless defined?(::ActiveRecord::Base) && defined?(::Rails) && ::Rails.respond_to?(:env)
|
|
74
|
+
|
|
75
|
+
config = ::ActiveRecord::Base.configurations.configs_for(
|
|
76
|
+
env_name: ::Rails.env,
|
|
77
|
+
name: database_name
|
|
78
|
+
)
|
|
79
|
+
Array(config&.migrations_paths).first
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
# A missing config for `database_name` isn't an error — configs_for
|
|
82
|
+
# returns nil and we fall back to the db/pgbus_migrate convention. But a
|
|
83
|
+
# genuine failure here (malformed database.yml, an AR API change) would
|
|
84
|
+
# otherwise be invisible, so surface it via the generator's own output
|
|
85
|
+
# the way update_generator#resolve_connection does — not Pgbus.logger
|
|
86
|
+
# (unavailable/inappropriate at generate time; pgbus_failed_events is the
|
|
87
|
+
# runtime job-failure table, not a generator diagnostics sink).
|
|
88
|
+
say " ! could not resolve migrations_paths for #{database_name.inspect}: " \
|
|
89
|
+
"#{e.class}: #{e.message} (falling back to db/pgbus_migrate)", :yellow
|
|
90
|
+
nil
|
|
25
91
|
end
|
|
26
92
|
end
|
|
27
93
|
end
|
|
@@ -33,7 +33,7 @@ module Pgbus
|
|
|
33
33
|
say "automatically receive these settings."
|
|
34
34
|
say ""
|
|
35
35
|
say "Next steps:"
|
|
36
|
-
say " 1. Run: rails db:migrate#{
|
|
36
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
37
37
|
say " 2. Restart pgbus: bin/pgbus start"
|
|
38
38
|
say ""
|
|
39
39
|
end
|
|
@@ -36,7 +36,7 @@ module Pgbus
|
|
|
36
36
|
say "this setting."
|
|
37
37
|
say ""
|
|
38
38
|
say "Next steps:"
|
|
39
|
-
say " 1. Run: rails db:migrate#{
|
|
39
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
40
40
|
say " 2. Restart pgbus: bin/pgbus start"
|
|
41
41
|
say ""
|
|
42
42
|
end
|
|
@@ -31,7 +31,7 @@ module Pgbus
|
|
|
31
31
|
say ""
|
|
32
32
|
say "Next steps:"
|
|
33
33
|
say " 1. Review the migration in db/#{separate_database? ? "pgbus_migrate" : "migrate"}/"
|
|
34
|
-
say " 2. Run: rails db:migrate#{
|
|
34
|
+
say " 2. Run: rails db:migrate#{migrate_command_suffix}"
|
|
35
35
|
say ""
|
|
36
36
|
end
|
|
37
37
|
|
data/lib/pgbus/cli.rb
CHANGED
|
@@ -51,6 +51,16 @@ module Pgbus
|
|
|
51
51
|
Pgbus.configuration.execution_mode = options[:execution_mode].to_sym if options[:execution_mode]
|
|
52
52
|
apply_capsule_filter(options[:capsule]) if options[:capsule]
|
|
53
53
|
apply_role_filter(options)
|
|
54
|
+
apply_doctor_flag(options)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# --doctor runs the boot preflight in report mode; --doctor-strict aborts
|
|
58
|
+
# the boot on a fatal finding. Both set config.doctor_on_boot, which the
|
|
59
|
+
# supervisor reads at boot (issue #347). Strict wins if both are passed — it
|
|
60
|
+
# is the stronger gate, so a user asking for both clearly wants strict.
|
|
61
|
+
def apply_doctor_flag(options)
|
|
62
|
+
Pgbus.configuration.doctor_on_boot = :report if options[:doctor]
|
|
63
|
+
Pgbus.configuration.doctor_on_boot = :strict if options[:doctor_strict]
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
def parse_start_options(args)
|
|
@@ -81,6 +91,14 @@ module Pgbus
|
|
|
81
91
|
opts.on("--execution-mode MODE", "Execution mode: threads (default) or async") do |v|
|
|
82
92
|
options[:execution_mode] = v
|
|
83
93
|
end
|
|
94
|
+
|
|
95
|
+
opts.on("--doctor", "Run doctor preflight checks at boot and log the report (issue #347)") do
|
|
96
|
+
options[:doctor] = true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
opts.on("--doctor-strict", "Run doctor preflight and refuse to boot on a fatal finding") do
|
|
100
|
+
options[:doctor_strict] = true
|
|
101
|
+
end
|
|
84
102
|
end.parse!(args.dup)
|
|
85
103
|
options
|
|
86
104
|
end
|
|
@@ -199,6 +217,12 @@ module Pgbus
|
|
|
199
217
|
pattern)
|
|
200
218
|
--execution-mode Execution mode: threads (default) or async
|
|
201
219
|
(fiber-based, lower connection usage)
|
|
220
|
+
--doctor Run doctor preflight checks in the booting
|
|
221
|
+
supervisor and log the report (one Rails boot
|
|
222
|
+
instead of `pgbus doctor` + `pgbus start`)
|
|
223
|
+
--doctor-strict Like --doctor, but refuse to boot (exit non-zero,
|
|
224
|
+
fork nothing) if a fatal check fails (bad config
|
|
225
|
+
or an absent PGMQ schema)
|
|
202
226
|
|
|
203
227
|
Environment for `mcp`:
|
|
204
228
|
PGBUS_MCP_TOKEN If set, PGBUS_MCP_AUTH_TOKEN must match for
|
data/lib/pgbus/configuration.rb
CHANGED
|
@@ -89,6 +89,19 @@ module Pgbus
|
|
|
89
89
|
# PGMQ schema installation mode (:auto, :extension, :embedded)
|
|
90
90
|
attr_reader :pgmq_schema_mode
|
|
91
91
|
|
|
92
|
+
# Run doctor preflight checks inside the supervisor at boot (issue #347),
|
|
93
|
+
# so an entrypoint need not boot Rails twice (once for `pgbus doctor`, once
|
|
94
|
+
# for `pgbus start`). nil/false (default) — off, byte-identical to today.
|
|
95
|
+
# :report — run the preflight and log the report, always continue booting.
|
|
96
|
+
# :strict — additionally REFUSE to boot (raise before forking any worker)
|
|
97
|
+
# when a genuinely-fatal, non-transient check fails. The strict
|
|
98
|
+
# set is deliberately narrow (Configuration, PGMQ schema) so a
|
|
99
|
+
# transient boot-time DB blip — which the lenient queue bootstrap
|
|
100
|
+
# is designed to ride out via child crash-and-backoff — never
|
|
101
|
+
# aborts a whole fleet's cold boot in lockstep. Set via the
|
|
102
|
+
# `--doctor` / `--doctor-strict` start flags too.
|
|
103
|
+
attr_reader :doctor_on_boot
|
|
104
|
+
|
|
92
105
|
# Event consumers
|
|
93
106
|
attr_reader :event_consumers
|
|
94
107
|
|
|
@@ -248,6 +261,7 @@ module Pgbus
|
|
|
248
261
|
@worker_notify_database_url = nil
|
|
249
262
|
|
|
250
263
|
@pgmq_schema_mode = :auto
|
|
264
|
+
@doctor_on_boot = nil
|
|
251
265
|
|
|
252
266
|
@event_consumers = nil
|
|
253
267
|
|
|
@@ -566,6 +580,28 @@ module Pgbus
|
|
|
566
580
|
@pgmq_schema_mode = mode
|
|
567
581
|
end
|
|
568
582
|
|
|
583
|
+
VALID_DOCTOR_ON_BOOT_MODES = [nil, :report, :strict].freeze
|
|
584
|
+
|
|
585
|
+
# nil/false are both "off"; a String is coerced to a Symbol so the CLI flags
|
|
586
|
+
# and YAML-ish configs work. Validated at assignment time (like the other
|
|
587
|
+
# enum options), not in validate!.
|
|
588
|
+
def doctor_on_boot=(mode)
|
|
589
|
+
coerced = case mode
|
|
590
|
+
when nil, false then nil
|
|
591
|
+
when Symbol then mode
|
|
592
|
+
when String then mode.to_sym
|
|
593
|
+
else
|
|
594
|
+
raise Pgbus::ConfigurationError,
|
|
595
|
+
"Invalid doctor_on_boot type: #{mode.class}. Must be nil, false, String, or Symbol"
|
|
596
|
+
end
|
|
597
|
+
unless VALID_DOCTOR_ON_BOOT_MODES.include?(coerced)
|
|
598
|
+
raise Pgbus::ConfigurationError,
|
|
599
|
+
"Invalid doctor_on_boot: #{coerced.inspect}. Must be nil/false (off), :report, or :strict"
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
@doctor_on_boot = coerced
|
|
603
|
+
end
|
|
604
|
+
|
|
569
605
|
def validate!
|
|
570
606
|
if pool_size && !(pool_size.is_a?(Numeric) && pool_size.positive?)
|
|
571
607
|
raise Pgbus::ConfigurationError, "pool_size must be a positive number or nil (auto-tune)"
|
|
@@ -1439,9 +1475,15 @@ module Pgbus
|
|
|
1439
1475
|
# Rails 7.1+ db_config.configuration_hash returns the full config
|
|
1440
1476
|
config_hash = db_config.configuration_hash
|
|
1441
1477
|
|
|
1478
|
+
# Do NOT default host/port here. An absent `host:` in database.yml is a
|
|
1479
|
+
# Unix-socket connection; AR connects via libpq's socket default, so pgmq's
|
|
1480
|
+
# raw connections must too. Forcing TCP localhost:5432 diverges from AR on
|
|
1481
|
+
# socket-based configs (issue #343). `.compact` drops the absent keys and
|
|
1482
|
+
# libpq applies its own defaults (PGHOST / default socket dir). A
|
|
1483
|
+
# present-but-nil port is preserved as nil (also dropped by compact).
|
|
1442
1484
|
base = {
|
|
1443
|
-
host: config_hash[:host]
|
|
1444
|
-
port:
|
|
1485
|
+
host: config_hash[:host],
|
|
1486
|
+
port: config_hash[:port]&.to_i,
|
|
1445
1487
|
dbname: config_hash[:database],
|
|
1446
1488
|
user: config_hash[:username],
|
|
1447
1489
|
password: config_hash[:password]
|
data/lib/pgbus/doctor.rb
CHANGED
|
@@ -25,26 +25,47 @@ module Pgbus
|
|
|
25
25
|
|
|
26
26
|
STATUS_ICON = { ok: "✓", warn: "!", fail: "✗" }.freeze
|
|
27
27
|
|
|
28
|
+
# The ordered check suite, name → method. The name strings are the public,
|
|
29
|
+
# stable identity of each check (used in the report and to select subsets).
|
|
30
|
+
CHECKS = {
|
|
31
|
+
"Configuration" => :check_configuration,
|
|
32
|
+
"Database" => :check_database,
|
|
33
|
+
"PGMQ schema" => :check_pgmq_schema,
|
|
34
|
+
"Queues" => :check_queues,
|
|
35
|
+
"LISTEN/NOTIFY" => :check_notify,
|
|
36
|
+
"Process liveness" => :check_processes,
|
|
37
|
+
"GlobalID allowlist" => :check_allowed_global_id_models,
|
|
38
|
+
"Broadcast queue" => :check_broadcast_queue,
|
|
39
|
+
"Primary affinity" => :check_primary
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# Process liveness reads the pgbus_processes table (via HealthAnalyzer), so
|
|
43
|
+
# it is only meaningful once workers have registered. The supervisor boot
|
|
44
|
+
# preflight (issue #347) runs BEFORE forking any worker, so it excludes this
|
|
45
|
+
# check — otherwise stale prior-generation worker rows plus a visible backlog
|
|
46
|
+
# would produce a false STALLED verdict on a redeploy.
|
|
47
|
+
BOOT_SKIP = ["Process liveness"].freeze
|
|
48
|
+
|
|
49
|
+
# The subset of checks whose :fail is genuinely deploy-fatal AND not a
|
|
50
|
+
# transient the supervisor is designed to ride out. Only these abort a
|
|
51
|
+
# `:strict` boot. Configuration#validate! failing is a real config bug;
|
|
52
|
+
# an absent PGMQ schema means the migrations never ran. Deliberately NOT
|
|
53
|
+
# Queues/Database: the lenient queue bootstrap swallows a boot-time DB blip
|
|
54
|
+
# so children crash-and-backoff and recover, and verify_connection! already
|
|
55
|
+
# gated a hard-down DB before the preflight — making either strict-fatal
|
|
56
|
+
# would turn a tolerated transient into a fleet-wide lockstep boot abort.
|
|
57
|
+
STRICT_FATAL = ["Configuration", "PGMQ schema"].freeze
|
|
58
|
+
|
|
28
59
|
def initialize(config: Pgbus.configuration, client: Pgbus.client, data_source: nil)
|
|
29
60
|
@config = config
|
|
30
61
|
@client = client
|
|
31
|
-
@data_source = data_source
|
|
62
|
+
@data_source = data_source
|
|
32
63
|
end
|
|
33
64
|
|
|
34
65
|
# Run all checks and return an array of result hashes:
|
|
35
66
|
# { name:, status: :ok|:warn|:fail, detail: }
|
|
36
67
|
def run
|
|
37
|
-
@run ||=
|
|
38
|
-
check_configuration,
|
|
39
|
-
check_database,
|
|
40
|
-
check_pgmq_schema,
|
|
41
|
-
check_queues,
|
|
42
|
-
check_notify,
|
|
43
|
-
check_processes,
|
|
44
|
-
check_allowed_global_id_models,
|
|
45
|
-
check_broadcast_queue,
|
|
46
|
-
check_primary
|
|
47
|
-
].map(&:to_h)
|
|
68
|
+
@run ||= run_checks(CHECKS.keys)
|
|
48
69
|
end
|
|
49
70
|
|
|
50
71
|
# True when no check failed. Warnings do not fail the run — they surface a
|
|
@@ -53,11 +74,34 @@ module Pgbus
|
|
|
53
74
|
run.none? { |c| c[:status] == :fail }
|
|
54
75
|
end
|
|
55
76
|
|
|
77
|
+
# --- Supervisor boot preflight (issue #347) ---
|
|
78
|
+
|
|
79
|
+
# The checks safe to run inside the booting supervisor before any worker is
|
|
80
|
+
# forked: everything except the worker-dependent process-liveness check.
|
|
81
|
+
# Runs a genuine SUBSET — check_processes is never invoked — so there is no
|
|
82
|
+
# pre-fork HealthAnalyzer/DataSource round-trip.
|
|
83
|
+
def boot_checks
|
|
84
|
+
@boot_checks ||= run_checks(CHECKS.keys - BOOT_SKIP)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# True unless a strict-fatal check (see STRICT_FATAL) failed. Warnings and
|
|
88
|
+
# transient-shaped failures (Queues, Database) never block a `:strict` boot.
|
|
89
|
+
def boot_ok?
|
|
90
|
+
boot_checks.none? { |c| c[:status] == :fail && STRICT_FATAL.include?(c[:name]) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Human-readable report for the boot preflight — the boot_checks subset.
|
|
94
|
+
def boot_report
|
|
95
|
+
report(boot_checks)
|
|
96
|
+
end
|
|
97
|
+
|
|
56
98
|
# Human-readable report: one line per check, then a resolved-config summary
|
|
57
99
|
# with passwords redacted. Suitable for stdout in the CLI and rake task.
|
|
58
|
-
|
|
100
|
+
# Defaults to the full run; pass a filtered result array (e.g. boot_checks)
|
|
101
|
+
# to render a subset.
|
|
102
|
+
def report(checks = run)
|
|
59
103
|
lines = ["Pgbus Doctor", "=" * 40]
|
|
60
|
-
|
|
104
|
+
checks.each do |check|
|
|
61
105
|
icon = STATUS_ICON.fetch(check[:status], "?")
|
|
62
106
|
lines << format("%<icon>s %<name>-22s %<detail>s", icon: icon, name: check[:name], detail: check[:detail])
|
|
63
107
|
end
|
|
@@ -86,6 +130,21 @@ module Pgbus
|
|
|
86
130
|
|
|
87
131
|
private
|
|
88
132
|
|
|
133
|
+
# Run the named checks in CHECKS order and return an array of result hashes.
|
|
134
|
+
# Only the requested checks are INVOKED — a check omitted from `names` does
|
|
135
|
+
# no work (e.g. boot_checks never calls check_processes, so no HealthAnalyzer
|
|
136
|
+
# round-trip pre-fork).
|
|
137
|
+
def run_checks(names)
|
|
138
|
+
CHECKS.filter_map { |name, method| send(method).to_h if names.include?(name) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The dashboard data source backing the process-liveness check. Built lazily
|
|
142
|
+
# so the boot preflight (which excludes that check) never constructs a
|
|
143
|
+
# DataSource or touches the dashboard layer at the fork boundary.
|
|
144
|
+
def data_source
|
|
145
|
+
@data_source ||= Pgbus::Web::DataSource.new(client: @client)
|
|
146
|
+
end
|
|
147
|
+
|
|
89
148
|
# True in a Rails production environment. Guarded so the doctor still runs
|
|
90
149
|
# outside Rails (plain Ruby, tests) without assuming Rails is loaded.
|
|
91
150
|
def production?
|
|
@@ -181,7 +240,7 @@ module Pgbus
|
|
|
181
240
|
# from the shared HealthAnalyzer. STALLED (the silent-worker-wedge) is a
|
|
182
241
|
# failure; DEGRADED is a warning; OK passes.
|
|
183
242
|
def check_processes
|
|
184
|
-
verdict = Pgbus::MCP::HealthAnalyzer.new(
|
|
243
|
+
verdict = Pgbus::MCP::HealthAnalyzer.new(data_source).verdict
|
|
185
244
|
status = verdict[:status]
|
|
186
245
|
detail = Array(verdict[:reasons]).first || status
|
|
187
246
|
|
|
@@ -76,6 +76,14 @@ module Pgbus
|
|
|
76
76
|
# notify_trigger_current? makes those calls cheap no-ops.
|
|
77
77
|
bootstrap_queues
|
|
78
78
|
|
|
79
|
+
# Optional in-process doctor preflight (issue #347): run the diagnostic
|
|
80
|
+
# checks here — after config is loaded, the DB is verified reachable, and
|
|
81
|
+
# queues are bootstrapped, but BEFORE any worker is forked — so an
|
|
82
|
+
# entrypoint gets a single Rails boot instead of `pgbus doctor` + `pgbus
|
|
83
|
+
# start`. :strict aborts the boot (raising, so nothing forks) on a
|
|
84
|
+
# genuinely-fatal finding; :report only logs. Off by default.
|
|
85
|
+
run_doctor_preflight unless config.doctor_on_boot.nil?
|
|
86
|
+
|
|
79
87
|
boot_processes
|
|
80
88
|
monitor_loop
|
|
81
89
|
ensure
|
|
@@ -683,6 +691,33 @@ module Pgbus
|
|
|
683
691
|
ErrorReporter.report(e, { action: "bootstrap_queues" })
|
|
684
692
|
end
|
|
685
693
|
|
|
694
|
+
# In-process doctor preflight (issue #347). Runs the boot-safe subset of
|
|
695
|
+
# doctor checks (everything except the worker-dependent process-liveness
|
|
696
|
+
# check, which has no workers to observe yet) against the supervisor's own
|
|
697
|
+
# config and the already-verified shared client, and logs the report.
|
|
698
|
+
#
|
|
699
|
+
# In :strict mode, a genuinely-fatal check (Doctor::STRICT_FATAL —
|
|
700
|
+
# Configuration or an absent PGMQ schema) aborts the boot by raising
|
|
701
|
+
# Pgbus::ConfigurationError. Because this runs before boot_processes, no
|
|
702
|
+
# child is forked, and `run`'s `ensure shutdown` tears down the heartbeat
|
|
703
|
+
# and health server — the same fail-fast path verify_connection! uses.
|
|
704
|
+
# A transient-shaped failure (Queues, Database) is reported but never
|
|
705
|
+
# aborts: the lenient bootstrap above is built to let children ride out a
|
|
706
|
+
# boot-time DB blip, and a strict abort there would take down a whole
|
|
707
|
+
# fleet's cold boot in lockstep.
|
|
708
|
+
def run_doctor_preflight
|
|
709
|
+
doctor = Pgbus::Doctor.new(config: config, client: Pgbus.client)
|
|
710
|
+
report = doctor.boot_report
|
|
711
|
+
Pgbus.logger.info { "[Pgbus] doctor preflight (#{config.doctor_on_boot}):\n#{report}" }
|
|
712
|
+
|
|
713
|
+
return unless config.doctor_on_boot == :strict
|
|
714
|
+
return if doctor.boot_ok?
|
|
715
|
+
|
|
716
|
+
raise Pgbus::ConfigurationError,
|
|
717
|
+
"doctor preflight failed a fatal check (doctor_on_boot: :strict) — refusing to boot. " \
|
|
718
|
+
"See the report above."
|
|
719
|
+
end
|
|
720
|
+
|
|
686
721
|
def load_rails_app
|
|
687
722
|
return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
688
723
|
|
data/lib/pgbus/version.rb
CHANGED