pgbus 0.7.5 → 0.7.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d2034c37f18f53d3a55df8381e7d37581905b622487f9ae4beb0c1e0f35c964
4
- data.tar.gz: 419c3c7c6b62276903579ca53e39baf06ed1c0e818817e6139ceb004ad6026b3
3
+ metadata.gz: a28025e2b7af463cb6ff57d12e516d461b9b968ed7bb4eb4568be18d77ff2678
4
+ data.tar.gz: fd39f5774df158b0cf06e85a6ef52f0114d649008f56b4736a782ce895baae0b
5
5
  SHA512:
6
- metadata.gz: 40bf9610792cef84a8792035b513c660312ff91d0642432a088f544b84846996856d7bd318b7c06d530b8cd573a2dbb78f20ee132f53540a937a075625a54afc
7
- data.tar.gz: 50ddee092a20af26d348c13ae61f48a8566ec0e46687a958012dbdb508e35e2fde217ee277ddaf47333c9c5b20ad51197c0b7950c358b519c8590573433206aa
6
+ metadata.gz: 19ced25de78f93ccbf307e2bdd72a5dbc46050657c6e394bc9e6a16d8665fa19bc2a29cebf2fcbda8c31eb07e07d94545322eec80c5d61915ddd6bfc924de105
7
+ data.tar.gz: 9d8e2a9255ce011edf6bc2647fbd4d604db65b8fb2470c17735149599072f2824e510db4e97c4d241cdd4d6a9fef2ff7c23638738cd40de581ee56a07b783f6b
@@ -19,17 +19,20 @@ module Pgbus
19
19
  # stream and from the streamer on first subscription per stream.
20
20
  module EnsureStreamQueue
21
21
  def ensure_stream_queue(stream_name)
22
- ensure_queue(stream_name)
23
22
  full_name = config.queue_name(stream_name)
24
23
 
25
- # PGMQ's default NOTIFY throttle is 250ms — meant to coalesce
26
- # high-frequency worker queue inserts. Streams are latency-
27
- # sensitive and need every broadcast to fire a NOTIFY, even
28
- # when several are batched within a single millisecond.
29
- # Override the throttle to 0 specifically for stream queues.
30
- # Use the idempotent path to avoid deadlocks when multiple
31
- # processes race to set up the same stream queue.
32
- synchronized { enable_notify_if_needed(full_name, 0) }
24
+ with_stale_connection_retry do
25
+ ensure_queue(stream_name)
26
+
27
+ # PGMQ's default NOTIFY throttle is 250ms meant to coalesce
28
+ # high-frequency worker queue inserts. Streams are latency-
29
+ # sensitive and need every broadcast to fire a NOTIFY, even
30
+ # when several are batched within a single millisecond.
31
+ # Override the throttle to 0 specifically for stream queues.
32
+ # Use the idempotent path to avoid deadlocks when multiple
33
+ # processes race to set up the same stream queue.
34
+ synchronized { enable_notify_if_needed(full_name, 0) }
35
+ end
33
36
 
34
37
  # CREATE INDEX IF NOT EXISTS is idempotent in Postgres but still
35
38
  # requires a roundtrip and a brief ACCESS SHARE lock on the archive
data/lib/pgbus/client.rb CHANGED
@@ -85,9 +85,9 @@ module Pgbus
85
85
 
86
86
  def send_message(queue_name, payload, headers: nil, delay: 0, priority: nil)
87
87
  target = @queue_strategy.target_queue(queue_name, priority)
88
- ensure_queue(queue_name)
89
88
  Instrumentation.instrument("pgbus.client.send_message", queue: target) do
90
89
  with_stale_connection_retry do
90
+ ensure_queue(queue_name)
91
91
  synchronized { @pgmq.produce(target, serialize(payload), headers: headers && serialize(headers), delay: delay) }
92
92
  end
93
93
  end
@@ -95,10 +95,10 @@ module Pgbus
95
95
 
96
96
  def send_batch(queue_name, payloads, headers: nil, delay: 0)
97
97
  full_name = config.queue_name(queue_name)
