pgbus 0.9.9 → 0.9.10

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: 9848fb03a422ec1c28eda5f7c021ae41ecbdba5a9d4b8246635e14ea10d32213
4
- data.tar.gz: 766159bb02748854335f90fac50b5fa4bbae82307bcbeacea366d3428a5b6c75
3
+ metadata.gz: 26160848a1d65cad46065b62fb4b5df79a18756514a39f86b5c074d1a1fc4dad
4
+ data.tar.gz: f3c1551c80e1a2fce0188a20e732e3a84c57628abf18a3239569863cde546f56
5
5
  SHA512:
6
- metadata.gz: 6da2678bd4e68b0d18fd264af33f9bb463b1944b33db8ec4ecfc0dfdf20167275b7b8067fa9c5b082e3ff9294185c6ea8a9c71538c63704b03165e6d85690e07
7
- data.tar.gz: c39c733fe2c91107adedd58d1ae27b94123681e87cb7ec3ab15d60338f7b99b49fe05152f07ece13ab1a94efca4a68bdfce8a234bdbffe87ebd51fc153185135
6
+ metadata.gz: 856d96cd1085bbc88c96ad6528417fdea7227918c883af4395c1bbf9b8610bb786326bff3793fbb29f0fea8615d16a5661b782dd38e9a02bdf55748c03074e0a
7
+ data.tar.gz: c7b2061b500ad2d5e7e5bba04e9de24ec397006112e0102d9778f2d9114d9802d1467ad285d211183965b8f1cfb551271a265b8402b577a313f19a5410e35db0
data/CHANGELOG.md CHANGED
@@ -14,6 +14,9 @@
14
14
 
15
15
  ### Fixed
16
16
 
17
+ - **Durable stream `q_` queues no longer leak — the orphan sweep is now age-based, as its `streams_orphan_threshold` config always implied.** A durable stream's live `pgmq.q_<name>` table is never drained by normal delivery: the streamer peeks (`read_after` UNIONs `q_`+`a_` without claiming rows), so `queue_length` stays `> 0` for any durable stream ever broadcast to, and retention only prunes `a_` (which stays empty for streams). The orphan sweep dropped only *empty* queues, so it never collected a durable stream — a permanent leak of the queue table, archive table, indexes, and a `pgmq.meta` row, at worst one leaked queue *per request* for per-render/one-shot durable keys. `streams_orphan_threshold` (default 24h) was named and defaulted as an age but wired only as an on/off gate. `sweep_orphan_streams` now reads each queue's `pgmq.meta.created_at` (no schema change) and drops when the queue is empty **or** has aged past the threshold. Idempotent re-creation (`ensure_stream_queue`) plus the existing missing-queue tolerance in `read_after` keep it safe; a genuinely long-lived stream only loses replay history past the threshold, well clear of normal reconnect windows. A `nil` `created_at` is treated as young (never dropped on age). Refs #306.
18
+ - **`Pgbus::MCP.rack_app` now returns a clean 401 on auth failure instead of a 500.** The `RackApp` auth gate returned a *frozen* `UNAUTHORIZED` response triple; once mounted in Rails, response-mutating middleware downstream (`Rack::TempfileReaper` assigns `response[2]`, `Rack::ETag` adds a header) raised `FrozenError`, so every request with a missing/mismatched `PGBUS_MCP_TOKEN` surfaced as an opaque 500 in the host app's error tracker rather than the intended 401. The gate now builds a fresh, mutable array and headers hash per call (only the JSON body string stays memoized+frozen). A request-level spec drives the app through the real `TempfileReaper`/`ETag` stack to guard against regression. Refs #304.
19
+
17
20
  ### Security
18
21
 
19
22
  ## [0.9.8.1] - 2026-07-04
@@ -22,11 +22,12 @@ module Pgbus
22
22
  # / behind your VPN, never internet-exposed.
23
23
  class RackApp
24
24
  BEARER_PREFIX = "Bearer "
