pgbus 0.8.4 → 0.9.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/app/controllers/pgbus/batches_controller.rb +15 -0
- data/app/helpers/pgbus/application_helper.rb +12 -0
- data/app/views/layouts/pgbus/application.html.erb +2 -0
- data/app/views/pgbus/batches/_batches_table.html.erb +53 -0
- data/app/views/pgbus/batches/index.html.erb +8 -0
- data/app/views/pgbus/batches/show.html.erb +90 -0
- data/config/locales/da.yml +34 -0
- data/config/locales/de.yml +34 -0
- data/config/locales/en.yml +34 -0
- data/config/locales/es.yml +34 -0
- data/config/locales/fi.yml +34 -0
- data/config/locales/fr.yml +34 -0
- data/config/locales/it.yml +34 -0
- data/config/locales/ja.yml +34 -0
- data/config/locales/nb.yml +34 -0
- data/config/locales/nl.yml +34 -0
- data/config/locales/pt.yml +34 -0
- data/config/locales/sv.yml +34 -0
- data/config/routes.rb +1 -0
- data/lib/pgbus/client.rb +102 -4
- data/lib/pgbus/configuration.rb +25 -0
- data/lib/pgbus/process/supervisor.rb +2 -1
- data/lib/pgbus/process/worker.rb +38 -1
- data/lib/pgbus/serializer.rb +1 -1
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +96 -0
- data/lib/tasks/pgbus_queues.rake +54 -0
- metadata +8 -3
data/config/locales/nb.yml
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
nb:
|
|
3
3
|
pgbus:
|
|
4
|
+
batches:
|
|
5
|
+
index:
|
|
6
|
+
description: Jobbgrupper med fremdriftssporing og tilbakekall
|
|
7
|
+
empty: Ingen grupper funnet
|
|
8
|
+
headers:
|
|
9
|
+
batch_id: Gruppe-ID
|
|
10
|
+
created: Opprettet
|
|
11
|
+
description: Beskrivelse
|
|
12
|
+
jobs: Jobber
|
|
13
|
+
progress: Fremdrift
|
|
14
|
+
status: Status
|
|
15
|
+
title: Grupper
|
|
16
|
+
show:
|
|
17
|
+
back: Tilbake til grupper
|
|
18
|
+
batch_id: Gruppe-ID
|
|
19
|
+
completed: Fullført
|
|
20
|
+
created_at: Opprettet den
|
|
21
|
+
details: Detaljer
|
|
22
|
+
discarded: Forkastet
|
|
23
|
+
finished_at: Avsluttet den
|
|
24
|
+
not_found: Gruppe ikke funnet
|
|
25
|
+
on_discard: Ved forkastning
|
|
26
|
+
on_finish: Ved avslutning
|
|
27
|
+
on_success: Ved suksess
|
|
28
|
+
progress: Fremdrift
|
|
29
|
+
properties: Egenskaper
|
|
30
|
+
remaining: Gjenstående
|
|
31
|
+
title: Gruppe
|
|
32
|
+
total_jobs: Totalt antall jobber
|
|
4
33
|
dashboard:
|
|
5
34
|
processes_table:
|
|
6
35
|
empty: Ingen prosesser kjører
|
|
@@ -189,6 +218,10 @@ nb:
|
|
|
189
218
|
not_found: Hendelse ikke funnet
|
|
190
219
|
title: Hendelse %{event_id}
|
|
191
220
|
helpers:
|
|
221
|
+
batch_status:
|
|
222
|
+
finished: Fullført
|
|
223
|
+
pending: Avventer
|
|
224
|
+
processing: Behandler
|
|
192
225
|
bulk_select_all: Velg alle
|
|
193
226
|
bulk_select_row: Velg %{id}
|
|
194
227
|
bulk_selected: valgt
|
|
@@ -346,6 +379,7 @@ nb:
|
|
|
346
379
|
layout:
|
|
347
380
|
brand: Pgbus
|
|
348
381
|
nav:
|
|
382
|
+
batches: Grupper
|
|
349
383
|
dashboard: Dashbord
|
|
350
384
|
dlq: DLQ
|
|
351
385
|
events: Hendelser
|
data/config/locales/nl.yml
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
nl:
|
|
3
3
|
pgbus:
|
|
4
|
+
batches:
|
|
5
|
+
index:
|
|
6
|
+
description: Taakgroepen met voortgangsregistratie en callbacks
|
|
7
|
+
empty: Geen groepen gevonden
|
|
8
|
+
headers:
|
|
9
|
+
batch_id: Groep-ID
|
|
10
|
+
created: Aangemaakt
|
|
11
|
+
description: Beschrijving
|
|
12
|
+
jobs: Taken
|
|
13
|
+
progress: Voortgang
|
|
14
|
+
status: Status
|
|
15
|
+
title: Groepen
|
|
16
|
+
show:
|
|
17
|
+
back: Terug naar groepen
|
|
18
|
+
batch_id: Groep-ID
|
|
19
|
+
completed: Voltooid
|
|
20
|
+
created_at: Aangemaakt op
|
|
21
|
+
details: Details
|
|
22
|
+
discarded: Verworpen
|
|
23
|
+
finished_at: Voltooid op
|
|
24
|
+
not_found: Groep niet gevonden
|
|
25
|
+
on_discard: Bij verwerping
|
|
26
|
+
on_finish: Bij voltooiing
|
|
27
|
+
on_success: Bij succes
|
|
28
|
+
progress: Voortgang
|
|
29
|
+
properties: Eigenschappen
|
|
30
|
+
remaining: Resterend
|
|
31
|
+
title: Groep
|
|
32
|
+
total_jobs: Totaal taken
|
|
4
33
|
dashboard:
|
|
5
34
|
processes_table:
|
|
6
35
|
empty: Geen processen actief
|
|
@@ -189,6 +218,10 @@ nl:
|
|
|
189
218
|
not_found: Gebeurtenis niet gevonden
|
|
190
219
|
title: Gebeurtenis %{event_id}
|
|
191
220
|
helpers:
|
|
221
|
+
batch_status:
|
|
222
|
+
finished: Voltooid
|
|
223
|
+
pending: In afwachting
|
|
224
|
+
processing: Bezig
|
|
192
225
|
bulk_select_all: Alles selecteren
|
|
193
226
|
bulk_select_row: Selecteer %{id}
|
|
194
227
|
bulk_selected: geselecteerd
|
|
@@ -346,6 +379,7 @@ nl:
|
|
|
346
379
|
layout:
|
|
347
380
|
brand: Pgbus
|
|
348
381
|
nav:
|
|
382
|
+
batches: Groepen
|
|
349
383
|
dashboard: Dashboard
|
|
350
384
|
dlq: DLQ
|
|
351
385
|
events: Evenementen
|
data/config/locales/pt.yml
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
pt:
|
|
3
3
|
pgbus:
|
|
4
|
+
batches:
|
|
5
|
+
index:
|
|
6
|
+
description: Lotes de trabalhos com acompanhamento de progresso e callbacks
|
|
7
|
+
empty: Nenhum lote encontrado
|
|
8
|
+
headers:
|
|
9
|
+
batch_id: ID do lote
|
|
10
|
+
created: Criado
|
|
11
|
+
description: Descrição
|
|
12
|
+
jobs: Trabalhos
|
|
13
|
+
progress: Progresso
|
|
14
|
+
status: Status
|
|
15
|
+
title: Lotes
|
|
16
|
+
show:
|
|
17
|
+
back: Voltar para lotes
|
|
18
|
+
batch_id: ID do lote
|
|
19
|
+
completed: Concluídos
|
|
20
|
+
created_at: Criado em
|
|
21
|
+
details: Detalhes
|
|
22
|
+
discarded: Descartados
|
|
23
|
+
finished_at: Finalizado em
|
|
24
|
+
not_found: Lote não encontrado
|
|
25
|
+
on_discard: Ao descartar
|
|
26
|
+
on_finish: Ao finalizar
|
|
27
|
+
on_success: Ao ter sucesso
|
|
28
|
+
progress: Progresso
|
|
29
|
+
properties: Propriedades
|
|
30
|
+
remaining: Restantes
|
|
31
|
+
title: Lote
|
|
32
|
+
total_jobs: Total de trabalhos
|
|
4
33
|
dashboard:
|
|
5
34
|
processes_table:
|
|
6
35
|
empty: Nenhum processo em execução
|
|
@@ -189,6 +218,10 @@ pt:
|
|
|
189
218
|
not_found: Evento não encontrado
|
|
190
219
|
title: Evento %{event_id}
|
|
191
220
|
helpers:
|
|
221
|
+
batch_status:
|
|
222
|
+
finished: Finalizado
|
|
223
|
+
pending: Pendente
|
|
224
|
+
processing: Processando
|
|
192
225
|
bulk_select_all: Selecionar todos
|
|
193
226
|
bulk_select_row: Selecionar %{id}
|
|
194
227
|
bulk_selected: selecionado
|
|
@@ -346,6 +379,7 @@ pt:
|
|
|
346
379
|
layout:
|
|
347
380
|
brand: Pgbus
|
|
348
381
|
nav:
|
|
382
|
+
batches: Lotes
|
|
349
383
|
dashboard: Painel
|
|
350
384
|
dlq: DLQ
|
|
351
385
|
events: Eventos
|
data/config/locales/sv.yml
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
sv:
|
|
3
3
|
pgbus:
|
|
4
|
+
batches:
|
|
5
|
+
index:
|
|
6
|
+
description: Jobbgrupper med framstegsspårning och callbacks
|
|
7
|
+
empty: Inga grupper hittades
|
|
8
|
+
headers:
|
|
9
|
+
batch_id: Grupp-ID
|
|
10
|
+
created: Skapad
|
|
11
|
+
description: Beskrivning
|
|
12
|
+
jobs: Jobb
|
|
13
|
+
progress: Framsteg
|
|
14
|
+
status: Status
|
|
15
|
+
title: Grupper
|
|
16
|
+
show:
|
|
17
|
+
back: Tillbaka till grupper
|
|
18
|
+
batch_id: Grupp-ID
|
|
19
|
+
completed: Slutförda
|
|
20
|
+
created_at: Skapad den
|
|
21
|
+
details: Detaljer
|
|
22
|
+
discarded: Kasserade
|
|
23
|
+
finished_at: Avslutad den
|
|
24
|
+
not_found: Grupp hittades inte
|
|
25
|
+
on_discard: Vid kassering
|
|
26
|
+
on_finish: Vid avslut
|
|
27
|
+
on_success: Vid framgång
|
|
28
|
+
progress: Framsteg
|
|
29
|
+
properties: Egenskaper
|
|
30
|
+
remaining: Återstående
|
|
31
|
+
title: Grupp
|
|
32
|
+
total_jobs: Totalt antal jobb
|
|
4
33
|
dashboard:
|
|
5
34
|
processes_table:
|
|
6
35
|
empty: Inga processer körs
|
|
@@ -189,6 +218,10 @@ sv:
|
|
|
189
218
|
not_found: Händelse hittades inte
|
|
190
219
|
title: Händelse %{event_id}
|
|
191
220
|
helpers:
|
|
221
|
+
batch_status:
|
|
222
|
+
finished: Slutförd
|
|
223
|
+
pending: Väntande
|
|
224
|
+
processing: Bearbetas
|
|
192
225
|
bulk_select_all: Markera alla
|
|
193
226
|
bulk_select_row: Markera %{id}
|
|
194
227
|
bulk_selected: valda
|
|
@@ -346,6 +379,7 @@ sv:
|
|
|
346
379
|
layout:
|
|
347
380
|
brand: Pgbus
|
|
348
381
|
nav:
|
|
382
|
+
batches: Grupper
|
|
349
383
|
dashboard: Instrumentpanel
|
|
350
384
|
dlq: DLQ
|
|
351
385
|
events: Händelser
|
data/config/routes.rb
CHANGED
data/lib/pgbus/client.rb
CHANGED
|
@@ -351,6 +351,87 @@ module Pgbus
|
|
|
351
351
|
total
|
|
352
352
|
end
|
|
353
353
|
|
|
354
|
+
# --- Grouped reads (PGMQ v1.11.0+) ---
|
|
355
|
+
|
|
356
|
+
def read_grouped(queue_name, qty:, vt: nil)
|
|
357
|
+
full_name = config.queue_name(queue_name)
|
|
358
|
+
Instrumentation.instrument("pgbus.client.read_grouped", queue: full_name, qty: qty) do
|
|
359
|
+
with_stale_connection_retry do
|
|
360
|
+
synchronized { @pgmq.read_grouped(full_name, vt: vt || config.visibility_timeout, qty: qty) }
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def read_grouped_rr(queue_name, qty:, vt: nil)
|
|
366
|
+
full_name = config.queue_name(queue_name)
|
|
367
|
+
Instrumentation.instrument("pgbus.client.read_grouped_rr", queue: full_name, qty: qty) do
|
|
368
|
+
with_stale_connection_retry do
|
|
369
|
+
synchronized { @pgmq.read_grouped_rr(full_name, vt: vt || config.visibility_timeout, qty: qty) }
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def read_grouped_head(queue_name, qty:, vt: nil)
|
|
375
|
+
full_name = config.queue_name(queue_name)
|
|
376
|
+
with_stale_connection_retry do
|
|
377
|
+
synchronized { @pgmq.read_grouped_head(full_name, vt: vt || config.visibility_timeout, qty: qty) }
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# --- FIFO index management (PGMQ v1.11.0+) ---
|
|
382
|
+
|
|
383
|
+
def create_fifo_index(queue_name)
|
|
384
|
+
full_name = config.queue_name(queue_name)
|
|
385
|
+
with_stale_connection_retry do
|
|
386
|
+
synchronized { @pgmq.create_fifo_index(full_name) }
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def create_fifo_indexes_all
|
|
391
|
+
with_stale_connection_retry do
|
|
392
|
+
synchronized { @pgmq.create_fifo_indexes_all }
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# --- LISTEN/NOTIFY management (PGMQ v1.11.0+) ---
|
|
397
|
+
|
|
398
|
+
def wait_for_notify(queue_name, timeout: nil, &block)
|
|
399
|
+
full_name = config.queue_name(queue_name)
|
|
400
|
+
with_stale_connection_retry do
|
|
401
|
+
synchronized { @pgmq.wait_for_notify(full_name, timeout: timeout, &block) }
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def update_notify_insert(queue_name, throttle_interval_ms:)
|
|
406
|
+
full_name = config.queue_name(queue_name)
|
|
407
|
+
with_stale_connection_retry do
|
|
408
|
+
synchronized { @pgmq.update_notify_insert(full_name, throttle_interval_ms: throttle_interval_ms) }
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def list_notify_insert_throttles
|
|
413
|
+
with_stale_connection_retry do
|
|
414
|
+
synchronized { @pgmq.list_notify_insert_throttles }
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# --- Archive partitioning (requires pg_partman extension) ---
|
|
419
|
+
|
|
420
|
+
def convert_archive_partitioned(queue_name, partition_interval: "10000", retention_interval: "100000",
|
|
421
|
+
leading_partition: 10)
|
|
422
|
+
full_name = config.queue_name(queue_name)
|
|
423
|
+
with_stale_connection_retry do
|
|
424
|
+
synchronized do
|
|
425
|
+
@pgmq.convert_archive_partitioned(
|
|
426
|
+
full_name,
|
|
427
|
+
partition_interval: partition_interval,
|
|
428
|
+
retention_interval: retention_interval,
|
|
429
|
+
leading_partition: leading_partition
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
354
435
|
# Topic routing
|
|
355
436
|
def bind_topic(pattern, queue_name)
|
|
356
437
|
full_name = config.queue_name(queue_name)
|
|
@@ -503,6 +584,7 @@ module Pgbus
|
|
|
503
584
|
@pgmq.create(full_name)
|
|
504
585
|
tune_autovacuum(full_name)
|
|
505
586
|
enable_notify_if_needed(full_name, NOTIFY_THROTTLE_MS)
|
|
587
|
+
create_fifo_index_if_needed(full_name)
|
|
506
588
|
end
|
|
507
589
|
true
|
|
508
590
|
end
|
|
@@ -515,6 +597,12 @@ module Pgbus
|
|
|
515
597
|
@pgmq.enable_notify_insert(full_name, throttle_interval_ms: throttle_ms)
|
|
516
598
|
end
|
|
517
599
|
|
|
600
|
+
def create_fifo_index_if_needed(full_name)
|
|
601
|
+
return unless config.group_mode
|
|
602
|
+
|
|
603
|
+
@pgmq.create_fifo_index(full_name)
|
|
604
|
+
end
|
|
605
|
+
|
|
518
606
|
# Check whether the NOTIFY trigger already exists on this queue with the
|
|
519
607
|
# expected throttle interval. When it does, we can skip the destructive
|
|
520
608
|
# DROP TRIGGER + CREATE TRIGGER cycle that causes deadlocks when multiple
|
|
@@ -544,11 +632,21 @@ module Pgbus
|
|
|
544
632
|
false
|
|
545
633
|
end
|
|
546
634
|
|
|
635
|
+
# Apply PGMQ-tuned autovacuum + storage parameters to a queue's tables.
|
|
636
|
+
#
|
|
637
|
+
# Delegates to pgmq-ruby's tune_autovacuum (v0.7+), which sets the same
|
|
638
|
+
# queue/archive parameters pgbus used to apply by hand — vacuum scale
|
|
639
|
+
# factor 0.01/0.05, cost_delay 2/5, analyze scale factor 0.05, and
|
|
640
|
+
# fillfactor 70 on the queue table — plus a vacuum_threshold floor of 50.
|
|
641
|
+
# It quotes/lowercases the table name and runs both ALTER TABLEs in one
|
|
642
|
+
# pooled checkout. Tuning is best-effort: a failure here never blocks a
|
|
643
|
+
# queue from being usable, so we log and move on.
|
|
644
|
+
#
|
|
645
|
+
# Pgbus::AutovacuumTuning is still the source for the migration generators
|
|
646
|
+
# (sql_for_all_queues, sql_for_high_churn_tables) which tune pgbus-owned
|
|
647
|
+
# metadata tables the gem doesn't know about.
|
|
547
648
|
def tune_autovacuum(queue_name)
|
|
548
|
-
|
|
549
|
-
conn.exec(AutovacuumTuning.sql_for_queue(queue_name))
|
|
550
|
-
conn.exec(TableMaintenance.fillfactor_sql_for_queue(queue_name))
|
|
551
|
-
end
|
|
649
|
+
@pgmq.tune_autovacuum(queue_name)
|
|
552
650
|
rescue StandardError => e
|
|
553
651
|
Pgbus.logger.debug { "[Pgbus::Client] Autovacuum tuning failed for #{queue_name}: #{e.message}" }
|
|
554
652
|
end
|
data/lib/pgbus/configuration.rb
CHANGED
|
@@ -44,6 +44,12 @@ module Pgbus
|
|
|
44
44
|
# Priority queues
|
|
45
45
|
attr_accessor :priority_levels, :default_priority
|
|
46
46
|
|
|
47
|
+
# Grouped reads (PGMQ v1.11.0+ FIFO grouping).
|
|
48
|
+
# nil = disabled (default read_batch behavior).
|
|
49
|
+
# :fifo = use read_grouped (drains oldest group first, throughput-optimized).
|
|
50
|
+
# :round_robin = use read_grouped_rr (fair round-robin across groups).
|
|
51
|
+
attr_reader :group_mode
|
|
52
|
+
|
|
47
53
|
# Archive compaction. Only the user-facing retention window is configurable;
|
|
48
54
|
# the loop interval and batch size are tuned via constants on
|
|
49
55
|
# Pgbus::Process::Dispatcher.
|
|
@@ -146,6 +152,7 @@ module Pgbus
|
|
|
146
152
|
|
|
147
153
|
@priority_levels = nil
|
|
148
154
|
@default_priority = 1
|
|
155
|
+
@group_mode = nil
|
|
149
156
|
|
|
150
157
|
@archive_retention = 7 * 24 * 3600 # 7 days
|
|
151
158
|
|
|
@@ -300,6 +307,24 @@ module Pgbus
|
|
|
300
307
|
@streams_default_broadcast_mode = mode
|
|
301
308
|
end
|
|
302
309
|
|
|
310
|
+
VALID_GROUP_MODES = [nil, :fifo, :round_robin].freeze
|
|
311
|
+
|
|
312
|
+
def group_mode=(mode)
|
|
313
|
+
coerced = case mode
|
|
314
|
+
when nil then nil
|
|
315
|
+
when Symbol then mode
|
|
316
|
+
when String then mode.to_sym
|
|
317
|
+
else
|
|
318
|
+
raise ArgumentError,
|
|
319
|
+
"Invalid group_mode type: #{mode.class}. Must be nil, String, or Symbol"
|
|
320
|
+
end
|
|
321
|
+
unless VALID_GROUP_MODES.include?(coerced)
|
|
322
|
+
raise ArgumentError, "Invalid group_mode: #{coerced.inspect}. Must be nil, :fifo, or :round_robin"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
@group_mode = coerced
|
|
326
|
+
end
|
|
327
|
+
|
|
303
328
|
VALID_PGMQ_SCHEMA_MODES = %i[auto extension embedded].freeze
|
|
304
329
|
|
|
305
330
|
def pgmq_schema_mode=(mode)
|
|
@@ -69,6 +69,7 @@ module Pgbus
|
|
|
69
69
|
single_active = worker_config[:single_active_consumer] || worker_config["single_active_consumer"] || false
|
|
70
70
|
priority = worker_config[:consumer_priority] || worker_config["consumer_priority"] || 0
|
|
71
71
|
exec_mode = config.execution_mode_for(worker_config)
|
|
72
|
+
grp_mode = worker_config[:group_mode] || worker_config["group_mode"] || config.group_mode
|
|
72
73
|
|
|
73
74
|
pid = fork do
|
|
74
75
|
restore_signals
|
|
@@ -78,7 +79,7 @@ module Pgbus
|
|
|
78
79
|
worker = Worker.new(
|
|
79
80
|
queues: queues, threads: threads, config: config,
|
|
80
81
|
single_active_consumer: single_active, consumer_priority: priority,
|
|
81
|
-
execution_mode: exec_mode
|
|
82
|
+
execution_mode: exec_mode, group_mode: grp_mode
|
|
82
83
|
)
|
|
83
84
|
worker.run
|
|
84
85
|
end
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -11,12 +11,24 @@ module Pgbus
|
|
|
11
11
|
|
|
12
12
|
def initialize(queues:, threads: 5, config: Pgbus.configuration,
|
|
13
13
|
single_active_consumer: false, consumer_priority: 0,
|
|
14
|
-
execution_mode: :threads)
|
|
14
|
+
execution_mode: :threads, group_mode: nil)
|
|
15
15
|
@queues = Array(queues)
|
|
16
16
|
@wildcard = @queues.include?("*")
|
|
17
17
|
@threads = threads
|
|
18
18
|
@config = config
|
|
19
19
|
@execution_mode = ExecutionPools.normalize_mode(execution_mode)
|
|
20
|
+
@group_mode = case group_mode
|
|
21
|
+
when nil then nil
|
|
22
|
+
when Symbol then group_mode
|
|
23
|
+
when String then group_mode.to_sym
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"Invalid group_mode type: #{group_mode.class}. Must be nil, String, or Symbol"
|
|
27
|
+
end
|
|
28
|
+
unless Pgbus::Configuration::VALID_GROUP_MODES.include?(@group_mode)
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"Invalid group_mode: #{@group_mode.inspect}. Must be nil, :fifo, or :round_robin"
|
|
31
|
+
end
|
|
20
32
|
@single_active_consumer = single_active_consumer
|
|
21
33
|
@consumer_priority = consumer_priority
|
|
22
34
|
@lifecycle = Lifecycle.new
|
|
@@ -141,6 +153,8 @@ module Pgbus
|
|
|
141
153
|
|
|
142
154
|
if priority_enabled?
|
|
143
155
|
fetch_prioritized(active_queues, qty)
|
|
156
|
+
elsif @group_mode
|
|
157
|
+
fetch_grouped(active_queues, qty)
|
|
144
158
|
elsif active_queues.size == 1
|
|
145
159
|
queue = active_queues.first
|
|
146
160
|
messages = Pgbus.client.read_batch(queue, qty: qty) || []
|
|
@@ -210,6 +224,29 @@ module Pgbus
|
|
|
210
224
|
end
|
|
211
225
|
end
|
|
212
226
|
|
|
227
|
+
# Use grouped reads for fair or throughput-optimized multi-tenant processing.
|
|
228
|
+
# Each queue is read independently with the configured group strategy.
|
|
229
|
+
def fetch_grouped(active_queues, qty)
|
|
230
|
+
remaining = qty
|
|
231
|
+
results = []
|
|
232
|
+
|
|
233
|
+
active_queues.each do |queue|
|
|
234
|
+
break if remaining <= 0
|
|
235
|
+
|
|
236
|
+
messages = case @group_mode
|
|
237
|
+
when :round_robin
|
|
238
|
+
Pgbus.client.read_grouped_rr(queue, qty: remaining) || []
|
|
239
|
+
else # :fifo
|
|
240
|
+
Pgbus.client.read_grouped(queue, qty: remaining) || []
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
messages.each { |m| results << [queue, m] }
|
|
244
|
+
remaining -= messages.size
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
results
|
|
248
|
+
end
|
|
249
|
+
|
|
213
250
|
def priority_enabled?
|
|
214
251
|
config.priority_levels && config.priority_levels > 1
|
|
215
252
|
end
|
data/lib/pgbus/serializer.rb
CHANGED
|
@@ -58,7 +58,7 @@ module Pgbus
|
|
|
58
58
|
raise ArgumentError, "Invalid GlobalID: #{gid_string.inspect}" unless gid
|
|
59
59
|
|
|
60
60
|
allowed = Pgbus.configuration.allowed_global_id_models
|
|
61
|
-
if allowed
|
|
61
|
+
if allowed && allowed.empty?
|
|
62
62
|
raise ArgumentError,
|
|
63
63
|
"GlobalID deserialization is disabled (allowed_global_id_models is empty). " \
|
|
64
64
|
"Set to nil to allow all models, or add permitted classes."
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -590,6 +590,43 @@ module Pgbus
|
|
|
590
590
|
[]
|
|
591
591
|
end
|
|
592
592
|
|
|
593
|
+
# Batches
|
|
594
|
+
def batches(limit: 100)
|
|
595
|
+
BatchEntry.order(created_at: :desc).limit(limit).map { |r| format_batch(r) }
|
|
596
|
+
rescue StandardError => e
|
|
597
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching batches: #{e.message}" }
|
|
598
|
+
[]
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def batch_detail(batch_id)
|
|
602
|
+
record = BatchEntry.find_by(batch_id: batch_id)
|
|
603
|
+
return nil unless record
|
|
604
|
+
|
|
605
|
+
format_batch(record).merge(
|
|
606
|
+
properties: record.properties,
|
|
607
|
+
on_finish_class: record.on_finish_class,
|
|
608
|
+
on_success_class: record.on_success_class,
|
|
609
|
+
on_discard_class: record.on_discard_class
|
|
610
|
+
)
|
|
611
|
+
rescue StandardError => e
|
|
612
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching batch #{batch_id}: #{e.message}" }
|
|
613
|
+
nil
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def batches_count
|
|
617
|
+
BatchEntry.count
|
|
618
|
+
rescue StandardError => e
|
|
619
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error counting batches: #{e.message}" }
|
|
620
|
+
0
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def active_batches_count
|
|
624
|
+
BatchEntry.where.not(status: "finished").count
|
|
625
|
+
rescue StandardError => e
|
|
626
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error counting active batches: #{e.message}" }
|
|
627
|
+
0
|
|
628
|
+
end
|
|
629
|
+
|
|
593
630
|
# Job stats
|
|
594
631
|
def job_stats_summary(minutes: 60)
|
|
595
632
|
JobStat.summary(minutes: minutes)
|
|
@@ -874,6 +911,33 @@ module Pgbus
|
|
|
874
911
|
[]
|
|
875
912
|
end
|
|
876
913
|
|
|
914
|
+
# NOTIFY throttle status for all queues with notifications enabled.
|
|
915
|
+
# Returns an array of hashes: { queue_name:, throttle_interval_ms:, last_notified_at: }
|
|
916
|
+
def notify_throttles
|
|
917
|
+
@client.list_notify_insert_throttles.map do |throttle|
|
|
918
|
+
{
|
|
919
|
+
queue_name: throttle.queue_name,
|
|
920
|
+
throttle_interval_ms: throttle.throttle_interval_ms,
|
|
921
|
+
last_notified_at: throttle.last_notified_at
|
|
922
|
+
}
|
|
923
|
+
end
|
|
924
|
+
rescue StandardError => e
|
|
925
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching notify throttles: #{e.message}" }
|
|
926
|
+
[]
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
# FIFO group head sampling for a specific queue.
|
|
930
|
+
# Returns the oldest visible message from each distinct group (up to qty).
|
|
931
|
+
# Useful for detecting head-of-line stalls in multi-tenant queues.
|
|
932
|
+
def queue_group_heads(queue_name, qty: 20)
|
|
933
|
+
logical = logical_queue_name(queue_name)
|
|
934
|
+
messages = @client.read_grouped_head(logical, qty: qty) || []
|
|
935
|
+
messages.map { |m| format_pgmq_message(m, queue_name) }
|
|
936
|
+
rescue StandardError => e
|
|
937
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching group heads for #{queue_name}: #{e.message}" }
|
|
938
|
+
[]
|
|
939
|
+
end
|
|
940
|
+
|
|
877
941
|
private
|
|
878
942
|
|
|
879
943
|
def connection
|
|
@@ -1114,6 +1178,19 @@ module Pgbus
|
|
|
1114
1178
|
}
|
|
1115
1179
|
end
|
|
1116
1180
|
|
|
1181
|
+
def format_pgmq_message(msg, queue_name)
|
|
1182
|
+
{
|
|
1183
|
+
msg_id: msg.msg_id.to_i,
|
|
1184
|
+
read_ct: msg.read_ct.to_i,
|
|
1185
|
+
enqueued_at: msg.enqueued_at,
|
|
1186
|
+
last_read_at: msg.respond_to?(:last_read_at) ? msg.last_read_at : nil,
|
|
1187
|
+
vt: msg.respond_to?(:vt) ? msg.vt : nil,
|
|
1188
|
+
message: msg.message,
|
|
1189
|
+
headers: msg.headers,
|
|
1190
|
+
queue_name: queue_name
|
|
1191
|
+
}
|
|
1192
|
+
end
|
|
1193
|
+
|
|
1117
1194
|
def format_process(row)
|
|
1118
1195
|
heartbeat = row["last_heartbeat_at"]
|
|
1119
1196
|
heartbeat_time = heartbeat.is_a?(String) ? Time.parse(heartbeat) : heartbeat
|
|
@@ -1131,6 +1208,25 @@ module Pgbus
|
|
|
1131
1208
|
}
|
|
1132
1209
|
end
|
|
1133
1210
|
|
|
1211
|
+
def format_batch(record)
|
|
1212
|
+
total = record.total_jobs
|
|
1213
|
+
done = record.completed_jobs + record.discarded_jobs
|
|
1214
|
+
pct = total.positive? ? ((done * 100) / total) : 100
|
|
1215
|
+
|
|
1216
|
+
{
|
|
1217
|
+
batch_id: record.batch_id,
|
|
1218
|
+
description: record.description,
|
|
1219
|
+
status: record.status,
|
|
1220
|
+
total_jobs: total,
|
|
1221
|
+
completed_jobs: record.completed_jobs,
|
|
1222
|
+
discarded_jobs: record.discarded_jobs,
|
|
1223
|
+
failed_jobs: record.failed_jobs,
|
|
1224
|
+
progress_pct: pct,
|
|
1225
|
+
created_at: record.created_at,
|
|
1226
|
+
finished_at: record.finished_at
|
|
1227
|
+
}
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1134
1230
|
def sanitize_name(name)
|
|
1135
1231
|
QueueNameValidator.sanitize!(name)
|
|
1136
1232
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :pgbus do
|
|
4
|
+
namespace :queues do
|
|
5
|
+
desc "Create FIFO indexes on all pgbus queues (required for grouped reads)"
|
|
6
|
+
task fifo_indexes: :environment do
|
|
7
|
+
puts "Creating FIFO indexes on all pgbus queues..."
|
|
8
|
+
Pgbus.client.create_fifo_indexes_all
|
|
9
|
+
puts "Done. FIFO indexes created on all queues."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "Create FIFO index on a specific queue (QUEUE=name)"
|
|
13
|
+
task fifo_index: :environment do
|
|
14
|
+
queue = ENV.fetch("QUEUE") do
|
|
15
|
+
abort "Usage: rake pgbus:queues:fifo_index QUEUE=<queue_name>"
|
|
16
|
+
end
|
|
17
|
+
puts "Creating FIFO index on queue '#{queue}'..."
|
|
18
|
+
Pgbus.client.create_fifo_index(queue)
|
|
19
|
+
puts "Done."
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
namespace :archives do
|
|
24
|
+
desc "Convert a queue's archive table to pg_partman-managed partitions (QUEUE=name)"
|
|
25
|
+
task partition: :environment do
|
|
26
|
+
queue = ENV.fetch("QUEUE") do
|
|
27
|
+
abort "Usage: rake pgbus:archives:partition QUEUE=<queue_name> " \
|
|
28
|
+
"[INTERVAL=10000] [RETENTION=100000] [LEADING_PARTITION=10]"
|
|
29
|
+
end
|
|
30
|
+
interval = ENV.fetch("INTERVAL", "10000")
|
|
31
|
+
retention = ENV.fetch("RETENTION", "100000")
|
|
32
|
+
leading_raw = ENV.fetch("LEADING_PARTITION", "10")
|
|
33
|
+
leading = begin
|
|
34
|
+
Integer(leading_raw, 10)
|
|
35
|
+
rescue ArgumentError, TypeError
|
|
36
|
+
abort "LEADING_PARTITION must be a positive integer, got #{leading_raw.inspect}"
|
|
37
|
+
end
|
|
38
|
+
abort "LEADING_PARTITION must be a positive integer" if leading <= 0
|
|
39
|
+
|
|
40
|
+
puts "Converting archive table for queue '#{queue}' to partitioned..."
|
|
41
|
+
puts " Partition interval: #{interval}"
|
|
42
|
+
puts " Retention interval: #{retention}"
|
|
43
|
+
puts " Leading partitions: #{leading}"
|
|
44
|
+
|
|
45
|
+
Pgbus.client.convert_archive_partitioned(
|
|
46
|
+
queue,
|
|
47
|
+
partition_interval: interval,
|
|
48
|
+
retention_interval: retention,
|
|
49
|
+
leading_partition: leading
|
|
50
|
+
)
|
|
51
|
+
puts "Done. Archive table is now partitioned."
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|