pgbus 0.9.10 → 0.10.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 +24 -7
- data/README.md +34 -7
- data/Rakefile +22 -1
- data/app/models/pgbus/stream_queue.rb +87 -0
- data/lib/generators/pgbus/add_stream_queues_generator.rb +50 -0
- data/lib/generators/pgbus/install_generator.rb +3 -3
- data/lib/generators/pgbus/templates/add_stream_queues.rb.erb +11 -0
- data/lib/generators/pgbus/templates/initializer.rb.erb +62 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +14 -0
- data/lib/generators/pgbus/update_generator.rb +6 -66
- data/lib/pgbus/client/ensure_stream_queue.rb +15 -1
- data/lib/pgbus/client/read_after.rb +7 -6
- data/lib/pgbus/client/resizable_pool.rb +160 -0
- data/lib/pgbus/client.rb +261 -4
- data/lib/pgbus/configuration.rb +244 -11
- data/lib/pgbus/doctor.rb +73 -2
- data/lib/pgbus/engine.rb +30 -3
- data/lib/pgbus/generators/migration_detector.rb +7 -0
- data/lib/pgbus/process/dispatcher.rb +89 -19
- data/lib/pgbus/process/notify_listener.rb +18 -2
- data/lib/pgbus/process/worker.rb +19 -3
- data/lib/pgbus/streams/pool_autoscaler.rb +202 -0
- data/lib/pgbus/streams/pool_trigger.rb +124 -0
- data/lib/pgbus/streams/turbo_broadcastable.rb +23 -0
- data/lib/pgbus/streams.rb +2 -2
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/streamer/connection.rb +22 -6
- data/lib/pgbus/web/streamer/falcon_connection.rb +17 -5
- data/lib/pgbus/web/streamer/instance.rb +75 -5
- data/lib/pgbus/web/streamer/io_writer.rb +11 -5
- data/lib/pgbus/web/streamer/listener.rb +61 -1
- data/lib/pgbus/web/streamer/outbound_pump.rb +260 -0
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +89 -7
- metadata +9 -4
- data/lib/generators/pgbus/templates/pgbus.yml.erb +0 -76
- data/lib/pgbus/config_loader.rb +0 -73
- data/lib/pgbus/generators/config_converter.rb +0 -345
data/lib/pgbus/client.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "client/read_after"
|
|
|
7
7
|
require_relative "client/ensure_stream_queue"
|
|
8
8
|
require_relative "client/notify_stream"
|
|
9
9
|
require_relative "client/connection_health"
|
|
10
|
+
require_relative "client/resizable_pool"
|
|
10
11
|
|
|
11
12
|
module Pgbus
|
|
12
13
|
class Client
|
|
@@ -59,6 +60,12 @@ module Pgbus
|
|
|
59
60
|
# operations through a mutex.
|
|
60
61
|
@pgmq = PGMQ::Client.new(conn_opts, pool_size: 1, pool_timeout: config.pool_timeout)
|
|
61
62
|
@pgmq_mutex = Mutex.new
|
|
63
|
+
# No dedicated streams pool on this path: a second PGMQ::Client would
|
|
64
|
+
# still funnel through the same non-thread-safe AR raw_connection.
|
|
65
|
+
# Stream publish + replay share the single serialized connection —
|
|
66
|
+
# @streams_pgmq aliases @pgmq so the code paths are uniform, and
|
|
67
|
+
# #with_streams_connection falls back to with_raw_connection.
|
|
68
|
+
@streams_pgmq = @pgmq
|
|
62
69
|
else
|
|
63
70
|
# With a String URL or Hash params, pgmq-ruby creates its own dedicated
|
|
64
71
|
# PG::Connection per pool slot — no shared state with ActiveRecord.
|
|
@@ -72,13 +79,49 @@ module Pgbus
|
|
|
72
79
|
# Both raise clean PG errors — no Ruby Timeout, no Thread#raise. Only
|
|
73
80
|
# safe on this dedicated-connection branch — never on the shared-AR Proc
|
|
74
81
|
# path, where statement_timeout would leak into application queries.
|
|
75
|
-
conn_opts = apply_connection_bounds(conn_opts)
|
|
82
|
+
conn_opts = wrap_session_gucs(apply_connection_bounds(conn_opts))
|
|
76
83
|
@pgmq = PGMQ::Client.new(conn_opts, pool_size: config.resolved_pool_size, pool_timeout: config.pool_timeout)
|
|
77
84
|
@pgmq_mutex = nil
|
|
85
|
+
# Dedicated streams pool (issue #315): isolates the durable-stream
|
|
86
|
+
# publish INSERT (#send_stream_message) and the dispatcher's per-wake
|
|
87
|
+
# replay reads (#read_after) from the job pool, so a saturated worker
|
|
88
|
+
# pool can't delay a broadcast on pool checkout, and each wake reuses a
|
|
89
|
+
# persistent connection instead of a fresh PG.connect per call. Its own
|
|
90
|
+
# PGMQ::Client → its own connection_pool, sized independently of worker
|
|
91
|
+
# thread counts.
|
|
92
|
+
# Build the streams pool from streams_connection_options (which defaults
|
|
93
|
+
# to connection_options but honors streams_database_url/host/port for a
|
|
94
|
+
# separate/direct streams DB — issue #315), bounds-applied, and tagged
|
|
95
|
+
# with a per-process application_name so the autoscaler can count peer
|
|
96
|
+
# processes from pg_stat_activity (issue #323 P1/P2). Snapshot it so a
|
|
97
|
+
# hot-swap rebuilds a byte-identical pool at a new size.
|
|
98
|
+
@streams_conn_opts = wrap_session_gucs(
|
|
99
|
+
tag_application_name(
|
|
100
|
+
apply_connection_bounds(config.streams_connection_options)
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
@streams_pgmq = PGMQ::Client.new(@streams_conn_opts, pool_size: config.streams_pool_size,
|
|
104
|
+
pool_timeout: config.streams_pool_timeout)
|
|
78
105
|
end
|
|
79
106
|
|
|
107
|
+
# Wrap the streams pool so its live reference can be atomically hot-swapped
|
|
108
|
+
# to a new size under load without losing broadcasts or leaking connections
|
|
109
|
+
# (issue #323 spike; #resize_streams_pool). All streams-pool access goes
|
|
110
|
+
# through this — see #streams_pool. Default behavior with no swap is
|
|
111
|
+
# byte-identical (one AtomicReference read + a counter bump per op).
|
|
112
|
+
@streams_pool = ResizablePool.new(
|
|
113
|
+
@streams_pgmq,
|
|
114
|
+
shared: @shared_connection,
|
|
115
|
+
drain_timeout: config.streams_pool_timeout + 1.0,
|
|
116
|
+
logger: Pgbus.logger
|
|
117
|
+
)
|
|
118
|
+
|
|
80
119
|
@queues_created = Concurrent::Map.new
|
|
81
120
|
@stream_indexes_created = Concurrent::Map.new
|
|
121
|
+
# Guards the one-time build of the publisher autoscale trigger (issue #323).
|
|
122
|
+
# NOT @pgmq_mutex — that is nil on the dedicated path (the only path the
|
|
123
|
+
# trigger exists on), so it wouldn't serialize concurrent first-publishers.
|
|
124
|
+
@streams_trigger_mutex = Mutex.new
|
|
82
125
|
@queue_strategy = QueueFactory.for(config)
|
|
83
126
|
@schema_ensured = schema_ensured
|
|
84
127
|
@connection_health = ConnectionHealth.new(
|
|
@@ -122,9 +165,22 @@ module Pgbus
|
|
|
122
165
|
# carries the underlying error plus which config source was in use.
|
|
123
166
|
def verify_connection!
|
|
124
167
|
synchronized do
|
|
125
|
-
@pgmq.with_connection
|
|
168
|
+
@pgmq.with_connection do |conn|
|
|
169
|
+
conn.exec("SELECT 1")
|
|
170
|
+
# When require_primary is set, reject a connection that landed on a
|
|
171
|
+
# read-only replica at boot rather than letting a read/write-splitting
|
|
172
|
+
# pooler silently route pgmq's VOLATILE read/archive to a standby,
|
|
173
|
+
# where workers read nothing and jobs stop with a healthy heartbeat
|
|
174
|
+
# (issue #332). Off by default, so a single-primary deployment is
|
|
175
|
+
# unaffected.
|
|
176
|
+
Process::PrimaryValidator.validate_primary!(conn) if config.require_primary
|
|
177
|
+
end
|
|
126
178
|
end
|
|
127
179
|
true
|
|
180
|
+
rescue Process::ReplicaConnectionError => e
|
|
181
|
+
raise ConfigurationError,
|
|
182
|
+
"Database connection via #{connection_source} landed on a read-only replica " \
|
|
183
|
+
"(require_primary is set): #{e.message}"
|
|
128
184
|
rescue PGMQ::Errors::ConnectionError, PG::Error => e
|
|
129
185
|
raise ConfigurationError, "Database connection failed via #{connection_source}: #{e.message}"
|
|
130
186
|
end
|
|
@@ -141,6 +197,17 @@ module Pgbus
|
|
|
141
197
|
true
|
|
142
198
|
end
|
|
143
199
|
|
|
200
|
+
# Whether the job connection currently lands on a read-only replica
|
|
201
|
+
# (pg_is_in_recovery() => t). Used by the doctor to warn about a
|
|
202
|
+
# read/write-splitting pooler that could route pgmq's VOLATILE read/archive
|
|
203
|
+
# to a standby, silently stalling job processing (issue #332). Raw PG error
|
|
204
|
+
# propagates so the caller can render the reason.
|
|
205
|
+
def in_recovery?
|
|
206
|
+
with_raw_connection do |conn|
|
|
207
|
+
conn.exec(Process::PrimaryValidator::RECOVERY_QUERY).getvalue(0, 0) == "t"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
144
211
|
# The logical queue names pgbus expects to exist based on the configuration
|
|
145
212
|
# (default queue + worker capsules + recurring tasks). Public wrapper around
|
|
146
213
|
# collect_configured_queues so the doctor can diff configured-vs-existing
|
|
@@ -240,6 +307,38 @@ module Pgbus
|
|
|
240
307
|
end
|
|
241
308
|
end
|
|
242
309
|
|
|
310
|
+
# Durable stream broadcast. Unlike #send_message, this ALWAYS targets the
|
|
311
|
+
# bare queue (config.queue_name) and never the priority strategy's
|
|
312
|
+
# _p0.._pN sub-queues: streams are delivered by a non-consuming peek
|
|
313
|
+
# (read_after) on the bare queue, and the streamer LISTENs on the bare
|
|
314
|
+
# channel, so a broadcast routed to _p1 would never reach the browser
|
|
315
|
+
# (issue #310). ensure_stream_queue creates the bare queue + NOTIFY
|
|
316
|
+
# trigger + archive index, mirroring this bare-name write path.
|
|
317
|
+
def send_stream_message(stream_name, payload, headers: nil, delay: 0)
|
|
318
|
+
target = config.queue_name(stream_name)
|
|
319
|
+
# Capture the produced msg_id — it is this method's return value (callers
|
|
320
|
+
# like Stream#broadcast rely on it), so the autoscale trigger below must NOT
|
|
321
|
+
# become the last expression.
|
|
322
|
+
msg_id = Instrumentation.instrument("pgbus.client.send_message", queue: target) do
|
|
323
|
+
with_stale_connection_retry do
|
|
324
|
+
ensure_stream_queue(stream_name)
|
|
325
|
+
# Publish through the dedicated streams pool (issue #315) so a
|
|
326
|
+
# saturated job pool can't block a broadcast on pool checkout. On the
|
|
327
|
+
# shared-AR path @streams_pgmq aliases @pgmq and synchronized still
|
|
328
|
+
# serializes on the mutex.
|
|
329
|
+
synchronized do
|
|
330
|
+
streams_pool.produce(target, serialize(payload), headers: headers && serialize(headers), delay: delay)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
# Opportunistically autoscale the streams pool from the publish path so a
|
|
335
|
+
# pure-publisher process (no streamer) still grows under a broadcast storm
|
|
336
|
+
# (issue #323 follow-up). Throttled + fail-soft — never delays or breaks the
|
|
337
|
+
# broadcast; nil (a no-op) unless autoscale is on and the pool is dedicated.
|
|
338
|
+
streams_pool_trigger&.maybe_check
|
|
339
|
+
msg_id
|
|
340
|
+
end
|
|
341
|
+
|
|
243
342
|
def send_batch(queue_name, payloads, headers: nil, delay: 0)
|
|
244
343
|
full_name = config.queue_name(queue_name)
|
|
245
344
|
serialized, serialized_headers = serialize_batch(payloads, headers)
|
|
@@ -446,6 +545,16 @@ module Pgbus
|
|
|
446
545
|
{}
|
|
447
546
|
end
|
|
448
547
|
|
|
548
|
+
# Same shape as #pool_stats but for the dedicated streams pool (issue #315).
|
|
549
|
+
# On the shared-AR path @streams_pgmq aliases @pgmq, so this reports the job
|
|
550
|
+
# pool's counters — accurate, since streams share that connection there.
|
|
551
|
+
def streams_pool_stats
|
|
552
|
+
streams_pool.stats.merge(pool_timeout: config.streams_pool_timeout)
|
|
553
|
+
rescue StandardError => e
|
|
554
|
+
Pgbus.logger.debug { "[Pgbus::Client] streams_pool_stats unavailable: #{e.class}: #{e.message}" }
|
|
555
|
+
{}
|
|
556
|
+
end
|
|
557
|
+
|
|
449
558
|
def list_queues
|
|
450
559
|
with_stale_connection_retry do
|
|
451
560
|
synchronized { @pgmq.list_queues }
|
|
@@ -642,7 +751,47 @@ module Pgbus
|
|
|
642
751
|
end
|
|
643
752
|
|
|
644
753
|
def close
|
|
645
|
-
|
|
754
|
+
# Stop the publisher autoscale executor (if one was ever built) so its
|
|
755
|
+
# background thread doesn't leak (issue #323). Outside `synchronized` — it
|
|
756
|
+
# takes no pool lock and shutdown waits on a possibly-running check.
|
|
757
|
+
@streams_pool_trigger.shutdown if defined?(@streams_pool_trigger) && @streams_pool_trigger
|
|
758
|
+
synchronized do
|
|
759
|
+
@pgmq.close
|
|
760
|
+
# Close the CURRENT streams pool too (issue #315) so its connections
|
|
761
|
+
# don't leak. close_current reads the live (possibly hot-swapped, #323)
|
|
762
|
+
# pool once under the swap mutex and skips it when it aliases @pgmq (the
|
|
763
|
+
# shared-AR path) so we don't double-close the same pool.
|
|
764
|
+
@streams_pool.close_current(job_pool: @pgmq)
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Opt-in hot-swap of the dedicated streams pool to a new size (issue #323
|
|
769
|
+
# spike). Builds a fresh PGMQ::Client at new_size with the SAME bounds-applied
|
|
770
|
+
# connection options, atomically swaps the live reference, then drains +
|
|
771
|
+
# closes the old pool (bounded, never Thread#kill). NOT called automatically —
|
|
772
|
+
# there is no control loop here; a caller triggers it explicitly.
|
|
773
|
+
#
|
|
774
|
+
# No-op on the shared-AR (Proc) path (the streams pool aliases the job pool,
|
|
775
|
+
# which is non-thread-safe and forced to pool_size 1 — swapping it would
|
|
776
|
+
# corrupt the job pool), and no-op when the size is unchanged.
|
|
777
|
+
#
|
|
778
|
+
# @return [ResizablePool::SwapStats] on a swap, or {swapped: false, reason:}
|
|
779
|
+
def resize_streams_pool(new_size)
|
|
780
|
+
raise ArgumentError, "new_size must be a positive integer" unless new_size.is_a?(Integer) && new_size.positive?
|
|
781
|
+
return { swapped: false, reason: :shared_connection } if @shared_connection
|
|
782
|
+
return { swapped: false, reason: :unchanged } if streams_pool.stats[:size] == new_size
|
|
783
|
+
|
|
784
|
+
from_size = streams_pool.stats[:size]
|
|
785
|
+
new_pgmq = PGMQ::Client.new(
|
|
786
|
+
@streams_conn_opts, pool_size: new_size, pool_timeout: config.streams_pool_timeout
|
|
787
|
+
)
|
|
788
|
+
@streams_pool.swap(new_pgmq, from_size: from_size, to_size: new_size)
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# Accumulated streams-pool swap telemetry (issue #323) — for the bench and a
|
|
792
|
+
# future control loop. Zero-valued before any swap.
|
|
793
|
+
def streams_swap_stats
|
|
794
|
+
@streams_pool.stats_snapshot
|
|
646
795
|
end
|
|
647
796
|
|
|
648
797
|
private
|
|
@@ -767,7 +916,14 @@ module Pgbus
|
|
|
767
916
|
PG.connect(opts)
|
|
768
917
|
when Hash
|
|
769
918
|
owned = true
|
|
770
|
-
|
|
919
|
+
# :variables is a database.yml convention, not a libpq keyword —
|
|
920
|
+
# strip it before PG.connect and apply the GUCs via SET so this
|
|
921
|
+
# raw bootstrap/DDL connection matches the pooled connections
|
|
922
|
+
# (issue #332). Empty/absent variables is a plain connect.
|
|
923
|
+
variables = opts[:variables]
|
|
924
|
+
conn = PG.connect(**opts.except(:variables))
|
|
925
|
+
variables&.each { |name, value| conn.exec("SET #{name} = '#{value}'") }
|
|
926
|
+
conn
|
|
771
927
|
else
|
|
772
928
|
raise ConfigurationError, "Cannot resolve raw PG connection from #{opts.class}"
|
|
773
929
|
end
|
|
@@ -776,6 +932,22 @@ module Pgbus
|
|
|
776
932
|
conn&.close if owned
|
|
777
933
|
end
|
|
778
934
|
|
|
935
|
+
# Yields a PG connection from the dedicated streams pool for the streamer's
|
|
936
|
+
# replay reads (read_after / stream_current_msg_id / stream_oldest_msg_id).
|
|
937
|
+
# On the dedicated (String/Hash) path this checks out a persistent pooled
|
|
938
|
+
# connection — no fresh PG.connect per call (issue #315). On the shared-AR
|
|
939
|
+
# (Proc) path there is no separate pool (@streams_pgmq aliases @pgmq and
|
|
940
|
+
# points at the non-thread-safe AR raw_connection), so we fall back to
|
|
941
|
+
# with_raw_connection, which reuses that same shared connection. Callers
|
|
942
|
+
# already wrap this in `synchronized`, so the shared path stays serialized.
|
|
943
|
+
def with_streams_connection(&)
|
|
944
|
+
if @shared_connection
|
|
945
|
+
with_raw_connection(&)
|
|
946
|
+
else
|
|
947
|
+
streams_pool.with_connection(&)
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
|
|
779
951
|
def ensure_single_queue(full_name)
|
|
780
952
|
return if @queues_created[full_name]
|
|
781
953
|
|
|
@@ -871,6 +1043,40 @@ module Pgbus
|
|
|
871
1043
|
end
|
|
872
1044
|
end
|
|
873
1045
|
|
|
1046
|
+
# The ResizablePool wrapping the streams pool. All streams-pool reads
|
|
1047
|
+
# (produce / with_connection / stats) go through it so the underlying
|
|
1048
|
+
# PGMQ::Client can be atomically hot-swapped to a new size (issue #323).
|
|
1049
|
+
attr_reader :streams_pool
|
|
1050
|
+
|
|
1051
|
+
# Lazily-built publisher-side autoscale trigger (issue #323). nil (a no-op)
|
|
1052
|
+
# unless streams_pool_autoscale is on AND this is the dedicated-connection
|
|
1053
|
+
# path (resize is a no-op on the shared-AR path). Built once — on the first
|
|
1054
|
+
# publish, then reused. Runs its headroom query through the job pool (@pgmq),
|
|
1055
|
+
# so it never competes with the streams pool it resizes.
|
|
1056
|
+
#
|
|
1057
|
+
# Double-checked under @streams_trigger_mutex so concurrent first-publishers
|
|
1058
|
+
# (the integration spec fires 8) can't each build their own trigger — that
|
|
1059
|
+
# would give each thread a trigger with its own throttle, defeating the
|
|
1060
|
+
# once-per-interval throttle on the first window. Steady-state publishes hit
|
|
1061
|
+
# the fast `defined?` path with no lock.
|
|
1062
|
+
def streams_pool_trigger
|
|
1063
|
+
return @streams_pool_trigger if defined?(@streams_pool_trigger)
|
|
1064
|
+
|
|
1065
|
+
@streams_trigger_mutex.synchronize do
|
|
1066
|
+
return @streams_pool_trigger if defined?(@streams_pool_trigger)
|
|
1067
|
+
|
|
1068
|
+
@streams_pool_trigger =
|
|
1069
|
+
if config.streams_pool_autoscale && !@shared_connection
|
|
1070
|
+
autoscaler = Streams::PoolAutoscaler.new(client: self, config: config)
|
|
1071
|
+
Streams::PoolTrigger.new(
|
|
1072
|
+
autoscaler: autoscaler, job_pool: @pgmq,
|
|
1073
|
+
interval: config.streams_pool_autoscale_interval,
|
|
1074
|
+
application_name_prefix: config.streams_application_name
|
|
1075
|
+
)
|
|
1076
|
+
end
|
|
1077
|
+
end
|
|
1078
|
+
end
|
|
1079
|
+
|
|
874
1080
|
# Substrings that indicate the pooled PG::Connection was already dead
|
|
875
1081
|
# *before* pgmq-ruby tried to use it — typically killed by a connection
|
|
876
1082
|
# pooler (PgBouncer server_idle_timeout / client_idle_timeout), an admin
|
|
@@ -1100,6 +1306,57 @@ module Pgbus
|
|
|
1100
1306
|
# read_timeout is already failing — and keeps a single server-side mechanism.
|
|
1101
1307
|
#
|
|
1102
1308
|
# Returns conn_opts unchanged when read_timeout is nil (bounding disabled).
|
|
1309
|
+
# Stamp a per-process application_name on the streams-pool connection options
|
|
1310
|
+
# so the autoscaler can count peer processes via
|
|
1311
|
+
# `pg_stat_activity.application_name LIKE '<prefix>_%'` (issue #323 P1). The
|
|
1312
|
+
# suffix is the pid so DISTINCT application_name is an exact process count.
|
|
1313
|
+
# application_name is a cosmetic session GUC — appending it can't break the
|
|
1314
|
+
# connection. The Proc (shared-AR) path never reaches here.
|
|
1315
|
+
def tag_application_name(conn_opts)
|
|
1316
|
+
name = "#{config.streams_application_name}_#{::Process.pid}"
|
|
1317
|
+
case conn_opts
|
|
1318
|
+
when Hash
|
|
1319
|
+
conn_opts.merge(application_name: name)
|
|
1320
|
+
when String
|
|
1321
|
+
# Two libpq string forms (mirrors #append_connection_bounds): URI form
|
|
1322
|
+
# carries params as `?key=value&…` query pairs; key=value conninfo form
|
|
1323
|
+
# is space-separated. Appending wins over any earlier application_name.
|
|
1324
|
+
if conn_opts.start_with?("postgres://", "postgresql://")
|
|
1325
|
+
separator = conn_opts.include?("?") ? "&" : "?"
|
|
1326
|
+
"#{conn_opts}#{separator}application_name=#{name}"
|
|
1327
|
+
else
|
|
1328
|
+
"#{conn_opts} application_name=#{name}"
|
|
1329
|
+
end
|
|
1330
|
+
else
|
|
1331
|
+
conn_opts
|
|
1332
|
+
end
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
# In :session GUC mode, a Hash conn_opts carrying database.yml `:variables`
|
|
1336
|
+
# must apply those GUCs via post-connect `SET` rather than the libpq
|
|
1337
|
+
# `options` STARTUP param (which a transaction-mode PgBouncer rejects). We
|
|
1338
|
+
# can't pass `:variables` to PG.connect (not a libpq keyword), so wrap the
|
|
1339
|
+
# opts in a fresh-connect factory Proc: pgmq-ruby natively accepts a callable
|
|
1340
|
+
# per pool slot (pgmq connection.rb), and it must return a UNIQUE
|
|
1341
|
+
# PG::Connection each call (pgmq guards against a shared object). Applies to
|
|
1342
|
+
# a Hash with `:variables` only — a String URL or no variables passes through
|
|
1343
|
+
# unchanged, as does :options mode (where forward_connection_variables
|
|
1344
|
+
# already baked the GUCs into `options`). See issue #332.
|
|
1345
|
+
def wrap_session_gucs(conn_opts)
|
|
1346
|
+
return conn_opts unless config.connection_guc_mode == :session
|
|
1347
|
+
return conn_opts unless conn_opts.is_a?(Hash)
|
|
1348
|
+
|
|
1349
|
+
variables = conn_opts[:variables]
|
|
1350
|
+
return conn_opts if variables.nil? || variables.empty?
|
|
1351
|
+
|
|
1352
|
+
pg_opts = conn_opts.except(:variables)
|
|
1353
|
+
lambda do
|
|
1354
|
+
conn = PG.connect(pg_opts)
|
|
1355
|
+
variables.each { |name, value| conn.exec("SET #{name} = '#{value}'") }
|
|
1356
|
+
conn
|
|
1357
|
+
end
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1103
1360
|
def apply_connection_bounds(conn_opts)
|
|
1104
1361
|
timeout = config.read_timeout
|
|
1105
1362
|
return conn_opts unless timeout&.positive?
|