25
- UNAUTHORIZED = [
26
- 401,
27
- { "Content-Type" => "application/json", "WWW-Authenticate" => "Bearer" },
28
- [{ jsonrpc: "2.0", id: nil, error: { code: -32_001, message: "Unauthorized" } }.to_json]
29
- ].freeze
25
+ # Only the JSON body string is frozen and reused. The outer response triple
26
+ # and its headers hash MUST be built fresh per call (#unauthorized) so
27
+ # downstream Rack middleware can mutate them — Rack::TempfileReaper assigns
28
+ # response[2] and Rack::ETag adds headers. Returning a frozen array/hash
29
+ # raised FrozenError → 500 instead of 401 (issue #304).
30
+ UNAUTHORIZED_BODY = { jsonrpc: "2.0", id: nil, error: { code: -32_001, message: "Unauthorized" } }.to_json.freeze
30
31
 
31
32
  # @param data_source [Pgbus::Web::DataSource] read layer the tools query.
32
33
  # Built once and shared: DataSource acquires Pgbus::BusRecord.connection
@@ -55,13 +56,24 @@ module Pgbus
55
56
  # the gem's transport has no auth of its own.
56
57
  def call(env)
57
58
  request = Rack::Request.new(env)
58
- return UNAUTHORIZED unless authorized?(request)
59
+ return unauthorized unless authorized?(request)
59
60
 
60
61
  @transport.handle_request(request)
61
62
  end
62
63
 
63
64
  private
64
65
 
66
+ # A fresh, mutable 401 response triple each call. See UNAUTHORIZED_BODY.
67
+ # Header keys are lowercase per the Rack 3 SPEC (matches stream_app.rb and
68
+ # prometheus_exporter.rb); Rack::Headers looks them up case-insensitively.
69
+ def unauthorized
70
+ [
71
+ 401,
72
+ { "content-type" => "application/json", "www-authenticate" => "Bearer" },
73
+ [UNAUTHORIZED_BODY]
74
+ ]
75
+ end
76
+
65
77
  # An explicit auth callable wins; otherwise fall back to the bearer token;
66
78
  # otherwise (neither configured) allow — the constructor already warned.
67
79
  def authorized?(request)
@@ -539,17 +539,29 @@ module Pgbus
539
539
  break if interrupted?("Orphan stream sweep", index, queue_names.size)
540
540
  next unless full_name.start_with?("#{prefix}_")
541
541
 
542
+ safe_name = QueueNameValidator.sanitize!(full_name)
542
543
  row = conn.select_one(<<~SQL, "Pgbus Orphan Check")
543
- SELECT count(*) AS queue_length
544
- FROM pgmq.q_#{QueueNameValidator.sanitize!(full_name)}
544
+ SELECT
545
+ (SELECT count(*) FROM pgmq.q_#{safe_name}) AS queue_length,
546
+ (SELECT EXTRACT(epoch FROM (NOW() - created_at))::int
547
+ FROM pgmq.meta WHERE queue_name = '#{safe_name}') AS age_sec
545
548
  SQL
546
549
 
547
550
  next unless row
548
- next if row["queue_length"].to_i.positive?
551
+
552
+ empty = !row["queue_length"].to_i.positive?
553
+ # Durable stream q_ tables are never drained by delivery (non-consuming
554
+ # peek), so a non-empty queue that has aged past the threshold is a
555
+ # leak, not a live stream — drop it. A nil age_sec (unknown created_at)
556
+ # is treated as young, never as infinitely old.
557
+ age_sec = row["age_sec"]
558
+ stale = !age_sec.nil? && age_sec.to_i >= threshold.to_i
559
+ next unless empty || stale
549
560
 
550
561
  Pgbus.client.drop_queue(full_name, prefixed: false)
551
562
  dropped += 1
552
- Pgbus.logger.info { "[Pgbus] Dropped orphan stream queue: #{full_name}" }
563
+ reason = empty ? "empty" : "aged past #{threshold.to_i}s threshold"
564
+ Pgbus.logger.info { "[Pgbus] Dropped orphan stream queue (#{reason}): #{full_name}" }
553
565
  outcome.record_success
554
566
  rescue StandardError => e
555
567
  Pgbus.logger.warn { "[Pgbus] Orphan stream sweep failed for #{full_name}: #{e.message}" }
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.9.9"
4
+ VERSION = "0.9.10"
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.9.9
4
+ version: 0.9.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson