pgbus 0.3.3 → 0.3.5
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/Rakefile +15 -0
- 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/controllers/pgbus/recurring_tasks_controller.rb +5 -3
- data/app/frontend/pgbus/application.js +58 -1
- 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/app/views/pgbus/queues/show.html.erb +58 -21
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +2 -1
- data/config/locales/da.yml +21 -9
- data/config/locales/de.yml +21 -9
- data/config/locales/en.yml +51 -9
- data/config/locales/es.yml +21 -9
- data/config/locales/fi.yml +21 -9
- data/config/locales/fr.yml +21 -9
- data/config/locales/it.yml +21 -9
- data/config/locales/ja.yml +21 -9
- data/config/locales/nb.yml +21 -9
- data/config/locales/nl.yml +21 -9
- data/config/locales/pt.yml +21 -9
- data/config/locales/sv.yml +21 -9
- 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 +107 -0
- data/lib/pgbus/uniqueness.rb +24 -39
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +49 -18
- metadata +6 -1
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への公開待ちのトランザクショナルアウトボックスエントリ
|
|
@@ -305,23 +301,35 @@ ja:
|
|
|
305
301
|
purge_confirm: "%{name} からすべてのメッセージを削除しますか?"
|
|
306
302
|
resume: 再開
|
|
307
303
|
show:
|
|
304
|
+
arguments: 引数
|
|
308
305
|
delete_confirm: このキューを完全に削除しますか?この操作は取り消せません。
|
|
309
306
|
delete_queue: キューを削除
|
|
310
307
|
depth: 深さ:
|
|
311
308
|
discard: 破棄
|
|
312
309
|
discard_confirm: このメッセージを破棄しますか?
|
|
313
310
|
empty: キューは空です
|
|
311
|
+
full_json_payload: 完全なJSONペイロード
|
|
314
312
|
headers:
|
|
315
|
-
actions: アクション
|
|
316
313
|
enqueued: エンキュー済み
|
|
317
314
|
id: ID
|
|
318
|
-
|
|
315
|
+
job_class: ジョブクラス
|
|
319
316
|
reads: 読み取り回数
|
|
320
317
|
vt: VT
|
|
318
|
+
headers_section: ヘッダー
|
|
319
|
+
job_id: ジョブID:
|
|
321
320
|
message_discard_failed: メッセージを破棄できませんでした。
|
|
322
321
|
message_discarded: メッセージを破棄しました。
|
|
323
322
|
message_retried: メッセージの可視性をリセットしました。
|
|
324
323
|
message_retry_failed: メッセージをリトライできませんでした。
|
|
324
|
+
metadata: メタデータ
|
|
325
|
+
metadata_labels:
|
|
326
|
+
last_read: 最終読み取り:
|
|
327
|
+
locale: ロケール:
|
|
328
|
+
priority: 優先度:
|
|
329
|
+
queue: キュー:
|
|
330
|
+
scheduled: スケジュール済み:
|
|
331
|
+
timezone: タイムゾーン:
|
|
332
|
+
visible_at: 表示可能日時:
|
|
325
333
|
pause: 一時停止
|
|
326
334
|
pause_confirm: 処理を一時停止しますか?
|
|
327
335
|
purge_confirm: すべてのメッセージを削除しますか?
|
|
@@ -337,6 +345,10 @@ ja:
|
|
|
337
345
|
one: "%{count} タスクが設定されています"
|
|
338
346
|
other: "%{count} タスクが設定されています"
|
|
339
347
|
title: 定期タスク
|
|
348
|
+
toggle:
|
|
349
|
+
disabled: タスクが無効になりました
|
|
350
|
+
enabled: タスクが有効になりました
|
|
351
|
+
failed: タスクの切り替えに失敗しました
|
|
340
352
|
show:
|
|
341
353
|
back: 戻る
|
|
342
354
|
configuration: 設定
|
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
|
|
@@ -305,23 +301,35 @@ nb:
|
|
|
305
301
|
purge_confirm: Rens alle meldinger fra %{name}?
|
|
306
302
|
resume: Gjenoppta
|
|
307
303
|
show:
|
|
304
|
+
arguments: Argumenter
|
|
308
305
|
delete_confirm: Slette denne køen permanent? Dette kan ikke angres.
|
|
309
306
|
delete_queue: Slett kø
|
|
310
307
|
depth: 'Dybde:'
|
|
311
308
|
discard: Forkast
|
|
312
309
|
discard_confirm: Forkaste denne meldingen?
|
|
313
310
|
empty: Køen er tom
|
|
311
|
+
full_json_payload: Full JSON-payload
|
|
314
312
|
headers:
|
|
315
|
-
actions: Handlinger
|
|
316
313
|
enqueued: I kø
|
|
317
314
|
id: ID
|
|
318
|
-
|
|
315
|
+
job_class: Jobbklasse
|
|
319
316
|
reads: Lesninger
|
|
320
317
|
vt: VT
|
|
318
|
+
headers_section: Overskrifter
|
|
319
|
+
job_id: 'Jobb-ID:'
|
|
321
320
|
message_discard_failed: Kunne ikke forkaste meldingen.
|
|
322
321
|
message_discarded: Melding forkastet.
|
|
323
322
|
message_retried: Meldingens synlighet tilbakestilt.
|
|
324
323
|
message_retry_failed: Kunne ikke prøve meldingen igjen.
|
|
324
|
+
metadata: Metadata
|
|
325
|
+
metadata_labels:
|
|
326
|
+
last_read: 'Sist lest:'
|
|
327
|
+
locale: 'Språk:'
|
|
328
|
+
priority: 'Prioritet:'
|
|
329
|
+
queue: 'Kø:'
|
|
330
|
+
scheduled: 'Planlagt:'
|
|
331
|
+
timezone: 'Tidssone:'
|
|
332
|
+
visible_at: 'Synlig fra:'
|
|
325
333
|
pause: Pause
|
|
326
334
|
pause_confirm: Pause behandling?
|
|
327
335
|
purge_confirm: Rens alle meldinger?
|
|
@@ -337,6 +345,10 @@ nb:
|
|
|
337
345
|
one: "%{count} oppgave konfigurert"
|
|
338
346
|
other: "%{count} oppgaver konfigurert"
|
|
339
347
|
title: Gjentakende oppgaver
|
|
348
|
+
toggle:
|
|
349
|
+
disabled: Oppgave deaktivert
|
|
350
|
+
enabled: Oppgave aktivert
|
|
351
|
+
failed: Kunne ikke endre oppgave
|
|
340
352
|
show:
|
|
341
353
|
back: Tilbake
|
|
342
354
|
configuration: Konfigurasjon
|
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
|
|
@@ -305,23 +301,35 @@ nl:
|
|
|
305
301
|
purge_confirm: Alle berichten verwijderen uit %{name}?
|
|
306
302
|
resume: Hervatten
|
|
307
303
|
show:
|
|
304
|
+
arguments: Argumenten
|
|
308
305
|
delete_confirm: Deze wachtrij permanent verwijderen? Dit kan niet ongedaan worden gemaakt.
|
|
309
306
|
delete_queue: Wachtrij verwijderen
|
|
310
307
|
depth: 'Diepte:'
|
|
311
308
|
discard: Verwerpen
|
|
312
309
|
discard_confirm: Dit bericht verwerpen?
|
|
313
310
|
empty: Wachtrij is leeg
|
|
311
|
+
full_json_payload: Volledige JSON payload
|
|
314
312
|
headers:
|
|
315
|
-
actions: Acties
|
|
316
313
|
enqueued: In de wachtrij geplaatst
|
|
317
314
|
id: ID
|
|
318
|
-
|
|
315
|
+
job_class: Taakklasse
|
|
319
316
|
reads: Lezingen
|
|
320
317
|
vt: VT
|
|
318
|
+
headers_section: Headers
|
|
319
|
+
job_id: 'Taak ID:'
|
|
321
320
|
message_discard_failed: Kon bericht niet verwerpen.
|
|
322
321
|
message_discarded: Bericht verworpen.
|
|
323
322
|
message_retried: Zichtbaarheid van bericht gereset.
|
|
324
323
|
message_retry_failed: Kon bericht niet opnieuw proberen.
|
|
324
|
+
metadata: Metadata
|
|
325
|
+
metadata_labels:
|
|
326
|
+
last_read: 'Laatst gelezen:'
|
|
327
|
+
locale: 'Locale:'
|
|
328
|
+
priority: 'Prioriteit:'
|
|
329
|
+
queue: 'Wachtrij:'
|
|
330
|
+
scheduled: 'Gepland:'
|
|
331
|
+
timezone: 'Tijdzone:'
|
|
332
|
+
visible_at: 'Zichtbaar op:'
|
|
325
333
|
pause: Pauzeren
|
|
326
334
|
pause_confirm: Verwerking pauzeren?
|
|
327
335
|
purge_confirm: Alle berichten verwijderen?
|
|
@@ -337,6 +345,10 @@ nl:
|
|
|
337
345
|
one: "%{count} taak geconfigureerd"
|
|
338
346
|
other: "%{count} taken geconfigureerd"
|
|
339
347
|
title: Terugkerende taken
|
|
348
|
+
toggle:
|
|
349
|
+
disabled: Taak uitgeschakeld
|
|
350
|
+
enabled: Taak ingeschakeld
|
|
351
|
+
failed: Kon taak niet omschakelen
|
|
340
352
|
show:
|
|
341
353
|
back: Terug
|
|
342
354
|
configuration: Configuratie
|
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
|
|
@@ -305,23 +301,35 @@ pt:
|
|
|
305
301
|
purge_confirm: Limpar todas as mensagens de %{name}?
|
|
306
302
|
resume: Retomar
|
|
307
303
|
show:
|
|
304
|
+
arguments: Argumentos
|
|
308
305
|
delete_confirm: Excluir permanentemente esta fila? Esta ação não pode ser desfeita.
|
|
309
306
|
delete_queue: Excluir fila
|
|
310
307
|
depth: 'Profundidade:'
|
|
311
308
|
discard: Descartar
|
|
312
309
|
discard_confirm: Descartar esta mensagem?
|
|
313
310
|
empty: Fila está vazia
|
|
311
|
+
full_json_payload: Carga JSON completa
|
|
314
312
|
headers:
|
|
315
|
-
actions: Ações
|
|
316
313
|
enqueued: Enfileirado
|
|
317
314
|
id: ID
|
|
318
|
-
|
|
315
|
+
job_class: Classe do Trabalho
|
|
319
316
|
reads: Leituras
|
|
320
317
|
vt: VT
|
|
318
|
+
headers_section: Cabeçalhos
|
|
319
|
+
job_id: 'ID do Trabalho:'
|
|
321
320
|
message_discard_failed: Não foi possível descartar a mensagem.
|
|
322
321
|
message_discarded: Mensagem descartada.
|
|
323
322
|
message_retried: Visibilidade da mensagem redefinida.
|
|
324
323
|
message_retry_failed: Não foi possível tentar novamente a mensagem.
|
|
324
|
+
metadata: Metadados
|
|
325
|
+
metadata_labels:
|
|
326
|
+
last_read: 'Última leitura:'
|
|
327
|
+
locale: 'Localidade:'
|
|
328
|
+
priority: 'Prioridade:'
|
|
329
|
+
queue: 'Fila:'
|
|
330
|
+
scheduled: 'Agendado:'
|
|
331
|
+
timezone: 'Fuso horário:'
|
|
332
|
+
visible_at: 'Visível em:'
|
|
325
333
|
pause: Pausar
|
|
326
334
|
pause_confirm: Pausar processamento?
|
|
327
335
|
purge_confirm: Limpar todas as mensagens?
|
|
@@ -337,6 +345,10 @@ pt:
|
|
|
337
345
|
one: "%{count} tarefa configurada"
|
|
338
346
|
other: "%{count} tarefas configuradas"
|
|
339
347
|
title: Tarefas Recorrentes
|
|
348
|
+
toggle:
|
|
349
|
+
disabled: Tarefa desativada
|
|
350
|
+
enabled: Tarefa ativada
|
|
351
|
+
failed: Falha ao alternar tarefa
|
|
340
352
|
show:
|
|
341
353
|
back: Voltar
|
|
342
354
|
configuration: Configuração
|
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
|
|
@@ -305,23 +301,35 @@ sv:
|
|
|
305
301
|
purge_confirm: Rensa alla meddelanden från %{name}?
|
|
306
302
|
resume: Återuppta
|
|
307
303
|
show:
|
|
304
|
+
arguments: Argument
|
|
308
305
|
delete_confirm: Ta bort denna kö permanent? Detta kan inte ångras.
|
|
309
306
|
delete_queue: Ta bort kö
|
|
310
307
|
depth: 'Djup:'
|
|
311
308
|
discard: Kassera
|
|
312
309
|
discard_confirm: Kassera detta meddelande?
|
|
313
310
|
empty: Kön är tom
|
|
311
|
+
full_json_payload: Fullständig JSON-payload
|
|
314
312
|
headers:
|
|
315
|
-
actions: Åtgärder
|
|
316
313
|
enqueued: Inlagd
|
|
317
314
|
id: ID
|
|
318
|
-
|
|
315
|
+
job_class: Jobbklass
|
|
319
316
|
reads: Läsningar
|
|
320
317
|
vt: VT
|
|
318
|
+
headers_section: Headers
|
|
319
|
+
job_id: 'Jobb-ID:'
|
|
321
320
|
message_discard_failed: Kunde inte kassera meddelandet.
|
|
322
321
|
message_discarded: Meddelande kasserat.
|
|
323
322
|
message_retried: Meddelandets synlighet återställd.
|
|
324
323
|
message_retry_failed: Kunde inte försöka igen med meddelandet.
|
|
324
|
+
metadata: Metadata
|
|
325
|
+
metadata_labels:
|
|
326
|
+
last_read: 'Senast läst:'
|
|
327
|
+
locale: 'Språk:'
|
|
328
|
+
priority: 'Prioritet:'
|
|
329
|
+
queue: 'Kö:'
|
|
330
|
+
scheduled: 'Schemalagt:'
|
|
331
|
+
timezone: 'Tidszon:'
|
|
332
|
+
visible_at: 'Synlig vid:'
|
|
325
333
|
pause: Pausa
|
|
326
334
|
pause_confirm: Pausa bearbetning?
|
|
327
335
|
purge_confirm: Rensa alla meddelanden?
|
|
@@ -337,6 +345,10 @@ sv:
|
|
|
337
345
|
one: "%{count} uppgift konfigurerad"
|
|
338
346
|
other: "%{count} uppgifter konfigurerade"
|
|
339
347
|
title: Återkommande uppgifter
|
|
348
|
+
toggle:
|
|
349
|
+
disabled: Uppgift inaktiverad
|
|
350
|
+
enabled: Uppgift aktiverad
|
|
351
|
+
failed: Kunde inte ändra uppgift
|
|
340
352
|
show:
|
|
341
353
|
back: Tillbaka
|
|
342
354
|
configuration: Konfiguration
|
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
|