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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
class Client
|
|
7
|
+
# Native prepared statements (issue #605): pgbus-owned static SQL for the
|
|
8
|
+
# seven hot-path queue operations, run as named prepared statements on the
|
|
9
|
+
# pooled connection instead of through pgmq's plpgsql functions.
|
|
10
|
+
#
|
|
11
|
+
# pgmq.read / archive / delete / send / send_batch build their statement
|
|
12
|
+
# with FORMAT(...) and run it with EXECUTE. plpgsql never caches the plan
|
|
13
|
+
# of a dynamic EXECUTE, so Postgres parses and plans the statement on every
|
|
14
|
+
# call. The SQL below is the same statement those functions build
|
|
15
|
+
# (lib/pgbus/pgmq_schema/pgmq_v1.13.1.sql), written once per queue table,
|
|
16
|
+
# so Postgres can keep its plan for the life of the connection.
|
|
17
|
+
#
|
|
18
|
+
# Each queue is checked once per process: its q_ and a_ tables must have
|
|
19
|
+
# every column the statements touch (REQUIRED_COLUMNS). A queue whose table
|
|
20
|
+
# lacks one (a pgmq extension older than `headers`), or
|
|
21
|
+
# `config.native_statements = false`, keeps calling the pgmq functions.
|
|
22
|
+
#
|
|
23
|
+
# Statements are prepared on demand: exec_prepared first; an
|
|
24
|
+
# InvalidSqlStatementName means this session has not prepared it yet, so
|
|
25
|
+
# prepare and run it again. The server is the registry: nothing records
|
|
26
|
+
# which connection holds which statement. Errors are not wrapped here: the
|
|
27
|
+
# call runs inside @pgmq.with_connection, which retries a lost connection
|
|
28
|
+
# once and raises PGMQ::Errors::ConnectionError, exactly as on the function
|
|
29
|
+
# path.
|
|
30
|
+
#
|
|
31
|
+
# Inside a transaction the server has begun (the shared-AR path, an
|
|
32
|
+
# enqueue inside the app's transaction once Rails has sent its BEGIN) a
|
|
33
|
+
# failed exec_prepared would abort the caller's transaction, so there the
|
|
34
|
+
# same SQL runs through exec_params.
|
|
35
|
+
#
|
|
36
|
+
# Connections built from ActiveRecord's config whose database.yml sets
|
|
37
|
+
# `prepared_statements: false` (what a transaction-mode PgBouncer asks
|
|
38
|
+
# of Rails) send the same SQL through exec_params from the start.
|
|
39
|
+
#
|
|
40
|
+
# A transaction-mode pooler without prepared-statement support (PgBouncer
|
|
41
|
+
# before 1.21, or 1.21+ without max_prepared_statements) drops prepared
|
|
42
|
+
# statements between transactions. When that is seen (see
|
|
43
|
+
# NativeStatements.note_missing) the process switches to exec_params of the
|
|
44
|
+
# same static SQL for good and logs it once.
|
|
45
|
+
module NativeStatements
|
|
46
|
+
REQUIRED_COLUMNS = %w[msg_id read_ct enqueued_at last_read_at vt message headers].freeze
|
|
47
|
+
|
|
48
|
+
COLUMNS_SQL = "SELECT table_name, column_name FROM information_schema.columns " \
|
|
49
|
+
"WHERE table_schema = 'pgmq' AND table_name IN ($1, $2)"
|
|
50
|
+
|
|
51
|
+
# Statements found missing on a backend that already had them, in a row,
|
|
52
|
+
# before the process stops preparing.
|
|
53
|
+
LOST_STATEMENT_LIMIT = 3
|
|
54
|
+
|
|
55
|
+
# Raised when an archive or delete re-run after a lost connection comes
|
|
56
|
+
# back short (run_native). It carries the original error's text, which
|
|
57
|
+
# can match a stale-connection pattern, so with_stale_connection_retry
|
|
58
|
+
# checks completion_lost? first and never re-runs it.
|
|
59
|
+
COMPLETION_NOT_RESENT = "Database connection lost while an archive or delete ran, " \
|
|
60
|
+
"and the re-run found ids gone: it may have committed"
|
|
61
|
+
|
|
62
|
+
# Reads of up to this many messages run a statement with the LIMIT
|
|
63
|
+
# written in: with `LIMIT $1` the generic plan assumes a tenth of the
|
|
64
|
+
# table and hash-joins a full scan, so Postgres plans a custom plan on
|
|
65
|
+
# every call; a literal LIMIT gets the cached generic plan. A larger qty
|
|
66
|
+
# runs the `LIMIT $1` statement.
|
|
67
|
+
LITERAL_READ_LIMIT = 64
|
|
68
|
+
|
|
69
|
+
# Entries kept in @prepared_on before it is cleared. Every reconnect
|
|
70
|
+
# brings a new backend pid, so a long-lived process would otherwise grow
|
|
71
|
+
# it for good. Clearing only forgets history: a statement then missing
|
|
72
|
+
# reads as a first prepare, which can delay the switch, never cause it.
|
|
73
|
+
PREPARED_ON_LIMIT = 10_000
|
|
74
|
+
|
|
75
|
+
Statement = Data.define(:name, :sql)
|
|
76
|
+
|
|
77
|
+
# Process-wide, built at load: the pooler is a property of the process's
|
|
78
|
+
# database route, not of one Client instance.
|
|
79
|
+
@lost_in_a_row = Concurrent::AtomicFixnum.new(0)
|
|
80
|
+
@unprepared = Concurrent::AtomicBoolean.new(false)
|
|
81
|
+
# "backend_pid:statement name" for every statement this process has
|
|
82
|
+
# prepared. A statement missing on a backend listed here was dropped
|
|
83
|
+
# under us (a pooler moved the session); one missing on a new backend
|
|
84
|
+
# (a fresh connection, a reconnect) is only a first prepare.
|
|
85
|
+
@prepared_on = Concurrent::Map.new
|
|
86
|
+
# Whether the database.yml prepared_statements: false notice was logged.
|
|
87
|
+
@ar_unprepared_logged = Concurrent::AtomicBoolean.new(false)
|
|
88
|
+
|
|
89
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable -- the four are
|
|
90
|
+
# Concurrent primitives built once at load and never reassigned; every
|
|
91
|
+
# read and write below goes through their atomic operations.
|
|
92
|
+
class << self
|
|
93
|
+
# The error run_native raises for a completion whose re-run came back
|
|
94
|
+
# short (the client's stale retry and the consumer key on it).
|
|
95
|
+
def completion_lost?(error)
|
|
96
|
+
defined?(PGMQ::Errors::ConnectionError) && error.is_a?(PGMQ::Errors::ConnectionError) &&
|
|
97
|
+
error.message.start_with?(COMPLETION_NOT_RESENT)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# True once the process has given up on prepared statements.
|
|
101
|
+
def unprepared?
|
|
102
|
+
@unprepared.true?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def note_prepared(backend_pid, name)
|
|
106
|
+
@prepared_on.clear if @prepared_on.size >= PREPARED_ON_LIMIT
|
|
107
|
+
@prepared_on["#{backend_pid}:#{name}"] = true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def prepared_count
|
|
111
|
+
@prepared_on.size
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A statement this session needed to prepare. Counts toward the switch
|
|
115
|
+
# only when this backend had prepared it before.
|
|
116
|
+
def note_missing(backend_pid, name)
|
|
117
|
+
return unless @prepared_on.key?("#{backend_pid}:#{name}")
|
|
118
|
+
|
|
119
|
+
unprepare!("#{LOST_STATEMENT_LIMIT} prepared statements in a row disappeared from their connection") if
|
|
120
|
+
@lost_in_a_row.increment >= LOST_STATEMENT_LIMIT
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Logs, once per process, that database.yml turned preparing off.
|
|
124
|
+
def note_ar_unprepared
|
|
125
|
+
return unless @ar_unprepared_logged.make_true
|
|
126
|
+
|
|
127
|
+
Pgbus.logger.info do
|
|
128
|
+
"[Pgbus::Client] Native statements run unprepared: database.yml sets prepared_statements: false"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def note_executed
|
|
133
|
+
@lost_in_a_row.value = 0 unless @lost_in_a_row.value.zero?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def unprepare!(reason)
|
|
137
|
+
return unless @unprepared.make_true
|
|
138
|
+
|
|
139
|
+
Pgbus.logger.warn do
|
|
140
|
+
"[Pgbus::Client] Native statements: #{reason}, which is what a transaction-mode pooler " \
|
|
141
|
+
"without prepared-statement support does (PgBouncer < 1.21, or no max_prepared_statements). " \
|
|
142
|
+
"This process now sends the same SQL unprepared. Set max_prepared_statements on the pooler, " \
|
|
143
|
+
"or config.native_statements = false to use the pgmq functions."
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Test seam: forget the switch and every recorded statement.
|
|
148
|
+
def reset!
|
|
149
|
+
@lost_in_a_row.value = 0
|
|
150
|
+
@unprepared.make_false
|
|
151
|
+
@prepared_on.clear
|
|
152
|
+
@ar_unprepared_logged.make_false
|
|
153
|
+
end
|
|
154
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
155
|
+
|
|
156
|
+
# The seven statements for one queue, keyed by op. Names are
|
|
157
|
+
# pgbus_<op>_<12 hex of SHA-256(sql)>: 63 bytes is the identifier limit,
|
|
158
|
+
# and hashing the SQL (which names the table) means two pgbus versions
|
|
159
|
+
# behind one pooler never run each other's text under one name.
|
|
160
|
+
#
|
|
161
|
+
# :reads holds the literal-LIMIT reads, indexed by qty (1 to
|
|
162
|
+
# LITERAL_READ_LIMIT); their one parameter is the visibility timeout.
|
|
163
|
+
def statements_for(queue_table, archive_table)
|
|
164
|
+
queue = "pgmq.#{quote_ident(queue_table)}"
|
|
165
|
+
statements = sql_for(queue, "pgmq.#{quote_ident(archive_table)}").to_h { |op, sql| [op, statement(op, sql)] }
|
|
166
|
+
reads = (1..LITERAL_READ_LIMIT).map { |qty| statement(:read, read_sql(queue, qty.to_s, "$1")) }
|
|
167
|
+
statements.merge(reads: [nil, *reads].freeze).freeze
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def statement(kind, sql)
|
|
173
|
+
Statement.new(name: "pgbus_#{kind}_#{Digest::SHA256.hexdigest(sql)[0, 12]}", sql: sql.freeze)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def read_sql(queue, limit, vt)
|
|
177
|
+
"WITH cte AS (SELECT msg_id FROM #{queue} WHERE vt <= clock_timestamp() " \
|
|
178
|
+
"ORDER BY msg_id ASC LIMIT #{limit} FOR UPDATE SKIP LOCKED) " \
|
|
179
|
+
"UPDATE #{queue} m SET last_read_at = clock_timestamp(), " \
|
|
180
|
+
"vt = clock_timestamp() + make_interval(secs => #{vt}::integer), read_ct = read_ct + 1 " \
|
|
181
|
+
"FROM cte WHERE m.msg_id = cte.msg_id " \
|
|
182
|
+
"RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def quote_ident(name)
|
|
186
|
+
%("#{name.gsub('"', '""')}")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# pgmq.read, pgmq.archive (both overloads), pgmq.delete (both),
|
|
190
|
+
# pgmq.send and pgmq._send_batch of pgmq 1.13.1, with the table names
|
|
191
|
+
# filled in. The delay is computed once per statement, as the pgmq
|
|
192
|
+
# wrappers do before calling the implementation.
|
|
193
|
+
def sql_for(queue, archive)
|
|
194
|
+
archived = "RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers"
|
|
195
|
+
archive_insert = "INSERT INTO #{archive} (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers) " \
|
|
196
|
+
"SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers FROM archived " \
|
|
197
|
+
"RETURNING msg_id"
|
|
198
|
+
{
|
|
199
|
+
read: read_sql(queue, "$1::integer", "$2"),
|
|
200
|
+
archive: "WITH archived AS (DELETE FROM #{queue} WHERE msg_id = $1::bigint #{archived}) #{archive_insert}",
|
|
201
|
+
archive_batch: "WITH archived AS (DELETE FROM #{queue} WHERE msg_id = ANY($1::bigint[]) #{archived}) " \
|
|
202
|
+
"#{archive_insert}",
|
|
203
|
+
delete: "DELETE FROM #{queue} WHERE msg_id = $1::bigint RETURNING msg_id",
|
|
204
|
+
delete_batch: "DELETE FROM #{queue} WHERE msg_id = ANY($1::bigint[]) RETURNING msg_id",
|
|
205
|
+
send: "INSERT INTO #{queue} (vt, message, headers) " \
|
|
206
|
+
"VALUES (clock_timestamp() + make_interval(secs => $3::integer), $1::jsonb, $2::jsonb) " \
|
|
207
|
+
"RETURNING msg_id",
|
|
208
|
+
send_batch: "INSERT INTO #{queue} (vt, message, headers) " \
|
|
209
|
+
"SELECT d.vt, unnest($1::jsonb[]), unnest(coalesce($2::jsonb[], ARRAY[]::jsonb[])) " \
|
|
210
|
+
"FROM (SELECT clock_timestamp() + make_interval(secs => $3::integer) AS vt) d " \
|
|
211
|
+
"RETURNING msg_id"
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Whether this queue runs the native statements. Checked on first use
|
|
217
|
+
# (not at queue creation), so a queue created by another process is
|
|
218
|
+
# covered the same way.
|
|
219
|
+
def native_ready?(full_name)
|
|
220
|
+
native_statements_for(full_name) ? true : false
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
# Checks the queue's q_ and a_ columns and caches the outcome: the
|
|
226
|
+
# statements when every column is there, false when one is missing.
|
|
227
|
+
# A queue table that does not exist yet is not cached, so the queue is
|
|
228
|
+
# checked again once something creates it. Runs on @pgmq.with_connection
|
|
229
|
+
# without taking @pgmq_mutex: every caller already runs inside
|
|
230
|
+
# `synchronized`.
|
|
231
|
+
def detect_native_support!(full_name)
|
|
232
|
+
return false unless config.native_statements
|
|
233
|
+
|
|
234
|
+
queue_table = "q_#{full_name.downcase}"
|
|
235
|
+
archive_table = "a_#{full_name.downcase}"
|
|
236
|
+
columns = native_table_columns(queue_table, archive_table)
|
|
237
|
+
return false unless columns.key?(queue_table)
|
|
238
|
+
|
|
239
|
+
missing = native_missing_columns(columns, queue_table, archive_table)
|
|
240
|
+
value = missing.empty? ? NativeStatements.statements_for(queue_table, archive_table) : false
|
|
241
|
+
previous = @native_statements.put_if_absent(full_name, value)
|
|
242
|
+
return previous unless previous.nil?
|
|
243
|
+
|
|
244
|
+
log_native_unsupported(full_name, missing) unless value
|
|
245
|
+
value
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Whether the connections come from ActiveRecord's config with
|
|
249
|
+
# `prepared_statements: false`: the hash pgbus extracts from it, or a
|
|
250
|
+
# connection_params Proc handing out ActiveRecord's own connection. A
|
|
251
|
+
# database_url or a connection_params Hash is pgbus's own route. Read
|
|
252
|
+
# once, in Client#initialize.
|
|
253
|
+
def ar_disables_prepared_statements?
|
|
254
|
+
return false if config.database_url || config.connection_params.is_a?(Hash)
|
|
255
|
+
return false unless defined?(::ActiveRecord::Base)
|
|
256
|
+
|
|
257
|
+
base = config.connects_to ? Pgbus::BusRecord : ::ActiveRecord::Base
|
|
258
|
+
return false unless [false, "false"].include?(base.connection_db_config.configuration_hash[:prepared_statements])
|
|
259
|
+
|
|
260
|
+
NativeStatements.note_ar_unprepared
|
|
261
|
+
true
|
|
262
|
+
rescue StandardError
|
|
263
|
+
# Client#initialize reads config.connection_options first, whose
|
|
264
|
+
# Configuration#extract_ar_connection_hash makes this same
|
|
265
|
+
# connection_db_config call, logs its failure and falls back to the
|
|
266
|
+
# shared connection; there is no setting to read.
|
|
267
|
+
false
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def native_statements_for(full_name)
|
|
271
|
+
return unless config.native_statements
|
|
272
|
+
|
|
273
|
+
cached = @native_statements[full_name]
|
|
274
|
+
return cached unless cached.nil?
|
|
275
|
+
|
|
276
|
+
detect_native_support!(full_name)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def native_table_columns(queue_table, archive_table)
|
|
280
|
+
rows = @pgmq.with_connection { |conn| conn.exec_params(COLUMNS_SQL, [queue_table, archive_table]).to_a }
|
|
281
|
+
rows.each_with_object(Hash.new { |h, k| h[k] = [] }) do |row, by_table|
|
|
282
|
+
by_table[row["table_name"]] << row["column_name"]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def native_missing_columns(columns, queue_table, archive_table)
|
|
287
|
+
missing = (REQUIRED_COLUMNS - columns[queue_table]).map { |c| "#{queue_table}.#{c}" }
|
|
288
|
+
missing + (REQUIRED_COLUMNS - columns[archive_table]).map { |c| "#{archive_table}.#{c}" }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def log_native_unsupported(full_name, missing)
|
|
292
|
+
Pgbus.logger.info do
|
|
293
|
+
"[Pgbus::Client] Queue #{full_name} uses the pgmq functions, not native statements: " \
|
|
294
|
+
"pgmq.#{missing.join(", pgmq.")} missing (a pgmq install older than the headers column?)"
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# The seven operations, native when the queue is ready, the pgmq-ruby
|
|
299
|
+
# call otherwise. Return values match pgmq-ruby's: Messages with String
|
|
300
|
+
# fields, String ids, true/false for one archive/delete.
|
|
301
|
+
|
|
302
|
+
# The send statements bind the delay as $3::integer. is_a?(Integer) also
|
|
303
|
+
# takes a whole-second ActiveSupport::Duration, which binds as its seconds;
|
|
304
|
+
# a fractional delay or a Time goes through pgmq-ruby.
|
|
305
|
+
def exec_produce(name, message, headers:, delay:)
|
|
306
|
+
statements = delay.is_a?(Integer) && native_statements_for(name)
|
|
307
|
+
return @pgmq.produce(name, message, headers: headers, delay: delay) unless statements
|
|
308
|
+
|
|
309
|
+
run_native(statements[:send], [message, headers, delay])[0]["msg_id"]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def exec_produce_batch(name, messages, headers:, delay:)
|
|
313
|
+
statements = messages.any? && (headers.nil? || headers.length == messages.length) &&
|
|
314
|
+
delay.is_a?(Integer) && native_statements_for(name)
|
|
315
|
+
return @pgmq.produce_batch(name, messages, headers: headers, delay: delay) unless statements
|
|
316
|
+
|
|
317
|
+
encoder = PG::TextEncoder::Array.new
|
|
318
|
+
params = [encoder.encode(messages), headers && encoder.encode(headers), delay]
|
|
319
|
+
run_native(statements[:send_batch], params).map { |row| row["msg_id"] }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def exec_read_batch(name, vt:, qty:)
|
|
323
|
+
statements = native_statements_for(name)
|
|
324
|
+
return @pgmq.read_batch(name, vt: vt, qty: qty) unless statements
|
|
325
|
+
|
|
326
|
+
literal = qty.is_a?(Integer) && qty.positive? && statements[:reads][qty]
|
|
327
|
+
result = literal ? run_native(literal, [vt]) : run_native(statements[:read], [qty, vt])
|
|
328
|
+
result.map { |row| PGMQ::Message.new(row) }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# async: commit with synchronous_commit off (AsyncCommit, issue #619);
|
|
332
|
+
# true only for a job's completion under completion_synchronous_commit
|
|
333
|
+
# = false.
|
|
334
|
+
def exec_archive(name, msg_id, async: false)
|
|
335
|
+
statements = native_statements_for(name)
|
|
336
|
+
return @pgmq.archive(name, msg_id) unless statements
|
|
337
|
+
|
|
338
|
+
run_native(statements[:archive], [msg_id], rows: 1, async: async).ntuples.positive?
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def exec_archive_batch(name, msg_ids, async: false)
|
|
342
|
+
statements = msg_ids.any? && native_statements_for(name)
|
|
343
|
+
return @pgmq.archive_batch(name, msg_ids) unless statements
|
|
344
|
+
|
|
345
|
+
params = [PG::TextEncoder::Array.new.encode(msg_ids)]
|
|
346
|
+
run_native(statements[:archive_batch], params, rows: msg_ids, async: async).map { |row| row["msg_id"] }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def exec_delete(name, msg_id, async: false)
|
|
350
|
+
statements = native_statements_for(name)
|
|
351
|
+
return @pgmq.delete(name, msg_id) unless statements
|
|
352
|
+
|
|
353
|
+
run_native(statements[:delete], [msg_id], rows: 1, async: async).ntuples.positive?
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def exec_delete_batch(name, msg_ids, async: false)
|
|
357
|
+
statements = msg_ids.any? && native_statements_for(name)
|
|
358
|
+
return @pgmq.delete_batch(name, msg_ids) unless statements
|
|
359
|
+
|
|
360
|
+
params = [PG::TextEncoder::Array.new.encode(msg_ids)]
|
|
361
|
+
run_native(statements[:delete_batch], params, rows: msg_ids, async: async).map { |row| row["msg_id"] }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# rows: the rows an archive or delete returns when every id was still
|
|
365
|
+
# there. pgmq-ruby's with_connection re-runs its block after a lost
|
|
366
|
+
# connection ("server closed the connection"). Usually the socket died
|
|
367
|
+
# idle in the pool and nothing ran, but it can follow a commit whose
|
|
368
|
+
# reply was lost: the re-run then finds those ids gone, and the executor
|
|
369
|
+
# would take its own completed job for a duplicate and skip its signals.
|
|
370
|
+
# So a re-run that comes back short raises instead; the executor's
|
|
371
|
+
# retry-once then reads the gone message as :ambiguous and claims it.
|
|
372
|
+
# A re-run that finds every id is certain: the first run never committed.
|
|
373
|
+
# rows is 1 or the batch's ids (counted once each, only on a re-run). An
|
|
374
|
+
# id another worker had already taken also makes a re-run come back
|
|
375
|
+
# short; every waiter of that batch then claims :ambiguous, the side the
|
|
376
|
+
# executor already errs on.
|
|
377
|
+
def run_native(statement, params, rows: nil, async: false)
|
|
378
|
+
attempt = 0
|
|
379
|
+
lost = nil
|
|
380
|
+
@pgmq.with_connection do |conn|
|
|
381
|
+
attempt += 1
|
|
382
|
+
result = exec_native(conn, statement, params, async)
|
|
383
|
+
if rows && attempt > 1 && result.ntuples < (rows.is_a?(Array) ? rows.uniq.size : rows)
|
|
384
|
+
raise PGMQ::Errors::ConnectionError, "#{COMPLETION_NOT_RESENT}: #{lost}"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
result
|
|
388
|
+
rescue PG::Error => e
|
|
389
|
+
lost = e.message
|
|
390
|
+
raise
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# async (a job's completion under completion_synchronous_commit = false)
|
|
395
|
+
# goes through AsyncCommit, unless it runs inside a caller's transaction.
|
|
396
|
+
def exec_native(conn, statement, params, async)
|
|
397
|
+
return conn.exec_params(statement.sql, params) if inside_caller_transaction?(conn)
|
|
398
|
+
|
|
399
|
+
unprepared = @native_unprepared || NativeStatements.unprepared?
|
|
400
|
+
return exec_async_commit(conn, statement, params, !unprepared) if async
|
|
401
|
+
return conn.exec_params(statement.sql, params) if unprepared
|
|
402
|
+
|
|
403
|
+
begin
|
|
404
|
+
result = conn.exec_prepared(statement.name, params)
|
|
405
|
+
rescue PG::InvalidSqlStatementName
|
|
406
|
+
return prepare_and_exec(conn, statement, params)
|
|
407
|
+
end
|
|
408
|
+
NativeStatements.note_executed
|
|
409
|
+
result
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def prepare_and_exec(conn, statement, params)
|
|
413
|
+
pid = conn.backend_pid
|
|
414
|
+
NativeStatements.note_missing(pid, statement.name)
|
|
415
|
+
begin
|
|
416
|
+
conn.prepare(statement.name, statement.sql)
|
|
417
|
+
rescue PG::DuplicatePstatement
|
|
418
|
+
# Prepared on this session between our two calls; run it.
|
|
419
|
+
end
|
|
420
|
+
NativeStatements.note_prepared(pid, statement.name)
|
|
421
|
+
begin
|
|
422
|
+
conn.exec_prepared(statement.name, params)
|
|
423
|
+
rescue PG::InvalidSqlStatementName
|
|
424
|
+
# Gone again right after its own prepare: the pooler ran the two calls
|
|
425
|
+
# on different server connections.
|
|
426
|
+
NativeStatements.unprepare!("a statement disappeared right after it was prepared")
|
|
427
|
+
conn.exec_params(statement.sql, params)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|