pgbus 0.20.0 → 0.20.1
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/CHANGELOG.md +3 -0
- data/README.md +3 -2
- data/app/frontend/pgbus/modules/charts.js +18 -0
- data/app/frontend/pgbus/modules/charts.min.js +1 -1
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -1
- data/app/helpers/pgbus/workflows_helper.rb +2 -2
- data/app/models/pgbus/blocked_execution.rb +12 -0
- data/app/models/pgbus/uniqueness_key.rb +88 -3
- data/app/models/pgbus/workflow_step.rb +7 -0
- data/app/views/pgbus/workflows/_timeline.html.erb +11 -1
- data/config/locales/da.yml +4 -1
- data/config/locales/de.yml +4 -1
- data/config/locales/en.yml +4 -1
- data/config/locales/es.yml +4 -1
- data/config/locales/fi.yml +4 -1
- data/config/locales/fr.yml +4 -1
- data/config/locales/it.yml +4 -1
- data/config/locales/ja.yml +4 -1
- data/config/locales/nb.yml +4 -1
- data/config/locales/nl.yml +4 -1
- data/config/locales/pt.yml +4 -1
- data/config/locales/sv.yml +4 -1
- data/lib/pgbus/active_job/adapter.rb +81 -3
- data/lib/pgbus/active_job/delivery.rb +20 -6
- data/lib/pgbus/active_job/executor.rb +18 -8
- data/lib/pgbus/concurrency/blocked_execution.rb +14 -6
- data/lib/pgbus/configuration.rb +43 -1
- data/lib/pgbus/dead_letter_header.rb +9 -1
- data/lib/pgbus/instrumentation.rb +3 -2
- data/lib/pgbus/integrations/appsignal/dashboard.json +30 -1
- data/lib/pgbus/metrics/workflow_metrics.rb +11 -1
- data/lib/pgbus/process/consumer.rb +2 -1
- data/lib/pgbus/process/dispatcher.rb +13 -3
- data/lib/pgbus/process/worker.rb +10 -1
- data/lib/pgbus/recurring/schedule.rb +1 -1
- data/lib/pgbus/streams/presence.rb +65 -36
- data/lib/pgbus/uniqueness.rb +23 -6
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/workflow_list.rb +50 -7
- data/lib/pgbus/web/data_source.rb +123 -15
- data/lib/pgbus/web/workflow_state.rb +16 -3
- data/lib/pgbus/web/workflow_timeline.rb +23 -6
- data/lib/pgbus/workflow/step_definition.rb +4 -2
- data/lib/pgbus/workflow/wait_timeout.rb +25 -4
- data/sig/pgbus/configuration.rbs +1 -0
- metadata +1 -1
data/config/locales/it.yml
CHANGED
|
@@ -1349,6 +1349,8 @@ it:
|
|
|
1349
1349
|
unknown_html: 'Stato: %{status}.'
|
|
1350
1350
|
waiting_batch_html: In attesa del lotto %{batch} (%{done} lavori su %{total} completati).
|
|
1351
1351
|
waiting_signal_html: In attesa del segnale %{signal} da %{ago}.
|
|
1352
|
+
waiting_signal_late_html: In attesa del segnale %{signal} da %{ago} (in ritardo da %{late}). Il prossimo avviso di ritardo parte %{time}.
|
|
1353
|
+
waiting_signal_notify_html: In attesa del segnale %{signal} da %{ago}. Un avviso di ritardo parte %{time} e l'attesa continua.
|
|
1352
1354
|
waiting_signal_timeout_html: In attesa del segnale %{signal} da %{ago}. L'attesa scade %{time}.
|
|
1353
1355
|
waiting_step_html: In attesa di %{step}.
|
|
1354
1356
|
not_found:
|
|
@@ -1451,7 +1453,7 @@ it:
|
|
|
1451
1453
|
ready: Pronti
|
|
1452
1454
|
waiting: In attesa
|
|
1453
1455
|
timeline:
|
|
1454
|
-
caption: 'Ogni tentativo, il più vecchio in alto: atteso (la sua attesa), in coda (previsto, non ancora preso), eseguito (colorato in base a come è finito) e in attesa (di un segnale o del suo lotto), con le pause su una riga a parte. La tabella dei tentativi qui sotto riporta le stesse durate.'
|
|
1456
|
+
caption: 'Ogni tentativo, il più vecchio in alto: atteso (la sua attesa), in coda (previsto, non ancora preso), eseguito (colorato in base a come è finito) e in attesa (di un segnale o del suo lotto), con le pause su una riga a parte. La tabella dei tentativi qui sotto riporta le stesse durate. Una linea tratteggiata segna ogni volta che un''attesa ha superato la scadenza.'
|
|
1455
1457
|
empty: Ancora nessun tentativo.
|
|
1456
1458
|
paused_period_html: In pausa dalle %{from} per %{duration}
|
|
1457
1459
|
paused_row: In pausa
|
|
@@ -1465,3 +1467,4 @@ it:
|
|
|
1465
1467
|
waited: Atteso
|
|
1466
1468
|
waiting: In attesa
|
|
1467
1469
|
title: Cronologia
|
|
1470
|
+
wait_timeout_html: 'L''attesa di %{step} ha superato la scadenza alle %{time} (on_timeout: %{action}).'
|
data/config/locales/ja.yml
CHANGED
|
@@ -1349,6 +1349,8 @@ ja:
|
|
|
1349
1349
|
unknown_html: 'ステータス: %{status}。'
|
|
1350
1350
|
waiting_batch_html: バッチ %{batch} を待機中です(%{total} 件中 %{done} 件のジョブが完了)。
|
|
1351
1351
|
waiting_signal_html: シグナル %{signal} を %{ago} から待機中です。
|
|
1352
|
+
waiting_signal_late_html: シグナル %{signal} を %{ago} から待機中です(%{late} から遅延)。次の遅延通知は %{time} に送られます。
|
|
1353
|
+
waiting_signal_notify_html: シグナル %{signal} を %{ago} から待機中です。遅延通知は %{time} に送られ、待機は続きます。
|
|
1352
1354
|
waiting_signal_timeout_html: シグナル %{signal} を %{ago} から待機中です。待機は %{time} にタイムアウトします。
|
|
1353
1355
|
waiting_step_html: "%{step} を待機中です。"
|
|
1354
1356
|
not_found:
|
|
@@ -1451,7 +1453,7 @@ ja:
|
|
|
1451
1453
|
ready: 実行待ち
|
|
1452
1454
|
waiting: 待機中
|
|
1453
1455
|
timeline:
|
|
1454
|
-
caption:
|
|
1456
|
+
caption: 各試行を古い順に表示します。待機(待ち時間)、キュー(期限到来、未取得)、実行(終了状態で色分け)、待機中(シグナルまたはバッチ待ち)で、一時停止は別の行に表示します。下の試行テーブルにも同じ時間が載っています。破線は待機が期限を過ぎた時点を示します。
|
|
1455
1457
|
empty: 試行はまだありません。
|
|
1456
1458
|
paused_period_html: "%{from} から %{duration} 一時停止"
|
|
1457
1459
|
paused_row: 一時停止
|
|
@@ -1465,3 +1467,4 @@ ja:
|
|
|
1465
1467
|
waited: 待機済み
|
|
1466
1468
|
waiting: 待機中
|
|
1467
1469
|
title: タイムライン
|
|
1470
|
+
wait_timeout_html: "%{step} の待機が %{time} に期限を過ぎました(on_timeout: %{action})。"
|
data/config/locales/nb.yml
CHANGED
|
@@ -1349,6 +1349,8 @@ nb:
|
|
|
1349
1349
|
unknown_html: 'Status: %{status}.'
|
|
1350
1350
|
waiting_batch_html: Venter på gruppen %{batch} (%{done} av %{total} jobber ferdige).
|
|
1351
1351
|
waiting_signal_html: Venter på signalet %{signal} siden %{ago}.
|
|
1352
|
+
waiting_signal_late_html: Venter på signalet %{signal} siden %{ago} (forsinket siden %{late}). Neste forsinkelsesvarsel sendes %{time}.
|
|
1353
|
+
waiting_signal_notify_html: Venter på signalet %{signal} siden %{ago}. Et forsinkelsesvarsel sendes %{time}, og ventingen fortsetter.
|
|
1352
1354
|
waiting_signal_timeout_html: Venter på signalet %{signal} siden %{ago}. Ventetiden utløper %{time}.
|
|
1353
1355
|
waiting_step_html: Venter på %{step}.
|
|
1354
1356
|
not_found:
|
|
@@ -1451,7 +1453,7 @@ nb:
|
|
|
1451
1453
|
ready: Klare
|
|
1452
1454
|
waiting: Venter
|
|
1453
1455
|
timeline:
|
|
1454
|
-
caption: 'Hvert forsøk, eldst øverst: ventet (ventetiden), i kø (forfalt, ikke hentet ennå), kjørte (farget etter utfall) og venter (på et signal eller sin gruppe), med pausene på en egen rad. Forsøkstabellen under viser de samme varighetene.'
|
|
1456
|
+
caption: 'Hvert forsøk, eldst øverst: ventet (ventetiden), i kø (forfalt, ikke hentet ennå), kjørte (farget etter utfall) og venter (på et signal eller sin gruppe), med pausene på en egen rad. Forsøkstabellen under viser de samme varighetene. En stiplet linje markerer hver gang en ventetid passerte fristen.'
|
|
1455
1457
|
empty: Ingen forsøk ennå.
|
|
1456
1458
|
paused_period_html: Satt på pause %{from} i %{duration}
|
|
1457
1459
|
paused_row: På pause
|
|
@@ -1465,3 +1467,4 @@ nb:
|
|
|
1465
1467
|
waited: Ventet
|
|
1466
1468
|
waiting: Venter
|
|
1467
1469
|
title: Tidslinje
|
|
1470
|
+
wait_timeout_html: 'Ventetiden for %{step} passerte fristen %{time} (on_timeout: %{action}).'
|
data/config/locales/nl.yml
CHANGED
|
@@ -1349,6 +1349,8 @@ nl:
|
|
|
1349
1349
|
unknown_html: 'Status: %{status}.'
|
|
1350
1350
|
waiting_batch_html: Wacht op groep %{batch} (%{done} van %{total} taken klaar).
|
|
1351
1351
|
waiting_signal_html: Wacht op signaal %{signal}, gestart %{ago}.
|
|
1352
|
+
waiting_signal_late_html: Wacht op signaal %{signal}, gestart %{ago} (te laat sinds %{late}). De volgende melding over de vertraging gaat %{time} uit.
|
|
1353
|
+
waiting_signal_notify_html: Wacht op signaal %{signal}, gestart %{ago}. Een melding over de vertraging gaat %{time} uit, en het wachten gaat door.
|
|
1352
1354
|
waiting_signal_timeout_html: Wacht op signaal %{signal}, gestart %{ago}. De wachttijd verloopt %{time}.
|
|
1353
1355
|
waiting_step_html: Wacht op %{step}.
|
|
1354
1356
|
not_found:
|
|
@@ -1451,7 +1453,7 @@ nl:
|
|
|
1451
1453
|
ready: Klaar
|
|
1452
1454
|
waiting: Wachtend
|
|
1453
1455
|
timeline:
|
|
1454
|
-
caption: 'Elke poging, oudste bovenaan: gewacht (de wachttijd), in wachtrij (aan de beurt, nog niet opgepakt), uitgevoerd (gekleurd naar hoe het afliep) en wachtend (op een signaal of de groep), met de pauzes op een eigen rij. De pogingentabel hieronder toont dezelfde duren.'
|
|
1456
|
+
caption: 'Elke poging, oudste bovenaan: gewacht (de wachttijd), in wachtrij (aan de beurt, nog niet opgepakt), uitgevoerd (gekleurd naar hoe het afliep) en wachtend (op een signaal of de groep), met de pauzes op een eigen rij. De pogingentabel hieronder toont dezelfde duren. Een stippellijn markeert elke keer dat een wachttijd zijn deadline passeerde.'
|
|
1455
1457
|
empty: Nog geen pogingen.
|
|
1456
1458
|
paused_period_html: Gepauzeerd op %{from} gedurende %{duration}
|
|
1457
1459
|
paused_row: Gepauzeerd
|
|
@@ -1465,3 +1467,4 @@ nl:
|
|
|
1465
1467
|
waited: Gewacht
|
|
1466
1468
|
waiting: Wachtend
|
|
1467
1469
|
title: Tijdlijn
|
|
1470
|
+
wait_timeout_html: 'De wachttijd van %{step} verstreek op %{time} (on_timeout: %{action}).'
|
data/config/locales/pt.yml
CHANGED
|
@@ -1349,6 +1349,8 @@ pt:
|
|
|
1349
1349
|
unknown_html: 'Status: %{status}.'
|
|
1350
1350
|
waiting_batch_html: Aguardando o lote %{batch} (%{done} de %{total} trabalhos concluídos).
|
|
1351
1351
|
waiting_signal_html: Aguardando o sinal %{signal} desde %{ago}.
|
|
1352
|
+
waiting_signal_late_html: Aguardando o sinal %{signal} desde %{ago} (atrasado desde %{late}). O próximo aviso de atraso sai %{time}.
|
|
1353
|
+
waiting_signal_notify_html: Aguardando o sinal %{signal} desde %{ago}. Um aviso de atraso sai %{time} e a espera continua.
|
|
1352
1354
|
waiting_signal_timeout_html: Aguardando o sinal %{signal} desde %{ago}. A espera expira %{time}.
|
|
1353
1355
|
waiting_step_html: Aguardando %{step}.
|
|
1354
1356
|
not_found:
|
|
@@ -1451,7 +1453,7 @@ pt:
|
|
|
1451
1453
|
ready: Prontos
|
|
1452
1454
|
waiting: Aguardando
|
|
1453
1455
|
timeline:
|
|
1454
|
-
caption: 'Cada tentativa, a mais antiga no topo: esperou (sua espera), na fila (vencida, ainda não pega), executou (colorida conforme terminou) e aguardando (um sinal ou seu lote), com as pausas em linha própria. A tabela de tentativas abaixo lista as mesmas durações.'
|
|
1456
|
+
caption: 'Cada tentativa, a mais antiga no topo: esperou (sua espera), na fila (vencida, ainda não pega), executou (colorida conforme terminou) e aguardando (um sinal ou seu lote), com as pausas em linha própria. A tabela de tentativas abaixo lista as mesmas durações. Uma linha tracejada marca cada vez que uma espera passou do prazo.'
|
|
1455
1457
|
empty: Ainda não há tentativas.
|
|
1456
1458
|
paused_period_html: Em pausa desde %{from} durante %{duration}
|
|
1457
1459
|
paused_row: Em pausa
|
|
@@ -1465,3 +1467,4 @@ pt:
|
|
|
1465
1467
|
waited: Esperou
|
|
1466
1468
|
waiting: Aguardando
|
|
1467
1469
|
title: Linha do tempo
|
|
1470
|
+
wait_timeout_html: 'A espera de %{step} passou do prazo em %{time} (on_timeout: %{action}).'
|
data/config/locales/sv.yml
CHANGED
|
@@ -1349,6 +1349,8 @@ sv:
|
|
|
1349
1349
|
unknown_html: 'Status: %{status}.'
|
|
1350
1350
|
waiting_batch_html: Väntar på gruppen %{batch} (%{done} av %{total} jobb klara).
|
|
1351
1351
|
waiting_signal_html: Väntar på signalen %{signal} sedan %{ago}.
|
|
1352
|
+
waiting_signal_late_html: Väntar på signalen %{signal} sedan %{ago} (sen sedan %{late}). Nästa förseningsavisering går ut %{time}.
|
|
1353
|
+
waiting_signal_notify_html: Väntar på signalen %{signal} sedan %{ago}. En förseningsavisering går ut %{time}, och väntan fortsätter.
|
|
1352
1354
|
waiting_signal_timeout_html: Väntar på signalen %{signal} sedan %{ago}. Väntan tar slut %{time}.
|
|
1353
1355
|
waiting_step_html: Väntar på %{step}.
|
|
1354
1356
|
not_found:
|
|
@@ -1451,7 +1453,7 @@ sv:
|
|
|
1451
1453
|
ready: Redo
|
|
1452
1454
|
waiting: Väntar
|
|
1453
1455
|
timeline:
|
|
1454
|
-
caption: 'Varje försök, äldst överst: väntade (dess väntetid), köad (förfallen, ännu inte hämtad), körde (färgad efter utfall) och väntar (på en signal eller sin grupp), med pauserna på en egen rad. Försökstabellen nedan visar samma tider.'
|
|
1456
|
+
caption: 'Varje försök, äldst överst: väntade (dess väntetid), köad (förfallen, ännu inte hämtad), körde (färgad efter utfall) och väntar (på en signal eller sin grupp), med pauserna på en egen rad. Försökstabellen nedan visar samma tider. En streckad linje markerar varje gång en väntan passerade sin tidsgräns.'
|
|
1455
1457
|
empty: Inga försök ännu.
|
|
1456
1458
|
paused_period_html: Pausad %{from} i %{duration}
|
|
1457
1459
|
paused_row: Pausad
|
|
@@ -1465,3 +1467,4 @@ sv:
|
|
|
1465
1467
|
waited: Väntade
|
|
1466
1468
|
waiting: Väntar
|
|
1467
1469
|
title: Tidslinje
|
|
1470
|
+
wait_timeout_html: 'Väntan för %{step} passerade sin tidsgräns %{time} (on_timeout: %{action}).'
|
|
@@ -80,6 +80,7 @@ module Pgbus
|
|
|
80
80
|
# reset: once the send returns, msg_id is set and the rescue routes
|
|
81
81
|
# on that instead.
|
|
82
82
|
sending = false
|
|
83
|
+
retry_lock = hand_lock_to_retry(active_job, payload_hash, queue)
|
|
83
84
|
|
|
84
85
|
if key && concurrency
|
|
85
86
|
# The check and the park commit together, under the semaphore row
|
|
@@ -115,6 +116,9 @@ module Pgbus
|
|
|
115
116
|
# Bind before backfill so a live message is never left with an unbound
|
|
116
117
|
# uniqueness row if execution-row bookkeeping raises.
|
|
117
118
|
bind_acquired_uniqueness_lock(queue, msg_id) if msg_id
|
|
119
|
+
bind_retry_lock(retry_lock, queue, msg_id) if msg_id
|
|
120
|
+
# Dropped by its concurrency limit: no retry will run, the attempt does.
|
|
121
|
+
return_retry_lock(retry_lock) unless msg_id || blocked
|
|
118
122
|
Batch.backfill_execution(payload_hash, msg_id, physical_queue(queue, priority)) if msg_id
|
|
119
123
|
# A retry re-enqueue that is now live (sent, or parked as a blocked
|
|
120
124
|
# execution) must stop the original attempt from signalling completion.
|
|
@@ -135,6 +139,7 @@ module Pgbus
|
|
|
135
139
|
# database error from the slot upsert or the park is not ambiguous.
|
|
136
140
|
if msg_id.nil? && !(sending && ambiguous_delivery?(e))
|
|
137
141
|
rollback_acquired_uniqueness_lock
|
|
142
|
+
return_retry_lock(retry_lock)
|
|
138
143
|
uncount_batch_job(payload_hash, active_job)
|
|
139
144
|
else
|
|
140
145
|
# Drop the thread-local so a later discard on this thread cannot
|
|
@@ -247,9 +252,10 @@ module Pgbus
|
|
|
247
252
|
# the executor still holds the key (it releases only on success/DLQ). So
|
|
248
253
|
# a re-enqueue with executions > 0 would otherwise hit its own held key,
|
|
249
254
|
# be rejected as a duplicate (JobNotUnique), and dead-letter the original
|
|
250
|
-
# while losing the retry. Let the retry through; the
|
|
251
|
-
#
|
|
252
|
-
#
|
|
255
|
+
# while losing the retry. Let the retry through (issue #333); the key
|
|
256
|
+
# row is handed to it (hand_lock_to_retry, then bind_retry_lock once
|
|
257
|
+
# sent), so the attempt's release leaves it held until the job finally
|
|
258
|
+
# succeeds or dead-letters (issue #564).
|
|
253
259
|
return false if active_job.executions.to_i.positive?
|
|
254
260
|
|
|
255
261
|
result = Uniqueness.acquire_enqueue_lock(uniqueness_key, active_job, queue_name: queue)
|
|
@@ -285,6 +291,78 @@ module Pgbus
|
|
|
285
291
|
Pgbus.logger.warn { "[Pgbus] Uniqueness bind failed: #{e.message}" }
|
|
286
292
|
end
|
|
287
293
|
|
|
294
|
+
# The job's :until_executed key and the delivery of the attempt running
|
|
295
|
+
# it, when this enqueue is that attempt's retry: the retry is let
|
|
296
|
+
# through the key the attempt holds and never acquires one, so the key
|
|
297
|
+
# row has to follow it. nil for a first attempt (it acquired its own
|
|
298
|
+
# row), a retry enqueued outside the executor (no attempt to take it
|
|
299
|
+
# from), or a job run inline inside another job, whose delivery it sees.
|
|
300
|
+
def retry_uniqueness_lock(active_job, payload_hash)
|
|
301
|
+
key = Uniqueness.extract_key(payload_hash)
|
|
302
|
+
return unless key && Uniqueness.extract_strategy(payload_hash) == :until_executed
|
|
303
|
+
return unless active_job.executions.to_i.positive?
|
|
304
|
+
|
|
305
|
+
delivery = Delivery.current
|
|
306
|
+
[key, delivery] if delivery && delivery[:job_id] == active_job.job_id
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Take the attempt's row for its retry before the retry exists — sent or
|
|
310
|
+
# parked — the way a first enqueue acquires before its send (see
|
|
311
|
+
# UniquenessKey.unbind!). Returns [key, delivery, stamp] for
|
|
312
|
+
# bind_retry_lock / return_retry_lock, or nil when there is nothing to
|
|
313
|
+
# hand over. A failure is logged and the retry goes out without the row,
|
|
314
|
+
# which then stays with the attempt.
|
|
315
|
+
def hand_lock_to_retry(active_job, payload_hash, queue)
|
|
316
|
+
key, delivery = retry_uniqueness_lock(active_job, payload_hash)
|
|
317
|
+
return unless key
|
|
318
|
+
|
|
319
|
+
stamp = UniquenessKey.unbind!(key, from_queue_name: Uniqueness.lock_queue_name(delivery[:queue_name]),
|
|
320
|
+
from_msg_id: delivery[:msg_id], from_enqueued_at: delivery[:enqueued_at],
|
|
321
|
+
queue_name: Uniqueness.lock_queue_name(queue))
|
|
322
|
+
return [key, delivery, stamp] if stamp
|
|
323
|
+
|
|
324
|
+
warn_retry_lock_not_held(key)
|
|
325
|
+
nil
|
|
326
|
+
rescue StandardError => e
|
|
327
|
+
Pgbus.logger.warn { "[Pgbus] Uniqueness handoff to retry failed for #{key}: #{e.message}" }
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Point the handed-over row at the sent retry. The retry is live, so a
|
|
332
|
+
# failure is logged, not raised: the row stays unbound and stamped
|
|
333
|
+
# before the send, and the retry's own release takes it.
|
|
334
|
+
def bind_retry_lock(retry_lock, queue, msg_id)
|
|
335
|
+
return unless retry_lock
|
|
336
|
+
|
|
337
|
+
key, _delivery, stamp = retry_lock
|
|
338
|
+
UniquenessKey.bind!(key, queue_name: Uniqueness.lock_queue_name(queue), msg_id: msg_id, acquired_at: stamp)
|
|
339
|
+
rescue StandardError => e
|
|
340
|
+
Pgbus.logger.warn { "[Pgbus] Uniqueness bind to retry failed for #{key}: #{e.message}" }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# No retry came of it (dropped by its concurrency limit, or the send
|
|
344
|
+
# failed before any message existed): bind the row back to the attempt,
|
|
345
|
+
# which runs on and releases it when it finishes.
|
|
346
|
+
def return_retry_lock(retry_lock)
|
|
347
|
+
return unless retry_lock
|
|
348
|
+
|
|
349
|
+
key, delivery, stamp = retry_lock
|
|
350
|
+
UniquenessKey.bind!(key, queue_name: Uniqueness.lock_queue_name(delivery[:queue_name]),
|
|
351
|
+
msg_id: delivery[:msg_id], acquired_at: stamp)
|
|
352
|
+
rescue StandardError => e
|
|
353
|
+
Pgbus.logger.warn { "[Pgbus] Uniqueness return to the attempt failed for #{key}: #{e.message}" }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# The row is gone (released by an operator, reaped) or a successor's:
|
|
357
|
+
# the retry runs without the key, and a fresh enqueue is not deduped
|
|
358
|
+
# against it.
|
|
359
|
+
def warn_retry_lock_not_held(key)
|
|
360
|
+
Pgbus.logger.warn do
|
|
361
|
+
"[Pgbus] Uniqueness key #{key} is no longer held by the attempt that enqueued its retry; " \
|
|
362
|
+
"the retry runs without it"
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
288
366
|
# Reverses inject_batch_metadata for a job discarded at enqueue time
|
|
289
367
|
# (uniqueness duplicate or concurrency :discard conflict): the message is
|
|
290
368
|
# never sent, so it can never signal completion. Only applies to a job
|
|
@@ -4,23 +4,30 @@ module Pgbus
|
|
|
4
4
|
module ActiveJob
|
|
5
5
|
# The PGMQ delivery of the job running in this fiber:
|
|
6
6
|
#
|
|
7
|
-
# Pgbus::ActiveJob::Delivery.current
|
|
7
|
+
# Pgbus::ActiveJob::Delivery.current
|
|
8
|
+
# # => { msg_id: 42, read_ct: 2, queue_name: "default",
|
|
9
|
+
# # enqueued_at: "2026-10-18 10:30:00+00", job_id: "8f0c…" }
|
|
8
10
|
#
|
|
9
11
|
# Set by Executor#execute around perform_now and cleared after, also when
|
|
10
12
|
# the job raises; nil outside the executor (perform_now in a test, a
|
|
11
13
|
# top-level inline-adapter job). A job run inline from inside an executing
|
|
12
14
|
# job sees the enclosing job's delivery; StepJob only acts on it when the
|
|
13
|
-
# step row's msg_id matches,
|
|
15
|
+
# step row's msg_id matches, and the adapter only when job_id is its own,
|
|
16
|
+
# so that is harmless. Fiber-local (Thread.current[]), so concurrent jobs under
|
|
14
17
|
# execution_mode: :async each see their own. Pgbus::Workflow::StepJob reads
|
|
15
|
-
# read_ct to tell a redelivery after a worker died from a first delivery
|
|
18
|
+
# read_ct to tell a redelivery after a worker died from a first delivery;
|
|
19
|
+
# the adapter reads the message identity and enqueued_at to move the
|
|
20
|
+
# job's uniqueness lock onto a retry the job enqueues (issue #564).
|
|
16
21
|
#
|
|
17
|
-
# The
|
|
22
|
+
# The values sit in one fiber-local each and the Hash is built only
|
|
18
23
|
# when read: setting them allocates nothing, which keeps the executor's
|
|
19
24
|
# per-job allocation budget (spec/pgbus/active_job/executor_allocation_budget_spec.rb).
|
|
20
25
|
module Delivery
|
|
21
26
|
MSG_ID_KEY = :pgbus_delivery_msg_id
|
|
22
27
|
READ_CT_KEY = :pgbus_delivery_read_ct
|
|
23
28
|
QUEUE_NAME_KEY = :pgbus_delivery_queue_name
|
|
29
|
+
ENQUEUED_AT_KEY = :pgbus_delivery_enqueued_at
|
|
30
|
+
JOB_ID_KEY = :pgbus_delivery_job_id
|
|
24
31
|
|
|
25
32
|
module_function
|
|
26
33
|
|
|
@@ -28,21 +35,28 @@ module Pgbus
|
|
|
28
35
|
msg_id = Thread.current[MSG_ID_KEY]
|
|
29
36
|
return nil if msg_id.nil?
|
|
30
37
|
|
|
31
|
-
{ msg_id: msg_id, read_ct: Thread.current[READ_CT_KEY], queue_name: Thread.current[QUEUE_NAME_KEY]
|
|
38
|
+
{ msg_id: msg_id, read_ct: Thread.current[READ_CT_KEY], queue_name: Thread.current[QUEUE_NAME_KEY],
|
|
39
|
+
enqueued_at: Thread.current[ENQUEUED_AT_KEY], job_id: Thread.current[JOB_ID_KEY] }
|
|
32
40
|
end
|
|
33
41
|
|
|
34
|
-
def with(msg_id:, read_ct:, queue_name:)
|
|
42
|
+
def with(msg_id:, read_ct:, queue_name:, enqueued_at: nil, job_id: nil)
|
|
35
43
|
previous_msg_id = Thread.current[MSG_ID_KEY]
|
|
36
44
|
previous_read_ct = Thread.current[READ_CT_KEY]
|
|
37
45
|
previous_queue_name = Thread.current[QUEUE_NAME_KEY]
|
|
46
|
+
previous_enqueued_at = Thread.current[ENQUEUED_AT_KEY]
|
|
47
|
+
previous_job_id = Thread.current[JOB_ID_KEY]
|
|
38
48
|
Thread.current[MSG_ID_KEY] = msg_id
|
|
39
49
|
Thread.current[READ_CT_KEY] = read_ct
|
|
40
50
|
Thread.current[QUEUE_NAME_KEY] = queue_name
|
|
51
|
+
Thread.current[ENQUEUED_AT_KEY] = enqueued_at
|
|
52
|
+
Thread.current[JOB_ID_KEY] = job_id
|
|
41
53
|
yield
|
|
42
54
|
ensure
|
|
43
55
|
Thread.current[MSG_ID_KEY] = previous_msg_id
|
|
44
56
|
Thread.current[READ_CT_KEY] = previous_read_ct
|
|
45
57
|
Thread.current[QUEUE_NAME_KEY] = previous_queue_name
|
|
58
|
+
Thread.current[ENQUEUED_AT_KEY] = previous_enqueued_at
|
|
59
|
+
Thread.current[JOB_ID_KEY] = previous_job_id
|
|
46
60
|
end
|
|
47
61
|
end
|
|
48
62
|
end
|
|
@@ -33,7 +33,7 @@ module Pgbus
|
|
|
33
33
|
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: message.msg_id.to_i)
|
|
34
34
|
signal_concurrency(payload)
|
|
35
35
|
signal_batch_discarded(payload)
|
|
36
|
-
|
|
36
|
+
release_uniqueness_lock(Uniqueness.extract_key(payload), queue_name, message)
|
|
37
37
|
record_stat(payload, queue_name, "dead_lettered", execution_start, message: message)
|
|
38
38
|
instrument(
|
|
39
39
|
"pgbus.job_dead_lettered",
|
|
@@ -100,7 +100,10 @@ module Pgbus
|
|
|
100
100
|
assign_batch_id(job, payload)
|
|
101
101
|
Pgbus.logger.debug { "[Pgbus::Executor] running #{log_tag(message, queue_name)} job_class=#{job_class}" }
|
|
102
102
|
with_visibility_heartbeat(job, queue_name, msg_id, source_queue, payload) do
|
|
103
|
-
Delivery.with(msg_id: msg_id, read_ct: read_count, queue_name: queue_name
|
|
103
|
+
Delivery.with(msg_id: msg_id, read_ct: read_count, queue_name: queue_name,
|
|
104
|
+
enqueued_at: message.enqueued_at, job_id: payload["job_id"]) do
|
|
105
|
+
execute_job(job)
|
|
106
|
+
end
|
|
104
107
|
end
|
|
105
108
|
# retry_on re-enqueues from inside perform_now and returns normally:
|
|
106
109
|
# this attempt is done (archive it) but the job is not — the retry
|
|
@@ -121,7 +124,7 @@ module Pgbus
|
|
|
121
124
|
end
|
|
122
125
|
Pgbus.logger.debug { "[Pgbus::Executor] archived #{log_tag(message, queue_name)} job_class=#{job_class}" }
|
|
123
126
|
job_succeeded = true
|
|
124
|
-
release_uniqueness_lock(uniqueness_key)
|
|
127
|
+
release_uniqueness_lock(uniqueness_key, queue_name, message)
|
|
125
128
|
# Only FailedEventRecorder.record! writes this table, and only after a
|
|
126
129
|
# failed attempt, so a first delivery has no row: skip the per-job
|
|
127
130
|
# DELETE round trip (issue #484). A stale row from a dropped-and-
|
|
@@ -146,7 +149,7 @@ module Pgbus
|
|
|
146
149
|
# A failed :while_executing attempt is no longer executing — release
|
|
147
150
|
# so the retry (same message, after VT) can acquire. :until_executed
|
|
148
151
|
# keeps its lock until success/DLQ by design (#126, #333).
|
|
149
|
-
release_uniqueness_lock(uniqueness_key) if uniqueness_strategy == :while_executing
|
|
152
|
+
release_uniqueness_lock(uniqueness_key, queue_name, message) if uniqueness_strategy == :while_executing
|
|
150
153
|
instrument(
|
|
151
154
|
"pgbus.job_failed",
|
|
152
155
|
queue: queue_name,
|
|
@@ -192,10 +195,15 @@ module Pgbus
|
|
|
192
195
|
job.batch_id = batch_id
|
|
193
196
|
end
|
|
194
197
|
|
|
195
|
-
|
|
198
|
+
# Releases only the row this message holds, never the key as such: a
|
|
199
|
+
# row the job moved onto its retry (issue #564), or a successor's, stays.
|
|
200
|
+
# The queue is spelled as the row was bound (Uniqueness.lock_queue_name),
|
|
201
|
+
# as for release_duplicate_execution_lock.
|
|
202
|
+
def release_uniqueness_lock(key, queue_name, message)
|
|
196
203
|
return unless key
|
|
197
204
|
|
|
198
|
-
Uniqueness.
|
|
205
|
+
UniquenessKey.release_held_by!(key, queue_name: Uniqueness.lock_queue_name(queue_name, config: config),
|
|
206
|
+
msg_id: message.msg_id.to_i, enqueued_at: message.enqueued_at)
|
|
199
207
|
rescue StandardError => e
|
|
200
208
|
Pgbus.logger.warn { "[Pgbus] Uniqueness release failed: #{e.message}" }
|
|
201
209
|
end
|
|
@@ -425,7 +433,8 @@ module Pgbus
|
|
|
425
433
|
|
|
426
434
|
# Same queue the execution lock was acquired under, so the match
|
|
427
435
|
# cannot land on another queue's message of the same id.
|
|
428
|
-
UniquenessKey.release_if_bound!(uniqueness_key, queue_name: queue_name,
|
|
436
|
+
UniquenessKey.release_if_bound!(uniqueness_key, queue_name: Uniqueness.lock_queue_name(queue_name, config: config),
|
|
437
|
+
msg_id: msg_id)
|
|
429
438
|
rescue StandardError => e
|
|
430
439
|
Pgbus.logger.warn { "[Pgbus] Duplicate-execution lock release failed: #{e.message}" }
|
|
431
440
|
end
|
|
@@ -442,7 +451,8 @@ module Pgbus
|
|
|
442
451
|
end
|
|
443
452
|
headers = DeadLetterHeader.build(
|
|
444
453
|
existing: message.headers, reason: DeadLetterHeader::REASON_MAX_RETRIES, source: "worker",
|
|
445
|
-
source_queue: source_queue || config.queue_name(queue_name),
|
|
454
|
+
source_queue: source_queue || config.queue_name(queue_name), source_msg_id: message.msg_id.to_i,
|
|
455
|
+
source_enqueued_at: message.enqueued_at, attempts: message.read_ct.to_i,
|
|
446
456
|
max_retries: config.max_retries, error: last_error
|
|
447
457
|
)
|
|
448
458
|
if source_queue
|
|
@@ -16,13 +16,21 @@ module Pgbus
|
|
|
16
16
|
# once. Handing it a pre-serialized String stored a JSON *string*
|
|
17
17
|
# (double-encoded), which every document reader of the column —
|
|
18
18
|
# `payload->>'job_id'`, the job-class lookup, `scheduled_at` — misread.
|
|
19
|
+
#
|
|
20
|
+
# created_at is left to the column's CURRENT_TIMESTAMP default: the
|
|
21
|
+
# job's uniqueness lock is stamped by the database too, and discarding
|
|
22
|
+
# a parked job releases only a lock acquired no later than the park
|
|
23
|
+
# (UniquenessKey.release_if_unbound!), so both sides need one clock.
|
|
19
24
|
def insert(concurrency_key:, queue_name:, payload:, duration:, priority: 0)
|
|
20
|
-
Pgbus::BlockedExecution.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
Pgbus::BlockedExecution.insert!(
|
|
26
|
+
{
|
|
27
|
+
concurrency_key: concurrency_key,
|
|
28
|
+
queue_name: queue_name,
|
|
29
|
+
payload: payload,
|
|
30
|
+
priority: priority,
|
|
31
|
+
expires_at: Time.current + duration
|
|
32
|
+
},
|
|
33
|
+
record_timestamps: false
|
|
26
34
|
)
|
|
27
35
|
end
|
|
28
36
|
|
data/lib/pgbus/configuration.rb
CHANGED
|
@@ -13,7 +13,8 @@ module Pgbus
|
|
|
13
13
|
attr_accessor :database_url, :connection_params, :pool_size, :pool_timeout
|
|
14
14
|
|
|
15
15
|
# Queue settings
|
|
16
|
-
attr_accessor :default_queue
|
|
16
|
+
attr_accessor :default_queue
|
|
17
|
+
attr_reader :queue_prefix # rubocop:disable Style/AccessorGrouping -- the writer normalizes (see queue_prefix=)
|
|
17
18
|
|
|
18
19
|
# Worker settings
|
|
19
20
|
attr_accessor :polling_interval, :prefetch_limit, :execution_mode
|
|
@@ -597,6 +598,25 @@ module Pgbus
|
|
|
597
598
|
@finalized = false
|
|
598
599
|
end
|
|
599
600
|
|
|
601
|
+
# Stored the way the queue tables spell it ("my-app" → "my_app"). Table
|
|
602
|
+
# names are unchanged (queue_name normalizes the joined name the same
|
|
603
|
+
# way), but code that strips the prefix off a table name to get its queue
|
|
604
|
+
# back (the dashboard, the worker, a job's uniqueness lock queue) then
|
|
605
|
+
# finds it. A value normalize cannot spell (nothing table-safe left, or
|
|
606
|
+
# too long) is kept as given, as before: validate! rejects one that is
|
|
607
|
+
# too long, and queue_name drops one with nothing table-safe in it.
|
|
608
|
+
def queue_prefix=(value)
|
|
609
|
+
@queue_prefix = if value.is_a?(String) && !value.empty?
|
|
610
|
+
begin
|
|
611
|
+
QueueNameValidator.normalize(value)
|
|
612
|
+
rescue ArgumentError
|
|
613
|
+
value
|
|
614
|
+
end
|
|
615
|
+
else
|
|
616
|
+
value
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
600
620
|
def queue_name(name)
|
|
601
621
|
full = "#{queue_prefix}_#{name}"
|
|
602
622
|
QueueNameValidator.normalize(full)
|
|
@@ -1002,6 +1022,27 @@ module Pgbus
|
|
|
1002
1022
|
raise Pgbus::ConfigurationError, "capsule processes must be a positive Integer (got #{value.inspect})"
|
|
1003
1023
|
end
|
|
1004
1024
|
|
|
1025
|
+
# A prefix with a leading, trailing or doubled underscore is kept as
|
|
1026
|
+
# written (its tables already exist under it), but queue_name collapses
|
|
1027
|
+
# the separator after it only for a queue name that needs normalizing:
|
|
1028
|
+
# "myapp_" gives myapp__orders yet myapp_orders_handler for
|
|
1029
|
+
# "orders-handler". A job's own release and the dashboard derive the same
|
|
1030
|
+
# lock queue for it and still match, but the reaper cannot resolve that
|
|
1031
|
+
# name back to the table, so a lock orphaned there is never reclaimed and
|
|
1032
|
+
# has to be released by key. Legal, but warned.
|
|
1033
|
+
def warn_queue_prefix_underscores
|
|
1034
|
+
return unless queue_prefix.match?(/\A_|_\z|__/)
|
|
1035
|
+
|
|
1036
|
+
suggested = queue_prefix.squeeze("_").delete_prefix("_").delete_suffix("_")
|
|
1037
|
+
suggestion = suggested.empty? ? "" : "; prefer #{suggested.inspect} for new deployments"
|
|
1038
|
+
Pgbus.logger.warn do
|
|
1039
|
+
"[Pgbus] queue_prefix #{queue_prefix.inspect} has a leading, trailing or doubled underscore — a queue " \
|
|
1040
|
+
"whose name needs normalizing (\"orders-handler\") gets a table spelled unlike its queue, so the " \
|
|
1041
|
+
"reaper cannot find that table: a uniqueness lock orphaned there (a worker crash before its release) " \
|
|
1042
|
+
"is never reclaimed and has to be released by key#{suggestion}"
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1005
1046
|
# Buffered claims are kept invisible only by the visibility heartbeat.
|
|
1006
1047
|
# Without it a message buffered longer than visibility_timeout can be
|
|
1007
1048
|
# claimed by another worker while it still waits here. Legal, but warned.
|
|
@@ -1075,6 +1116,7 @@ module Pgbus
|
|
|
1075
1116
|
{ queue_prefix: queue_prefix, default_queue: default_queue }.each do |name, value|
|
|
1076
1117
|
raise Pgbus::ConfigurationError, "#{name} must be a non-empty String" unless value.is_a?(String) && !value.empty?
|
|
1077
1118
|
end
|
|
1119
|
+
warn_queue_prefix_underscores
|
|
1078
1120
|
|
|
1079
1121
|
# web_auth gates the dashboard; a non-callable value silently leaves it
|
|
1080
1122
|
# open (mirror streams_presence_member).
|
|
@@ -30,7 +30,13 @@ module Pgbus
|
|
|
30
30
|
|
|
31
31
|
# error: FailedEventRecorder.last_error's Hash, or nil when no failure
|
|
32
32
|
# was recorded. Returns the merged headers as a JSON string.
|
|
33
|
-
|
|
33
|
+
# source_msg_id / source_enqueued_at: the message's id in source_queue and
|
|
34
|
+
# when it was sent (the copy gets a new id and enqueued_at in the DLQ).
|
|
35
|
+
# A discard of the copy uses both to release only the uniqueness lock the
|
|
36
|
+
# original still holds: bound to it, or unbound and acquired before its
|
|
37
|
+
# send, never a lock it handed to a retry after (issue #564).
|
|
38
|
+
def build(existing:, reason:, source:, source_queue:, attempts:, max_retries:, error: nil, source_msg_id: nil,
|
|
39
|
+
source_enqueued_at: nil, now: Time.now.utc)
|
|
34
40
|
headers = decode(existing, warn: true)
|
|
35
41
|
retries = count(headers.delete(RETRIES_KEY))
|
|
36
42
|
block = {
|
|
@@ -42,6 +48,8 @@ module Pgbus
|
|
|
42
48
|
"max_retries" => max_retries,
|
|
43
49
|
"dead_lettered_at" => iso(now)
|
|
44
50
|
}
|
|
51
|
+
block["source_msg_id"] = source_msg_id if source_msg_id
|
|
52
|
+
block["source_enqueued_at"] = iso(source_enqueued_at) if source_enqueued_at
|
|
45
53
|
block["retries_from_dlq"] = retries if retries.positive?
|
|
46
54
|
block.merge!(error_fields(error)) if error
|
|
47
55
|
JSON.generate(headers.merge(KEY => block))
|
|
@@ -79,9 +79,10 @@ module Pgbus
|
|
|
79
79
|
# payload: workflow_class, workflow_id, name, duplicate,
|
|
80
80
|
# woke (true when it woke the step waiting for it)
|
|
81
81
|
# pgbus.workflow.wait_timed_out — a waiting step's wait_timeout: passed; its on_timeout
|
|
82
|
-
# applied (the sweep, or TestHelpers#expire_wait)
|
|
82
|
+
# applied (the sweep, or TestHelpers#expire_wait); under
|
|
83
|
+
# notify the step keeps waiting, once per sweep that finds it late
|
|
83
84
|
# payload: workflow_class, workflow_id, step_name, attempt,
|
|
84
|
-
# action (pause! | cancel! | skip! | run)
|
|
85
|
+
# action (pause! | cancel! | skip! | run | notify)
|
|
85
86
|
# pgbus.workflow.gauge — emitted by each dispatcher workflow sweep
|
|
86
87
|
# payload: statuses ([{ workflow_class:, status:, count: }]),
|
|
87
88
|
# stuck ([{ workflow_class:, reason:, count: }]), zero-filled
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
],
|
|
5
5
|
"dashboard": {
|
|
6
6
|
"title": "Pgbus",
|
|
7
|
-
"description": "PostgreSQL-native job processing and event bus built on PGMQ.\nMonitor job throughput, queue depth, latency, dead letter queues,\nworker processes, event handlers, and stream broadcasts.\nWorkflow step outcomes and run times, and
|
|
7
|
+
"description": "PostgreSQL-native job processing and event bus built on PGMQ.\nMonitor job throughput, queue depth, latency, dead letter queues,\nworker processes, event handlers, and stream broadcasts.\nWorkflow step outcomes and run times, stuck workflows and late waits.\n",
|
|
8
8
|
"visuals": [
|
|
9
9
|
{
|
|
10
10
|
"type": "timeseries",
|
|
@@ -520,6 +520,35 @@
|
|
|
520
520
|
]
|
|
521
521
|
}
|
|
522
522
|
]
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"type": "timeseries",
|
|
526
|
+
"display": "LINE",
|
|
527
|
+
"title": "Late workflow waits",
|
|
528
|
+
"description": "Deadlines an on_timeout: :notify wait missed, by class and step: the run keeps waiting for its signal.",
|
|
529
|
+
"line_label": "%workflow_class% - %step%",
|
|
530
|
+
"format": "number",
|
|
531
|
+
"draw_null_as_zero": true,
|
|
532
|
+
"metrics": [
|
|
533
|
+
{
|
|
534
|
+
"name": "pgbus_workflow_wait_overdue_total",
|
|
535
|
+
"fields": [
|
|
536
|
+
{
|
|
537
|
+
"field": "COUNTER"
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
"tags": [
|
|
541
|
+
{
|
|
542
|
+
"key": "workflow_class",
|
|
543
|
+
"value": "*"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"key": "step",
|
|
547
|
+
"value": "*"
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
}
|
|
551
|
+
]
|
|
523
552
|
}
|
|
524
553
|
]
|
|
525
554
|
}
|
|
@@ -12,6 +12,7 @@ module Pgbus
|
|
|
12
12
|
# pgbus_workflows_paused_total{workflow_class, error_class} counter (error_class absent for a manual pause)
|
|
13
13
|
# pgbus_workflows_cancelled_total{workflow_class} counter
|
|
14
14
|
# pgbus_workflow_stalls_total{workflow_class, step, detection} counter (detection: redelivery | sweep)
|
|
15
|
+
# pgbus_workflow_wait_overdue_total{workflow_class, step} counter (a late notice of an on_timeout: :notify wait)
|
|
15
16
|
# pgbus_workflows{workflow_class, status} gauge, from each sweep
|
|
16
17
|
# pgbus_workflows_stuck{workflow_class, reason} gauge, from each sweep
|
|
17
18
|
#
|
|
@@ -20,7 +21,7 @@ module Pgbus
|
|
|
20
21
|
module WorkflowMetrics
|
|
21
22
|
EVENTS = %w[
|
|
22
23
|
pgbus.workflow.step pgbus.workflow_finished pgbus.workflow_paused pgbus.workflow_cancelled
|
|
23
|
-
pgbus.workflow.stall_detected pgbus.workflow.gauge
|
|
24
|
+
pgbus.workflow.stall_detected pgbus.workflow.wait_timed_out pgbus.workflow.gauge
|
|
24
25
|
].freeze
|
|
25
26
|
|
|
26
27
|
module_function
|
|
@@ -37,6 +38,7 @@ module Pgbus
|
|
|
37
38
|
when "pgbus.workflow_cancelled" then [counter("pgbus_workflows_cancelled_total", workflow_class: klass)]
|
|
38
39
|
when "pgbus.workflow.stall_detected"
|
|
39
40
|
[counter("pgbus_workflow_stalls_total", workflow_class: klass, step: payload[:step_name], detection: payload[:detection])]
|
|
41
|
+
when "pgbus.workflow.wait_timed_out" then wait_timed_out_samples(klass, payload)
|
|
40
42
|
when "pgbus.workflow.gauge" then gauge_samples(payload)
|
|
41
43
|
else []
|
|
42
44
|
end
|
|
@@ -49,6 +51,14 @@ module Pgbus
|
|
|
49
51
|
samples
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
# Only :notify keeps waiting past the deadline; every other on_timeout
|
|
55
|
+
# ends the wait and is counted by the step or run it ends.
|
|
56
|
+
def wait_timed_out_samples(klass, payload)
|
|
57
|
+
return [] unless payload[:action] == "notify"
|
|
58
|
+
|
|
59
|
+
[counter("pgbus_workflow_wait_overdue_total", workflow_class: klass, step: payload[:step_name])]
|
|
60
|
+
end
|
|
61
|
+
|
|
52
62
|
def gauge_samples(payload)
|
|
53
63
|
statuses = Array(payload[:statuses]).map do |row|
|
|
54
64
|
[:gauge, "pgbus_workflows", row[:count], { workflow_class: row[:workflow_class], status: row[:status] }]
|
|
@@ -296,7 +296,8 @@ module Pgbus
|
|
|
296
296
|
last_error = FailedEventRecorder.last_error(queue_name: queue_name, msg_id: message.msg_id.to_i)
|
|
297
297
|
headers = DeadLetterHeader.build(
|
|
298
298
|
existing: message.headers, reason: DeadLetterHeader::REASON_MAX_RETRIES, source: "consumer",
|
|
299
|
-
source_queue: config.queue_name(queue_name),
|
|
299
|
+
source_queue: config.queue_name(queue_name), source_msg_id: message.msg_id.to_i,
|
|
300
|
+
source_enqueued_at: message.enqueued_at, attempts: message.read_ct.to_i,
|
|
300
301
|
max_retries: config.max_retries, error: last_error
|
|
301
302
|
)
|
|
302
303
|
Pgbus.client.move_to_dead_letter(queue_name, message, headers: headers)
|