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.
@@ -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] streamer teardown raised: #{error.class}: #{error.message}"
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.12.4
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