pgbus 0.6.7 → 0.6.8

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.
@@ -10,6 +10,28 @@ nb:
10
10
  pid: PID
11
11
  status: Status
12
12
  title: Aktive prosesser
13
+ queue_health:
14
+ dead_tuples: Døde tupler
15
+ headers:
16
+ bloat: Bloat
17
+ dead: Døde
18
+ kind: Type
19
+ last_vacuum: Siste Vacuum
20
+ live: Levende
21
+ table: Tabell
22
+ kinds:
23
+ archive: Arkiv
24
+ queue: Kø
25
+ last_vacuum: Siste Vacuum
26
+ live: Levende
27
+ mvcc_horizon: MVCC-horisont
28
+ oldest_open_txn: Eldste åpne transaksjon
29
+ oldest_vacuum_ago: Eldste tabell-vacuum
30
+ tables_need_vacuum:
31
+ one: "%{count} tabell trenger vacuum"
32
+ other: "%{count} tabeller trenger vacuum"
33
+ title: Køhelse
34
+ worst_bloat: Verste Bloat
13
35
  queues_table:
14
36
  empty: Ingen køer funnet
15
37
  headers:
@@ -337,6 +359,15 @@ nb:
337
359
  resume: Gjenoppta
338
360
  retry: Prøv igjen
339
361
  retry_confirm: Tilbakestill synlighetstidsavbrudd og prøv igjen?
362
+ table_health:
363
+ headers:
364
+ bloat: Bloat
365
+ dead: Døde
366
+ last_vacuum: Siste Vacuum
367
+ live: Levende
368
+ table: Tabell
369
+ oldest_txn: 'Eldste åpne transaksjon:'
370
+ title: Tabellhelse
340
371
  total: 'Totalt:'
341
372
  visible: 'Synlig:'
342
373
  recurring_tasks:
@@ -345,10 +376,6 @@ nb:
345
376
  one: "%{count} oppgave konfigurert"
346
377
  other: "%{count} oppgaver konfigurert"
347
378
  title: Gjentakende oppgaver
348
- toggle:
349
- disabled: Oppgave deaktivert
350
- enabled: Oppgave aktivert
351
- failed: Kunne ikke endre oppgave
352
379
  show:
353
380
  back: Tilbake
354
381
  configuration: Konfigurasjon
@@ -392,3 +419,7 @@ nb:
392
419
  task: Oppgave
393
420
  never: Aldri
394
421
  run_now: Kjør nå
422
+ toggle:
423
+ disabled: Oppgave deaktivert
424
+ enabled: Oppgave aktivert
425
+ failed: Kunne ikke endre oppgave
@@ -10,6 +10,28 @@ nl:
10
10
  pid: PID
11
11
  status: Status
12
12
  title: Actieve processen
13
+ queue_health:
14
+ dead_tuples: Dode tupels
15
+ headers:
16
+ bloat: Bloat
17
+ dead: Dood
18
+ kind: Type
19
+ last_vacuum: Laatste Vacuum
20
+ live: Actief
21
+ table: Tabel
22
+ kinds:
23
+ archive: Archief
24
+ queue: Wachtrij
25
+ last_vacuum: Laatste Vacuum
26
+ live: Actief
27
+ mvcc_horizon: MVCC-horizon
28
+ oldest_open_txn: Oudste open transactie
29
+ oldest_vacuum_ago: Oudste tabel-vacuum
30
+ tables_need_vacuum:
31
+ one: "%{count} tabel heeft vacuum nodig"
32
+ other: "%{count} tabellen hebben vacuum nodig"
33
+ title: Wachtrijgezondheid
34
+ worst_bloat: Ergste Bloat
13
35
  queues_table:
14
36
  empty: Geen wachtrijen gevonden
15
37
  headers:
@@ -337,6 +359,15 @@ nl:
337
359
  resume: Hervatten
338
360
  retry: Opnieuw proberen
339
361
  retry_confirm: Zichtbaarheidstimeout resetten en opnieuw proberen?
362
+ table_health:
363
+ headers:
364
+ bloat: Bloat
365
+ dead: Dood
366
+ last_vacuum: Laatste Vacuum
367
+ live: Actief
368
+ table: Tabel
369
+ oldest_txn: 'Oudste open transactie:'
370
+ title: Tabelgezondheid
340
371
  total: 'Totaal:'
341
372
  visible: 'Zichtbaar:'
342
373
  recurring_tasks:
@@ -345,10 +376,6 @@ nl:
345
376
  one: "%{count} taak geconfigureerd"
346
377
  other: "%{count} taken geconfigureerd"
347
378
  title: Terugkerende taken
348
- toggle:
349
- disabled: Taak uitgeschakeld
350
- enabled: Taak ingeschakeld
351
- failed: Kon taak niet omschakelen
352
379
  show:
353
380
  back: Terug
354
381
  configuration: Configuratie
@@ -392,3 +419,7 @@ nl:
392
419
  task: Taak
393
420
  never: Nooit
394
421
  run_now: Nu uitvoeren
422
+ toggle:
423
+ disabled: Taak uitgeschakeld
424
+ enabled: Taak ingeschakeld
425
+ failed: Kon taak niet omschakelen
@@ -10,6 +10,28 @@ pt:
10
10
  pid: PID
11
11
  status: Status
12
12
  title: Processos Ativos
13
+ queue_health:
14
+ dead_tuples: Tuplas mortas
15
+ headers:
16
+ bloat: Bloat
17
+ dead: Mortas
18
+ kind: Tipo
19
+ last_vacuum: Último Vacuum
20
+ live: Ativas
21
+ table: Tabela
22
+ kinds:
23
+ archive: Arquivo
24
+ queue: Fila
25
+ last_vacuum: Último Vacuum
26
+ live: Ativas
27
+ mvcc_horizon: Horizonte MVCC
28
+ oldest_open_txn: Transação aberta mais antiga
29
+ oldest_vacuum_ago: Vacuum de tabela mais antigo
30
+ tables_need_vacuum:
31
+ one: "%{count} tabela precisa de vacuum"
32
+ other: "%{count} tabelas precisam de vacuum"
33
+ title: Saúde das Filas
34
+ worst_bloat: Pior Bloat
13
35
  queues_table:
14
36
  empty: Nenhuma fila encontrada
15
37
  headers:
@@ -337,6 +359,15 @@ pt:
337
359
  resume: Retomar
338
360
  retry: Tentar novamente
339
361
  retry_confirm: Redefinir tempo de visibilidade e tentar novamente?
362
+ table_health:
363
+ headers:
364
+ bloat: Bloat
365
+ dead: Mortas
366
+ last_vacuum: Último Vacuum
367
+ live: Ativas
368
+ table: Tabela
369
+ oldest_txn: 'Transação aberta mais antiga:'
370
+ title: Saúde das Tabelas
340
371
  total: 'Total:'
341
372
  visible: 'Visível:'
342
373
  recurring_tasks:
@@ -345,10 +376,6 @@ pt:
345
376
  one: "%{count} tarefa configurada"
346
377
  other: "%{count} tarefas configuradas"
347
378
  title: Tarefas Recorrentes
348
- toggle:
349
- disabled: Tarefa desativada
350
- enabled: Tarefa ativada
351
- failed: Falha ao alternar tarefa
352
379
  show:
353
380
  back: Voltar
354
381
  configuration: Configuração
@@ -392,3 +419,7 @@ pt:
392
419
  task: Tarefa
393
420
  never: Nunca
394
421
  run_now: Executar Agora
422
+ toggle:
423
+ disabled: Tarefa desativada
424
+ enabled: Tarefa ativada
425
+ failed: Falha ao alternar tarefa
@@ -10,6 +10,28 @@ sv:
10
10
  pid: PID
11
11
  status: Status
12
12
  title: Aktiva processer
13
+ queue_health:
14
+ dead_tuples: Döda tuplar
15
+ headers:
16
+ bloat: Bloat
17
+ dead: Döda
18
+ kind: Typ
19
+ last_vacuum: Senaste Vacuum
20
+ live: Levande
21
+ table: Tabell
22
+ kinds:
23
+ archive: Arkiv
24
+ queue: Kö
25
+ last_vacuum: Senaste Vacuum
26
+ live: Levande
27
+ mvcc_horizon: MVCC-horisont
28
+ oldest_open_txn: Äldsta öppna transaktion
29
+ oldest_vacuum_ago: Äldsta tabell-vacuum
30
+ tables_need_vacuum:
31
+ one: "%{count} tabell behöver vacuum"
32
+ other: "%{count} tabeller behöver vacuum"
33
+ title: Köhälsa
34
+ worst_bloat: Värsta Bloat
13
35
  queues_table:
14
36
  empty: Inga köer hittades
15
37
  headers:
@@ -337,6 +359,15 @@ sv:
337
359
  resume: Återuppta
338
360
  retry: Försök igen
339
361
  retry_confirm: Återställ synlighetstimeout och försök igen?
362
+ table_health:
363
+ headers:
364
+ bloat: Bloat
365
+ dead: Döda
366
+ last_vacuum: Senaste Vacuum
367
+ live: Levande
368
+ table: Tabell
369
+ oldest_txn: 'Äldsta öppna transaktion:'
370
+ title: Tabellhälsa
340
371
  total: 'Totalt:'
341
372
  visible: 'Synliga:'
342
373
  recurring_tasks:
@@ -345,10 +376,6 @@ sv:
345
376
  one: "%{count} uppgift konfigurerad"
346
377
  other: "%{count} uppgifter konfigurerade"
347
378
  title: Återkommande uppgifter
348
- toggle:
349
- disabled: Uppgift inaktiverad
350
- enabled: Uppgift aktiverad
351
- failed: Kunde inte ändra uppgift
352
379
  show:
353
380
  back: Tillbaka
354
381
  configuration: Konfiguration
@@ -392,3 +419,7 @@ sv:
392
419
  task: Uppgift
393
420
  never: Aldrig
394
421
  run_now: Kör nu
422
+ toggle:
423
+ disabled: Uppgift inaktiverad
424
+ enabled: Uppgift aktiverad
425
+ failed: Kunde inte ändra uppgift
@@ -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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Pgbus
7
+ module Generators
8
+ class TuneAutovacuumGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Tune autovacuum settings on PGMQ queue and archive tables for optimal queue health"
14
+
15
+ class_option :database,
16
+ type: :string,
17
+ default: nil,
18
+ desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
19
+
20
+ def create_migration_file
21
+ if separate_database?
22
+ migration_template "tune_autovacuum.rb.erb",
23
+ "db/pgbus_migrate/tune_pgbus_autovacuum.rb"
24
+ else
25
+ migration_template "tune_autovacuum.rb.erb",
26
+ "db/migrate/tune_pgbus_autovacuum.rb"
27
+ end
28
+ end
29
+
30
+ def display_post_install
31
+ say ""
32
+ say "Pgbus autovacuum tuning migration created!", :green
33
+ say ""
34
+ say "This migration applies aggressive autovacuum settings to all existing"
35
+ say "PGMQ queue and archive tables. New queues created at runtime will"
36
+ say "automatically receive these settings."
37
+ say ""
38
+ say "Next steps:"
39
+ say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
40
+ say " 2. Restart pgbus: bin/pgbus start"
41
+ say ""
42
+ end
43
+
44
+ private
45
+
46
+ def migration_version
47
+ "[#{ActiveRecord::Migration.current_version}]"
48
+ end
49
+
50
+ def separate_database?
51
+ options[:database].present?
52
+ end
53
+ end
54
+ end
55
+ 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 { @pgmq.create(dlq_name) }
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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.6.7"
4
+ VERSION = "0.6.8"
5
5
  end