pgbus 0.11.2 → 0.11.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 727dd6a7515414a2a426857ea19cc8cf33b8c5ca2311ecde116bf643114e8d85
4
- data.tar.gz: c68f0e3c246b554059160e901e96d61f80c5a5a9aa1716d6e4787f84cfdbe655
3
+ metadata.gz: df6f1cbda597599aaad006242fbb0b964edaa8fa599c413dfde949fc3b7e19a2
4
+ data.tar.gz: 4a57b939f12238bf4aac8fc601ee5cd529fd1311e6c6994dacfb55f925545e4b
5
5
  SHA512:
6
- metadata.gz: f515f94c6e585a038ab468ca30dc49a284c9666ddb0116998532ce2afdf602ac8309972fbe79606ad055b73bd95da418210778cf80a9e3062288855d67a3b746
7
- data.tar.gz: dd748949d2330236bfbc46b94c04ff907268e067830a2c4b91b3ff6b811324b2383d3c46bc950add9511c98355b39b39d9f3a58c03a3fed361838c866b24bf1c
6
+ metadata.gz: 336b503f39a11252b461b98c124617d16f252a20e8ef343ea48d637d8552a6048ff8c0d9465d95fcb7c737616010da63669a8db3eacb9971d79b6ae9286ed432
7
+ data.tar.gz: 33d5b40c21ef5213f4014f27577e19c617ee0e71420dc3c3432c4e3d35f941cafbe0e6a41d33bac42cc9ec2eb65a159d3685f1cce28ff44e97a14ca9749134ab
data/CHANGELOG.md CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  ### Added
12
12
 