98
- ensure_queue(queue_name)
99
98
  serialized, serialized_headers = serialize_batch(payloads, headers)
100
99
  Instrumentation.instrument("pgbus.client.send_batch", queue: full_name, size: payloads.size) do
101
100
  with_stale_connection_retry do
101
+ ensure_queue(queue_name)
102
102
  synchronized { @pgmq.produce_batch(full_name, serialized, headers: serialized_headers, delay: delay) }
103
103
  end
104
104
  end
@@ -107,14 +107,18 @@ module Pgbus
107
107
  def read_message(queue_name, vt: nil)
108
108
  full_name = config.queue_name(queue_name)
109
109
  Instrumentation.instrument("pgbus.client.read_message", queue: full_name) do
110
- synchronized { @pgmq.read(full_name, vt: vt || config.visibility_timeout) }
110
+ with_stale_connection_retry do
111
+ synchronized { @pgmq.read(full_name, vt: vt || config.visibility_timeout) }
112
+ end
111
113
  end
112
114
  end
113
115
 
114
116
  def read_batch(queue_name, qty:, vt: nil)
115
117
  full_name = config.queue_name(queue_name)
116
118
  Instrumentation.instrument("pgbus.client.read_batch", queue: full_name, qty: qty) do
117
- synchronized { @pgmq.read_batch(full_name, vt: vt || config.visibility_timeout, qty: qty) }
119
+ with_stale_connection_retry do
120
+ synchronized { @pgmq.read_batch(full_name, vt: vt || config.visibility_timeout, qty: qty) }
121
+ end
118
122
  end
119
123
  end
120
124
 
@@ -134,7 +138,9 @@ module Pgbus
134
138
  break if remaining <= 0
135
139
 
136
140
  msgs = Instrumentation.instrument("pgbus.client.read_batch", queue: pq_name, qty: remaining) do
137
- synchronized { @pgmq.read_batch(pq_name, vt: vt || config.visibility_timeout, qty: remaining) }
141
+ with_stale_connection_retry do
142
+ synchronized { @pgmq.read_batch(pq_name, vt: vt || config.visibility_timeout, qty: remaining) }
143
+ end
138
144
  end || []
139
145
 
140
146
  msgs.each { |m| results << [pq_name, m] }
@@ -146,14 +152,16 @@ module Pgbus
146
152
 
147
153
  def read_with_poll(queue_name, qty:, vt: nil, max_poll_seconds: 5, poll_interval_ms: 100)
148
154
  full_name = config.queue_name(queue_name)
149
- synchronized do
150
- @pgmq.read_with_poll(
151
- full_name,
152
- vt: vt || config.visibility_timeout,
153
- qty: qty,
154
- max_poll_seconds: max_poll_seconds,
155
- poll_interval_ms: poll_interval_ms
156
- )
155
+ with_stale_connection_retry do
156
+ synchronized do
157
+ @pgmq.read_with_poll(
158
+ full_name,
159
+ vt: vt || config.visibility_timeout,
160
+ qty: qty,
161
+ max_poll_seconds: max_poll_seconds,
162
+ poll_interval_ms: poll_interval_ms
163
+ )
164
+ end
157
165
  end
158
166
  end
159
167
 
@@ -168,8 +176,10 @@ module Pgbus
168
176
  def read_multi(queue_names, qty:, vt: nil, limit: nil)
169
177
  full_names = queue_names.map { |q| config.queue_name(q) }
170
178
  Instrumentation.instrument("pgbus.client.read_multi", queues: full_names, qty: qty, limit: limit) do
171
- synchronized do
172
- @pgmq.read_multi(full_names, vt: vt || config.visibility_timeout, qty: qty, limit: limit)
179
+ with_stale_connection_retry do
180
+ synchronized do
181
+ @pgmq.read_multi(full_names, vt: vt || config.visibility_timeout, qty: qty, limit: limit)
182
+ end
173
183
  end
174
184
  end
175
185
  end
@@ -178,74 +188,99 @@ module Pgbus
178
188
  # the full PGMQ queue name (e.g. from priority sub-queues or dashboard).
