pgbus 0.6.7 → 0.6.9
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/app/controllers/pgbus/dashboard_controller.rb +4 -0
- data/app/controllers/pgbus/queues_controller.rb +1 -0
- data/app/views/pgbus/dashboard/_queue_health.html.erb +78 -0
- data/app/views/pgbus/dashboard/show.html.erb +2 -0
- data/app/views/pgbus/queues/show.html.erb +37 -0
- data/config/locales/da.yml +35 -4
- data/config/locales/de.yml +35 -4
- data/config/locales/en.yml +35 -4
- data/config/locales/es.yml +35 -4
- data/config/locales/fi.yml +35 -4
- data/config/locales/fr.yml +35 -4
- data/config/locales/it.yml +35 -4
- data/config/locales/ja.yml +35 -4
- data/config/locales/nb.yml +35 -4
- data/config/locales/nl.yml +35 -4
- data/config/locales/pt.yml +35 -4
- data/config/locales/sv.yml +35 -4
- data/lib/generators/pgbus/add_failed_events_index_generator.rb +4 -11
- data/lib/generators/pgbus/add_job_locks_generator.rb +4 -11
- data/lib/generators/pgbus/add_job_stats_generator.rb +4 -11
- data/lib/generators/pgbus/add_job_stats_latency_generator.rb +4 -11
- data/lib/generators/pgbus/add_job_stats_queue_index_generator.rb +4 -11
- data/lib/generators/pgbus/add_outbox_generator.rb +4 -11
- data/lib/generators/pgbus/add_presence_generator.rb +4 -11
- data/lib/generators/pgbus/add_queue_states_generator.rb +4 -11
- data/lib/generators/pgbus/add_recurring_generator.rb +4 -11
- data/lib/generators/pgbus/add_stream_stats_generator.rb +4 -11
- data/lib/generators/pgbus/install_generator.rb +4 -11
- data/lib/generators/pgbus/migrate_job_locks_generator.rb +4 -11
- data/lib/generators/pgbus/migration_path.rb +28 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +6 -0
- data/lib/generators/pgbus/templates/tune_autovacuum.rb.erb +38 -0
- data/lib/generators/pgbus/tune_autovacuum_generator.rb +48 -0
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +4 -11
- data/lib/pgbus/autovacuum_tuning.rb +93 -0
- data/lib/pgbus/client.rb +13 -1
- data/lib/pgbus/engine.rb +1 -0
- data/lib/pgbus/generators/migration_detector.rb +38 -3
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +133 -1
- data/lib/pgbus/web/metrics_serializer.rb +71 -2
- data/lib/tasks/pgbus_autovacuum.rake +40 -0
- metadata +7 -1
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
4
|
require "rails/generators/active_record"
|
|
5
|
+
require_relative "migration_path"
|
|
5
6
|
|
|
6
7
|
module Pgbus
|
|
7
8
|
module Generators
|
|
8
9
|
class InstallGenerator < Rails::Generators::Base
|
|
9
10
|
include ActiveRecord::Generators::Migration
|
|
11
|
+
include MigrationPath
|
|
10
12
|
|
|
11
13
|
source_root File.expand_path("templates", __dir__)
|
|
12
14
|
|
|
@@ -25,13 +27,8 @@ module Pgbus
|
|
|
25
27
|
"Migrations go to db/pgbus_migrate/ and schema to db/pgbus_schema.rb"
|
|
26
28
|
|
|
27
29
|
def create_migration_file
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"db/pgbus_migrate/create_pgbus_tables.rb"
|
|
31
|
-
else
|
|
32
|
-
migration_template "migration.rb.erb",
|
|
33
|
-
"db/migrate/create_pgbus_tables.rb"
|
|
34
|
-
end
|
|
30
|
+
migration_template "migration.rb.erb",
|
|
31
|
+
File.join(pgbus_migrate_path, "create_pgbus_tables.rb")
|
|
35
32
|
end
|
|
36
33
|
|
|
37
34
|
def create_config_file
|
|
@@ -121,10 +118,6 @@ module Pgbus
|
|
|
121
118
|
def database_name
|
|
122
119
|
options[:database]
|
|
123
120
|
end
|
|
124
|
-
|
|
125
|
-
def separate_database?
|
|
126
|
-
database_name.present?
|
|
127
|
-
end
|
|
128
121
|
end
|
|
129
122
|
end
|
|
130
123
|
end
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
4
|
require "rails/generators/active_record"
|
|
5
|
+
require_relative "migration_path"
|
|
5
6
|
|
|
6
7
|
module Pgbus
|
|
7
8
|
module Generators
|
|
8
9
|
class MigrateJobLocksGenerator < Rails::Generators::Base
|
|
9
10
|
include ActiveRecord::Generators::Migration
|
|
11
|
+
include MigrationPath
|
|
10
12
|
|
|
11
13
|
source_root File.expand_path("templates", __dir__)
|
|
12
14
|
|
|
@@ -18,13 +20,8 @@ module Pgbus
|
|
|
18
20
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
19
21
|
|
|
20
22
|
def create_migration_file
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"db/pgbus_migrate/migrate_pgbus_job_locks_to_uniqueness_keys.rb"
|
|
24
|
-
else
|
|
25
|
-
migration_template "migrate_job_locks_to_uniqueness_keys.rb.erb",
|
|
26
|
-
"db/migrate/migrate_pgbus_job_locks_to_uniqueness_keys.rb"
|
|
27
|
-
end
|
|
23
|
+
migration_template "migrate_job_locks_to_uniqueness_keys.rb.erb",
|
|
24
|
+
File.join(pgbus_migrate_path, "migrate_pgbus_job_locks_to_uniqueness_keys.rb")
|
|
28
25
|
end
|
|
29
26
|
|
|
30
27
|
def display_post_install
|
|
@@ -47,10 +44,6 @@ module Pgbus
|
|
|
47
44
|
def migration_version
|
|
48
45
|
"[#{ActiveRecord::Migration.current_version}]"
|
|
49
46
|
end
|
|
50
|
-
|
|
51
|
-
def separate_database?
|
|
52
|
-
options[:database].present?
|
|
53
|
-
end
|
|
54
47
|
end
|
|
55
48
|
end
|
|
56
49
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Generators
|
|
5
|
+
# Shared migration path logic for all pgbus generators.
|
|
6
|
+
#
|
|
7
|
+
# When --database is passed, uses Rails' built-in db_migrate_path which
|
|
8
|
+
# reads migrations_paths from database.yml. This is the standard Rails
|
|
9
|
+
# multi-database mechanism and respects custom paths like db/pgbus_migrate/.
|
|
10
|
+
#
|
|
11
|
+
# Falls back to db/migrate/ when no --database is set (single-database mode).
|
|
12
|
+
module MigrationPath
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def pgbus_migrate_path
|
|
16
|
+
if separate_database?
|
|
17
|
+
db_migrate_path
|
|
18
|
+
else
|
|
19
|
+
"db/migrate"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def separate_database?
|
|
24
|
+
options[:database].present?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -150,6 +150,12 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
150
150
|
# Create default queues via PGMQ
|
|
151
151
|
execute "SELECT pgmq.create('pgbus_default')"
|
|
152
152
|
execute "SELECT pgmq.create('pgbus_default_dlq')"
|
|
153
|
+
|
|
154
|
+
# Tune autovacuum for queue/archive tables and high-churn pgbus tables.
|
|
155
|
+
# Default settings are too conservative for the insert/delete churn of
|
|
156
|
+
# queue processing and concurrency lock management.
|
|
157
|
+
execute Pgbus::AutovacuumTuning.sql_for_all_queues
|
|
158
|
+
execute Pgbus::AutovacuumTuning.sql_for_high_churn_tables
|
|
153
159
|
end
|
|
154
160
|
|
|
155
161
|
def down
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class TunePgbusAutovacuum < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
# Apply aggressive autovacuum settings to all existing PGMQ queue and
|
|
4
|
+
# archive tables. Queue tables have very high insert/delete churn from
|
|
5
|
+
# the read→process→archive cycle; default autovacuum settings (vacuum
|
|
6
|
+
# at 20% dead tuples) allow dead tuple accumulation that bloats indexes
|
|
7
|
+
# and degrades lock acquisition times.
|
|
8
|
+
#
|
|
9
|
+
# New queues created after this migration automatically receive these
|
|
10
|
+
# settings via Pgbus::Client at queue creation time.
|
|
11
|
+
execute Pgbus::AutovacuumTuning.sql_for_all_queues
|
|
12
|
+
|
|
13
|
+
# Also tune pgbus-owned tables with high write churn:
|
|
14
|
+
# - pgbus_semaphores: rapid upsert+increment per job, periodic expiry
|
|
15
|
+
# - pgbus_uniqueness_keys: INSERT on enqueue, DELETE on completion
|
|
16
|
+
# - pgbus_processed_events: INSERT per event, bulk DELETE on TTL expiry
|
|
17
|
+
execute Pgbus::AutovacuumTuning.sql_for_high_churn_tables
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def down
|
|
21
|
+
# Reset to PostgreSQL defaults
|
|
22
|
+
execute <<~SQL
|
|
23
|
+
DO $$
|
|
24
|
+
DECLARE
|
|
25
|
+
q RECORD;
|
|
26
|
+
BEGIN
|
|
27
|
+
FOR q IN SELECT queue_name FROM pgmq.meta LOOP
|
|
28
|
+
EXECUTE format('ALTER TABLE pgmq.q_%I RESET (autovacuum_vacuum_scale_factor, autovacuum_vacuum_cost_delay, autovacuum_analyze_scale_factor)', q.queue_name);
|
|
29
|
+
EXECUTE format('ALTER TABLE pgmq.a_%I RESET (autovacuum_vacuum_scale_factor, autovacuum_vacuum_cost_delay, autovacuum_analyze_scale_factor)', q.queue_name);
|
|
30
|
+
END LOOP;
|
|
31
|
+
END $$;
|
|
32
|
+
SQL
|
|
33
|
+
|
|
34
|
+
Pgbus::AutovacuumTuning::HIGH_CHURN_TABLES.each do |table|
|
|
35
|
+
execute "ALTER TABLE #{table} RESET (autovacuum_vacuum_scale_factor, autovacuum_vacuum_cost_delay, autovacuum_analyze_scale_factor)"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
require_relative "migration_path"
|
|
6
|
+
|
|
7
|
+
module Pgbus
|
|
8
|
+
module Generators
|
|
9
|
+
class TuneAutovacuumGenerator < Rails::Generators::Base
|
|
10
|
+
include ActiveRecord::Generators::Migration
|
|
11
|
+
include MigrationPath
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
desc "Tune autovacuum settings on PGMQ queue and archive tables for optimal queue health"
|
|
16
|
+
|
|
17
|
+
class_option :database,
|
|
18
|
+
type: :string,
|
|
19
|
+
default: nil,
|
|
20
|
+
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
21
|
+
|
|
22
|
+
def create_migration_file
|
|
23
|
+
migration_template "tune_autovacuum.rb.erb",
|
|
24
|
+
File.join(pgbus_migrate_path, "tune_pgbus_autovacuum.rb")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def display_post_install
|
|
28
|
+
say ""
|
|
29
|
+
say "Pgbus autovacuum tuning migration created!", :green
|
|
30
|
+
say ""
|
|
31
|
+
say "This migration applies aggressive autovacuum settings to all existing"
|
|
32
|
+
say "PGMQ queue and archive tables. New queues created at runtime will"
|
|
33
|
+
say "automatically receive these settings."
|
|
34
|
+
say ""
|
|
35
|
+
say "Next steps:"
|
|
36
|
+
say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
|
|
37
|
+
say " 2. Restart pgbus: bin/pgbus start"
|
|
38
|
+
say ""
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def migration_version
|
|
44
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
4
|
require "rails/generators/active_record"
|
|
5
|
+
require_relative "migration_path"
|
|
5
6
|
|
|
6
7
|
module Pgbus
|
|
7
8
|
module Generators
|
|
8
9
|
class UpgradePgmqGenerator < Rails::Generators::Base
|
|
9
10
|
include ActiveRecord::Generators::Migration
|
|
11
|
+
include MigrationPath
|
|
10
12
|
|
|
11
13
|
source_root File.expand_path("templates", __dir__)
|
|
12
14
|
|
|
@@ -18,13 +20,8 @@ module Pgbus
|
|
|
18
20
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
19
21
|
|
|
20
22
|
def create_migration_file
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"db/pgbus_migrate/upgrade_pgmq_to_v#{target_version_slug}.rb"
|
|
24
|
-
else
|
|
25
|
-
migration_template "upgrade_pgmq.rb.erb",
|
|
26
|
-
"db/migrate/upgrade_pgmq_to_v#{target_version_slug}.rb"
|
|
27
|
-
end
|
|
23
|
+
migration_template "upgrade_pgmq.rb.erb",
|
|
24
|
+
File.join(pgbus_migrate_path, "upgrade_pgmq_to_v#{target_version_slug}.rb")
|
|
28
25
|
end
|
|
29
26
|
|
|
30
27
|
def display_post_upgrade
|
|
@@ -51,10 +48,6 @@ module Pgbus
|
|
|
51
48
|
def target_version_slug
|
|
52
49
|
target_version.tr(".", "_")
|
|
53
50
|
end
|
|
54
|
-
|
|
55
|
-
def separate_database?
|
|
56
|
-
options[:database].present?
|
|
57
|
-
end
|
|
58
51
|
end
|
|
59
52
|
end
|
|
60
53
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# Shared autovacuum storage parameters for tables with high write churn.
|
|
5
|
+
#
|
|
6
|
+
# Queue tables (q_*) have high insert/delete churn: every read + archive
|
|
7
|
+
# cycle deletes from q_ and inserts into a_. Default autovacuum settings
|
|
8
|
+
# (vacuum at 20% dead tuples) are far too conservative — dead tuples
|
|
9
|
+
# accumulate, bloat B-tree indexes, and eventually degrade lock acquisition
|
|
10
|
+
# times. See: https://planetscale.com/blog/keeping-a-postgres-queue-healthy
|
|
11
|
+
#
|
|
12
|
+
# Several pgbus-owned tables share similar churn patterns:
|
|
13
|
+
# - pgbus_semaphores: rapid upsert+increment per job, periodic expiry
|
|
14
|
+
# - pgbus_uniqueness_keys: INSERT on enqueue, DELETE on completion
|
|
15
|
+
# - pgbus_processed_events: INSERT per event, bulk DELETE on TTL expiry
|
|
16
|
+
#
|
|
17
|
+
# Used by:
|
|
18
|
+
# - Client#ensure_single_queue (runtime, on queue creation)
|
|
19
|
+
# - CreatePgbusTables migration (fresh install)
|
|
20
|
+
# - TunePgbusAutovacuum migration (upgrade for existing installations)
|
|
21
|
+
module AutovacuumTuning
|
|
22
|
+
# Queue tables: very aggressive — high delete rate from read+archive.
|
|
23
|
+
QUEUE_SETTINGS = {
|
|
24
|
+
"autovacuum_vacuum_scale_factor" => "0.01",
|
|
25
|
+
"autovacuum_vacuum_cost_delay" => "2",
|
|
26
|
+
"autovacuum_analyze_scale_factor" => "0.05"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Archive tables: moderately aggressive — append-heavy with periodic purge.
|
|
30
|
+
ARCHIVE_SETTINGS = {
|
|
31
|
+
"autovacuum_vacuum_scale_factor" => "0.05",
|
|
32
|
+
"autovacuum_vacuum_cost_delay" => "5",
|
|
33
|
+
"autovacuum_analyze_scale_factor" => "0.05"
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# High-churn pgbus tables: rapid INSERT/DELETE or upsert cycles.
|
|
37
|
+
# - semaphores: upsert + increment per job acquire, decrement on release, periodic expiry
|
|
38
|
+
# - uniqueness_keys: INSERT on enqueue, DELETE on job completion (fast lifecycle)
|
|
39
|
+
# - processed_events: INSERT per event handler, bulk DELETE on idempotency TTL expiry
|
|
40
|
+
HIGH_CHURN_SETTINGS = {
|
|
41
|
+
"autovacuum_vacuum_scale_factor" => "0.02",
|
|
42
|
+
"autovacuum_vacuum_cost_delay" => "2",
|
|
43
|
+
"autovacuum_analyze_scale_factor" => "0.05"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
HIGH_CHURN_TABLES = %w[
|
|
47
|
+
pgbus_semaphores
|
|
48
|
+
pgbus_uniqueness_keys
|
|
49
|
+
pgbus_processed_events
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
# Generate ALTER TABLE SQL for a single queue's tables.
|
|
54
|
+
def sql_for_queue(queue_name)
|
|
55
|
+
[
|
|
56
|
+
alter_table_sql("pgmq.q_#{queue_name}", QUEUE_SETTINGS),
|
|
57
|
+
alter_table_sql("pgmq.a_#{queue_name}", ARCHIVE_SETTINGS)
|
|
58
|
+
].join("\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generate ALTER TABLE SQL for all queues discovered via pgmq.meta.
|
|
62
|
+
def sql_for_all_queues
|
|
63
|
+
<<~SQL
|
|
64
|
+
DO $$
|
|
65
|
+
DECLARE
|
|
66
|
+
q RECORD;
|
|
67
|
+
BEGIN
|
|
68
|
+
FOR q IN SELECT queue_name FROM pgmq.meta LOOP
|
|
69
|
+
EXECUTE format('ALTER TABLE pgmq.q_%I SET (#{settings_clause(QUEUE_SETTINGS)})', q.queue_name);
|
|
70
|
+
EXECUTE format('ALTER TABLE pgmq.a_%I SET (#{settings_clause(ARCHIVE_SETTINGS)})', q.queue_name);
|
|
71
|
+
END LOOP;
|
|
72
|
+
END $$;
|
|
73
|
+
SQL
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Generate ALTER TABLE SQL for pgbus-owned high-churn tables.
|
|
77
|
+
def sql_for_high_churn_tables
|
|
78
|
+
HIGH_CHURN_TABLES.map { |table| alter_table_sql(table, HIGH_CHURN_SETTINGS, if_exists: true) }.join("\n")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def alter_table_sql(table, settings, if_exists: false)
|
|
84
|
+
prefix = if_exists ? "ALTER TABLE IF EXISTS" : "ALTER TABLE"
|
|
85
|
+
"#{prefix} #{table} SET (#{settings_clause(settings)});"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def settings_clause(settings)
|
|
89
|
+
settings.map { |k, v| "#{k} = #{v}" }.join(", ")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/pgbus/client.rb
CHANGED
|
@@ -75,7 +75,10 @@ module Pgbus
|
|
|
75
75
|
return if @queues_created[dlq_name]
|
|
76
76
|
|
|
77
77
|
@queues_created.compute_if_absent(dlq_name) do
|
|
78
|
-
synchronized
|
|
78
|
+
synchronized do
|
|
79
|
+
@pgmq.create(dlq_name)
|
|
80
|
+
tune_autovacuum(dlq_name)
|
|
81
|
+
end
|
|
79
82
|
true
|
|
80
83
|
end
|
|
81
84
|
end
|
|
@@ -446,12 +449,21 @@ module Pgbus
|
|
|
446
449
|
@queues_created.compute_if_absent(full_name) do
|
|
447
450
|
synchronized do
|
|
448
451
|
@pgmq.create(full_name)
|
|
452
|
+
tune_autovacuum(full_name)
|
|
449
453
|
@pgmq.enable_notify_insert(full_name, throttle_interval_ms: NOTIFY_THROTTLE_MS) if config.listen_notify
|
|
450
454
|
end
|
|
451
455
|
true
|
|
452
456
|
end
|
|
453
457
|
end
|
|
454
458
|
|
|
459
|
+
def tune_autovacuum(queue_name)
|
|
460
|
+
with_raw_connection do |conn|
|
|
461
|
+
conn.exec(AutovacuumTuning.sql_for_queue(queue_name))
|
|
462
|
+
end
|
|
463
|
+
rescue StandardError => e
|
|
464
|
+
Pgbus.logger.debug { "[Pgbus::Client] Autovacuum tuning failed for #{queue_name}: #{e.message}" }
|
|
465
|
+
end
|
|
466
|
+
|
|
455
467
|
# Serialize PGMQ operations through a mutex when sharing a connection
|
|
456
468
|
# with ActiveRecord (Proc path). When pgmq-ruby owns its own connections
|
|
457
469
|
# (String/Hash path), the internal connection_pool handles concurrency.
|
data/lib/pgbus/engine.rb
CHANGED
|
@@ -47,6 +47,7 @@ module Pgbus
|
|
|
47
47
|
rake_tasks do
|
|
48
48
|
load File.expand_path("../tasks/pgbus_pgmq.rake", __dir__)
|
|
49
49
|
load File.expand_path("../tasks/pgbus_streams.rake", __dir__)
|
|
50
|
+
load File.expand_path("../tasks/pgbus_autovacuum.rake", __dir__)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
initializer "pgbus.i18n" do
|
|
@@ -73,7 +73,8 @@ module Pgbus
|
|
|
73
73
|
add_queue_states: "pgbus:add_queue_states",
|
|
74
74
|
add_outbox: "pgbus:add_outbox",
|
|
75
75
|
add_recurring: "pgbus:add_recurring",
|
|
76
|
-
add_failed_events_index: "pgbus:add_failed_events_index"
|
|
76
|
+
add_failed_events_index: "pgbus:add_failed_events_index",
|
|
77
|
+
tune_autovacuum: "pgbus:tune_autovacuum"
|
|
77
78
|
}.freeze
|
|
78
79
|
|
|
79
80
|
# Human-friendly description of each migration for the generator
|
|
@@ -88,7 +89,8 @@ module Pgbus
|
|
|
88
89
|
add_queue_states: "queue states table (pause/resume)",
|
|
89
90
|
add_outbox: "outbox entries table (transactional outbox)",
|
|
90
91
|
add_recurring: "recurring tasks + executions tables",
|
|
91
|
-
add_failed_events_index: "unique index on pgbus_failed_events (queue_name, msg_id)"
|
|
92
|
+
add_failed_events_index: "unique index on pgbus_failed_events (queue_name, msg_id)",
|
|
93
|
+
tune_autovacuum: "autovacuum tuning for PGMQ queue and archive tables"
|
|
92
94
|
}.freeze
|
|
93
95
|
|
|
94
96
|
def initialize(connection)
|
|
@@ -110,7 +112,8 @@ module Pgbus
|
|
|
110
112
|
*queue_states_migrations,
|
|
111
113
|
*outbox_migrations,
|
|
112
114
|
*recurring_migrations,
|
|
113
|
-
*failed_events_index_migrations
|
|
115
|
+
*failed_events_index_migrations,
|
|
116
|
+
*autovacuum_migrations
|
|
114
117
|
]
|
|
115
118
|
end
|
|
116
119
|
|
|
@@ -193,6 +196,15 @@ module Pgbus
|
|
|
193
196
|
[:add_failed_events_index]
|
|
194
197
|
end
|
|
195
198
|
|
|
199
|
+
# Autovacuum tuning: check if any PGMQ queue table already has
|
|
200
|
+
# custom autovacuum settings applied. If not, queue the migration.
|
|
201
|
+
def autovacuum_migrations
|
|
202
|
+
return [] unless pgmq_schema_exists?
|
|
203
|
+
return [] if autovacuum_already_tuned?
|
|
204
|
+
|
|
205
|
+
[:tune_autovacuum]
|
|
206
|
+
end
|
|
207
|
+
|
|
196
208
|
# --- schema probes -------------------------------------------------
|
|
197
209
|
|
|
198
210
|
def table_exists?(name)
|
|
@@ -212,6 +224,29 @@ module Pgbus
|
|
|
212
224
|
rescue StandardError
|
|
213
225
|
false
|
|
214
226
|
end
|
|
227
|
+
|
|
228
|
+
def pgmq_schema_exists?
|
|
229
|
+
result = connection.select_value("SELECT 1 FROM information_schema.schemata WHERE schema_name = 'pgmq'")
|
|
230
|
+
result.present?
|
|
231
|
+
rescue StandardError
|
|
232
|
+
false
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def autovacuum_already_tuned?
|
|
236
|
+
queue_name = connection.select_value("SELECT queue_name FROM pgmq.meta ORDER BY queue_name LIMIT 1")
|
|
237
|
+
return true unless queue_name # no queues = nothing to tune, skip
|
|
238
|
+
|
|
239
|
+
result = connection.select_value(<<~SQL)
|
|
240
|
+
SELECT reloptions::text LIKE '%autovacuum_vacuum_scale_factor%'
|
|
241
|
+
FROM pg_class
|
|
242
|
+
WHERE relname = 'q_#{queue_name}'
|
|
243
|
+
AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'pgmq')
|
|
244
|
+
SQL
|
|
245
|
+
|
|
246
|
+
[true, "t"].include?(result)
|
|
247
|
+
rescue StandardError
|
|
248
|
+
true # if we can't tell, assume already tuned (safe default)
|
|
249
|
+
end
|
|
215
250
|
end
|
|
216
251
|
end
|
|
217
252
|
end
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Pgbus
|
|
|
21
21
|
|
|
22
22
|
throughput = compute_throughput(queues)
|
|
23
23
|
|
|
24
|
+
health = queue_health_stats
|
|
25
|
+
|
|
24
26
|
{
|
|
25
27
|
total_queues: queues.size,
|
|
26
28
|
total_depth: total_depth,
|
|
@@ -29,7 +31,10 @@ module Pgbus
|
|
|
29
31
|
failed_count: failed_events_count,
|
|
30
32
|
dlq_depth: dlq_depth,
|
|
31
33
|
recurring_count: recurring_tasks_count,
|
|
32
|
-
throughput_rate: throughput
|
|
34
|
+
throughput_rate: throughput,
|
|
35
|
+
total_dead_tuples: health[:total_dead_tuples],
|
|
36
|
+
tables_needing_vacuum: health[:tables_needing_vacuum],
|
|
37
|
+
oldest_transaction_age_sec: health[:oldest_transaction_age_sec]
|
|
33
38
|
}
|
|
34
39
|
end
|
|
35
40
|
|
|
@@ -629,6 +634,50 @@ module Pgbus
|
|
|
629
634
|
[]
|
|
630
635
|
end
|
|
631
636
|
|
|
637
|
+
# Queue health — vacuum stats, dead tuples, bloat, MVCC horizon.
|
|
638
|
+
# Returns aggregate health across all queue and archive tables, plus
|
|
639
|
+
# the oldest open transaction age (MVCC horizon pinning risk).
|
|
640
|
+
def queue_health_stats
|
|
641
|
+
tables = fetch_all_table_stats
|
|
642
|
+
|
|
643
|
+
total_dead = tables.sum { |t| t[:dead_tuples] }
|
|
644
|
+
total_live = tables.sum { |t| t[:live_tuples] }
|
|
645
|
+
worst_bloat = tables.map { |t| t[:bloat_ratio] }.max || 0.0
|
|
646
|
+
needs_vacuum = tables.count { |t| t[:bloat_ratio] > 0.1 }
|
|
647
|
+
oldest_vacuum = tables.filter_map { |t| t[:last_vacuum_ago_sec] }.max
|
|
648
|
+
|
|
649
|
+
{
|
|
650
|
+
total_dead_tuples: total_dead,
|
|
651
|
+
total_live_tuples: total_live,
|
|
652
|
+
worst_bloat_ratio: worst_bloat.round(4),
|
|
653
|
+
tables_needing_vacuum: needs_vacuum,
|
|
654
|
+
oldest_vacuum_ago_sec: oldest_vacuum,
|
|
655
|
+
oldest_transaction_age_sec: oldest_transaction_age,
|
|
656
|
+
tables: tables
|
|
657
|
+
}
|
|
658
|
+
rescue StandardError => e
|
|
659
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching queue health stats: #{e.class}: #{e.message}" }
|
|
660
|
+
{
|
|
661
|
+
total_dead_tuples: 0, total_live_tuples: 0, worst_bloat_ratio: 0.0,
|
|
662
|
+
tables_needing_vacuum: 0, oldest_vacuum_ago_sec: nil,
|
|
663
|
+
oldest_transaction_age_sec: nil, tables: []
|
|
664
|
+
}
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Per-queue health stats for the queue detail view.
|
|
668
|
+
def queue_health_detail(queue_name)
|
|
669
|
+
sanitized = sanitize_name(queue_name)
|
|
670
|
+
tables = [
|
|
671
|
+
fetch_table_stats("pgmq", "q_#{sanitized}", "queue"),
|
|
672
|
+
fetch_table_stats("pgmq", "a_#{sanitized}", "archive")
|
|
673
|
+
].compact
|
|
674
|
+
|
|
675
|
+
{ tables: tables, oldest_transaction_age_sec: oldest_transaction_age }
|
|
676
|
+
rescue StandardError => e
|
|
677
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching health detail for #{queue_name}: #{e.message}" }
|
|
678
|
+
{ tables: [], oldest_transaction_age_sec: nil }
|
|
679
|
+
end
|
|
680
|
+
|
|
632
681
|
# Stream stats — only populated when streams_stats_enabled is
|
|
633
682
|
# true AND the migration has been run. Controllers should gate
|
|
634
683
|
# rendering on `stream_stats_available?` to avoid showing empty
|
|
@@ -674,6 +723,89 @@ module Pgbus
|
|
|
674
723
|
Pgbus::BusRecord.connection
|
|
675
724
|
end
|
|
676
725
|
|
|
726
|
+
# Single query to fetch pg_stat_user_tables stats for all queue and
|
|
727
|
+
# archive tables. Avoids 2*N catalog queries on the dashboard.
|
|
728
|
+
def fetch_all_table_stats
|
|
729
|
+
rows = connection.select_all(<<~SQL, "Pgbus All Table Health")
|
|
730
|
+
WITH rels AS (
|
|
731
|
+
SELECT queue_name, 'q_' || queue_name AS relname, 'queue' AS kind FROM pgmq.meta
|
|
732
|
+
UNION ALL
|
|
733
|
+
SELECT queue_name, 'a_' || queue_name AS relname, 'archive' AS kind FROM pgmq.meta
|
|
734
|
+
)
|
|
735
|
+
SELECT
|
|
736
|
+
'pgmq.' || r.relname AS table_name,
|
|
737
|
+
r.kind,
|
|
738
|
+
s.n_live_tup,
|
|
739
|
+
s.n_dead_tup,
|
|
740
|
+
EXTRACT(epoch FROM (NOW() - COALESCE(s.last_vacuum, s.last_autovacuum)))::int AS last_vacuum_ago_sec,
|
|
741
|
+
s.last_vacuum,
|
|
742
|
+
s.last_autovacuum
|
|
743
|
+
FROM rels r
|
|
744
|
+
LEFT JOIN pg_stat_user_tables s
|
|
745
|
+
ON s.schemaname = 'pgmq' AND s.relname = r.relname
|
|
746
|
+
ORDER BY r.queue_name, r.kind
|
|
747
|
+
SQL
|
|
748
|
+
|
|
749
|
+
rows.to_a.filter_map { |row| build_table_health_row(row) }
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# Fetch pg_stat_user_tables stats for a single table (used by queue_health_detail).
|
|
753
|
+
def fetch_table_stats(schema, table_name, kind)
|
|
754
|
+
row = connection.select_one(<<~SQL, "Pgbus Table Health", [schema, table_name])
|
|
755
|
+
SELECT
|
|
756
|
+
n_live_tup,
|
|
757
|
+
n_dead_tup,
|
|
758
|
+
EXTRACT(epoch FROM (NOW() - COALESCE(last_vacuum, last_autovacuum)))::int AS last_vacuum_ago_sec,
|
|
759
|
+
last_vacuum,
|
|
760
|
+
last_autovacuum
|
|
761
|
+
FROM pg_stat_user_tables
|
|
762
|
+
WHERE schemaname = $1 AND relname = $2
|
|
763
|
+
SQL
|
|
764
|
+
|
|
765
|
+
return nil unless row
|
|
766
|
+
|
|
767
|
+
build_table_health_row(row.merge("table_name" => "#{schema}.#{table_name}", "kind" => kind))
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def build_table_health_row(row)
|
|
771
|
+
return nil unless row["n_live_tup"] || row["n_dead_tup"]
|
|
772
|
+
|
|
773
|
+
live = row["n_live_tup"].to_i
|
|
774
|
+
dead = row["n_dead_tup"].to_i
|
|
775
|
+
total = live + dead
|
|
776
|
+
bloat = total.positive? ? (dead.to_f / total) : 0.0
|
|
777
|
+
|
|
778
|
+
{
|
|
779
|
+
table: row["table_name"],
|
|
780
|
+
kind: row["kind"],
|
|
781
|
+
live_tuples: live,
|
|
782
|
+
dead_tuples: dead,
|
|
783
|
+
bloat_ratio: bloat.round(4),
|
|
784
|
+
last_vacuum_ago_sec: row["last_vacuum_ago_sec"]&.to_i,
|
|
785
|
+
last_vacuum: row["last_vacuum"],
|
|
786
|
+
last_autovacuum: row["last_autovacuum"]
|
|
787
|
+
}
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Age of the oldest open transaction in seconds — indicates MVCC
|
|
791
|
+
# horizon pinning risk. Returns nil if no active transactions.
|
|
792
|
+
def oldest_transaction_age
|
|
793
|
+
row = connection.select_one(<<~SQL, "Pgbus Oldest Transaction")
|
|
794
|
+
SELECT EXTRACT(epoch FROM (NOW() - xact_start))::int AS age_sec
|
|
795
|
+
FROM pg_stat_activity
|
|
796
|
+
WHERE state != 'idle'
|
|
797
|
+
AND xact_start IS NOT NULL
|
|
798
|
+
AND pid != pg_backend_pid()
|
|
799
|
+
ORDER BY xact_start ASC
|
|
800
|
+
LIMIT 1
|
|
801
|
+
SQL
|
|
802
|
+
|
|
803
|
+
row&.dig("age_sec")&.to_i
|
|
804
|
+
rescue StandardError => e
|
|
805
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching oldest transaction age: #{e.class}: #{e.message}" }
|
|
806
|
+
nil
|
|
807
|
+
end
|
|
808
|
+
|
|
677
809
|
# name is the full PGMQ queue name (already prefixed)
|
|
678
810
|
def query_queue_messages(name, limit, offset)
|
|
679
811
|
query_queue_messages_raw(name, limit, offset).map { |m| m.merge(queue: name) }
|