pgbus 0.15.1 → 0.15.2
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 +3 -0
- data/lib/pgbus/client.rb +1 -2
- data/lib/pgbus/execution_pools/async_pool.rb +1 -2
- data/lib/pgbus/process/heartbeat.rb +29 -3
- data/lib/pgbus/process/notify_listener.rb +18 -9
- data/lib/pgbus/process/supervisor.rb +22 -3
- data/lib/pgbus/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13ec61547a6821edab1a7aa96e794d522cc6fdcd38dd354bf601c9c760a2fded
|
|
4
|
+
data.tar.gz: b79ef51aa82c336ba98e3df071d40e9de5467bec196fb53edaf8a4fa5507327d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8647f5cff936fae72bc95ea49c793fc68e514c29c2955d88b668dd6e5905a95418f7efdb8082e77048a584b9a0c86aa2cea42bbcd1f0777df349606ddbd4db08
|
|
7
|
+
data.tar.gz: ee10d3f15f0e03a8a46d359c5aaf19f3329ed628dd4ae72d98fe00aec4b9689ee43005b73884b62e997fb3305c0b2c5ec2ce9a747646e558a1ade1cbeca27f0e
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
### Fixed
|
|
4
4
|
|
|
5
|
+
- **Worker forks no longer kill the supervisor's shared LISTEN connection (issue #437).** Under `worker_notify_scope = :supervisor` every fork produced one `[Pgbus::NotifyListener] connection error (PG::ConnectionBad: PQconsumeInput() server closed the connection unexpectedly …) — reconnecting` in the supervisor ~2 s after `Worker started`, and a LISTEN gap (polling fallback) until `reconnect!` completed. The child's fork hygiene closed the Ruby `IO` wrapper of the inherited LISTEN socket, but pg builds `socket_io` with `autoclose=false`, so the fd stayed open — and when the child's GC freed the inherited `PG::Connection`, its `PQfinish` sent a libpq Terminate down that fd, i.e. down the **parent's** connection. `NotifyListener#close_inherited_socket!` now repoints the fd at `/dev/null` (`socket_io.reopen(IO::NULL)`, the ActiveRecord `PostgreSQLAdapter#discard!` idiom) so the eventual `PQfinish` is harmless. Regression-covered by a real-fork integration spec that asserts the parent's `pgbus-listen` backend pid is unchanged and no reconnect is logged. Refs #437.
|
|
6
|
+
- **A process whose `pgbus_processes` row is deleted underneath it now re-registers instead of staying invisible forever (issue #438).** `Heartbeat#beat` updated its row by id; when the row was gone — the dispatcher's stale-process reaper after a ≥ 5-minute heartbeat gap, a manual cleanup, another host's clock skew — `update_all` matched 0 rows, raised nothing, and the process (healthy, logging, serving `/readyz`) was absent from `pgbus_processes` until it restarted, so the supervisor's DB loop-tick check silently fell back to the pipe and `ConsumerPriority` could not see it. The beat now treats an affected-row count of 0 as "row is gone": it logs one WARN naming the old id, kind and pid (so whatever deleted the row can be hunted), re-registers through the existing boot path, and lands that beat's `last_heartbeat_at` / metadata on the new row. A mutex plus a stopped flag keep a beat that races `stop` from resurrecting a row `deregister_process` just deleted. Zero extra queries on the happy path. Refs #438.
|
|
7
|
+
- **Supervisor logs a worker recycle as a clean exit, not a crash (issue #438).** A worker or consumer hitting `max_jobs` / `max_memory` / `max_lifetime` exits 0 by design, but the supervisor logged `Child worker pid=N exited unexpectedly (status=0)`, burying real crashes. A clean exit outside shutdown is now INFO `exited cleanly (status=0) — restarting (worker recycle)`; non-zero exits keep the WARN, and a signaled exit reports `signal=N` instead of an empty status so an OOM SIGKILL is distinguishable. Restart policy is unchanged. Refs #438.
|
|
5
8
|
- **`current_attributes` capture skips an unpersisted record instead of raising at enqueue (issue #435).** An attribute holding an Active Record instance with no id — a dev-mode fallback record assigned to `Current`, a form-built model captured before `save`, a destroyed record whose locate is guaranteed to fail — made every `perform_later` in that context raise `Pgbus::CurrentAttributesError`, even though such a record can never round-trip (no id → no GlobalID) and capture is ambient: the enqueuer never opted into persisting that attribute per-call, so its momentary state must not abort the enqueue. `capture` now skips any attribute value that answers `persisted?` falsey (so destroyed-but-id-bearing records are skipped too, not just `new_record?`) with a debug log naming the class, attribute and why; the rest of the class's attributes still persist. The `except:` guidance and the loud `CurrentAttributesError` remain for genuinely unserializable values — objects without the Active Record duck-type (`respond_to?(:persisted?)`) are untouched. Applies to jobs and event-bus publish alike (same capture path). Refs #435.
|
|
6
9
|
|
|
7
10
|
### Added
|
data/lib/pgbus/client.rb
CHANGED
|
@@ -1753,11 +1753,10 @@ module Pgbus
|
|
|
1753
1753
|
timeout = config.read_timeout
|
|
1754
1754
|
return mapping_statement_timeout(&block) unless timeout&.positive?
|
|
1755
1755
|
|
|
1756
|
-
# rubocop:disable Pgbus/NoRubyTimeout -- deliberate last-resort bound; see above
|
|
1756
|
+
# rubocop:disable-next Pgbus/NoRubyTimeout -- deliberate last-resort bound; see above
|
|
1757
1757
|
Timeout.timeout(timeout + READ_TIMEOUT_SLACK, WedgedReadTimeout) do
|
|
1758
1758
|
mapping_statement_timeout(&block)
|
|
1759
1759
|
end
|
|
1760
|
-
# rubocop:enable Pgbus/NoRubyTimeout
|
|
1761
1760
|
rescue WedgedReadTimeout
|
|
1762
1761
|
reload_pool_after_wedged_timeout
|
|
1763
1762
|
raise
|
|
@@ -95,7 +95,7 @@ module Pgbus
|
|
|
95
95
|
"Add `gem \"async\"` to your Gemfile. Original error: #{e.message}"
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# rubocop:disable Lint/RescueException
|
|
98
|
+
# rubocop:disable-next Lint/RescueException
|
|
99
99
|
def start_reactor
|
|
100
100
|
Thread.new do
|
|
101
101
|
Thread.current.name = "pgbus-async-reactor-#{object_id}"
|
|
@@ -122,7 +122,6 @@ module Pgbus
|
|
|
122
122
|
raise
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
|
-
# rubocop:enable Lint/RescueException
|
|
126
125
|
|
|
127
126
|
def wait_for_executions(semaphore)
|
|
128
127
|
loop do
|
|
@@ -18,6 +18,12 @@ module Pgbus
|
|
|
18
18
|
@loop_tick_supplier = loop_tick_supplier
|
|
19
19
|
@metadata_supplier = metadata_supplier
|
|
20
20
|
@timer = nil
|
|
21
|
+
@stopped = false
|
|
22
|
+
# Guards @process_id between the timer thread (beat) and the main
|
|
23
|
+
# thread (stop): TimerTask#shutdown does not wait for an in-flight
|
|
24
|
+
# beat, and a beat that re-registers after deregister_process ran
|
|
25
|
+
# would leave a zombie row (issue #438).
|
|
26
|
+
@mutex = Mutex.new
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def start
|
|
@@ -27,18 +33,19 @@ module Pgbus
|
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
def stop
|
|
36
|
+
@stopped = true
|
|
30
37
|
@timer&.shutdown
|
|
31
|
-
deregister_process
|
|
38
|
+
@mutex.synchronize { deregister_process }
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
def beat
|
|
35
|
-
return unless @process_id
|
|
42
|
+
return unless @process_id && !@stopped
|
|
36
43
|
|
|
37
44
|
@on_beat&.call
|
|
38
45
|
updates = { last_heartbeat_at: Time.current }
|
|
39
46
|
metadata = beat_metadata
|
|
40
47
|
updates[:metadata] = metadata unless metadata.nil?
|
|
41
|
-
|
|
48
|
+
@mutex.synchronize { write_beat(updates) }
|
|
42
49
|
rescue StandardError => e
|
|
43
50
|
Pgbus.logger.warn { "[Pgbus] Heartbeat failed: #{e.message}" }
|
|
44
51
|
end
|
|
@@ -59,6 +66,25 @@ module Pgbus
|
|
|
59
66
|
metadata
|
|
60
67
|
end
|
|
61
68
|
|
|
69
|
+
# update_all by id matches 0 rows when the row was deleted underneath a
|
|
70
|
+
# live process (stale-process reaper after a heartbeat gap, manual
|
|
71
|
+
# cleanup, another host's clock skew). Nothing raises, so treat the
|
|
72
|
+
# count as the signal: re-register and log once so the cause of the
|
|
73
|
+
# deletion is discoverable, then land this beat's updates on the new
|
|
74
|
+
# row (issue #438). Skipped after stop so a beat racing deregistration
|
|
75
|
+
# cannot resurrect the row.
|
|
76
|
+
def write_beat(updates)
|
|
77
|
+
return if ProcessEntry.where(id: @process_id).update_all(updates).positive? || @stopped
|
|
78
|
+
|
|
79
|
+
old_id = @process_id
|
|
80
|
+
Pgbus.logger.warn do
|
|
81
|
+
"[Pgbus] Process row id=#{old_id} kind=#{@kind} pid=#{::Process.pid} is gone " \
|
|
82
|
+
"(stale-process reaper, manual cleanup, or clock skew?) — re-registering"
|
|
83
|
+
end
|
|
84
|
+
register_process
|
|
85
|
+
ProcessEntry.where(id: @process_id).update_all(updates) if @process_id != old_id
|
|
86
|
+
end
|
|
87
|
+
|
|
62
88
|
def register_process
|
|
63
89
|
record = ProcessEntry.create!(
|
|
64
90
|
kind: @kind,
|
|
@@ -143,13 +143,21 @@ module Pgbus
|
|
|
143
143
|
@state_mutex.synchronize { @running }
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
# Called ONLY inside a just-forked child (
|
|
147
|
-
# this process's copy of the LISTEN socket
|
|
146
|
+
# Called ONLY inside a just-forked child (issues #381 / #437): release
|
|
147
|
+
# this process's copy of the LISTEN socket WITHOUT PQfinish — #close
|
|
148
148
|
# would send a libpq Terminate over the socket shared with the parent,
|
|
149
|
-
# killing the parent's LISTEN session.
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
149
|
+
# killing the parent's LISTEN session.
|
|
150
|
+
#
|
|
151
|
+
# Closing the IO wrapper is not enough: pg builds socket_io with
|
|
152
|
+
# autoclose=false, so IO#close leaves the fd open, and when GC frees the
|
|
153
|
+
# inherited PG::Connection its PQfinish still writes Terminate on that
|
|
154
|
+
# fd — i.e. on the PARENT's connection (issue #437, one reconnect per
|
|
155
|
+
# fork). Repointing the fd at /dev/null (IO#reopen, the ActiveRecord
|
|
156
|
+
# PostgreSQLAdapter#discard! idiom) makes the eventual PQfinish harmless.
|
|
157
|
+
#
|
|
158
|
+
# The listener thread does not exist in the child (fork copies only the
|
|
159
|
+
# calling thread), so there is no concurrent owner and the single-owner
|
|
160
|
+
# rule (#375) does not apply.
|
|
153
161
|
def close_inherited_socket!
|
|
154
162
|
conn = @state_mutex.synchronize do
|
|
155
163
|
c = @conn
|
|
@@ -157,10 +165,11 @@ module Pgbus
|
|
|
157
165
|
@running = false
|
|
158
166
|
c
|
|
159
167
|
end
|
|
160
|
-
conn&.socket_io&.
|
|
168
|
+
conn&.socket_io&.reopen(IO::NULL)
|
|
161
169
|
rescue StandardError => e
|
|
162
|
-
# Best-effort
|
|
163
|
-
#
|
|
170
|
+
# Best-effort but never silent: the child keeps booting either way.
|
|
171
|
+
# If this fails the child's GC-time PQfinish will hit the parent's
|
|
172
|
+
# session, which the parent's reconnect! survives.
|
|
164
173
|
@logger.warn do
|
|
165
174
|
"[Pgbus::NotifyListener] inherited socket cleanup failed: #{e.class}: #{e.message}"
|
|
166
175
|
end
|
|
@@ -613,9 +613,7 @@ module Pgbus
|
|
|
613
613
|
if @shutting_down
|
|
614
614
|
Pgbus.logger.info { "[Pgbus] Child #{info[:type]} pid=#{pid} exited (status=#{status.exitstatus})" }
|
|
615
615
|
else
|
|
616
|
-
|
|
617
|
-
"[Pgbus] Child #{info[:type]} pid=#{pid} exited unexpectedly (status=#{status&.exitstatus})"
|
|
618
|
-
end
|
|
616
|
+
log_child_exit(info, pid, status)
|
|
619
617
|
schedule_restart(info, status)
|
|
620
618
|
end
|
|
621
619
|
rescue Errno::ECHILD
|
|
@@ -623,6 +621,27 @@ module Pgbus
|
|
|
623
621
|
end
|
|
624
622
|
end
|
|
625
623
|
|
|
624
|
+
# A clean exit outside shutdown is a worker/consumer recycle (max_jobs,
|
|
625
|
+
# max_memory, max_lifetime) — expected, so INFO. Anything else is a
|
|
626
|
+
# crash: WARN, naming the signal when there is one so an OOM SIGKILL
|
|
627
|
+
# reads differently from an exit code (issue #438).
|
|
628
|
+
def log_child_exit(info, pid, status)
|
|
629
|
+
if status&.success?
|
|
630
|
+
Pgbus.logger.info do
|
|
631
|
+
"[Pgbus] Child #{info[:type]} pid=#{pid} exited cleanly (status=0) — restarting (worker recycle)"
|
|
632
|
+
end
|
|
633
|
+
else
|
|
634
|
+
Pgbus.logger.warn do
|
|
635
|
+
detail = if status && status.exitstatus.nil? && status.signaled?
|
|
636
|
+
"signal=#{status.termsig}"
|
|
637
|
+
else
|
|
638
|
+
"status=#{status&.exitstatus}"
|
|
639
|
+
end
|
|
640
|
+
"[Pgbus] Child #{info[:type]} pid=#{pid} exited unexpectedly (#{detail})"
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
626
645
|
# Restart policy: a clean exit (worker recycling) or a crash after a
|
|
627
646
|
# stable run restarts immediately with a fresh crash streak. A crash
|
|
628
647
|
# within RESTART_STABLE_UPTIME of forking is a crash loop — the child
|
data/lib/pgbus/version.rb
CHANGED
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.15.
|
|
4
|
+
version: 0.15.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -424,13 +424,13 @@ files:
|
|
|
424
424
|
- lib/tasks/pgbus_pgmq.rake
|
|
425
425
|
- lib/tasks/pgbus_queues.rake
|
|
426
426
|
- lib/tasks/pgbus_streams.rake
|
|
427
|
-
homepage: https://github.com/
|
|
427
|
+
homepage: https://github.com/zoolutions/pgbus
|
|
428
428
|
licenses:
|
|
429
429
|
- MIT
|
|
430
430
|
metadata:
|
|
431
|
-
homepage_uri: https://github.com/
|
|
432
|
-
source_code_uri: https://github.com/
|
|
433
|
-
changelog_uri: https://github.com/
|
|
431
|
+
homepage_uri: https://github.com/zoolutions/pgbus
|
|
432
|
+
source_code_uri: https://github.com/zoolutions/pgbus/tree/main
|
|
433
|
+
changelog_uri: https://github.com/zoolutions/pgbus/blob/main/CHANGELOG.md
|
|
434
434
|
documentation_uri: https://pgbus.zoolutions.llc
|
|
435
435
|
rubygems_mfa_required: 'true'
|
|
436
436
|
rdoc_options: []
|