179
189
  def delete_message(queue_name, msg_id, prefixed: true)
180
190
  name = prefixed ? config.queue_name(queue_name) : queue_name
181
- synchronized { @pgmq.delete(name, msg_id) }
191
+ with_stale_connection_retry do
192
+ synchronized { @pgmq.delete(name, msg_id) }
193
+ end
182
194
  end
183
195
 
184
196
  # Archive a message. Pass prefixed: false when queue_name is already
185
197
  # the full PGMQ queue name.
186
198
  def archive_message(queue_name, msg_id, prefixed: true)
187
199
  name = prefixed ? config.queue_name(queue_name) : queue_name
188
- synchronized { @pgmq.archive(name, msg_id) }
200
+ with_stale_connection_retry do
201
+ synchronized { @pgmq.archive(name, msg_id) }
202
+ end
189
203
  end
190
204
 
191
205
  # Batch archive — moves multiple messages to the archive table in one call.
192
206
  def archive_batch(queue_name, msg_ids, prefixed: true)
193
207
  name = prefixed ? config.queue_name(queue_name) : queue_name
194
- synchronized { @pgmq.archive_batch(name, msg_ids) }
208
+ with_stale_connection_retry do
209
+ synchronized { @pgmq.archive_batch(name, msg_ids) }
210
+ end
195
211
  end
196
212
 
197
213
  # Batch delete — permanently removes multiple messages in one call.
198
214
  def delete_batch(queue_name, msg_ids, prefixed: true)
199
215
  name = prefixed ? config.queue_name(queue_name) : queue_name
200
- synchronized { @pgmq.delete_batch(name, msg_ids) }
216
+ with_stale_connection_retry do
217
+ synchronized { @pgmq.delete_batch(name, msg_ids) }
218
+ end
201
219
  end
202
220
 
203
221
  # Set visibility timeout. Pass prefixed: false when queue_name is already
204
222
  # the full PGMQ queue name.
205
223
  def set_visibility_timeout(queue_name, msg_id, vt:, prefixed: true)
206
224
  name = prefixed ? config.queue_name(queue_name) : queue_name
207
- synchronized { @pgmq.set_vt(name, msg_id, vt: vt) }
225
+ with_stale_connection_retry do
226
+ synchronized { @pgmq.set_vt(name, msg_id, vt: vt) }
227
+ end
208
228
  end
209
229
 
230
+ # Open a PGMQ transaction. The caller block may run twice if the first
231
+ # attempt hits a pre-flight stale-connection error — safe because no SQL
232
+ # was sent on the first attempt (the connection was dead before the BEGIN).
210
233
  def transaction(&block)
211
- synchronized { @pgmq.transaction(&block) }
234
+ with_stale_connection_retry do
235
+ synchronized { @pgmq.transaction(&block) }
236
+ end
212
237
  end
213
238
 
214
239
  def move_to_dead_letter(queue_name, message)
215
- ensure_dead_letter_queue(queue_name)
216
240
  dlq_name = config.dead_letter_queue_name(queue_name)
217
241
  full_queue = config.queue_name(queue_name)
218
242
 
219
- synchronized do
220
- @pgmq.transaction do |txn|
221
- txn.produce(dlq_name, message.message, headers: message.headers)
222
- txn.delete(full_queue, message.msg_id.to_i)
243
+ with_stale_connection_retry do
244
+ ensure_dead_letter_queue(queue_name)
245
+ synchronized do
246
+ @pgmq.transaction do |txn|
247
+ txn.produce(dlq_name, message.message, headers: message.headers)
248
+ txn.delete(full_queue, message.msg_id.to_i)
249
+ end
223
250
  end
224
251
  end
225
252
  end
226
253
 
227
254
  def metrics(queue_name = nil)
228
- synchronized do
229
- if queue_name
230
- @pgmq.metrics(config.queue_name(queue_name))
231
- else
232
- @pgmq.metrics_all
255
+ with_stale_connection_retry do
256
+ synchronized do
257
+ if queue_name
258
+ @pgmq.metrics(config.queue_name(queue_name))
259
+ else
260
+ @pgmq.metrics_all
261
+ end
233
262
  end