13
+ - **`pgbus dashboard` — print the vendored AppSignal dashboards as import-ready JSON.** The gem ships four AppSignal dashboard definitions (the main one submitted to appsignal/public_config#80 plus the health/streams/throughput extras), but they're stored in the automated-dashboard format — a `metric_keys` trigger wrapping the `dashboard` object — which AppSignal's "Import dashboard" dialog doesn't accept, so trying one on a real app meant hand-unwrapping JSON. `pgbus dashboard [main|health|streams|throughput]` prints the inner dashboard object ready to paste into the import dialog (`pgbus dashboard health | pbcopy`); `pgbus dashboard --list` enumerates the available names and titles.
13
14
  - **`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.
14
15
  - **`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.
15
16
  - **`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.
@@ -33,6 +34,7 @@
33
34
 
34
35
  ### Fixed
35
36
 
37
+ - **AppSignal main dashboard: job and event lines now split by `job_class` and `routing_key`.** Applied the upstream review suggestions committed on appsignal/public_config#80 to the vendored `dashboard.json`: the "Job status per queue" panel gains a `job_class` wildcard tag filter and label (`%job_class% - %queue% - %status%`), and "Event handler status" gains `routing_key` (`%handler% - %status% - %routing_key%`). Without the wildcard tag entry a `%placeholder%` in the line label never resolves, so lines from different job classes / routing keys were collapsed together. The subscriber already emitted both tags; only the dashboard definition lagged.
36
38
  - **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
39
  - **`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.
38
40
  - **`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.
data/README.md CHANGED
@@ -820,15 +820,23 @@ end
820
820
 
821
821
  #### Dashboards
822
822
 
823
- Three importable AppSignal dashboards ship with the gem:
823
+ Four importable AppSignal dashboards ship with the gem:
824
824
 
825
- | File | Purpose |
826
- |------|---------|
827
- | `lib/pgbus/integrations/appsignal/dashboards/pgbus_throughput.json` | Jobs/sec, perform-duration percentiles, send/read counts |
828
- | `lib/pgbus/integrations/appsignal/dashboards/pgbus_health.json` | Queue depth, oldest message age, DLQ, dead tuples, MVCC horizon, worker recycles |
829
- | `lib/pgbus/integrations/appsignal/dashboards/pgbus_streams.json` | Broadcasts, fanout, active SSE connections, outbox, recurring tasks |
825
+ | Name | File | Purpose |
826
+ |------|------|---------|
827
+ | `main` | `lib/pgbus/integrations/appsignal/dashboard.json` | All-in-one overview (the automated dashboard submitted to appsignal/public_config) |
828
+ | `throughput` | `lib/pgbus/integrations/appsignal/dashboards/pgbus_throughput.json` | Jobs/sec, perform-duration percentiles, send/read counts |
829
+ | `health` | `lib/pgbus/integrations/appsignal/dashboards/pgbus_health.json` | Queue depth, oldest message age, DLQ, dead tuples, MVCC horizon, worker recycles |
830
+ | `streams` | `lib/pgbus/integrations/appsignal/dashboards/pgbus_streams.json` | Broadcasts, fanout, active SSE connections, outbox, recurring tasks |
831
+
832
+ The files are stored in AppSignal's automated-dashboard format (a `metric_keys` trigger wrapping
833
+ the dashboard object); `pgbus dashboard <name>` unwraps one into the JSON that AppSignal's
834
+ "New dashboard" → "Import dashboard" dialog accepts:
830
835
 
831
- Import via the AppSignal dashboard UI ("New dashboard" → "Import JSON") or the AppSignal API.
836
+ ```bash
837
+ pgbus dashboard --list # available names and titles
838
+ pgbus dashboard health | pbcopy # paste into the import dialog
839
+ ```
832
840
 
833
841
  ### Metrics adapter (Prometheus / StatsD)
834
842
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Pgbus
6
+ module CLI
7
+ # Prints the vendored AppSignal dashboard definitions as import-ready
8
+ # JSON. The files ship in the automated-dashboard format used by
9
+ # appsignal/public_config (a `metric_keys` trigger wrapping a
10
+ # `dashboard` object); AppSignal's "Import dashboard" dialog wants the
11
+ # inner object only, so this command unwraps it:
12
+ #
13
+ # pgbus dashboard # main dashboard
14
+ # pgbus dashboard health # one of the extra dashboards
15
+ # pgbus dashboard --list # available names and titles
16
+ module Dashboard
17
+ module_function
18
+
19
+ def start(args)
20
+ name = args.first || "main"
21
+ return list if ["--list", "list"].include?(name)
22
+
23
+ path = available[name]
24
+ unless path
25
+ warn "Unknown dashboard: #{name.inspect} (available: #{available.keys.join(", ")})"
26
+ exit 1
27
+ end
28
+
29
+ puts JSON.pretty_generate(definition(path))
30
+ end
31
+
32
+ def list
33
+ available.each do |name, path|
34
+ puts format("%-12s %s", name, definition(path)["title"])
35
+ end
36
+ end
37
+
38
+ # The main dashboard plus the extras, keyed by their short CLI name
39
+ # (pgbus_health.json => "health").
40
+ def available
41
+ require "pgbus/integrations/appsignal"
42
+
43
+ extras = Dir[File.join(Integrations::Appsignal::DASHBOARDS_DIR, "*.json")].to_h do |path|
44
+ [File.basename(path, ".json").delete_prefix("pgbus_"), path]
45
+ end
46
+ { "main" => Integrations::Appsignal::DASHBOARD_PATH }.merge(extras)
47
+ end
48
+
49
+ def definition(path)
50
+ JSON.parse(File.read(path)).fetch("dashboard")
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/pgbus/cli.rb CHANGED
@@ -18,6 +18,8 @@ module Pgbus
18
18
  list_queues
19
19
  when "dlq"
20
20
  DLQ.start(args[1..] || [])
21
+ when "dashboard"
22
+ Dashboard.start(args[1..] || [])
21
23
  when "mcp"
22
24
  start_mcp_server
23
25
  when "doctor"
@@ -196,6 +198,8 @@ module Pgbus
196
198
  queues List queues with metrics
197
199
  dlq Inspect and drain dead-letter queues
198
200
  (list/show/retry/retry-all/purge)
201
+ dashboard Print an AppSignal dashboard as import-ready JSON
202
+ (main/health/streams/throughput; --list to enumerate)
199
203
  mcp Start the read-only MCP diagnostic server over stdio
200
204
  doctor Run environment diagnostics (config, DB, PGMQ, queues,
201
205
  LISTEN/NOTIFY, process liveness); exits 1 on any failure
data/lib/pgbus/client.rb CHANGED
@@ -1099,6 +1099,14 @@ module Pgbus
1099
1099
  ].freeze
1100
1100
  private_constant :STALE_CONNECTION_PATTERNS
1101
1101
 
1102
+ # Compiled once: matches an error message containing any stale-connection
1103
+ # pattern as a substring, case-insensitively (replacing the previous
1104
+ # `message.downcase` + substring scan). Regexp.union escapes the literal
1105
+ # parens in "pqsocket() ...", so they match literally.
1106
+ STALE_CONNECTION_PATTERN =
1107
+ Regexp.new(Regexp.union(STALE_CONNECTION_PATTERNS).source, Regexp::IGNORECASE).freeze
1108
+ private_constant :STALE_CONNECTION_PATTERN
1109
+
1102
1110
  # How many times a matched stale-connection error is retried before it
1103
1111
  # propagates. Two attempts (not one) so a transient window — a PgBouncer
1104
1112
  # restart or a brief failover — that outlasts the first immediate retry
@@ -1482,8 +1490,11 @@ module Pgbus
1482
1490
  end
1483
1491
 
1484
1492
  def stale_connection_error?(error)
1485
- message = error.message.to_s.downcase
1486
- STALE_CONNECTION_PATTERNS.any? { |pattern| message.include?(pattern) }
1493
+ # Substring match against any stale-connection pattern. Deliberately a
1494
+ # single Regexp rather than `STALE_CONNECTION_PATTERNS.any? { |p| msg.include?(p) }`:
1495
+ # Style/ArrayIntersect misfires on that shape and "corrects" it to
1496
+ # `patterns.intersect?(msg)`, which raises TypeError (String isn't an Array).
1497
+ STALE_CONNECTION_PATTERN.match?(error.message.to_s)
1487
1498
  end
1488
1499
 
1489
1500
  # Substring pgmq-ruby uses when a pool checkout times out — a
@@ -11,7 +11,7 @@
11
11
  "display": "LINE",
12
12
  "title": "Job status per queue",
13
13
  "description": "Processed, failed, and dead-lettered jobs per queue.",
14
- "line_label": "%queue% - %status%",
14
+ "line_label": "%job_class% - %queue% - %status%",
15
15
  "format": "number",
16
16
  "draw_null_as_zero": true,
17
17
  "metrics": [
@@ -23,6 +23,10 @@
23
23
  }
24
24
  ],
25
25
  "tags": [
26
+ {
27
+ "key": "job_class",
28
+ "value": "*"
29
+ },
26
30
  {
27
31
  "key": "queue",
28
32
  "value": "*"
@@ -232,7 +236,7 @@
232
236
  "display": "LINE",
233
237
  "title": "Event handler status",
234
238
  "description": "Processed and failed event handler invocations.",
235
- "line_label": "%handler% - %status%",
239
+ "line_label": "%handler% - %status% - %routing_key%",
236
240
  "format": "number",
237
241
  "draw_null_as_zero": true,
238
242
  "metrics": [
@@ -251,6 +255,10 @@
251
255
  {
252
256
  "key": "status",
253
257
  "value": "*"
258
+ },
259
+ {
260
+ "key": "routing_key",
261
+ "value": "*"
254
262
  }
255
263
  ]
256
264
  }
data/lib/pgbus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.11.2"
4
+ VERSION = "0.11.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.11.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -253,6 +253,7 @@ files:
253
253
  - lib/pgbus/bus_record.rb
254
254
  - lib/pgbus/circuit_breaker.rb
255
255
  - lib/pgbus/cli.rb
256
+ - lib/pgbus/cli/dashboard.rb
256
257
  - lib/pgbus/cli/dlq.rb
257
258
  - lib/pgbus/client.rb
258
259
  - lib/pgbus/client/connection_health.rb