pgbus 0.3.3 → 0.3.4
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/dead_letter_controller.rb +17 -0
- data/app/controllers/pgbus/jobs_controller.rb +36 -0
- data/app/controllers/pgbus/locks_controller.rb +25 -0
- data/app/frontend/pgbus/application.js +45 -0
- data/app/models/pgbus/job_lock.rb +16 -8
- data/app/models/pgbus/uniqueness_key.rb +36 -0
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +22 -2
- data/app/views/pgbus/dead_letter/index.html.erb +9 -1
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +36 -6
- data/app/views/pgbus/jobs/_failed_table.html.erb +35 -4
- data/app/views/pgbus/locks/index.html.erb +53 -28
- data/config/locales/da.yml +3 -7
- data/config/locales/de.yml +3 -7
- data/config/locales/en.yml +33 -7
- data/config/locales/es.yml +3 -7
- data/config/locales/fi.yml +3 -7
- data/config/locales/fr.yml +3 -7
- data/config/locales/it.yml +3 -7
- data/config/locales/ja.yml +3 -7
- data/config/locales/nb.yml +3 -7
- data/config/locales/nl.yml +3 -7
- data/config/locales/pt.yml +3 -7
- data/config/locales/sv.yml +3 -7
- data/config/routes.rb +12 -1
- data/lib/generators/pgbus/migrate_job_locks_generator.rb +56 -0
- data/lib/generators/pgbus/templates/add_uniqueness_keys.rb.erb +13 -0
- data/lib/generators/pgbus/templates/migrate_job_locks_to_uniqueness_keys.rb.erb +33 -0
- data/lib/pgbus/active_job/executor.rb +34 -20
- data/lib/pgbus/client.rb +18 -2
- data/lib/pgbus/process/dispatcher.rb +33 -10
- data/lib/pgbus/process/worker.rb +4 -1
- data/lib/pgbus/recurring/schedule.rb +38 -35
- data/lib/pgbus/stat_buffer.rb +92 -0
- data/lib/pgbus/uniqueness.rb +24 -39
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +46 -15
- metadata +6 -1
data/config/locales/en.yml
CHANGED
|
@@ -44,6 +44,12 @@ en:
|
|
|
44
44
|
index:
|
|
45
45
|
discard_all: Discard All
|
|
46
46
|
discard_all_confirm: Permanently discard all DLQ messages?
|
|
47
|
+
discard_selected: Discard Selected
|
|
48
|
+
discard_selected_confirm: Discard selected DLQ messages?
|
|
49
|
+
discarded_selected:
|
|
50
|
+
one: Discarded 1 DLQ message.
|
|
51
|
+
other: Discarded %{count} DLQ messages.
|
|
52
|
+
none_selected: No messages selected.
|
|
47
53
|
retry_all: Retry All
|
|
48
54
|
retry_all_confirm: Retry all DLQ messages?
|
|
49
55
|
title: Dead Letter Queue
|
|
@@ -114,6 +120,9 @@ en:
|
|
|
114
120
|
not_found: Event not found
|
|
115
121
|
title: Event %{event_id}
|
|
116
122
|
helpers:
|
|
123
|
+
bulk_select_all: Select all
|
|
124
|
+
bulk_select_row: Select %{id}
|
|
125
|
+
bulk_selected: selected
|
|
117
126
|
paused_badge: Paused
|
|
118
127
|
queue_badge:
|
|
119
128
|
dlq: DLQ
|
|
@@ -216,6 +225,12 @@ en:
|
|
|
216
225
|
discard_all: Discard All
|
|
217
226
|
discard_all_confirm: Discard all failed jobs?
|
|
218
227
|
discard_all_enqueued_notice: Discarded %{count} enqueued jobs and released their locks.
|
|
228
|
+
discard_selected: Discard Selected
|
|
229
|
+
discard_selected_confirm: Discard selected items?
|
|
230
|
+
discarded_selected:
|
|
231
|
+
one: Discarded 1 selected item.
|
|
232
|
+
other: Discarded %{count} selected items.
|
|
233
|
+
none_selected: No items selected.
|
|
219
234
|
retry_all: Retry All
|
|
220
235
|
retry_all_confirm: Retry all failed jobs?
|
|
221
236
|
title: Jobs
|
|
@@ -253,18 +268,29 @@ en:
|
|
|
253
268
|
toggle_menu: Toggle menu
|
|
254
269
|
locks:
|
|
255
270
|
index:
|
|
271
|
+
all_locks_discarded:
|
|
272
|
+
one: Discarded 1 lock.
|
|
273
|
+
other: Discarded %{count} locks.
|
|
256
274
|
description: Active uniqueness locks preventing duplicate job execution
|
|
275
|
+
discard: Discard
|
|
276
|
+
discard_all: Discard All
|
|
277
|
+
discard_all_confirm: Permanently discard all locks? This may allow duplicate job execution.
|
|
278
|
+
discard_confirm: Discard this lock? The associated job may be enqueued again.
|
|
279
|
+
discard_selected: Discard Selected
|
|
280
|
+
discard_selected_confirm: Discard selected locks?
|
|
257
281
|
empty: No active locks
|
|
258
|
-
executing: Executing
|
|
259
282
|
headers:
|
|
260
283
|
age: Age
|
|
261
|
-
expires: Expires
|
|
262
|
-
job_class: Job Class
|
|
263
284
|
lock_key: Lock Key
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
msg_id: Message ID
|
|
286
|
+
queue_name: Queue
|
|
287
|
+
lock_discard_failed: Could not discard lock.
|
|
288
|
+
lock_discarded: Lock discarded.
|
|
289
|
+
locks_discarded:
|
|
290
|
+
one: Discarded 1 lock.
|
|
291
|
+
other: Discarded %{count} locks.
|
|
292
|
+
none_selected: No locks selected.
|
|
293
|
+
title: Uniqueness Keys
|
|
268
294
|
outbox:
|
|
269
295
|
index:
|
|
270
296
|
description: Transactional outbox entries pending publication to PGMQ
|
data/config/locales/es.yml
CHANGED
|
@@ -239,16 +239,12 @@ es:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Bloqueos de unicidad activos que impiden la ejecución duplicada del trabajo
|
|
241
241
|
empty: No hay bloqueos activos
|
|
242
|
-
executing: Ejecutando
|
|
243
242
|
headers:
|
|
244
243
|
age: Antigüedad
|
|
245
|
-
expires: Expira
|
|
246
|
-
job_class: Clase de trabajo
|
|
247
244
|
lock_key: Clave de bloqueo
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Bloqueos de trabajo
|
|
245
|
+
msg_id: ID de mensaje
|
|
246
|
+
queue_name: Cola
|
|
247
|
+
title: Claves de unicidad
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Entradas de bandeja de salida transaccional pendientes de publicación en PGMQ
|
data/config/locales/fi.yml
CHANGED
|
@@ -239,16 +239,12 @@ fi:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Aktiiviset ainutlaatuisuuden lukot estävät päällekkäisen työn suorittamisen
|
|
241
241
|
empty: Ei aktiivisia lukkoja
|
|
242
|
-
executing: Suoritetaan
|
|
243
242
|
headers:
|
|
244
243
|
age: Ikä
|
|
245
|
-
expires: Vanhenee
|
|
246
|
-
job_class: Työluokka
|
|
247
244
|
lock_key: Lukon avain
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Työn lukot
|
|
245
|
+
msg_id: Viestin tunnus
|
|
246
|
+
queue_name: Jono
|
|
247
|
+
title: Yksikäsitteisyysavaimet
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Transaktionaaliset lähetyslaatikon merkinnät odottavat julkaisua PGMQ:lle
|
data/config/locales/fr.yml
CHANGED
|
@@ -239,16 +239,12 @@ fr:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Verrous d'unicité actifs empêchant l'exécution de travaux en double
|
|
241
241
|
empty: Aucun verrou actif
|
|
242
|
-
executing: Exécution
|
|
243
242
|
headers:
|
|
244
243
|
age: Âge
|
|
245
|
-
expires: Expire
|
|
246
|
-
job_class: Classe de travail
|
|
247
244
|
lock_key: Clé de verrou
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Verrous de travail
|
|
245
|
+
msg_id: ID du message
|
|
246
|
+
queue_name: File
|
|
247
|
+
title: Clés d'unicité
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Entrées de boîte d'envoi transactionnelle en attente de publication vers PGMQ
|
data/config/locales/it.yml
CHANGED
|
@@ -239,16 +239,12 @@ it:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Blocchi di unicità attivi che impediscono l'esecuzione duplicata del lavoro
|
|
241
241
|
empty: Nessun blocco attivo
|
|
242
|
-
executing: In esecuzione
|
|
243
242
|
headers:
|
|
244
243
|
age: Età
|
|
245
|
-
expires: Scade
|
|
246
|
-
job_class: Classe del lavoro
|
|
247
244
|
lock_key: Chiave di blocco
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Blocchi del lavoro
|
|
245
|
+
msg_id: ID messaggio
|
|
246
|
+
queue_name: Coda
|
|
247
|
+
title: Chiavi di unicità
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Voci dell'outbox transazionali in attesa di pubblicazione su PGMQ
|
data/config/locales/ja.yml
CHANGED
|
@@ -239,16 +239,12 @@ ja:
|
|
|
239
239
|
index:
|
|
240
240
|
description: 重複ジョブ実行を防ぐアクティブなユニークロック
|
|
241
241
|
empty: アクティブなロックはありません
|
|
242
|
-
executing: 実行中
|
|
243
242
|
headers:
|
|
244
243
|
age: 経過時間
|
|
245
|
-
expires: 有効期限
|
|
246
|
-
job_class: ジョブクラス
|
|
247
244
|
lock_key: ロックキー
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: ジョブロック
|
|
245
|
+
msg_id: メッセージID
|
|
246
|
+
queue_name: キュー
|
|
247
|
+
title: 一意性キー
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: PGMQへの公開待ちのトランザクショナルアウトボックスエントリ
|
data/config/locales/nb.yml
CHANGED
|
@@ -239,16 +239,12 @@ nb:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Aktive unike låser som forhindrer duplisert jobbkjøring
|
|
241
241
|
empty: Ingen aktive låser
|
|
242
|
-
executing: Utfører
|
|
243
242
|
headers:
|
|
244
243
|
age: Alder
|
|
245
|
-
expires: Utløper
|
|
246
|
-
job_class: Jobbklasse
|
|
247
244
|
lock_key: Låsenøkkel
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Jobblåser
|
|
245
|
+
msg_id: Meldings-ID
|
|
246
|
+
queue_name: Kø
|
|
247
|
+
title: Unikhetsnøkler
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Transaksjonelle utgående postkasseoppføringer som venter på publisering til PGMQ
|
data/config/locales/nl.yml
CHANGED
|
@@ -239,16 +239,12 @@ nl:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Actieve uniekheidsvergrendelingen voorkomen dubbele taakuitvoering
|
|
241
241
|
empty: Geen actieve vergrendelingen
|
|
242
|
-
executing: Uitvoeren
|
|
243
242
|
headers:
|
|
244
243
|
age: Leeftijd
|
|
245
|
-
expires: Verloopt
|
|
246
|
-
job_class: Taakklasse
|
|
247
244
|
lock_key: Vergrendelingssleutel
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Taakvergrendelingen
|
|
245
|
+
msg_id: Bericht-ID
|
|
246
|
+
queue_name: Wachtrij
|
|
247
|
+
title: Uniciteitsleutels
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Transactionele uitgaande berichten wachten op publicatie naar PGMQ
|
data/config/locales/pt.yml
CHANGED
|
@@ -239,16 +239,12 @@ pt:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Bloqueios de exclusividade ativos impedindo a execução duplicada do trabalho
|
|
241
241
|
empty: Nenhum bloqueio ativo
|
|
242
|
-
executing: Executando
|
|
243
242
|
headers:
|
|
244
243
|
age: Idade
|
|
245
|
-
expires: Expira
|
|
246
|
-
job_class: Classe do Trabalho
|
|
247
244
|
lock_key: Chave de Bloqueio
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Bloqueios de Trabalho
|
|
245
|
+
msg_id: ID da mensagem
|
|
246
|
+
queue_name: Fila
|
|
247
|
+
title: Chaves de unicidade
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Entradas da caixa de saída transacional pendentes de publicação para PGMQ
|
data/config/locales/sv.yml
CHANGED
|
@@ -239,16 +239,12 @@ sv:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Aktiva unika lås som förhindrar duplicerad jobbexekvering
|
|
241
241
|
empty: Inga aktiva lås
|
|
242
|
-
executing: Utför
|
|
243
242
|
headers:
|
|
244
243
|
age: Ålder
|
|
245
|
-
expires: Upphör
|
|
246
|
-
job_class: Jobbklass
|
|
247
244
|
lock_key: Låsningsnyckel
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Jobblås
|
|
245
|
+
msg_id: Meddelande-ID
|
|
246
|
+
queue_name: Kö
|
|
247
|
+
title: Unikhetsnycklar
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Transaktionella utboxposter som väntar på publicering till PGMQ
|
data/config/routes.rb
CHANGED
|
@@ -22,6 +22,8 @@ Pgbus::Engine.routes.draw do
|
|
|
22
22
|
post :retry_all
|
|
23
23
|
post :discard_all
|
|
24
24
|
post :discard_all_enqueued
|
|
25
|
+
post :discard_selected_failed
|
|
26
|
+
post :discard_selected_enqueued
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -48,11 +50,20 @@ Pgbus::Engine.routes.draw do
|
|
|
48
50
|
collection do
|
|
49
51
|
post :retry_all
|
|
50
52
|
post :discard_all
|
|
53
|
+
post :discard_selected
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
resources :outbox, only: [:index], controller: "outbox"
|
|
55
|
-
resources :locks, only: [:index]
|
|
58
|
+
resources :locks, only: [:index] do
|
|
59
|
+
member do
|
|
60
|
+
post :discard
|
|
61
|
+
end
|
|
62
|
+
collection do
|
|
63
|
+
post :discard_selected
|
|
64
|
+
post :discard_all
|
|
65
|
+
end
|
|
66
|
+
end
|
|
56
67
|
resource :insights, only: [:show], controller: "insights"
|
|
57
68
|
|
|
58
69
|
get :set_locale, to: "locale#update"
|
|
@@ -0,0 +1,56 @@
|
|
|
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 MigrateJobLocksGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Migrate pgbus_job_locks to lightweight pgbus_uniqueness_keys table"
|
|
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 "migrate_job_locks_to_uniqueness_keys.rb.erb",
|
|
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
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_post_install
|
|
31
|
+
say ""
|
|
32
|
+
say "Pgbus uniqueness keys migration created!", :green
|
|
33
|
+
say ""
|
|
34
|
+
say "This migration will:"
|
|
35
|
+
say " 1. Create the new pgbus_uniqueness_keys table (3 columns, 1 index)"
|
|
36
|
+
say " 2. Migrate existing locks from pgbus_job_locks"
|
|
37
|
+
say " 3. Drop the old pgbus_job_locks table (8 columns, 3 indexes)"
|
|
38
|
+
say ""
|
|
39
|
+
say "Next steps:"
|
|
40
|
+
say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
|
|
41
|
+
say " 2. Restart pgbus: bin/pgbus start"
|
|
42
|
+
say ""
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def migration_version
|
|
48
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def separate_database?
|
|
52
|
+
options[:database].present?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class AddPgbusUniquenessKeys < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :pgbus_uniqueness_keys, id: false do |t|
|
|
4
|
+
t.string :lock_key, null: false
|
|
5
|
+
t.string :queue_name, null: false
|
|
6
|
+
t.bigint :msg_id, null: false
|
|
7
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
add_index :pgbus_uniqueness_keys, :lock_key,
|
|
11
|
+
unique: true, name: "idx_pgbus_uniqueness_keys_key"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class MigratePgbusJobLocksToUniquenessKeys < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
# Create the new lightweight uniqueness keys table
|
|
4
|
+
unless table_exists?(:pgbus_uniqueness_keys)
|
|
5
|
+
create_table :pgbus_uniqueness_keys, id: false do |t|
|
|
6
|
+
t.string :lock_key, null: false
|
|
7
|
+
t.string :queue_name, null: false
|
|
8
|
+
t.bigint :msg_id, null: false
|
|
9
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :pgbus_uniqueness_keys, :lock_key,
|
|
13
|
+
unique: true, name: "idx_pgbus_uniqueness_keys_key"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Drop the old table. Require it to be empty — active locks should be
|
|
17
|
+
# drained before migrating (stop workers, let VT expire, restart).
|
|
18
|
+
if table_exists?(:pgbus_job_locks)
|
|
19
|
+
count = execute("SELECT COUNT(*) FROM pgbus_job_locks").first["count"].to_i
|
|
20
|
+
if count > 0
|
|
21
|
+
raise "pgbus_job_locks has #{count} active lock(s). " \
|
|
22
|
+
"Drain workers and wait for locks to clear before migrating."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
drop_table :pgbus_job_locks
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def down
|
|
30
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
31
|
+
"Cannot safely reconstruct pgbus_job_locks from pgbus_uniqueness_keys"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -7,9 +7,10 @@ module Pgbus
|
|
|
7
7
|
class Executor
|
|
8
8
|
attr_reader :client, :config
|
|
9
9
|
|
|
10
|
-
def initialize(client: Pgbus.client, config: Pgbus.configuration)
|
|
10
|
+
def initialize(client: Pgbus.client, config: Pgbus.configuration, stat_buffer: nil)
|
|
11
11
|
@client = client
|
|
12
12
|
@config = config
|
|
13
|
+
@stat_buffer = stat_buffer
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def execute(message, queue_name, source_queue: nil)
|
|
@@ -29,15 +30,14 @@ module Pgbus
|
|
|
29
30
|
job_class = payload["job_class"]
|
|
30
31
|
uniqueness_key = Uniqueness.extract_key(payload)
|
|
31
32
|
uniqueness_strategy = Uniqueness.extract_strategy(payload)
|
|
32
|
-
uniqueness_ttl = payload[Uniqueness::TTL_KEY] || Uniqueness::DEFAULT_LOCK_TTL
|
|
33
33
|
|
|
34
34
|
if uniqueness_key
|
|
35
35
|
case uniqueness_strategy
|
|
36
36
|
when :until_executed
|
|
37
|
-
#
|
|
38
|
-
# The
|
|
39
|
-
#
|
|
40
|
-
|
|
37
|
+
# No claim step needed — PGMQ's visibility timeout is the execution lock.
|
|
38
|
+
# The uniqueness key row was inserted at enqueue time and will be
|
|
39
|
+
# released on completion or DLQ.
|
|
40
|
+
nil
|
|
41
41
|
when :while_executing
|
|
42
42
|
# Acquire the lock now. If another worker is already executing
|
|
43
43
|
# this job, skip it — VT will expire and it'll be retried.
|
|
@@ -96,18 +96,20 @@ module Pgbus
|
|
|
96
96
|
def record_stat(payload, queue_name, status, start_time, message: nil)
|
|
97
97
|
return unless config.stats_enabled
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
enqueue_latency_ms = compute_enqueue_latency(message)
|
|
101
|
-
retry_count = message ? [message.read_ct.to_i - 1, 0].max : 0
|
|
102
|
-
|
|
103
|
-
JobStat.record!(
|
|
99
|
+
attrs = {
|
|
104
100
|
job_class: payload&.dig("job_class") || "unknown",
|
|
105
101
|
queue_name: queue_name,
|
|
106
102
|
status: status,
|
|
107
|
-
duration_ms:
|
|
108
|
-
enqueue_latency_ms:
|
|
109
|
-
retry_count:
|
|
110
|
-
|
|
103
|
+
duration_ms: ((monotonic_now - start_time) * 1000).round,
|
|
104
|
+
enqueue_latency_ms: compute_enqueue_latency(message),
|
|
105
|
+
retry_count: message ? [message.read_ct.to_i - 1, 0].max : 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if @stat_buffer
|
|
109
|
+
@stat_buffer.push(attrs)
|
|
110
|
+
else
|
|
111
|
+
JobStat.record!(**attrs)
|
|
112
|
+
end
|
|
111
113
|
rescue StandardError => e
|
|
112
114
|
Pgbus.logger.debug { "[Pgbus] Stat recording failed: #{e.message}" }
|
|
113
115
|
end
|
|
@@ -115,18 +117,30 @@ module Pgbus
|
|
|
115
117
|
def compute_enqueue_latency(message)
|
|
116
118
|
return unless message
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
return unless
|
|
120
|
+
enqueued_at = message.enqueued_at
|
|
121
|
+
return unless enqueued_at
|
|
122
|
+
|
|
123
|
+
# Fast path: numeric epoch (float seconds) avoids Time.parse entirely.
|
|
124
|
+
# PGMQ returns enqueued_at as a Time or string depending on the driver.
|
|
125
|
+
case enqueued_at
|
|
126
|
+
when Numeric
|
|
127
|
+
[((Time.now.to_f - enqueued_at) * 1000).round, 0].max
|
|
128
|
+
when Time
|
|
129
|
+
[((Time.now.utc - enqueued_at.utc) * 1000).round, 0].max
|
|
130
|
+
else
|
|
131
|
+
parse_enqueue_latency_from_string(enqueued_at.to_s)
|
|
132
|
+
end
|
|
133
|
+
rescue ArgumentError, TypeError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
120
136
|
|
|
121
|
-
|
|
137
|
+
def parse_enqueue_latency_from_string(str)
|
|
122
138
|
# PGMQ enqueued_at is TIMESTAMPTZ (always UTC internally).
|
|
123
139
|
# If the string lacks an explicit offset, assume UTC to avoid
|
|
124
140
|
# misinterpretation when the system timezone is non-UTC.
|
|
125
141
|
str = "#{str} UTC" unless str.match?(/[+-]\d{2}:?\d{2}\s*$|Z\s*$/i)
|
|
126
142
|
enqueued_at = Time.parse(str)
|
|
127
143
|
[((Time.now.utc - enqueued_at) * 1000).round, 0].max
|
|
128
|
-
rescue ArgumentError, TypeError
|
|
129
|
-
nil
|
|
130
144
|
end
|
|
131
145
|
|
|
132
146
|
def handle_failure(_message, _queue_name, error)
|
data/lib/pgbus/client.rb
CHANGED
|
@@ -76,8 +76,7 @@ module Pgbus
|
|
|
76
76
|
def send_batch(queue_name, payloads, headers: nil, delay: 0)
|
|
77
77
|
full_name = config.queue_name(queue_name)
|
|
78
78
|
ensure_queue(queue_name)
|
|
79
|
-
serialized = payloads
|
|
80
|
-
serialized_headers = headers&.map { |h| h.nil? ? nil : serialize(h) }
|
|
79
|
+
serialized, serialized_headers = serialize_batch(payloads, headers)
|
|
81
80
|
Instrumentation.instrument("pgbus.client.send_batch", queue: full_name, size: payloads.size) do
|
|
82
81
|
synchronized { @pgmq.produce_batch(full_name, serialized, headers: serialized_headers, delay: delay) }
|
|
83
82
|
end
|
|
@@ -378,5 +377,22 @@ module Pgbus
|
|
|
378
377
|
JSON.generate(data)
|
|
379
378
|
end
|
|
380
379
|
end
|
|
380
|
+
|
|
381
|
+
# Single-pass serialization of payloads and optional headers.
|
|
382
|
+
# Avoids two separate .map iterations over the same index range.
|
|
383
|
+
def serialize_batch(payloads, headers)
|
|
384
|
+
serialized = Array.new(payloads.size)
|
|
385
|
+
serialized_headers = headers ? Array.new(headers.size) : nil
|
|
386
|
+
|
|
387
|
+
payloads.each_with_index do |p, i|
|
|
388
|
+
serialized[i] = serialize(p)
|
|
389
|
+
if serialized_headers && i < headers.size
|
|
390
|
+
h = headers[i]
|
|
391
|
+
serialized_headers[i] = h.nil? ? nil : serialize(h)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
[serialized, serialized_headers]
|
|
396
|
+
end
|
|
381
397
|
end
|
|
382
398
|
end
|
|
@@ -144,16 +144,39 @@ module Pgbus
|
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def cleanup_job_locks
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
147
|
+
# Clean up orphaned uniqueness keys whose msg_id no longer exists
|
|
148
|
+
# in any PGMQ queue. This handles the rare case where a message is
|
|
149
|
+
# lost (e.g., queue table truncated) but the uniqueness key remains.
|
|
150
|
+
reaped = reap_orphaned_uniqueness_keys
|
|
151
|
+
Pgbus.logger.info { "[Pgbus] Reaped #{reaped} orphaned uniqueness keys" } if reaped.positive?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def reap_orphaned_uniqueness_keys
|
|
155
|
+
keys = UniquenessKey.all.to_a
|
|
156
|
+
return 0 if keys.empty?
|
|
157
|
+
|
|
158
|
+
threshold = Time.current - (config.visibility_timeout * 2)
|
|
159
|
+
|
|
160
|
+
orphaned = keys.select do |key|
|
|
161
|
+
# msg_id == 0 means pre-produce placeholder or :while_executing lock.
|
|
162
|
+
# These are live locks — never reap them based on msg_id alone.
|
|
163
|
+
# Only reap if old enough that the job is certainly gone.
|
|
164
|
+
next false if key.msg_id.zero? && (!key.created_at || key.created_at >= threshold)
|
|
165
|
+
next true if key.msg_id.zero? && key.created_at && key.created_at < threshold
|
|
166
|
+
|
|
167
|
+
# For real msg_ids, only reap if stale (old enough that VT has
|
|
168
|
+
# long expired). The message itself may still be in the queue
|
|
169
|
+
# awaiting retry — age is the only safe signal without scanning
|
|
170
|
+
# every queue table.
|
|
171
|
+
key.created_at && key.created_at < threshold
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
return 0 if orphaned.empty?
|
|
175
|
+
|
|
176
|
+
UniquenessKey.where(lock_key: orphaned.map(&:lock_key)).delete_all
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
Pgbus.logger.warn { "[Pgbus] Uniqueness key cleanup failed: #{e.message}" }
|
|
179
|
+
0
|
|
157
180
|
end
|
|
158
181
|
|
|
159
182
|
def cleanup_outbox
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Pgbus
|
|
|
25
25
|
@rate_counter = RateCounter.new(:processed, :failed, :dequeued)
|
|
26
26
|
@started_at = Time.current
|
|
27
27
|
@started_at_monotonic = monotonic_now
|
|
28
|
-
@
|
|
28
|
+
@stat_buffer = config.stats_enabled ? Pgbus::StatBuffer.new : nil
|
|
29
|
+
@executor = Pgbus::ActiveJob::Executor.new(stat_buffer: @stat_buffer)
|
|
29
30
|
@pool = Concurrent::FixedThreadPool.new(threads)
|
|
30
31
|
@circuit_breaker = Pgbus::CircuitBreaker.new(config: config)
|
|
31
32
|
@queue_lock = QueueLock.new if @single_active_consumer
|
|
@@ -62,6 +63,7 @@ module Pgbus
|
|
|
62
63
|
break if @lifecycle.draining? && @pool.queue_length.zero?
|
|
63
64
|
|
|
64
65
|
claim_and_execute if @lifecycle.can_process?
|
|
66
|
+
@stat_buffer&.flush_if_due
|
|
65
67
|
@wake_signal.wait(timeout: config.polling_interval) if @lifecycle.draining? || @lifecycle.paused?
|
|
66
68
|
end
|
|
67
69
|
|
|
@@ -318,6 +320,7 @@ module Pgbus
|
|
|
318
320
|
Pgbus.logger.info { "[Pgbus] Worker draining thread pool..." }
|
|
319
321
|
@pool.shutdown
|
|
320
322
|
@pool.wait_for_termination(30)
|
|
323
|
+
@stat_buffer&.stop
|
|
321
324
|
@queue_lock&.unlock_all
|
|
322
325
|
@heartbeat&.stop
|
|
323
326
|
restore_signals
|