234
263
  end
235
264
  end
236
265
 
237
266
  def list_queues
238
- synchronized { @pgmq.list_queues }
267
+ with_stale_connection_retry do
268
+ synchronized { @pgmq.list_queues }
269
+ end
239
270
  end
240
271
 
241
272
  def purge_queue(queue_name, prefixed: true)
242
273
  name = prefixed ? config.queue_name(queue_name) : queue_name
243
- synchronized { @pgmq.purge_queue(name) }
274
+ with_stale_connection_retry do
275
+ synchronized { @pgmq.purge_queue(name) }
276
+ end
244
277
  end
245
278
 
246
279
  def drop_queue(queue_name, prefixed: true)
247
280
  name = prefixed ? config.queue_name(queue_name) : queue_name
248
- result = synchronized { @pgmq.drop_queue(name) }
281
+ result = with_stale_connection_retry do
282
+ synchronized { @pgmq.drop_queue(name) }
283
+ end
249
284
  @queues_created.delete(name)
250
285
  result
251
286
  end
@@ -317,8 +352,10 @@ module Pgbus
317
352
  # Topic routing
318
353
  def bind_topic(pattern, queue_name)
319
354
  full_name = config.queue_name(queue_name)
320
- ensure_queue(queue_name)
321
- synchronized { @pgmq.bind_topic(pattern, full_name) }
355
+ with_stale_connection_retry do
356
+ ensure_queue(queue_name)
357
+ synchronized { @pgmq.bind_topic(pattern, full_name) }
358
+ end
322
359
  end
323
360
 
324
361
  def publish_to_topic(routing_key, payload, headers: nil, delay: 0)
@@ -541,15 +578,24 @@ module Pgbus
541
578
  "connection is closed",
542
579
  "connection has been closed",
543
580
  "connection not open",
544
- "no connection to the server"
581
+ "no connection to the server",
582
+ "ssl error: unexpected eof",
583
+ "ssl syscall error"
545
584
  ].freeze
546
585
  private_constant :STALE_CONNECTION_PATTERNS
547
586
 
548
- # Enqueue path guard: rescue PGMQ::Errors::ConnectionError once if its
549
- # message matches a known stale-socket pattern. pgmq-ruby's
550
- # auto_reconnect + verify_connection! already recovers on the *next*
551
- # checkout, so a single retry is sufficient. Other connection errors
552
- # (pool timeout, misconfiguration, truly unreachable DB) propagate.
587
+ # Rescue PGMQ::Errors::ConnectionError once if its message matches a
588
+ # known stale-socket pattern. pgmq-ruby's auto_reconnect + verify_connection!
589
+ # already recovers on the *next* checkout, so a single retry is sufficient.
590
+ # Other connection errors (pool timeout, misconfiguration, truly unreachable
591
+ # DB) propagate.
592
+ #
593
+ # Wraps every @pgmq.* call site. Pattern matching is intentionally narrow
594
+ # (pre-flight / idle-socket signals only), so retry is safe even for
595
+ # non-idempotent ops like delete/archive — a matched error means the
596
+ # connection was dead *before* pgmq-ruby tried to use it, so no SQL was
597
+ # ever sent. Mid-flight errors like "server closed the connection" are
598
+ # excluded from the pattern list for this reason.
553
599
  def with_stale_connection_retry
554
600
  attempts = 0
555
601
  begin
@@ -559,7 +605,7 @@ module Pgbus
559
605
  raise unless attempts == 1 && stale_connection_error?(e)
560
606
 
561
607
  Pgbus.logger.warn do
562
- "[Pgbus::Client] Retrying produce after stale pgmq connection: #{e.message}"
608
+ "[Pgbus::Client] Retrying after stale pgmq connection: #{e.message}"
563
609
  end
564
610
  retry
565
611
  end
data/lib/pgbus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.7.5"
4
+ VERSION = "0.7.6"
5
5
  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.7.5
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson