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 +4 -4
- data/CHANGELOG.md +1 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/health_server.rb +32 -3
- 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: 869b9dc59dc3fdab2396f6dcfa8e1fa04372abd23cdd527e0a07078d2d93a98d
|
|
4
|
+
data.tar.gz: 9fa6807b78c09b3ef397b98013165e866233ca8bceb6d660a3f12fe5a5b696d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
|
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?
|