pgbus 0.12.4 → 0.13.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 +10 -0
- data/README.md +2 -1
- data/Rakefile +17 -1
- data/lib/pgbus/client.rb +15 -0
- data/lib/pgbus/configuration/capsule_dsl.rb +8 -0
- data/lib/pgbus/configuration.rb +65 -5
- data/lib/pgbus/dedicated_connection.rb +29 -2
- data/lib/pgbus/doctor.rb +47 -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 +47 -2
- 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/failover_listener.rb +130 -0
- data/lib/pgbus/web/streamer/hub_client.rb +199 -0
- data/lib/pgbus/web/streamer/hub_protocol.rb +85 -0
- data/lib/pgbus/web/streamer/instance.rb +69 -20
- data/lib/pgbus/web/streamer/listener.rb +17 -2
- data/lib/pgbus/web/streamer/master_hub.rb +414 -0
- data/lib/pgbus/web/streamer/master_hub_boot.rb +149 -0
- data/lib/puma/plugin/pgbus_streams.rb +38 -2
- metadata +9 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module Web
|
|
7
|
+
module Streamer
|
|
8
|
+
# Deferred MasterHub startup for the Puma master (issue #382). The
|
|
9
|
+
# pgbus_streams plugin's `start` runs BEFORE `preload_app!` loads the
|
|
10
|
+
# Rails app (and with it the pgbus initializer), so the hub cannot be
|
|
11
|
+
# built eagerly. This class splits the two halves:
|
|
12
|
+
#
|
|
13
|
+
# 1. The socket path is exported to ENV IMMEDIATELY — workers inherit
|
|
14
|
+
# it across fork and connect lazily on first SSE use.
|
|
15
|
+
# 2. A poller thread waits for Pgbus.configuration to become ready
|
|
16
|
+
# (the initializer has run — with preload_app!, before the first
|
|
17
|
+
# fork), then builds and starts the MasterHub. Workers that race a
|
|
18
|
+
# still-booting hub simply fail to connect and fall back to their
|
|
19
|
+
# own listener until they recycle — degraded footprint, never
|
|
20
|
+
# degraded semantics.
|
|
21
|
+
#
|
|
22
|
+
# Without preload_app! the master never loads the app, the deadline
|
|
23
|
+
# expires quietly, no socket is ever bound, and every worker keeps
|
|
24
|
+
# today's per-worker listener — :master scope effectively requires
|
|
25
|
+
# preload_app!, documented on the docs site.
|
|
26
|
+
class MasterHubBoot
|
|
27
|
+
def self.default_socket_path
|
|
28
|
+
File.join(Dir.tmpdir, "pgbus-streams-hub-#{::Process.pid}.sock")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(socket_path: self.class.default_socket_path, hub_factory: nil,
|
|
32
|
+
poll_interval: 1.0, deadline: 120, logger: nil)
|
|
33
|
+
@socket_path = socket_path
|
|
34
|
+
@hub_factory = hub_factory || lambda do |socket_path:|
|
|
35
|
+
MasterHub.new(config: Pgbus.configuration, socket_path: socket_path)
|
|
36
|
+
end
|
|
37
|
+
@poll_interval = poll_interval
|
|
38
|
+
@deadline = deadline
|
|
39
|
+
@logger = logger
|
|
40
|
+
# Guards @hub and @running: written by the caller thread
|
|
41
|
+
# (start/stop) and the background poller. A hub whose start
|
|
42
|
+
# outlives stop's join budget is stopped by whichever side sees
|
|
43
|
+
# the flag last, so teardown can never leave a live hub behind.
|
|
44
|
+
@state_mutex = Mutex.new
|
|
45
|
+
@hub = nil
|
|
46
|
+
@running = false
|
|
47
|
+
@thread = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def start
|
|
51
|
+
ENV["PGBUS_STREAMS_HUB_SOCKET"] = @socket_path
|
|
52
|
+
@state_mutex.synchronize { @running = true }
|
|
53
|
+
@thread = Thread.new { wait_and_start }
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stop
|
|
58
|
+
to_stop = @state_mutex.synchronize do
|
|
59
|
+
@running = false
|
|
60
|
+
hub = @hub
|
|
61
|
+
@hub = nil
|
|
62
|
+
hub
|
|
63
|
+
end
|
|
64
|
+
@thread&.join(2)
|
|
65
|
+
@thread = nil
|
|
66
|
+
to_stop&.stop
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def running?
|
|
73
|
+
@state_mutex.synchronize { @running }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def wait_and_start
|
|
77
|
+
waited = 0.0
|
|
78
|
+
until configuration_ready?
|
|
79
|
+
return unless running?
|
|
80
|
+
return give_up if waited >= @deadline
|
|
81
|
+
|
|
82
|
+
sleep @poll_interval
|
|
83
|
+
waited += @poll_interval
|
|
84
|
+
end
|
|
85
|
+
return unless running? && master_scope?
|
|
86
|
+
|
|
87
|
+
hub = @hub_factory.call(socket_path: @socket_path)
|
|
88
|
+
hub.start
|
|
89
|
+
# Register-or-late-stop: if stop ran while the hub was building,
|
|
90
|
+
# this thread owns the teardown of the hub stop never saw.
|
|
91
|
+
late = @state_mutex.synchronize do
|
|
92
|
+
if @running
|
|
93
|
+
@hub = hub
|
|
94
|
+
nil
|
|
95
|
+
else
|
|
96
|
+
hub
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
late&.stop
|
|
100
|
+
return if late
|
|
101
|
+
|
|
102
|
+
log(:info) { "[Pgbus::Streamer::MasterHubBoot] master hub listening at #{@socket_path}" }
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
@state_mutex.synchronize { @hub = nil }
|
|
105
|
+
# The method-scoped hub may have STARTED before a later step raised
|
|
106
|
+
# (e.g. a failing logger after registration) — stop it here or its
|
|
107
|
+
# LISTEN connection outlives the boot failure. MasterHub#stop is
|
|
108
|
+
# idempotent and safe on a never-started hub.
|
|
109
|
+
begin
|
|
110
|
+
hub&.stop
|
|
111
|
+
rescue StandardError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
log(:error) do
|
|
115
|
+
"[Pgbus::Streamer::MasterHubBoot] master hub failed to start " \
|
|
116
|
+
"(#{e.class}: #{e.message}) — workers fall back to per-worker listeners"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Ready once the app's initializer has produced connection options a
|
|
121
|
+
# dedicated LISTEN connection can be built from (String URL or libpq
|
|
122
|
+
# Hash; the Proc fallback means "nothing configured yet").
|
|
123
|
+
def configuration_ready?
|
|
124
|
+
return false unless defined?(Pgbus) && Pgbus.configuration.streams_enabled
|
|
125
|
+
|
|
126
|
+
options = Pgbus.configuration.streams_connection_options
|
|
127
|
+
options.is_a?(String) || options.is_a?(Hash)
|
|
128
|
+
rescue StandardError
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def master_scope?
|
|
133
|
+
Pgbus.configuration.streams_listen_scope == :master
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def give_up
|
|
137
|
+
log(:info) do
|
|
138
|
+
"[Pgbus::Streamer::MasterHubBoot] configuration never became ready within #{@deadline}s " \
|
|
139
|
+
"(no preload_app!?) — no master hub; workers use per-worker listeners"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def log(level, &)
|
|
144
|
+
(@logger || Pgbus.logger).public_send(level, &)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -22,15 +22,51 @@ require "puma/plugin"
|
|
|
22
22
|
# and a non-Rails use case). Explicit opt-in is safer.
|
|
23
23
|
Puma::Plugin.create do
|
|
24
24
|
def start(launcher)
|
|
25
|
+
# Master-side streams hub (issue #382): in cluster mode, ONE LISTEN
|
|
26
|
+
# connection in the master serves every worker over a Unix socket. The
|
|
27
|
+
# socket path is exported to ENV here (pre-fork, so workers inherit it);
|
|
28
|
+
# the hub itself starts once the preloaded app has configured Pgbus (see
|
|
29
|
+
# MasterHubBoot). Any failure means no socket — workers keep their own
|
|
30
|
+
# per-worker listeners, trading connections for unchanged semantics.
|
|
31
|
+
boot_master_hub(launcher)
|
|
32
|
+
|
|
25
33
|
launcher.events.register(:after_stopped) do
|
|
34
|
+
teardown_master_hub(launcher)
|
|
26
35
|
teardown_streamer(launcher)
|
|
27
36
|
end
|
|
28
37
|
|
|
29
38
|
launcher.events.register(:before_restart) do
|
|
39
|
+
teardown_master_hub(launcher)
|
|
30
40
|
teardown_streamer(launcher)
|
|
31
41
|
end
|
|
32
42
|
end
|
|
33
43
|
|
|
44
|
+
def boot_master_hub(launcher)
|
|
45
|
+
return unless defined?(Pgbus::Web::Streamer::MasterHubBoot)
|
|
46
|
+
# Single mode: the master IS the (only) serving process — one listener
|
|
47
|
+
# per host already; a hub would just add a socket hop.
|
|
48
|
+
return unless cluster_mode?(launcher)
|
|
49
|
+
|
|
50
|
+
@master_hub_boot = Pgbus::Web::Streamer::MasterHubBoot.new
|
|
51
|
+
@master_hub_boot.start
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
@master_hub_boot = nil
|
|
54
|
+
log_error(launcher, e, "master hub boot")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cluster_mode?(launcher)
|
|
58
|
+
launcher.respond_to?(:options) && launcher.options[:workers].to_i.positive?
|
|
59
|
+
rescue StandardError
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def teardown_master_hub(launcher)
|
|
64
|
+
@master_hub_boot&.stop
|
|
65
|
+
@master_hub_boot = nil
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
log_error(launcher, e, "master hub teardown")
|
|
68
|
+
end
|
|
69
|
+
|
|
34
70
|
def teardown_streamer(launcher)
|
|
35
71
|
return unless defined?(Pgbus::Web::Streamer)
|
|
36
72
|
|
|
@@ -43,8 +79,8 @@ Puma::Plugin.create do
|
|
|
43
79
|
log_error(launcher, e)
|
|
44
80
|
end
|
|
45
81
|
|
|
46
|
-
def log_error(launcher, error)
|
|
47
|
-
message = "[Pgbus::Puma::Plugin]
|
|
82
|
+
def log_error(launcher, error, operation = "streamer teardown")
|
|
83
|
+
message = "[Pgbus::Puma::Plugin] #{operation} raised: #{error.class}: #{error.message}"
|
|
48
84
|
if launcher.respond_to?(:log_writer)
|
|
49
85
|
launcher.log_writer.log(message)
|
|
50
86
|
elsif defined?(Pgbus) && Pgbus.respond_to?(:logger)
|
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.1
|
|
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
|
|
@@ -382,11 +385,16 @@ files:
|
|
|
382
385
|
- lib/pgbus/web/stream_app.rb
|
|
383
386
|
- lib/pgbus/web/streamer.rb
|
|
384
387
|
- lib/pgbus/web/streamer/connection.rb
|
|
388
|
+
- lib/pgbus/web/streamer/failover_listener.rb
|
|
385
389
|
- lib/pgbus/web/streamer/falcon_connection.rb
|
|
386
390
|
- lib/pgbus/web/streamer/heartbeat.rb
|
|
391
|
+
- lib/pgbus/web/streamer/hub_client.rb
|
|
392
|
+
- lib/pgbus/web/streamer/hub_protocol.rb
|
|
387
393
|
- lib/pgbus/web/streamer/instance.rb
|
|
388
394
|
- lib/pgbus/web/streamer/io_writer.rb
|
|
389
395
|
- lib/pgbus/web/streamer/listener.rb
|
|
396
|
+
- lib/pgbus/web/streamer/master_hub.rb
|
|
397
|
+
- lib/pgbus/web/streamer/master_hub_boot.rb
|
|
390
398
|
- lib/pgbus/web/streamer/outbound_pump.rb
|
|
391
399
|
- lib/pgbus/web/streamer/registry.rb
|
|
392
400
|
- lib/pgbus/web/streamer/stream_counter.rb
|