pgbus 0.21.4 → 0.23.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 +67 -0
- data/README.md +37 -15
- data/Rakefile +15 -2
- data/app/assets/javascripts/pgbus/stream_source_element.js +17 -3
- data/app/assets/javascripts/pgbus/stream_source_element.min.js +4 -4
- data/app/assets/javascripts/pgbus/stream_source_element.min.js.map +1 -1
- data/app/controllers/pgbus/dead_letter_controller.rb +3 -1
- data/app/controllers/pgbus/jobs_controller.rb +2 -1
- data/app/models/pgbus/blocked_execution.rb +3 -2
- data/app/models/pgbus/queue_state.rb +6 -0
- data/app/models/pgbus/uniqueness_key.rb +11 -0
- data/config/locales/da.yml +1 -0
- data/config/locales/de.yml +1 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/es.yml +1 -0
- data/config/locales/fi.yml +1 -0
- data/config/locales/fr.yml +1 -0
- data/config/locales/it.yml +1 -0
- data/config/locales/ja.yml +1 -0
- data/config/locales/nb.yml +1 -0
- data/config/locales/nl.yml +1 -0
- data/config/locales/pt.yml +1 -0
- data/config/locales/sv.yml +1 -0
- data/lib/generators/pgbus/fold_queue_name_case_generator.rb +57 -0
- data/lib/generators/pgbus/templates/fold_queue_name_case.rb.erb +12 -0
- data/lib/generators/pgbus/templates/tune_autovacuum.rb.erb +2 -2
- data/lib/generators/pgbus/templates/tune_fillfactor.rb.erb +1 -1
- data/lib/pgbus/active_job/adapter.rb +3 -2
- data/lib/pgbus/active_job/executor.rb +197 -46
- data/lib/pgbus/active_job/quiet_logs.rb +57 -0
- data/lib/pgbus/autovacuum_tuning.rb +2 -2
- data/lib/pgbus/batch/sweep.rb +2 -0
- data/lib/pgbus/batch.rb +4 -1
- data/lib/pgbus/cli/dlq.rb +2 -1
- data/lib/pgbus/client/async_commit.rb +149 -0
- data/lib/pgbus/client/native_statements.rb +432 -0
- data/lib/pgbus/client.rb +156 -20
- data/lib/pgbus/concurrency/blocked_execution.rb +10 -1
- data/lib/pgbus/configuration.rb +158 -6
- data/lib/pgbus/dead_letter_header.rb +3 -0
- data/lib/pgbus/doctor.rb +55 -2
- data/lib/pgbus/event_bus/handler.rb +2 -0
- data/lib/pgbus/failed_event_recorder.rb +5 -2
- data/lib/pgbus/generators/migration_detector.rb +29 -2
- data/lib/pgbus/instrumentation.rb +16 -1
- data/lib/pgbus/metrics/subscriber.rb +27 -1
- data/lib/pgbus/process/completer.rb +189 -0
- data/lib/pgbus/process/consumer.rb +15 -1
- data/lib/pgbus/process/worker.rb +57 -4
- data/lib/pgbus/queue_name_fold.rb +145 -0
- data/lib/pgbus/queue_name_validator.rb +20 -2
- data/lib/pgbus/recurring/schedule.rb +22 -13
- data/lib/pgbus/serializer.rb +1 -0
- data/lib/pgbus/stat_buffer.rb +67 -26
- data/lib/pgbus/streams/cursor.rb +17 -6
- data/lib/pgbus/streams/envelope.rb +16 -0
- data/lib/pgbus/streams/key.rb +1 -1
- data/lib/pgbus/table_maintenance.rb +1 -1
- data/lib/pgbus/uniqueness.rb +43 -16
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/visibility_heartbeat.rb +16 -12
- data/lib/pgbus/web/data_source/dead_letter.rb +14 -5
- data/lib/pgbus/web/data_source/resend.rb +185 -0
- data/lib/pgbus/web/data_source/resend_slot.rb +139 -0
- data/lib/pgbus/web/data_source.rb +55 -37
- data/lib/pgbus/web/dead_letter_reason.rb +6 -0
- data/lib/pgbus/web/streamer/connection.rb +36 -29
- data/lib/pgbus/web/streamer/falcon_connection.rb +29 -25
- data/lib/pgbus/web/streamer/heartbeat.rb +14 -3
- data/lib/pgbus/web/streamer/instance.rb +8 -5
- data/lib/pgbus/web/streamer/listener.rb +239 -62
- data/lib/pgbus/web/streamer/outbound_pump.rb +6 -4
- data/lib/pgbus/web/streamer/registry.rb +55 -29
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +73 -12
- data/lib/tasks/pgbus_queues.rake +27 -0
- data/sig/pgbus/active_job/adapter.rbs +1 -1
- data/sig/pgbus/active_job/quiet_logs.rbs +11 -0
- data/sig/pgbus/client.rbs +7 -5
- data/sig/pgbus/configuration.rbs +12 -0
- data/sig/pgbus/process/completer.rbs +26 -0
- metadata +14 -3
data/lib/pgbus/client.rb
CHANGED
|
@@ -17,6 +17,8 @@ module Pgbus
|
|
|
17
17
|
include NotifyLockRetry
|
|
18
18
|
include EnsureStreamQueue
|
|
19
19
|
include NotifyStream
|
|
20
|
+
include NativeStatements
|
|
21
|
+
include AsyncCommit
|
|
20
22
|
|
|
21
23
|
attr_reader :pgmq, :config, :connection_health
|
|
22
24
|
|
|
@@ -28,6 +30,12 @@ module Pgbus
|
|
|
28
30
|
PGMQ_META_CHECK_SQL = "SELECT 1 FROM pg_tables WHERE schemaname = 'pgmq' AND tablename = 'meta' LIMIT 1"
|
|
29
31
|
private_constant :PGMQ_META_CHECK_SQL
|
|
30
32
|
|
|
33
|
+
# pgbus's own queues only ($1: the lower-case "<queue_prefix>_"): another
|
|
34
|
+
# PGMQ user of the same schema keeps its spelling.
|
|
35
|
+
MIXED_CASE_QUEUES_SQL = "SELECT queue_name FROM pgmq.meta WHERE queue_name <> lower(queue_name) " \
|
|
36
|
+
"AND left(lower(queue_name), length($1)) = $1 ORDER BY queue_name"
|
|
37
|
+
private_constant :MIXED_CASE_QUEUES_SQL
|
|
38
|
+
|
|
31
39
|
PGMQ_INSTALL_SAVEPOINT = "pgbus_pgmq_install"
|
|
32
40
|
private_constant :PGMQ_INSTALL_SAVEPOINT
|
|
33
41
|
|
|
@@ -129,7 +137,11 @@ module Pgbus
|
|
|
129
137
|
# Both raise clean PG errors — no Ruby Timeout, no Thread#raise. Only
|
|
130
138
|
# safe on this dedicated-connection branch — never on the shared-AR Proc
|
|
131
139
|
# path, where statement_timeout would leak into application queries.
|
|
132
|
-
conn_opts =
|
|
140
|
+
conn_opts = apply_connection_bounds(conn_opts)
|
|
141
|
+
# What the connect lambda SETs per session (:session GUC mode), kept so
|
|
142
|
+
# a session reset after a failed completion pipeline gets it again.
|
|
143
|
+
@session_gucs = session_gucs(conn_opts)
|
|
144
|
+
conn_opts = wrap_session_gucs(conn_opts)
|
|
133
145
|
@pgmq = PGMQ::Client.new(conn_opts, pool_size: config.resolved_pool_size, pool_timeout: config.pool_timeout)
|
|
134
146
|
@pgmq_mutex = nil
|
|
135
147
|
# Dedicated streams pool (issue #315): isolates the durable-stream
|
|
@@ -170,6 +182,15 @@ module Pgbus
|
|
|
170
182
|
|
|
171
183
|
@queues_created = Concurrent::Map.new
|
|
172
184
|
@stream_indexes_created = Concurrent::Map.new
|
|
185
|
+
# Per physical queue: its native statements, or false (issue #605).
|
|
186
|
+
@native_statements = Concurrent::Map.new
|
|
187
|
+
@native_unprepared = config.native_statements && ar_disables_prepared_statements?
|
|
188
|
+
# Job completions commit with synchronous_commit off (issue #619).
|
|
189
|
+
# Native statements only: the pgmq functions are never wrapped. Never on
|
|
190
|
+
# ActiveRecord's own connection (the Proc path): pgbus does not pipeline
|
|
191
|
+
# on, or reset, a connection it does not own.
|
|
192
|
+
@async_completion_commit = config.native_statements && !config.completion_synchronous_commit &&
|
|
193
|
+
!@shared_connection
|
|
173
194
|
# Guards the one-time build of the publisher autoscale trigger (issue #323).
|
|
174
195
|
# NOT @pgmq_mutex — that is nil on the dedicated path (the only path the
|
|
175
196
|
# trigger exists on), so it wouldn't serialize concurrent first-publishers.
|
|
@@ -299,6 +320,18 @@ module Pgbus
|
|
|
299
320
|
end
|
|
300
321
|
end
|
|
301
322
|
|
|
323
|
+
# pgmq.meta rows spelled with an upper-case letter (issue #633). PGMQ
|
|
324
|
+
# lower-cases table names, so each shares its table with the lower-case
|
|
325
|
+
# spelling pgbus now uses; `rails generate pgbus:fold_queue_name_case`
|
|
326
|
+
# renames them. Pgbus::Doctor reports them.
|
|
327
|
+
def mixed_case_queue_names
|
|
328
|
+
synchronized do
|
|
329
|
+
@pgmq.with_connection do |conn|
|
|
330
|
+
conn.exec_params(MIXED_CASE_QUEUES_SQL, ["#{config.queue_prefix.to_s.downcase}_"]).column_values(0)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
302
335
|
# The most recently recorded installed PGMQ schema version string (e.g.
|
|
303
336
|
# "1.5.0"), read from the pgbus_pgmq_schema_versions tracking table. Returns
|
|
304
337
|
# nil when nothing is tracked yet or the table does not exist — the same
|
|
@@ -356,7 +389,7 @@ module Pgbus
|
|
|
356
389
|
Instrumentation.instrument("pgbus.client.send_message", queue: target) do
|
|
357
390
|
with_stale_connection_retry do
|
|
358
391
|
ensure_queue(queue_name)
|
|
359
|
-
synchronized {
|
|
392
|
+
synchronized { exec_produce(target, serialize(payload), headers: headers && serialize(headers), delay: delay) }
|
|
360
393
|
end
|
|
361
394
|
end
|
|
362
395
|
end
|
|
@@ -403,7 +436,7 @@ module Pgbus
|
|
|
403
436
|
Instrumentation.instrument("pgbus.client.send_batch", queue: target, size: payloads.size) do
|
|
404
437
|
with_stale_connection_retry do
|
|
405
438
|
ensure_queue(queue_name)
|
|
406
|
-
synchronized {
|
|
439
|
+
synchronized { exec_produce_batch(target, serialized, headers: serialized_headers, delay: delay) }
|
|
407
440
|
end
|
|
408
441
|
end
|
|
409
442
|
end
|
|
@@ -424,7 +457,7 @@ module Pgbus
|
|
|
424
457
|
guarded_read do
|
|
425
458
|
Instrumentation.instrument("pgbus.client.read_batch", queue: full_name, qty: qty) do
|
|
426
459
|
with_stale_connection_retry do
|
|
427
|
-
synchronized { with_read_timeout {
|
|
460
|
+
synchronized { with_read_timeout { exec_read_batch(full_name, vt: vt || config.visibility_timeout, qty: qty) } }
|
|
428
461
|
end
|
|
429
462
|
end
|
|
430
463
|
end
|
|
@@ -547,35 +580,45 @@ module Pgbus
|
|
|
547
580
|
|
|
548
581
|
# Delete a message. Pass prefixed: false when queue_name is already
|
|
549
582
|
# the full PGMQ queue name (e.g. from priority sub-queues or dashboard).
|
|
550
|
-
|
|
583
|
+
#
|
|
584
|
+
# job_completion: true marks the call as a job's completion (the executor
|
|
585
|
+
# and the completer pass it). Under config.completion_synchronous_commit =
|
|
586
|
+
# false only those commit with synchronous_commit off (issue #619); every
|
|
587
|
+
# other caller (the event consumer, whose replay reads the archive, and the
|
|
588
|
+
# dashboard) keeps the durable commit. The same holds for the three below.
|
|
589
|
+
def delete_message(queue_name, msg_id, prefixed: true, job_completion: false)
|
|
551
590
|
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
591
|
+
async = job_completion && @async_completion_commit
|
|
552
592
|
with_stale_connection_retry do
|
|
553
|
-
synchronized {
|
|
593
|
+
synchronized { exec_delete(name, msg_id, async: async) }
|
|
554
594
|
end
|
|
555
595
|
end
|
|
556
596
|
|
|
557
597
|
# Archive a message. Pass prefixed: false when queue_name is already
|
|
558
598
|
# the full PGMQ queue name.
|
|
559
|
-
def archive_message(queue_name, msg_id, prefixed: true)
|
|
599
|
+
def archive_message(queue_name, msg_id, prefixed: true, job_completion: false)
|
|
560
600
|
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
601
|
+
async = job_completion && @async_completion_commit
|
|
561
602
|
with_stale_connection_retry do
|
|
562
|
-
synchronized {
|
|
603
|
+
synchronized { exec_archive(name, msg_id, async: async) }
|
|
563
604
|
end
|
|
564
605
|
end
|
|
565
606
|
|
|
566
607
|
# Batch archive — moves multiple messages to the archive table in one call.
|
|
567
|
-
def archive_batch(queue_name, msg_ids, prefixed: true)
|
|
608
|
+
def archive_batch(queue_name, msg_ids, prefixed: true, job_completion: false)
|
|
568
609
|
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
610
|
+
async = job_completion && @async_completion_commit
|
|
569
611
|
with_stale_connection_retry do
|
|
570
|
-
synchronized {
|
|
612
|
+
synchronized { exec_archive_batch(name, msg_ids, async: async) }
|
|
571
613
|
end
|
|
572
614
|
end
|
|
573
615
|
|
|
574
616
|
# Batch delete — permanently removes multiple messages in one call.
|
|
575
|
-
def delete_batch(queue_name, msg_ids, prefixed: true)
|
|
617
|
+
def delete_batch(queue_name, msg_ids, prefixed: true, job_completion: false)
|
|
576
618
|
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
619
|
+
async = job_completion && @async_completion_commit
|
|
577
620
|
with_stale_connection_retry do
|
|
578
|
-
synchronized {
|
|
621
|
+
synchronized { exec_delete_batch(name, msg_ids, async: async) }
|
|
579
622
|
end
|
|
580
623
|
end
|
|
581
624
|
|
|
@@ -700,12 +743,23 @@ module Pgbus
|
|
|
700
743
|
end
|
|
701
744
|
end
|
|
702
745
|
|
|
703
|
-
|
|
746
|
+
# +before_drop+ runs only when the table itself is dropped (not for a
|
|
747
|
+
# case duplicate, whose table stays), decided once: the dashboard
|
|
748
|
+
# releases the dropped messages' uniqueness locks there.
|
|
749
|
+
def drop_queue(queue_name, prefixed: true, &before_drop)
|
|
704
750
|
name = prefixed ? config.queue_name(queue_name) : queue_name
|
|
751
|
+
if case_duplicate?(name)
|
|
752
|
+
forget_case_duplicate(name)
|
|
753
|
+
return true
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
before_drop&.call
|
|
757
|
+
|
|
705
758
|
result = with_stale_connection_retry do
|
|
706
759
|
synchronized { @pgmq.drop_queue(name) }
|
|
707
760
|
end
|
|
708
761
|
@queues_created.delete(name)
|
|
762
|
+
forget_other_spellings(name)
|
|
709
763
|
# Failed-event rows are keyed by the name the executor saw (physical or
|
|
710
764
|
# logical); a recreated queue reuses msg_ids, so neither may survive.
|
|
711
765
|
FailedEventRecorder.clear_queue!([name, name.delete_prefix("#{config.queue_prefix}_")].uniq)
|
|
@@ -1602,6 +1656,67 @@ module Pgbus
|
|
|
1602
1656
|
duplicate_install_error?(error) || duplicate_install_error?(error.cause)
|
|
1603
1657
|
end
|
|
1604
1658
|
|
|
1659
|
+
# A mixed-case pgmq.meta row from before issue #633 whose lower-case
|
|
1660
|
+
# spelling is registered too: both name one table, the lower-case row is
|
|
1661
|
+
# the queue every current process uses. Dropping the duplicate (the
|
|
1662
|
+
# dashboard lists both until pgbus:fold_queue_name_case runs, and the
|
|
1663
|
+
# orphan stream sweep sees both) must not drop that table.
|
|
1664
|
+
def case_duplicate?(name)
|
|
1665
|
+
return false if name == name.downcase
|
|
1666
|
+
|
|
1667
|
+
synchronized { queue_registered?(name) && queue_registered?(name.downcase) }
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1670
|
+
# Removes only the duplicate's pgmq.meta row (its throttle and topic
|
|
1671
|
+
# bindings cascade); the table and the lower-case queue stay. The shared
|
|
1672
|
+
# table's insert trigger may carry the duplicate's name (an older process
|
|
1673
|
+
# armed it last), so it is re-armed under the lower-case name with the
|
|
1674
|
+
# throttle the lower-case row had (else the duplicate's): with the
|
|
1675
|
+
# duplicate's throttle row gone, that trigger would NOTIFY no more.
|
|
1676
|
+
def forget_case_duplicate(name)
|
|
1677
|
+
canonical = name.downcase
|
|
1678
|
+
with_stale_connection_retry do
|
|
1679
|
+
synchronized do
|
|
1680
|
+
throttle = notify_throttle_for(canonical, name)
|
|
1681
|
+
@pgmq.with_connection do |conn|
|
|
1682
|
+
conn.exec_params("DELETE FROM pgmq.meta WHERE queue_name = $1", [name])
|
|
1683
|
+
end
|
|
1684
|
+
@pgmq.enable_notify_insert(canonical, throttle_interval_ms: throttle) if throttle && !notify_trigger_current?(canonical, throttle)
|
|
1685
|
+
end
|
|
1686
|
+
end
|
|
1687
|
+
@queues_created.delete(name)
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1690
|
+
# The NOTIFY throttle of the first of +names+ that has one, or nil (no
|
|
1691
|
+
# throttle row, or a PGMQ without notify_insert_throttle).
|
|
1692
|
+
def notify_throttle_for(*names)
|
|
1693
|
+
@pgmq.with_connection do |conn|
|
|
1694
|
+
next nil unless conn.exec("SELECT to_regclass('pgmq.notify_insert_throttle')").getvalue(0, 0)
|
|
1695
|
+
|
|
1696
|
+
rows = conn.exec_params(
|
|
1697
|
+
"SELECT queue_name, throttle_interval_ms FROM pgmq.notify_insert_throttle WHERE queue_name = ANY($1::text[])",
|
|
1698
|
+
["{#{names.join(",")}}"]
|
|
1699
|
+
).to_a
|
|
1700
|
+
row = names.lazy.filter_map { |n| rows.find { |r| r["queue_name"] == n } }.first
|
|
1701
|
+
row && Integer(row["throttle_interval_ms"])
|
|
1702
|
+
end
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
# pgmq.drop_queue removes the table and only its own pgmq.meta row; a
|
|
1706
|
+
# mixed-case spelling from before issue #633 would be left naming a table
|
|
1707
|
+
# that is gone. Idempotent, so a stale pooled socket retries it.
|
|
1708
|
+
def forget_other_spellings(name)
|
|
1709
|
+
with_stale_connection_retry do
|
|
1710
|
+
synchronized do
|
|
1711
|
+
@pgmq.with_connection do |conn|
|
|
1712
|
+
conn.exec_params(
|
|
1713
|
+
"DELETE FROM pgmq.meta WHERE lower(queue_name) = lower($1) AND queue_name <> $1", [name]
|
|
1714
|
+
)
|
|
1715
|
+
end
|
|
1716
|
+
end
|
|
1717
|
+
end
|
|
1718
|
+
end
|
|
1719
|
+
|
|
1605
1720
|
# Whether pgmq.meta records the queue — the authoritative "create
|
|
1606
1721
|
# committed" signal (pgmq.create writes it atomically with the
|
|
1607
1722
|
# tables). The pooled checkout is a sequential sibling of the failed
|
|
@@ -1618,7 +1733,13 @@ module Pgbus
|
|
|
1618
1733
|
end
|
|
1619
1734
|
|
|
1620
1735
|
# Check whether the NOTIFY trigger already exists on this queue with the
|
|
1621
|
-
# expected throttle interval
|
|
1736
|
+
# expected throttle interval, armed under this spelling of its name. PGMQ
|
|
1737
|
+
# 1.13.1's trigger NOTIFYs on the channel of the name it was armed with
|
|
1738
|
+
# (its argument), and every spelling of a name shares the queue's table
|
|
1739
|
+
# (issue #633): a process from before #633 that re-arms it under a
|
|
1740
|
+
# mixed-case name moves its NOTIFYs to a channel no listener uses. A
|
|
1741
|
+
# trigger without an argument (older PGMQ) NOTIFYs on the table's name,
|
|
1742
|
+
# which is the lower-case spelling. When it does, we can skip the destructive
|
|
1622
1743
|
# DROP TRIGGER + CREATE TRIGGER cycle that causes deadlocks when multiple
|
|
1623
1744
|
# forked processes race during bootstrap.
|
|
1624
1745
|
#
|
|
@@ -1640,6 +1761,7 @@ module Pgbus
|
|
|
1640
1761
|
WHERE n.nspname = 'pgmq'
|
|
1641
1762
|
AND c.relname = pgmq.format_table_name($1, 'q')
|
|
1642
1763
|
AND t.tgname = '#{NOTIFY_TRIGGER_NAME}'
|
|
1764
|
+
AND (t.tgnargs = 0 OR t.tgargs = convert_to($1, 'UTF8') || '\\000'::bytea)
|
|
1643
1765
|
AND EXISTS (
|
|
1644
1766
|
SELECT 1 FROM pgmq.notify_insert_throttle
|
|
1645
1767
|
WHERE queue_name = $1
|
|
@@ -2027,20 +2149,30 @@ module Pgbus
|
|
|
2027
2149
|
# unchanged, as does :options mode (where forward_connection_variables
|
|
2028
2150
|
# already baked the GUCs into `options`). See issue #332.
|
|
2029
2151
|
def wrap_session_gucs(conn_opts)
|
|
2030
|
-
|
|
2031
|
-
return conn_opts unless
|
|
2032
|
-
|
|
2033
|
-
variables = conn_opts[:variables]
|
|
2034
|
-
return conn_opts if variables.nil? || variables.empty?
|
|
2152
|
+
variables = session_gucs(conn_opts)
|
|
2153
|
+
return conn_opts unless variables
|
|
2035
2154
|
|
|
2036
2155
|
pg_opts = conn_opts.except(:variables)
|
|
2037
2156
|
lambda do
|
|
2038
2157
|
conn = PG.connect(pg_opts)
|
|
2039
|
-
|
|
2158
|
+
apply_session_gucs(conn, variables)
|
|
2040
2159
|
conn
|
|
2041
2160
|
end
|
|
2042
2161
|
end
|
|
2043
2162
|
|
|
2163
|
+
# The database.yml variables :session GUC mode applies with SET after
|
|
2164
|
+
# connect, or nil when nothing is applied that way.
|
|
2165
|
+
def session_gucs(conn_opts)
|
|
2166
|
+
return unless config.connection_guc_mode == :session && conn_opts.is_a?(Hash)
|
|
2167
|
+
|
|
2168
|
+
variables = conn_opts[:variables]
|
|
2169
|
+
variables unless variables.nil? || variables.empty?
|
|
2170
|
+
end
|
|
2171
|
+
|
|
2172
|
+
def apply_session_gucs(conn, variables)
|
|
2173
|
+
variables.each { |name, value| conn.exec("SET #{name} = '#{value}'") }
|
|
2174
|
+
end
|
|
2175
|
+
|
|
2044
2176
|
def apply_connection_bounds(conn_opts)
|
|
2045
2177
|
timeout = config.read_timeout
|
|
2046
2178
|
return conn_opts unless timeout&.positive?
|
|
@@ -2182,6 +2314,10 @@ module Pgbus
|
|
|
2182
2314
|
end
|
|
2183
2315
|
|
|
2184
2316
|
def stale_connection_error?(error)
|
|
2317
|
+
# A completion whose re-run came back short may have committed: never
|
|
2318
|
+
# re-run it again (NativeStatements#run_native).
|
|
2319
|
+
return false if NativeStatements.completion_lost?(error)
|
|
2320
|
+
|
|
2185
2321
|
# Substring match against any stale-connection pattern. Deliberately a
|
|
2186
2322
|
# single Regexp rather than `STALE_CONNECTION_PATTERNS.any? { |p| msg.include?(p) }`:
|
|
2187
2323
|
# Style/ArrayIntersect misfires on that shape and "corrects" it to
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
module Pgbus
|
|
6
7
|
module Concurrency
|
|
7
8
|
module BlockedExecution
|
|
9
|
+
# Headers a parked job is to be sent with. pgbus_blocked_executions has
|
|
10
|
+
# no headers column, so a dashboard retry parked behind its limit
|
|
11
|
+
# (Web::DataSource::ResendSlot) carries them in its payload under this
|
|
12
|
+
# key; promote_next takes them out and sends them as headers.
|
|
13
|
+
HEADERS_KEY = "pgbus_park_headers"
|
|
14
|
+
|
|
8
15
|
class << self
|
|
9
16
|
# Insert a blocked execution for a job that hit the concurrency limit.
|
|
10
17
|
#
|
|
@@ -63,8 +70,10 @@ module Pgbus
|
|
|
63
70
|
|
|
64
71
|
# Carry the enqueuer's priority through: under priority routing it
|
|
65
72
|
# picks the _pN sub-queue, not just the release order (issue #423).
|
|
73
|
+
headers = released[:payload].is_a?(Hash) ? released[:payload].delete(HEADERS_KEY) : nil
|
|
66
74
|
msg_id = client.send_message(released[:queue_name], released[:payload],
|
|
67
|
-
delay: actual_delay, priority: released[:priority]
|
|
75
|
+
delay: actual_delay, priority: released[:priority],
|
|
76
|
+
**(headers ? { headers: headers } : {}))
|
|
68
77
|
end
|
|
69
78
|
|
|
70
79
|
return false unless released && msg_id
|
data/lib/pgbus/configuration.rb
CHANGED
|
@@ -24,6 +24,65 @@ module Pgbus
|
|
|
24
24
|
# one. 0 (default) is off. Overridable per capsule / event_consumers entry
|
|
25
25
|
# (`read_ahead:`); prefetch_limit still caps everything claimed.
|
|
26
26
|
attr_reader :read_ahead
|
|
27
|
+
# completion_batching (issue #606, opt-in, default false): a worker archives
|
|
28
|
+
# the jobs that finish close together with one archive_batch per flush
|
|
29
|
+
# instead of one pgmq.archive per job (Process::Completer). No timer: a lone
|
|
30
|
+
# job is archived at once, and whatever finishes during a flush goes in the
|
|
31
|
+
# next. completion_batch_size (default 100) caps the ids per statement.
|
|
32
|
+
# Threads only: validate! rejects it next to execution_mode :async, global
|
|
33
|
+
# or per capsule. The crash window widens from one job to one flush: on a
|
|
34
|
+
# hard crash (SIGKILL, OOM) the jobs waiting in the current flush re-run
|
|
35
|
+
# after visibility_timeout, and a flush that committed just before the
|
|
36
|
+
# crash leaves its jobs completed but unsignalled (uniqueness lock, batch,
|
|
37
|
+
# concurrency slot) until the reapers and sweeps heal them. Worker shutdown
|
|
38
|
+
# waits for a flush in flight at most immediate_shutdown_grace (QUIT) or
|
|
39
|
+
# the rest of the 5 s pool residual (TERM); a flush wedged past that is
|
|
40
|
+
# abandoned, and its jobs fail (Completer::Stopped) and re-run after
|
|
41
|
+
# visibility_timeout, or stay unsignalled if it had committed.
|
|
42
|
+
attr_accessor :completion_batching, :completion_batch_size
|
|
43
|
+
# completed_jobs (issue #607): how a successful job's message leaves its
|
|
44
|
+
# queue. :archive (default) is pgmq.archive, DELETE + INSERT into the
|
|
45
|
+
# queue's a_ table, which archive_retention compacts. :delete is
|
|
46
|
+
# pgmq.delete, the DELETE alone: no a_ rows, no a_ index upkeep, nothing for
|
|
47
|
+
# the compaction to trim, and no completed-job history in the archive.
|
|
48
|
+
# Equally reliable: either way the message is gone once the statement
|
|
49
|
+
# commits. Through the completer (completion_batching) it is delete_batch.
|
|
50
|
+
# ActiveJob only: EventBus handler queues keep archiving (event replay
|
|
51
|
+
# reads their archive), and failures and dead letters are unchanged.
|
|
52
|
+
# Batch::Sweep then cannot tell a deleted batch job from one whose message
|
|
53
|
+
# vanished another way (a purged queue): both count as completed, silently.
|
|
54
|
+
attr_reader :completed_jobs
|
|
55
|
+
# completion_synchronous_commit (issue #619, opt-in): false commits a job's
|
|
56
|
+
# completion (the executor's archive or delete, the completer's
|
|
57
|
+
# archive_batch or delete_batch) with `SET LOCAL synchronous_commit TO off`,
|
|
58
|
+
# in one libpq pipeline: BEGIN, the SET LOCAL, the prepared statement and
|
|
59
|
+
# COMMIT go out with one sync, so the job stops waiting for its WAL flush.
|
|
60
|
+
# A completion is then acknowledged before its WAL record reaches disk: a
|
|
61
|
+
# Postgres server crash (not a client crash, not a planned switchover), or a
|
|
62
|
+
# failover to a standby that has not yet received that WAL (it also stops
|
|
63
|
+
# waiting for a synchronous standby), within about three times
|
|
64
|
+
# wal_writer_delay can lose completions committed in that window, and each
|
|
65
|
+
# lost completion redelivers its job after visibility_timeout. Sends, retries, dead-letter moves, the EventBus
|
|
66
|
+
# consumer's archive and dashboard actions keep full durability, and a
|
|
67
|
+
# completion inside a caller's transaction is left to that transaction.
|
|
68
|
+
# Native statements on pgbus's own connections only: with
|
|
69
|
+
# native_statements = false, or on ActiveRecord's shared connection (a
|
|
70
|
+
# connection_params Proc), it has no effect (pgbus doctor warns). Needs
|
|
71
|
+
# libpq 14+ and pg 1.5+ (pipeline mode), checked by validate!. true
|
|
72
|
+
# (default) commits as before.
|
|
73
|
+
attr_reader :completion_synchronous_commit
|
|
74
|
+
# quiet_active_job_logs (issue #618): true makes each worker process
|
|
75
|
+
# unsubscribe ActiveJob's LogSubscriber from perform_start.active_job and
|
|
76
|
+
# perform.active_job (Pgbus::ActiveJob::QuietLogs), so the per-job
|
|
77
|
+
# "Performing …" / "Performed …" info lines stop, and with them ActiveJob's
|
|
78
|
+
# "Error performing …" line (the same perform listener writes it): an
|
|
79
|
+
# exception that escapes ActiveJob's handlers is still logged by
|
|
80
|
+
# ErrorReporter and recorded in pgbus_failed_events, one retry_on catches
|
|
81
|
+
# still logs ActiveJob's "Retrying …" line, and a job a before_perform
|
|
82
|
+
# callback halted logs nothing.
|
|
83
|
+
# Enqueued, Retrying, Stopped retrying and Discarded still log, and the web
|
|
84
|
+
# process is untouched. Without ActiveJob it does nothing.
|
|
85
|
+
attr_reader :quiet_active_job_logs
|
|
27
86
|
# visibility_heartbeat / visibility_heartbeat_interval: while a job runs,
|
|
28
87
|
# its message's visibility timeout is re-armed every interval seconds
|
|
29
88
|
# (default: a third of visibility_timeout), so a job that outlives the
|
|
@@ -214,6 +273,22 @@ module Pgbus
|
|
|
214
273
|
# See issue #332.
|
|
215
274
|
attr_reader :connection_guc_mode
|
|
216
275
|
|
|
276
|
+
# Run Client's read_batch, archive, delete, send and their batch forms as
|
|
277
|
+
# pgbus-owned static SQL in named prepared statements on the pooled
|
|
278
|
+
# connection, instead of pgmq's plpgsql functions, whose dynamic EXECUTE
|
|
279
|
+
# Postgres plans again on every call (issue #605). Default true. A queue
|
|
280
|
+
# whose table lacks a column the statements use (a pgmq older than the
|
|
281
|
+
# `headers` column) stays on the pgmq functions by itself.
|
|
282
|
+
#
|
|
283
|
+
# Behind a transaction-mode PgBouncer, prepared statements need PgBouncer
|
|
284
|
+
# >= 1.21 with `max_prepared_statements` set. Without it, a process that
|
|
285
|
+
# sees its statements disappear switches to sending the same SQL
|
|
286
|
+
# unprepared and logs a warning once, and connections built from a
|
|
287
|
+
# database.yml with `prepared_statements: false` send it unprepared from
|
|
288
|
+
# the start. false uses the pgmq functions everywhere: the full escape
|
|
289
|
+
# hatch.
|
|
290
|
+
attr_reader :native_statements
|
|
291
|
+
|
|
217
292
|
# Zombie message detection — logs a warning when a message is redelivered
|
|
218
293
|
# (read_ct > 1) without any prior failure recorded in pgbus_failed_events.
|
|
219
294
|
attr_accessor :zombie_detection
|
|
@@ -320,6 +395,11 @@ module Pgbus
|
|
|
320
395
|
|
|
321
396
|
@prefetch_limit = nil
|
|
322
397
|
@read_ahead = 0
|
|
398
|
+
@completion_batching = false
|
|
399
|
+
@completion_batch_size = 100
|
|
400
|
+
@completed_jobs = :archive
|
|
401
|
+
@completion_synchronous_commit = true
|
|
402
|
+
@quiet_active_job_logs = false
|
|
323
403
|
@execution_mode = :threads
|
|
324
404
|
|
|
325
405
|
@max_jobs_per_worker = nil
|
|
@@ -404,6 +484,7 @@ module Pgbus
|
|
|
404
484
|
@connects_to = nil
|
|
405
485
|
@require_primary = false
|
|
406
486
|
@connection_guc_mode = :options
|
|
487
|
+
@native_statements = true
|
|
407
488
|
|
|
408
489
|
@mcp_write_tools = false
|
|
409
490
|
|
|
@@ -516,12 +597,13 @@ module Pgbus
|
|
|
516
597
|
@streams_idle_timeout = 3_600 # 1 hour
|
|
517
598
|
# 250ms — this value plays two roles: (1) the TCP keepalive
|
|
518
599
|
# interval for the streamer's PG LISTEN connection, and (2) the
|
|
519
|
-
#
|
|
520
|
-
#
|
|
521
|
-
#
|
|
522
|
-
#
|
|
523
|
-
#
|
|
524
|
-
#
|
|
600
|
+
# budget (plus 1s) a stream's first connect waits for a stuck local
|
|
601
|
+
# Listener to acknowledge its ensure_listening (with
|
|
602
|
+
# streams_listen_scope :master, HubClient waits this cycle plus 2s
|
|
603
|
+
# before failing over). A live listener
|
|
604
|
+
# is woken through a pipe and acks within a thread hop, and a
|
|
605
|
+
# connect on a stream that already has a subscriber does not
|
|
606
|
+
# wait at all (issue #630), so connects no longer pay this cycle.
|
|
525
607
|
@streams_listen_health_check_ms = 250
|
|
526
608
|
@streams_write_deadline_ms = 5_000
|
|
527
609
|
# Deadline (ms) for a socket write made INSIDE the dispatcher's
|
|
@@ -797,6 +879,39 @@ module Pgbus
|
|
|
797
879
|
@connection_guc_mode = mode
|
|
798
880
|
end
|
|
799
881
|
|
|
882
|
+
VALID_COMPLETED_JOBS = %i[archive delete].freeze
|
|
883
|
+
|
|
884
|
+
def completed_jobs=(value)
|
|
885
|
+
mode = value.to_sym if value.is_a?(String) || value.is_a?(Symbol)
|
|
886
|
+
unless VALID_COMPLETED_JOBS.include?(mode)
|
|
887
|
+
raise Pgbus::ConfigurationError, "completed_jobs must be :archive or :delete (got #{value.inspect})"
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
@completed_jobs = mode
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def completion_synchronous_commit=(value)
|
|
894
|
+
unless [true, false].include?(value)
|
|
895
|
+
raise Pgbus::ConfigurationError, "completion_synchronous_commit must be true or false (got #{value.inspect})"
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
@completion_synchronous_commit = value
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def quiet_active_job_logs=(value)
|
|
902
|
+
unless [true, false].include?(value)
|
|
903
|
+
raise Pgbus::ConfigurationError, "quiet_active_job_logs must be true or false (got #{value.inspect})"
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
@quiet_active_job_logs = value
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
def native_statements=(value)
|
|
910
|
+
raise Pgbus::ConfigurationError, "native_statements must be true or false" unless [true, false].include?(value)
|
|
911
|
+
|
|
912
|
+
@native_statements = value
|
|
913
|
+
end
|
|
914
|
+
|
|
800
915
|
VALID_PGMQ_SCHEMA_MODES = %i[auto extension embedded].freeze
|
|
801
916
|
|
|
802
917
|
def pgmq_schema_mode=(mode)
|
|
@@ -986,6 +1101,8 @@ module Pgbus
|
|
|
986
1101
|
end
|
|
987
1102
|
|
|
988
1103
|
validate_read_ahead!
|
|
1104
|
+
validate_completion_batching!
|
|
1105
|
+
validate_completion_synchronous_commit!
|
|
989
1106
|
validate_capsule_processes!
|
|
990
1107
|
|
|
991
1108
|
if priority_levels && !(priority_levels.is_a?(Integer) && priority_levels >= 1 && priority_levels <= 10)
|
|
@@ -1756,6 +1873,41 @@ module Pgbus
|
|
|
1756
1873
|
|
|
1757
1874
|
private
|
|
1758
1875
|
|
|
1876
|
+
# The completer's job threads block on their flush, which would stall an
|
|
1877
|
+
# Async reactor, so batching is threads-only (issue #606). Every capsule
|
|
1878
|
+
# resolves its mode through execution_mode_for, as the supervisor does.
|
|
1879
|
+
def validate_completion_batching!
|
|
1880
|
+
unless [true, false].include?(completion_batching)
|
|
1881
|
+
raise Pgbus::ConfigurationError, "completion_batching must be true or false (got #{completion_batching.inspect})"
|
|
1882
|
+
end
|
|
1883
|
+
unless completion_batch_size.is_a?(Integer) && completion_batch_size.positive?
|
|
1884
|
+
raise Pgbus::ConfigurationError,
|
|
1885
|
+
"completion_batch_size must be a positive Integer (got #{completion_batch_size.inspect})"
|
|
1886
|
+
end
|
|
1887
|
+
return unless completion_batching
|
|
1888
|
+
|
|
1889
|
+
modes = [ExecutionPools.normalize_mode(execution_mode)] + Array(workers).map { |w| execution_mode_for(w) }
|
|
1890
|
+
return unless modes.include?(:async)
|
|
1891
|
+
|
|
1892
|
+
raise Pgbus::ConfigurationError,
|
|
1893
|
+
"completion_batching supports execution_mode :threads only; turn it off for a global or capsule " \
|
|
1894
|
+
"execution_mode :async"
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
# The asynchronous completion commit runs in libpq pipeline mode (issue
|
|
1898
|
+
# #619): libpq 14+ and a pg gem that wraps it (1.5+). Checked here, once,
|
|
1899
|
+
# rather than on every completion.
|
|
1900
|
+
def validate_completion_synchronous_commit!
|
|
1901
|
+
return if completion_synchronous_commit
|
|
1902
|
+
|
|
1903
|
+
require "pg" unless defined?(::PG::Connection)
|
|
1904
|
+
return if ::PG::Connection.method_defined?(:enter_pipeline_mode) && ::PG.library_version >= 140_000
|
|
1905
|
+
|
|
1906
|
+
raise Pgbus::ConfigurationError,
|
|
1907
|
+
"completion_synchronous_commit = false needs libpq 14+ and pg 1.5+ (pipeline mode); " \
|
|
1908
|
+
"this process has libpq #{::PG.library_version} and pg #{::PG::VERSION}"
|
|
1909
|
+
end
|
|
1910
|
+
|
|
1759
1911
|
# Shared resolver for a dedicated connection (streamer LISTEN / worker
|
|
1760
1912
|
# NotifyListener) that can override the base `connection_options` to bypass a
|
|
1761
1913
|
# pooler. Precedence: a full `url` wins outright; otherwise `host`/`port`
|
|
@@ -20,6 +20,9 @@ module Pgbus
|
|
|
20
20
|
ORIGINAL_KEY = "pgbus_original_headers"
|
|
21
21
|
VERSION = 1
|
|
22
22
|
REASON_MAX_RETRIES = "max_retries_exceeded"
|
|
23
|
+
# A :while_executing duplicate skipped on every read while another
|
|
24
|
+
# message held its key, until read_ct passed max_retries (issue #629).
|
|
25
|
+
REASON_UNIQUENESS_CONFLICT = "uniqueness_conflict"
|
|
23
26
|
# The release that started writing the block: rows without it predate it.
|
|
24
27
|
SINCE = "0.18.0"
|
|
25
28
|
MESSAGE_LIMIT = 1_000
|
data/lib/pgbus/doctor.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "pgbus/mcp/health_analyzer"
|
|
|
5
5
|
|
|
6
6
|
module Pgbus
|
|
7
7
|
# Preflight diagnostics for a pgbus deployment — the single command that
|
|
8
|
-
# answers "is this environment healthy enough to run?". Runs
|
|
8
|
+
# answers "is this environment healthy enough to run?". Runs fourteen checks and
|
|
9
9
|
# returns a machine-readable result plus a human report, so `pgbus doctor`
|
|
10
10
|
# and `rake pgbus:doctor` can gate a deploy or CI run (exit 0 on success,
|
|
11
11
|
# 1 on any failure).
|
|
@@ -36,13 +36,15 @@ module Pgbus
|
|
|
36
36
|
"Database" => :check_database,
|
|
37
37
|
"PGMQ schema" => :check_pgmq_schema,
|
|
38
38
|
"Queues" => :check_queues,
|
|
39
|
+
"Queue name case" => :check_queue_name_case,
|
|
39
40
|
"LISTEN/NOTIFY" => :check_notify,
|
|
40
41
|
"Process liveness" => :check_processes,
|
|
41
42
|
"GlobalID allowlist" => :check_allowed_global_id_models,
|
|
42
43
|
"Broadcast queue" => :check_broadcast_queue,
|
|
43
44
|
"Primary affinity" => :check_primary,
|
|
44
45
|
"Dedicated connections" => :check_dedicated_connections,
|
|
45
|
-
"Connection budget" => :check_connection_budget
|
|
46
|
+
"Connection budget" => :check_connection_budget,
|
|
47
|
+
"Completion commit" => :check_completion_commit
|
|
46
48
|
}.freeze
|
|
47
49
|
|
|
48
50
|
# Process liveness reads the pgbus_processes table (via HealthAnalyzer), so
|
|
@@ -128,6 +130,7 @@ module Pgbus
|
|
|
128
130
|
resolved_pool_size: safe { @config.resolved_pool_size },
|
|
129
131
|
listen_notify: @config.listen_notify,
|
|
130
132
|
roles: @config.roles || "all",
|
|
133
|
+
completion_commit: @config.completion_synchronous_commit ? "sync" : "async",
|
|
131
134
|
capsules: capsule_summary,
|
|
132
135
|
database_url: redacted_database_url,
|
|
133
136
|
connection_params: redacted_connection_params
|
|
@@ -252,6 +255,30 @@ module Pgbus
|
|
|
252
255
|
Check.new(name: "Queues", status: :fail, detail: "#{e.class}: #{e.message}")
|
|
253
256
|
end
|
|
254
257
|
|
|
258
|
+
# How many mixed-case queue names the queue-name case check lists.
|
|
259
|
+
CASE_NAMES_SHOWN = 5
|
|
260
|
+
private_constant :CASE_NAMES_SHOWN
|
|
261
|
+
|
|
262
|
+
# 4b. Queue-name case (issue #633). PGMQ spells every queue table in lower
|
|
263
|
+
# case, and pgbus now spells every queue name that way; a pgmq.meta row an
|
|
264
|
+
# older pgbus created as "pgbus_Orders" shares pgmq.q_pgbus_orders with the
|
|
265
|
+
# current spelling. Its messages are reachable, so this warns rather than
|
|
266
|
+
# fails: the dashboard lists the queue twice until the fold migration runs.
|
|
267
|
+
def check_queue_name_case
|
|
268
|
+
names = @client.mixed_case_queue_names
|
|
269
|
+
return Check.new(name: "Queue name case", status: :ok, detail: "every queue is spelled in lower case") if names.empty?
|
|
270
|
+
|
|
271
|
+
shown = names.first(CASE_NAMES_SHOWN).join(", ")
|
|
272
|
+
shown += ", …" if names.size > CASE_NAMES_SHOWN
|
|
273
|
+
Check.new(name: "Queue name case", status: :warn,
|
|
274
|
+
detail: "#{names.size} queue(s) in pgmq.meta spelled in mixed case (#{shown}); their tables are " \
|
|
275
|
+
"lower case and pgbus now uses that spelling — once every process runs this pgbus, run " \
|
|
276
|
+
"`rails generate pgbus:fold_queue_name_case` and migrate " \
|
|
277
|
+
"(`rake pgbus:queues:fold_case` once that migration has run)")
|
|
278
|
+
rescue StandardError => e
|
|
279
|
+
Check.new(name: "Queue name case", status: :warn, detail: "could not read pgmq.meta (#{e.class}: #{e.message})")
|
|
280
|
+
end
|
|
281
|
+
|
|
255
282
|
# 5. LISTEN/NOTIFY — when configured on, every configured queue should carry
|
|
256
283
|
# the insert-NOTIFY trigger. A missing trigger is a warning, not a failure:
|
|
257
284
|
# pgbus still works (it falls back to polling), it just loses instant wakeup.
|
|
@@ -395,6 +422,32 @@ module Pgbus
|
|
|
395
422
|
Check.new(name: "Dedicated connections", status: :fail, detail: "#{e.class}: #{e.message}")
|
|
396
423
|
end
|
|
397
424
|
|
|
425
|
+
# config.completion_synchronous_commit (issue #619): informational, except
|
|
426
|
+
# that false does nothing with native_statements off or on ActiveRecord's
|
|
427
|
+
# shared connection, which warns.
|
|
428
|
+
def check_completion_commit
|
|
429
|
+
return Check.new(name: "Completion commit", status: :ok, detail: "synchronous (default)") if
|
|
430
|
+
@config.completion_synchronous_commit
|
|
431
|
+
|
|
432
|
+
unless @config.native_statements
|
|
433
|
+
return Check.new(name: "Completion commit", status: :warn,
|
|
434
|
+
detail: "completion_synchronous_commit = false has no effect: completions run through " \
|
|
435
|
+
"pgmq's functions (native_statements = false)")
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
if @client.respond_to?(:shared_connection?) && @client.shared_connection?
|
|
439
|
+
return Check.new(name: "Completion commit", status: :warn,
|
|
440
|
+
detail: "completion_synchronous_commit = false has no effect: pgbus shares ActiveRecord's " \
|
|
441
|
+
"connection (connection_params Proc); give pgbus its own connections to use it")
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
Check.new(name: "Completion commit", status: :ok,
|
|
445
|
+
detail: "asynchronous: completions commit with synchronous_commit off; a Postgres server crash can " \
|
|
446
|
+
"lose the last completions, whose jobs run again after visibility_timeout")
|
|
447
|
+
rescue StandardError => e
|
|
448
|
+
Check.new(name: "Completion commit", status: :warn, detail: "#{e.class}: #{e.message}")
|
|
449
|
+
end
|
|
450
|
+
|
|
398
451
|
# How many direct LISTEN connections this config pins at steady state
|
|
399
452
|
# (issue #381) — informational (always :ok) so operators can do capacity
|
|
400
453
|
# math on the pooler's direct-connection budget from the doctor output.
|