pgbus 0.16.0 → 0.16.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2043a2be7395c42d40398ebbb862de8c3e142dcb17967980b069ac8e23313fb2
4
- data.tar.gz: 850d0b62d0fe06528f166e15b136efb11981ef5bbbeeae95e9f5a37ef282f7e1
3
+ metadata.gz: 869b9dc59dc3fdab2396f6dcfa8e1fa04372abd23cdd527e0a07078d2d93a98d
4
+ data.tar.gz: 9fa6807b78c09b3ef397b98013165e866233ca8bceb6d660a3f12fe5a5b696d8
5
5
  SHA512:
6
- metadata.gz: dab60674d617fd909bd54a1f4904eb0262611ac4e146390ac828decbb0e4145503a362a95c0f5b0c5e5ef565e17891a12aa1556ae7c28f3d6b336e7ddcbff5aa
7
- data.tar.gz: 10787c60e18aa5211d29872f0da662d9a6b98a12f1f4a35c8f37b6115046cad0d16b53e04254c827833dd412024b3173cc38eeb50daa1e7e84b30c1edf935276
6
+ metadata.gz: 519e747379272929b6add403ca50e3b7b9a64df5b326a73b8372c24dd87d5edea58076fc9d93e95bd78de1df5e46482f095da283a67bf093aceae659ca895218
7
+ data.tar.gz: 5f32250830c195bb1e35422fa4ff504ff3e95cd0ba8afdd8cb9b239f18e1f3fc860339bf7fa5ca2284438413e2b8dfbae6e530b922418df1f5464641cd9f0b4a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  ### Fixed
4
4
 
5
+ - **A client that connects to the health port and never sends a request line no longer wedges every later probe (issue #455).** `Pgbus::Web::HealthServer` serves every probe from one accept-loop thread and read the request line with an unbounded `client.gets("\r\n", MAX_REQUEST_LINE)`, so a silent connection parked that read forever: docker's `--health-cmd` (`bin/pgbus-health`) then timed out on every attempt and the container stayed unhealthy for the rest of its life while the supervisor and its workers were fine. Each accepted connection now carries a 1-second `IO#timeout` — deliberately under `HealthProbe::DEFAULT_TIMEOUT` (2s), so one wedged client can't push the next probe past its own deadline — and the resulting `IO::TimeoutError` (an `IOError`) falls into the existing per-connection `rescue StandardError`, which logs it, drops the connection and frees the loop. `#stop` no longer leaks the thread either: a loop parked on an already-accepted client never sees the listening socket close, so `stop` kills the thread when it outlives the join and joins again afterwards (`Thread#kill` is asynchronous), instead of returning "stopped" while it is still running. Only reachable from inside the container on the `health_bind` 127.0.0.1 default — a wedged health gate, not a security issue. Refs #455.
5
6
  - **The unbound-lock reaper no longer counts a dead-lettered copy as a live message, so a `:until_executed` job that once dead-lettered can be enqueued again.** `Client#uniqueness_keys_present` scanned every queue in `pgmq.meta` for the payload's `pgbus_uniqueness_key` — including `*_dlq` queues. A dead-lettered copy keeps the original payload (key included) but is not in flight: the executor already released the lock when it moved the message. For a legacy `pending`/`msg_id=0` row (pre-#418 enqueue, or a bind that failed) the DLQ copy made the key look "still here" forever, the reaper kept the row, and an `on_conflict: :discard` job — a heartbeat, typically — was discarded on every enqueue until someone purged the DLQ by hand. Seen in production as three heartbeats silently dead for nine days behind a 468-message DLQ. Dead-letter queues are now skipped in the scan; bound-lock probes (`message_exists?`) were never affected because they resolve the logical queue's physical tables only.
6
7
  - **`Pgbus::Testing.disabled!` can no longer turn a Capybara teardown race into a hung test process (issue #443).** The `streams_test_mode` stub closed immediately, so a page on a stream-bearing layout had its `EventSource` reconnect every ~3s for the whole example; a reconnect landing after `disabled!` had switched test mode off — a config-level `after` hook runs *before* `Capybara.reset_sessions!` — took the real path and started a live `Streamer` (listener/dispatcher/heartbeat threads + a LISTEN connection) inside the RSpec process. Its orphaned threads then shared the test's pinned AR connection and CI died at the job timeout with only `message type 0x5a arrived from server while idle`. Three changes: the stub now emits `retry: 86400000` so the browser does not reconnect at all; `Streamer::Instance#shutdown!` snapshots every component's threads (`#threads` on Listener/Dispatcher/Heartbeat/OutboundPump/HubClient/FailoverListener), logs one error naming those still alive after their bounded joins, and returns that list (`Streamer.reset!` forwards it, `nil` when nothing was live); and `Testing.disabled!` raises `Pgbus::Testing::StreamerLeakError` — pointing at `config.append_after` — when threads leaked, while a live streamer that shut down cleanly is only logged. README and docs now recommend `config.append_after` for the teardown hook.
7
8
  - **`Pgbus::MCP.rack_app` works on a real hostname again with `mcp` 0.23+ / 1.x.** Since mcp 0.23 the `StreamableHTTPTransport` validates the `Host` header (DNS-rebinding protection, on by default, loopback hosts only) and pgbus had no way to pass the transport's options through — so a gated mount at `https://app.example.com/pgbus/mcp` answered every request `403 "Invalid Host header"`, and consumers pinned `mcp < 1.0` to dodge it (which only helps while the lock stays on 0.22). The rack app now exposes `allowed_hosts:`, `allowed_origins:` and `dns_rebinding_protection:`; the check **follows the gate by default** — off when `token:`/`auth:` is configured (a rebound browser page can never carry the bearer secret, so the check is redundant there), on for the warned-about unauthenticated mount — and `true`/`false` forces it. `mcp >= 0.23` is the floor for `rack_app` (older gems raise `Pgbus::Error` naming the fix); the gem's own bundle now tracks `mcp` 1.x, so lift that `< 1.0` pin. Stdio (`pgbus mcp`) is unaffected.
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.16.0"
4
+ VERSION = "0.16.1"
5
5
  end
@@ -26,6 +26,17 @@ module Pgbus
26
26
  MAX_REQUEST_LINE = 8_192
27
27
  private_constant :MAX_REQUEST_LINE
28
28
 
29
+ # Per-connection I/O deadline. A client that connects and sends nothing
30
+ # would otherwise park the single accept loop forever and starve every
31
+ # later probe. Kept under HealthProbe::DEFAULT_TIMEOUT (2s) so one wedged
32
+ # connection still can't push the next probe past its own deadline.
33
+ READ_TIMEOUT = 1
34
+ private_constant :READ_TIMEOUT
35
+
36
+ # How long #stop waits for the accept loop before killing it.
37
+ JOIN_TIMEOUT = 2
38
+ private_constant :JOIN_TIMEOUT
39
+
29
40
  STATUS_TEXT = {
30
41
  200 => "OK", 404 => "Not Found", 405 => "Method Not Allowed", 503 => "Service Unavailable"
31
42
  }.freeze
@@ -36,11 +47,12 @@ module Pgbus
36
47
  # nil before #start.
37
48
  attr_reader :port
38
49
 
39
- def initialize(port:, bind: "127.0.0.1", app: nil, logger: nil)
50
+ def initialize(port:, bind: "127.0.0.1", app: nil, logger: nil, read_timeout: READ_TIMEOUT)
40
51
  @configured_port = port
41
52
  @bind = bind
42
53
  @app = app || HealthApp.new
43
54
  @logger = logger
55
+ @read_timeout = read_timeout
44
56
  @server = nil
45
57
  @thread = nil
46
58
  @port = nil
@@ -63,13 +75,25 @@ module Pgbus
63
75
  server = @server
64
76
  @server = nil
65
77
  close_socket(server)
66
- @thread&.join(2)
78
+ stop_thread(@thread)
67
79
  @thread = nil
68
80
  @port = nil
69
81
  end
70
82
 
71
83
  private
72
84
 
85
+ # A loop parked on an already-accepted client never sees the listening
86
+ # socket close, so kill it rather than return "stopped" while it is still
87
+ # running. Thread#kill is asynchronous — join again so #stop only returns
88
+ # once the thread is really gone.
89
+ def stop_thread(thread)
90
+ return unless thread
91
+ return if thread.join(JOIN_TIMEOUT)
92
+
93
+ thread.kill
94
+ thread.join(JOIN_TIMEOUT)
95
+ end
96
+
73
97
  # accept blocks until stop closes the socket, which raises here and ends
74
98
  # the loop. Any per-connection error is logged and swallowed so one bad
75
99
  # client can never take the probe surface down.
@@ -90,6 +114,9 @@ module Pgbus
90
114
  end
91
115
 
92
116
  def handle_client(client)
117
+ # IO#timeout= is core Ruby since 3.2 (below this gem's 3.3 floor) — no
118
+ # require, and there is no "io/timeout" file to require.
119
+ client.timeout = @read_timeout
93
120
  method, path = read_request_line(client)
94
121
  return unless method
95
122
 
@@ -103,7 +130,9 @@ module Pgbus
103
130
 
104
131
  # Read and parse only the first line: "METHOD PATH HTTP/x.y". Returns
105
132
  # [method, path] or [nil, nil] when the line is missing/garbage so the
106
- # caller drops the connection without dispatching.
133
+ # caller drops the connection without dispatching. A client that stays
134
+ # silent past the deadline raises IO::TimeoutError (an IOError, so the
135
+ # caller's rescue StandardError logs it and frees the loop).
107
136
  def read_request_line(client)
108
137
  line = client.gets("\r\n", MAX_REQUEST_LINE)
109
138
  return [nil, nil] if line.nil?
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.16.0
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson