pgbus 0.12.3 → 0.13.0
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 +9 -0
- data/README.md +2 -1
- data/Rakefile +12 -1
- data/lib/pgbus/client.rb +15 -0
- data/lib/pgbus/configuration/capsule_dsl.rb +8 -0
- data/lib/pgbus/configuration.rb +36 -5
- data/lib/pgbus/dedicated_connection.rb +29 -2
- data/lib/pgbus/doctor.rb +36 -2
- data/lib/pgbus/event_bus/registry.rb +22 -0
- data/lib/pgbus/process/consumer.rb +57 -22
- data/lib/pgbus/process/notify_hub.rb +273 -0
- data/lib/pgbus/process/notify_listener.rb +103 -26
- data/lib/pgbus/process/supervisor.rb +146 -34
- data/lib/pgbus/process/wake_pipe.rb +129 -0
- data/lib/pgbus/process/wildcard_queue_resolver.rb +35 -0
- data/lib/pgbus/process/worker.rb +58 -35
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/streamer/listener.rb +49 -30
- metadata +4 -1
|
@@ -15,7 +15,12 @@ module Pgbus
|
|
|
15
15
|
# only running `wait_for_notify` — all LISTEN/UNLISTEN SQL goes
|
|
16
16
|
# through a command queue that the listener thread drains between
|
|
17
17
|
# notifies
|
|
18
|
-
# - #stop joins the
|
|
18
|
+
# - #stop clears @running and joins; it never touches the connection
|
|
19
|
+
# - the connection is SINGLE-OWNER: the listener thread is the only
|
|
20
|
+
# thread that may exec, wait, or close on it, from construction
|
|
21
|
+
# through teardown. PG::Connection is not thread-safe and #close is
|
|
22
|
+
# PQfinish — freeing the PGconn under a concurrent libpq call is a
|
|
23
|
+
# process-killing SEGV, not a rescuable PG::Error (issue #375)
|
|
19
24
|
#
|
|
20
25
|
# Health check: `wait_for_notify(timeout)` returns nil on timeout. When
|
|
21
26
|
# it does, the listener runs `SELECT 1` as a TCP keepalive. If that
|
|
@@ -41,6 +46,10 @@ module Pgbus
|
|
|
41
46
|
|
|
42
47
|
RECONNECT_BACKOFF_SECONDS = 0.5
|
|
43
48
|
|
|
49
|
+
# Grace added to one health-check cycle when #stop joins the listener
|
|
50
|
+
# thread. See #stop_join_timeout.
|
|
51
|
+
STOP_JOIN_GRACE_SECONDS = 5
|
|
52
|
+
|
|
44
53
|
attr_reader :listening_to
|
|
45
54
|
|
|
46
55
|
# @param connection_factory [#call] builds a FRESH PG connection on each
|
|
@@ -98,18 +107,16 @@ module Pgbus
|
|
|
98
107
|
|
|
99
108
|
@running = false
|
|
100
109
|
@commands << [:stop]
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
end
|
|
112
|
-
@thread&.join(5)
|
|
110
|
+
# Deliberately does NOT touch @conn. PG::Connection is not
|
|
111
|
+
# thread-safe and #close is PQfinish: it frees the PGconn and its
|
|
112
|
+
# OpenSSL objects out from under whatever libpq call the listener
|
|
113
|
+
# thread is making on the same connection. That is a use-after-free —
|
|
114
|
+
# a process-killing SEGV, not a rescuable PG::Error (issue #375).
|
|
115
|
+
# The listener thread is the sole owner of @conn and closes it in
|
|
116
|
+
# run_loop's ensure; clearing @running above is the whole stop signal.
|
|
117
|
+
# It is observed within one wait_for_notify timeout (health_check_ms),
|
|
118
|
+
# which is what the join budget below is sized against.
|
|
119
|
+
@thread&.join(stop_join_timeout)
|
|
113
120
|
@thread = nil
|
|
114
121
|
self
|
|
115
122
|
end
|
|
@@ -148,13 +155,18 @@ module Pgbus
|
|
|
148
155
|
|
|
149
156
|
timeout_s = @health_check_ms / 1000.0
|
|
150
157
|
begin
|
|
151
|
-
@conn.wait_for_notify(timeout_s) do |channel, _pid, payload|
|
|
158
|
+
got_notify = @conn.wait_for_notify(timeout_s) do |channel, _pid, payload|
|
|
152
159
|
handle_notify(channel, payload)
|
|
153
|
-
end
|
|
160
|
+
end
|
|
161
|
+
# Skip the keepalive when a stop landed during the wait: the loop
|
|
162
|
+
# is about to exit and close this connection anyway, so the
|
|
163
|
+
# round-trip would only add latency to shutdown.
|
|
164
|
+
run_health_check if !got_notify && @running
|
|
154
165
|
rescue IOError => e
|
|
155
|
-
# #stop closes the
|
|
156
|
-
#
|
|
157
|
-
#
|
|
166
|
+
# #stop no longer closes the connection to interrupt the wait
|
|
167
|
+
# (issue #375), so an IOError here is a genuine socket failure,
|
|
168
|
+
# not the expected shutdown signal. Reconnect unless we're
|
|
169
|
+
# stopping, in which case exit cleanly.
|
|
158
170
|
break unless @running
|
|
159
171
|
|
|
160
172
|
@logger.warn { "[Pgbus::Streamer::Listener] IO error (#{e.class}: #{e.message}) — reconnecting" }
|
|
@@ -167,7 +179,15 @@ module Pgbus
|
|
|
167
179
|
end
|
|
168
180
|
end
|
|
169
181
|
ensure
|
|
170
|
-
|
|
182
|
+
# Bookkeeping only — no UNLISTEN round-trip. We close the connection
|
|
183
|
+
# on the next line and closing the session deregisters every LISTEN
|
|
184
|
+
# server-side, so the exec bought nothing while being the statement
|
|
185
|
+
# that raced #stop's PQfinish into a SEGV (issue #375).
|
|
186
|
+
@listening_to.clear
|
|
187
|
+
# The listener thread owns this connection, so the listener thread is
|
|
188
|
+
# the one that closes it — #stop only clears @running.
|
|
189
|
+
close_quietly(@conn)
|
|
190
|
+
@conn = nil
|
|
171
191
|
end
|
|
172
192
|
|
|
173
193
|
def drain_commands
|
|
@@ -204,6 +224,16 @@ module Pgbus
|
|
|
204
224
|
(@health_check_ms / 1000.0) + 1.0
|
|
205
225
|
end
|
|
206
226
|
|
|
227
|
+
# How long #stop waits for the listener thread to notice the cleared
|
|
228
|
+
# @running and finish teardown. The thread can be parked in
|
|
229
|
+
# wait_for_notify for one full health-check cycle before it re-checks
|
|
230
|
+
# the flag, so the budget is that cycle plus grace — a flat timeout
|
|
231
|
+
# would expire before a listener with a large health_check_ms had even
|
|
232
|
+
# one chance to observe the stop.
|
|
233
|
+
def stop_join_timeout
|
|
234
|
+
(@health_check_ms / 1000.0) + STOP_JOIN_GRACE_SECONDS
|
|
235
|
+
end
|
|
236
|
+
|
|
207
237
|
def do_listen(queue_name)
|
|
208
238
|
channel = channel_for(queue_name)
|
|
209
239
|
return if @listening_to.include?(channel)
|
|
@@ -322,17 +352,6 @@ module Pgbus
|
|
|
322
352
|
nil
|
|
323
353
|
end
|
|
324
354
|
|
|
325
|
-
def safe_unlisten_all
|
|
326
|
-
@listening_to.each do |channel|
|
|
327
|
-
# @conn may be nil if #stop interrupted the reconnect loop between
|
|
328
|
-
# closing the old connection and publishing a new one.
|
|
329
|
-
@conn&.exec(%(UNLISTEN "#{channel}"))
|
|
330
|
-
rescue PG::Error
|
|
331
|
-
# connection may be dead; nothing we can do
|
|
332
|
-
end
|
|
333
|
-
@listening_to.clear
|
|
334
|
-
end
|
|
335
|
-
|
|
336
355
|
def channel_for(queue_name)
|
|
337
356
|
"#{CHANNEL_PREFIX}#{queue_name}#{CHANNEL_SUFFIX}"
|
|
338
357
|
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.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -328,13 +328,16 @@ files:
|
|
|
328
328
|
- lib/pgbus/process/heartbeat.rb
|
|
329
329
|
- lib/pgbus/process/lifecycle.rb
|
|
330
330
|
- lib/pgbus/process/memory_usage.rb
|
|
331
|
+
- lib/pgbus/process/notify_hub.rb
|
|
331
332
|
- lib/pgbus/process/notify_listener.rb
|
|
332
333
|
- lib/pgbus/process/notify_probe.rb
|
|
333
334
|
- lib/pgbus/process/primary_validator.rb
|
|
334
335
|
- lib/pgbus/process/queue_lock.rb
|
|
335
336
|
- lib/pgbus/process/signal_handler.rb
|
|
336
337
|
- lib/pgbus/process/supervisor.rb
|
|
338
|
+
- lib/pgbus/process/wake_pipe.rb
|
|
337
339
|
- lib/pgbus/process/wake_signal.rb
|
|
340
|
+
- lib/pgbus/process/wildcard_queue_resolver.rb
|
|
338
341
|
- lib/pgbus/process/worker.rb
|
|
339
342
|
- lib/pgbus/queue_factory.rb
|
|
340
343
|
- lib/pgbus/queue_name_validator.rb
|