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,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module ActiveJob
|
|
5
|
+
# config.quiet_active_job_logs (issue #618): a worker process stops
|
|
6
|
+
# ActiveJob's per-job "Performing …" / "Performed …" info lines by
|
|
7
|
+
# unsubscribing ActiveJob::LogSubscriber from perform_start.active_job and
|
|
8
|
+
# perform.active_job. The perform listener also writes ActiveJob's
|
|
9
|
+
# "Error performing …" error line, for a job that raised (with its
|
|
10
|
+
# backtrace) and for one a before_perform callback halted, so those go too.
|
|
11
|
+
# An exception that escapes ActiveJob's handlers is still logged by
|
|
12
|
+
# ErrorReporter (class and message at error) and recorded in
|
|
13
|
+
# pgbus_failed_events; one retry_on catches still logs ActiveJob's
|
|
14
|
+
# "Retrying …" line (and "Stopped retrying …" at error when it gives up);
|
|
15
|
+
# a halted job logs nothing.
|
|
16
|
+
# The other ActiveJob events (Enqueued, Retrying, Stopped retrying,
|
|
17
|
+
# Discarded) still log as the host configured them, and the web process is
|
|
18
|
+
# untouched. Swapping
|
|
19
|
+
# ActiveJob::Base.logger instead would reach the web process too, and by
|
|
20
|
+
# default that logger is Rails.logger.
|
|
21
|
+
module QuietLogs
|
|
22
|
+
EVENTS = %w[perform_start.active_job perform.active_job].freeze
|
|
23
|
+
|
|
24
|
+
# Built at load; the applied notifiers are read and written under it.
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
@applied = {}.compare_by_identity
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Unsubscribes the LogSubscriber listeners once per notifier. Returns
|
|
30
|
+
# true when it did, false when that notifier was already done.
|
|
31
|
+
def apply!(notifier: ::ActiveSupport::Notifications.notifier)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
return false if @applied.key?(notifier)
|
|
34
|
+
|
|
35
|
+
@applied[notifier] = true
|
|
36
|
+
# all_listeners_for, not listeners_for: the latter skips a
|
|
37
|
+
# silenced subscriber (its logger above info), which would come
|
|
38
|
+
# back the moment the level drops.
|
|
39
|
+
EVENTS.each do |name|
|
|
40
|
+
notifier.all_listeners_for(name).each do |listener|
|
|
41
|
+
notifier.unsubscribe(listener) if log_subscriber?(listener)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Fanout::Subscribers::Evented exposes #delegate on Rails 7.1 and later.
|
|
51
|
+
def log_subscriber?(listener)
|
|
52
|
+
listener.respond_to?(:delegate) && listener.delegate.is_a?(::ActiveJob::LogSubscriber)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -66,8 +66,8 @@ module Pgbus
|
|
|
66
66
|
q RECORD;
|
|
67
67
|
BEGIN
|
|
68
68
|
FOR q IN SELECT queue_name FROM pgmq.meta LOOP
|
|
69
|
-
EXECUTE format('ALTER TABLE pgmq
|
|
70
|
-
EXECUTE format('ALTER TABLE pgmq
|
|
69
|
+
EXECUTE format('ALTER TABLE pgmq.%I SET (#{settings_clause(QUEUE_SETTINGS)})', 'q_' || lower(q.queue_name));
|
|
70
|
+
EXECUTE format('ALTER TABLE pgmq.%I SET (#{settings_clause(ARCHIVE_SETTINGS)})', 'a_' || lower(q.queue_name));
|
|
71
71
|
END LOOP;
|
|
72
72
|
END $$;
|
|
73
73
|
SQL
|
data/lib/pgbus/batch/sweep.rb
CHANGED
|
@@ -47,6 +47,8 @@ module Pgbus
|
|
|
47
47
|
|
|
48
48
|
dlq = client.dead_letter_physical_name(row.queue_name, prefixed: false)
|
|
49
49
|
return :failed if client.message_with_job_id?(dlq, job_id: row.job_id, prefixed: false) == true
|
|
50
|
+
# completed_jobs :delete (issue #607) leaves no archive row to find.
|
|
51
|
+
return :completed if Pgbus.configuration.completed_jobs == :delete
|
|
50
52
|
|
|
51
53
|
return :completed if client.message_archived?(row.queue_name, msg_id: row.msg_id, prefixed: false) == true
|
|
52
54
|
|
data/lib/pgbus/batch.rb
CHANGED
|
@@ -425,8 +425,11 @@ module Pgbus
|
|
|
425
425
|
end
|
|
426
426
|
|
|
427
427
|
def self.backfill_execution(payload, msg_id, queue_name)
|
|
428
|
-
return unless executions_migrated?
|
|
429
428
|
return unless payload && msg_id
|
|
429
|
+
# Only batch members have an execution row: without this, every plain
|
|
430
|
+
# enqueue paid a no-op UPDATE round trip after the send (issue #603).
|
|
431
|
+
return unless payload[METADATA_KEY]
|
|
432
|
+
return unless executions_migrated?
|
|
430
433
|
|
|
431
434
|
job_id = payload["job_id"]
|
|
432
435
|
return unless job_id
|
data/lib/pgbus/cli/dlq.rb
CHANGED
|
@@ -70,7 +70,8 @@ module Pgbus
|
|
|
70
70
|
if data_source.retry_dlq_message(detail[:queue_name], detail[:msg_id])
|
|
71
71
|
puts "Re-enqueued msg_id #{detail[:msg_id]} to #{origin_queue(detail[:queue_name])}."
|
|
72
72
|
else
|
|
73
|
-
warn "Failed to retry msg_id #{detail[:msg_id]}
|
|
73
|
+
warn "Failed to retry msg_id #{detail[:msg_id]}: it is gone, the retry failed, " \
|
|
74
|
+
"another copy of the job holds its uniqueness key, or its concurrency limit is full."
|
|
74
75
|
exit 1
|
|
75
76
|
end
|
|
76
77
|
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class Client
|
|
5
|
+
# Asynchronous completion commit (issue #619,
|
|
6
|
+
# `config.completion_synchronous_commit = false`). A completion statement
|
|
7
|
+
# (archive, delete and their batches) runs in its own transaction with
|
|
8
|
+
# synchronous_commit off, so the job is acknowledged without waiting for
|
|
9
|
+
# its WAL flush. The four queries go out in one libpq pipeline with one
|
|
10
|
+
# sync, so it is still one round trip (pg writes each query to the socket
|
|
11
|
+
# as it is sent, but waits for a reply only once, after the sync):
|
|
12
|
+
#
|
|
13
|
+
# BEGIN
|
|
14
|
+
# SET LOCAL synchronous_commit TO off
|
|
15
|
+
# <the prepared completion statement> <- the result the caller gets
|
|
16
|
+
# COMMIT
|
|
17
|
+
#
|
|
18
|
+
# SET LOCAL ends with the transaction, so the pooled connection never
|
|
19
|
+
# keeps the setting, and the statement stays prepared (no planning cost
|
|
20
|
+
# back, issue #605). Only a job's completion asks for it (the executor and
|
|
21
|
+
# the completer pass job_completion: true; the event consumer and the
|
|
22
|
+
# dashboard keep the durable commit), and NativeStatements#exec_native
|
|
23
|
+
# calls it only outside a caller's transaction: a completion inside one is
|
|
24
|
+
# the caller's to commit.
|
|
25
|
+
#
|
|
26
|
+
# A failed query aborts the rest of the pipeline and leaves the explicit
|
|
27
|
+
# BEGIN open in a failed state: the connection leaves pipeline mode, the
|
|
28
|
+
# transaction is rolled back, and the error is raised for run_native's
|
|
29
|
+
# re-run and short-row checks. A statement not yet prepared on this session
|
|
30
|
+
# runs through prepare_and_exec outside the pipeline, committed
|
|
31
|
+
# synchronously, once; the next call pipelines again.
|
|
32
|
+
module AsyncCommit
|
|
33
|
+
ASYNC_COMMIT_BEGIN = "BEGIN"
|
|
34
|
+
ASYNC_COMMIT_OFF = "SET LOCAL synchronous_commit TO off"
|
|
35
|
+
ASYNC_COMMIT_COMMIT = "COMMIT"
|
|
36
|
+
NO_PARAMS = [].freeze
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def exec_async_commit(conn, statement, params, prepared)
|
|
41
|
+
results = pipeline_async_commit(conn, statement, params, prepared)
|
|
42
|
+
failed = results.find { |result| result.result_status == PG::PGRES_FATAL_ERROR }
|
|
43
|
+
unless failed
|
|
44
|
+
NativeStatements.note_executed if prepared
|
|
45
|
+
return results[2]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
rollback_async_commit(conn)
|
|
49
|
+
begin
|
|
50
|
+
failed.check
|
|
51
|
+
rescue PG::InvalidSqlStatementName
|
|
52
|
+
raise unless prepared
|
|
53
|
+
|
|
54
|
+
prepare_and_exec(conn, statement, params)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Sends the four queries and one sync, and reads every result up to the
|
|
59
|
+
# sync: each query answers its result and then nil.
|
|
60
|
+
def pipeline_async_commit(conn, statement, params, prepared)
|
|
61
|
+
conn.enter_pipeline_mode
|
|
62
|
+
begin
|
|
63
|
+
conn.send_query_params(ASYNC_COMMIT_BEGIN, NO_PARAMS)
|
|
64
|
+
conn.send_query_params(ASYNC_COMMIT_OFF, NO_PARAMS)
|
|
65
|
+
if prepared
|
|
66
|
+
conn.send_query_prepared(statement.name, params)
|
|
67
|
+
else
|
|
68
|
+
conn.send_query_params(statement.sql, params)
|
|
69
|
+
end
|
|
70
|
+
conn.send_query_params(ASYNC_COMMIT_COMMIT, NO_PARAMS)
|
|
71
|
+
conn.pipeline_sync
|
|
72
|
+
read_pipeline(conn)
|
|
73
|
+
ensure
|
|
74
|
+
leave_pipeline(conn)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def read_pipeline(conn)
|
|
79
|
+
results = []
|
|
80
|
+
ended = false
|
|
81
|
+
loop do
|
|
82
|
+
result = conn.get_result
|
|
83
|
+
if result.nil?
|
|
84
|
+
# One nil closes each query's results; two in a row means libpq
|
|
85
|
+
# has nothing more, so the sync will not come. That session's
|
|
86
|
+
# state cannot be trusted: pgmq-ruby re-runs on a fresh one.
|
|
87
|
+
if ended
|
|
88
|
+
discard_session(conn, "A completion pipeline ended before its sync")
|
|
89
|
+
raise PG::ConnectionBad, "pipeline ended before its sync"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
ended = true
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
ended = false
|
|
96
|
+
break if result.result_status == PG::PGRES_PIPELINE_SYNC
|
|
97
|
+
|
|
98
|
+
results << result
|
|
99
|
+
end
|
|
100
|
+
results
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# A connection back in the pool in pipeline mode would break the next
|
|
104
|
+
# caller. A lost one is left as it is: pgmq-ruby resets a bad connection
|
|
105
|
+
# at its next checkout, and a reset leaves pipeline mode. One that cannot
|
|
106
|
+
# leave (results left unread by an interrupted read) is reset here.
|
|
107
|
+
def leave_pipeline(conn)
|
|
108
|
+
return if conn.pipeline_status == PG::PQ_PIPELINE_OFF || conn.status == PG::CONNECTION_BAD
|
|
109
|
+
|
|
110
|
+
conn.exit_pipeline_mode
|
|
111
|
+
rescue PG::Error => e
|
|
112
|
+
discard_session(conn, "Could not leave pipeline mode after a completion (#{e.message})")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Resets a session whose pipeline state cannot be trusted. A reset that
|
|
116
|
+
# fails (the server is gone) is logged and left there, so the error the
|
|
117
|
+
# caller is already raising is the one that surfaces; pgmq-ruby resets a
|
|
118
|
+
# bad connection at its next checkout.
|
|
119
|
+
# A reset reconnects with the same conninfo (the read bounds ride in it)
|
|
120
|
+
# but does not re-run the connect lambda, so the :session-mode SETs are
|
|
121
|
+
# applied again here.
|
|
122
|
+
def discard_session(conn, reason)
|
|
123
|
+
Pgbus.logger.warn { "[Pgbus::Client] #{reason}, resetting the connection" }
|
|
124
|
+
begin
|
|
125
|
+
conn.reset
|
|
126
|
+
rescue PG::Error => e
|
|
127
|
+
Pgbus.logger.warn { "[Pgbus::Client] Resetting the connection failed: #{e.message}" }
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
reapply_session_gucs(conn)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def reapply_session_gucs(conn)
|
|
134
|
+
apply_session_gucs(conn, @session_gucs) if @session_gucs
|
|
135
|
+
rescue PG::Error => e
|
|
136
|
+
Pgbus.logger.warn do
|
|
137
|
+
"[Pgbus::Client] Re-applying the session's database.yml variables after the reset failed: #{e.message}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The failed BEGIN block (INERROR) is still open after the sync. A failed
|
|
142
|
+
# COMMIT has already ended it (IDLE); a lost connection reports UNKNOWN
|
|
143
|
+
# and has nothing to roll back.
|
|
144
|
+
def rollback_async_commit(conn)
|
|
145
|
+
conn.exec("ROLLBACK") if [PG::PQTRANS_INERROR, PG::PQTRANS_INTRANS].include?(conn.transaction_status)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|