pgbus 0.23.2 → 0.24.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 +8 -0
- data/lib/pgbus/client.rb +19 -0
- data/lib/pgbus/event_bus/registry.rb +77 -0
- data/lib/pgbus/version.rb +1 -1
- data/sig/pgbus/client.rbs +2 -0
- 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: d4d5cae2b9bd98099c3a64a88d9671f66b60dcc108d826095d5d97b9fafde703
|
|
4
|
+
data.tar.gz: 8c6533b41dc54cdca5f344718c3280059e1f930269ddfcf75f65426efb08a14e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80f77818760dd8c1290808ea1fa62ef42c7b5fb9490e65f93766648df2e769a152bcdbf8e9362edc878f08177dde4230e632ffdaebbb46139099255ffa2db8b5
|
|
7
|
+
data.tar.gz: 332be03140b01225aba67f96e57842459c57c103ed0578c7d4a4d446209250bc77d27563268c1b7773d6b1d844a115e2bae7676fc385fb1e8695d846621bb1af
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.24.0] - 2026-11-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`Pgbus::Client#unbind_topic` and `Registry#prune_stale_bindings!`: a narrowed subscription can drop its old topic binding (issue #657).** A subscriber queue is named after its handler, so narrowing a handler's subscription (from `"#"` to `"order.created"`, say) bound the new pattern on the same queue and left the old `pgmq.topic_bindings` row: every publish only the old pattern matched still wrote a message into that queue, which the consumer archived as unrouted, and the only way out was raw SQL against the pgmq schema. `Client#unbind_topic(pattern, queue_name)` removes one binding from a logical queue name (prefixed as `#bind_topic` does, with the same stale-connection retry) and returns `true`, or `false` when there was none; it never creates the queue. `Client#topic_patterns(queue_name)` lists the patterns bound to a queue. `Pgbus::EventBus::Registry.instance.prune_stale_bindings!` binds each registered subscriber queue's declared patterns first, so a queue is never left without a binding, then unbinds every other pattern on it, and returns `{ queue_name => [removed patterns] }` for the queues it changed (`{}` on a second run). Queues with no subscriber registered in the process are not touched, so run it with every subscriber registered: a pattern only an unloaded subscriber declares counts as stale. Queues are compared by their physical name, so a Symbol `queue_name:` and two spellings that name one table (`"Audit-Q"`, `"audit_q"`) count as one queue and keep each other's patterns; the result is keyed by the first registered spelling. `queue_names:` (logical names, not the prefixed ones `#event_queue_names` returns) limits it to the named queues, `queue_names: []` prunes nothing, and a name no registered subscriber uses raises `ArgumentError` before anything changes, so a typo or a process without the handlers loaded cannot strip a live queue; a removed handler's queue is cleaned up with `Client#topic_patterns` and `Client#unbind_topic`. `setup_all!` does not call it: during a rolling deploy a process still on the old release owns the old pattern, so run it as an explicit step (a deploy task, a rake task) once every process runs the new subscriptions. Refs #657.
|
|
8
|
+
|
|
9
|
+
## [0.23.2] - 2026-11-06
|
|
10
|
+
|
|
3
11
|
### Fixed
|
|
4
12
|
|
|
5
13
|
- **A `<pgbus-stream-source>` moved in the DOM resumes from the last durable id it received, so it no longer replays and re-applies frames it already applied (issue #644).** `connectedCallback` reset the resume cursor from the `since-id` attribute on every connect, and `since-id` is the render-time watermark, never updated afterwards. An element disconnected and reconnected as the same instance (Turbo Drive carrying a `data-turbo-permanent` element into the next page's body, or app code calling `appendChild` / `replaceWith` / `insertBefore` on it) reconnected with `?since=<render-time watermark>`, the server replayed every durable frame since the page rendered, and each `replace` / `update` ran again over whatever the target had become since. The element now keeps the cursor it earned on the same `src`: a connect sends `?since=` with the greater of that cursor and `since-id` (both durable msg_ids of one stream, compared as `BigInt`; when only one parses as an integer, that one), so a re-render that carries a newer watermark still wins. A cursor earned on another `src` (the element was re-pointed) is dropped and `since-id` is used alone. The old connection's read loop now stops on its own abort signal, checked between the frames of one network chunk too, so a `pgbus:message` listener that moves the element mid-chunk no longer has the rest of that chunk delivered by both the old loop and the new connection's replay. The first connect is unchanged, and an ephemeral frame still never moves the cursor (issue #621). The workaround of writing `since-id` from a `pgbus:message` listener is no longer needed. No new API. Refs #644.
|
data/lib/pgbus/client.rb
CHANGED
|
@@ -1098,6 +1098,25 @@ module Pgbus
|
|
|
1098
1098
|
end
|
|
1099
1099
|
end
|
|
1100
1100
|
|
|
1101
|
+
# Removes the +pattern+ binding from the logical +queue_name+ (issue #657).
|
|
1102
|
+
# True when a binding was removed, false when there was none. Never creates
|
|
1103
|
+
# the queue.
|
|
1104
|
+
def unbind_topic(pattern, queue_name)
|
|
1105
|
+
full_name = config.queue_name(queue_name)
|
|
1106
|
+
with_stale_connection_retry do
|
|
1107
|
+
synchronized { @pgmq.unbind_topic(pattern, full_name) }
|
|
1108
|
+
end
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
# The patterns bound to the logical +queue_name+ in pgmq.topic_bindings
|
|
1112
|
+
# (issue #657).
|
|
1113
|
+
def topic_patterns(queue_name)
|
|
1114
|
+
full_name = config.queue_name(queue_name)
|
|
1115
|
+
with_stale_connection_retry do
|
|
1116
|
+
synchronized { @pgmq.list_topic_bindings(queue_name: full_name).map { |binding| binding[:pattern] } }
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1101
1120
|
def publish_to_topic(routing_key, payload, headers: nil, delay: 0)
|
|
1102
1121
|
with_stale_connection_retry do
|
|
1103
1122
|
synchronized do
|
|
@@ -62,6 +62,40 @@ module Pgbus
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Makes each subscriber queue's pgmq.topic_bindings equal to the patterns
|
|
66
|
+
# registered for it (issue #657). A subscriber queue is named after its
|
|
67
|
+
# handler, so narrowing a subscription ("#" → "order.created") binds the
|
|
68
|
+
# new pattern on the same queue and leaves the old row, which keeps
|
|
69
|
+
# copying events the consumer archives as unrouted.
|
|
70
|
+
#
|
|
71
|
+
# For every registered queue (or only +queue_names+, logical names, not
|
|
72
|
+
# the prefixed ones #event_queue_names returns): bind the patterns its
|
|
73
|
+
# subscribers declare first, so the queue is never left without a
|
|
74
|
+
# binding, then unbind every other pattern bound to it. Run it with every
|
|
75
|
+
# subscriber registered: a pattern only an unloaded subscriber declares
|
|
76
|
+
# counts as stale. +queue_names: []+ prunes nothing; a name no registered
|
|
77
|
+
# subscriber uses raises ArgumentError before anything changes (a typo, a
|
|
78
|
+
# prefixed name or a process without the handlers loaded would otherwise
|
|
79
|
+
# strip a live queue). A removed handler's queue is cleaned up with
|
|
80
|
+
# Pgbus.client.topic_patterns and Pgbus.client.unbind_topic.
|
|
81
|
+
#
|
|
82
|
+
# Returns { queue_name => [removed patterns] } for the queues it changed;
|
|
83
|
+
# a second run returns {}.
|
|
84
|
+
#
|
|
85
|
+
# Deliberately not part of setup_all!: during a rolling deploy a process
|
|
86
|
+
# still on the old release owns the old pattern, so pruning is an explicit
|
|
87
|
+
# step to run once every process runs the new subscriptions.
|
|
88
|
+
def prune_stale_bindings!(queue_names: nil)
|
|
89
|
+
declared = declared_bindings
|
|
90
|
+
targets = queue_names.nil? ? declared.keys : physical_targets(queue_names, declared)
|
|
91
|
+
|
|
92
|
+
targets.each_with_object({}) do |physical, removed|
|
|
93
|
+
queue_name, patterns = declared.fetch(physical)
|
|
94
|
+
stale = prune_queue_bindings(queue_name, patterns)
|
|
95
|
+
removed[queue_name] = stale unless stale.empty?
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
65
99
|
# Subscribers a message read from +queue_name+ must be dispatched to
|
|
66
100
|
# (issue #469). Every subscriber gets its own queue, so a topic with N
|
|
67
101
|
# matching subscribers produces N queue copies of each event; selecting by
|
|
@@ -133,6 +167,49 @@ module Pgbus
|
|
|
133
167
|
|
|
134
168
|
private
|
|
135
169
|
|
|
170
|
+
# { physical queue name => [logical name, declared patterns] }. Keyed by
|
|
171
|
+
# the physical name so a Symbol and a String, or two spellings
|
|
172
|
+
# QueueNameValidator folds to one table ("Audit-Q", "audit_q"), count as
|
|
173
|
+
# one queue; the logical name is the first registered spelling.
|
|
174
|
+
def declared_bindings
|
|
175
|
+
subscribers = @mutex.synchronize { @subscribers.dup }
|
|
176
|
+
subscribers.each_with_object({}) do |subscriber, declared|
|
|
177
|
+
physical = Pgbus.configuration.queue_name(subscriber.queue_name)
|
|
178
|
+
entry = declared[physical] ||= [subscriber.queue_name.to_s, []]
|
|
179
|
+
entry[1] |= [subscriber.pattern]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# The physical names of +queue_names+; raises before any change when one
|
|
184
|
+
# names no registered subscriber queue.
|
|
185
|
+
def physical_targets(queue_names, declared)
|
|
186
|
+
targets = queue_names.to_h { |name| [name, Pgbus.configuration.queue_name(name)] }
|
|
187
|
+
unknown = targets.reject { |_name, physical| declared.key?(physical) }.keys
|
|
188
|
+
unless unknown.empty?
|
|
189
|
+
raise ArgumentError, "No event subscriber is registered on queue(s) #{unknown.join(", ")}; " \
|
|
190
|
+
"prune_stale_bindings! takes logical subscriber queue names"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
targets.values.uniq
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Binds +patterns+ to +queue_name+, unbinds every other pattern bound to
|
|
197
|
+
# it, and returns the patterns this call removed (one another process
|
|
198
|
+
# removed first is not reported).
|
|
199
|
+
def prune_queue_bindings(queue_name, patterns)
|
|
200
|
+
client = Pgbus.client
|
|
201
|
+
patterns.each { |pattern| client.bind_topic(pattern, queue_name) }
|
|
202
|
+
|
|
203
|
+
stale = client.topic_patterns(queue_name) - patterns
|
|
204
|
+
removed = stale.select { |pattern| client.unbind_topic(pattern, queue_name) }
|
|
205
|
+
unless removed.empty?
|
|
206
|
+
Pgbus.logger.info do
|
|
207
|
+
"[Pgbus] Unbound stale topic patterns #{removed.inspect} from event queue #{queue_name}"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
removed
|
|
211
|
+
end
|
|
212
|
+
|
|
136
213
|
# True when running inside a rake schema/asset task, where setup_all!(safe:)
|
|
137
214
|
# should skip rather than open a connection. Delegates to the shared,
|
|
138
215
|
# public detector (issue #409) so there is a single implementation.
|
data/lib/pgbus/version.rb
CHANGED
data/sig/pgbus/client.rbs
CHANGED
|
@@ -71,7 +71,9 @@ module Pgbus
|
|
|
71
71
|
def streams_swap_stats: () -> untyped
|
|
72
72
|
def synchronizing?: () -> bool
|
|
73
73
|
def target_queue: (untyped queue_name, ?untyped priority) -> untyped
|
|
74
|
+
def topic_patterns: (untyped queue_name) -> Array[String]
|
|
74
75
|
def transaction: () ?{ (*untyped) -> untyped } -> untyped
|
|
76
|
+
def unbind_topic: (untyped pattern, untyped queue_name) -> bool
|
|
75
77
|
def uniqueness_keys_present: (untyped lock_keys) -> untyped
|
|
76
78
|
def update_notify_insert: (untyped queue_name, throttle_interval_ms: untyped) -> untyped
|
|
77
79
|
def verify_connection!: () -> untyped
|