pgbus 0.2.9 → 0.3.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/app/controllers/pgbus/api/insights_controller.rb +6 -1
- data/app/controllers/pgbus/frontends_controller.rb +68 -0
- data/app/controllers/pgbus/insights_controller.rb +2 -0
- data/app/controllers/pgbus/jobs_controller.rb +5 -0
- data/app/frontend/pgbus/application.js +90 -0
- data/app/frontend/pgbus/modules/charts.js +106 -0
- data/app/frontend/pgbus/style.css +2 -0
- data/app/frontend/pgbus/tailwind.css +64 -0
- data/app/frontend/pgbus/vendor/apexcharts.js +38 -0
- data/app/frontend/pgbus/vendor/turbo.js +6696 -0
- data/app/models/pgbus/job_stat.rb +85 -13
- data/app/views/layouts/pgbus/application.html.erb +20 -141
- data/app/views/pgbus/insights/show.html.erb +86 -80
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +8 -1
- data/config/locales/da.yml +3 -0
- data/config/locales/de.yml +3 -0
- data/config/locales/en.yml +19 -0
- data/config/locales/es.yml +3 -0
- data/config/locales/fi.yml +3 -0
- data/config/locales/fr.yml +3 -0
- data/config/locales/it.yml +3 -0
- data/config/locales/ja.yml +3 -0
- data/config/locales/nb.yml +3 -0
- data/config/locales/nl.yml +3 -0
- data/config/locales/pt.yml +3 -0
- data/config/locales/sv.yml +3 -0
- data/config/routes.rb +6 -0
- data/lib/generators/pgbus/add_job_stats_latency_generator.rb +52 -0
- data/lib/generators/pgbus/templates/add_job_stats_latency.rb.erb +9 -0
- data/lib/pgbus/active_job/executor.rb +24 -5
- data/lib/pgbus/recurring/schedule.rb +86 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +107 -0
- metadata +10 -1
data/config/locales/fr.yml
CHANGED
|
@@ -179,6 +179,8 @@ fr:
|
|
|
179
179
|
title: Travaux en file d'attente
|
|
180
180
|
discard: Rejeter
|
|
181
181
|
discard_confirm: Rejeter ce message ?
|
|
182
|
+
discard_all: Tout supprimer
|
|
183
|
+
discard_all_confirm: Supprimer tous les travaux en file d'attente et libérer leurs verrous ? Cette action est irréversible.
|
|
182
184
|
retry: Réessayer
|
|
183
185
|
retry_confirm: Réinitialiser le délai de visibilité et réessayer ?
|
|
184
186
|
failed_table:
|
|
@@ -197,6 +199,7 @@ fr:
|
|
|
197
199
|
index:
|
|
198
200
|
discard_all: Tout ignorer
|
|
199
201
|
discard_all_confirm: Ignorer tous les travaux échoués ?
|
|
202
|
+
discard_all_enqueued_notice: "%{count} travaux en file d'attente supprimés et verrous libérés."
|
|
200
203
|
retry_all: Tout réessayer
|
|
201
204
|
retry_all_confirm: Réessayer tous les travaux échoués ?
|
|
202
205
|
title: Travaux
|
data/config/locales/it.yml
CHANGED
|
@@ -179,6 +179,8 @@ it:
|
|
|
179
179
|
title: Lavori in coda
|
|
180
180
|
discard: Scarta
|
|
181
181
|
discard_confirm: Scartare questo messaggio?
|
|
182
|
+
discard_all: Scarta tutti
|
|
183
|
+
discard_all_confirm: Scartare tutti i lavori in coda e rilasciare i relativi blocchi? Questa azione non può essere annullata.
|
|
182
184
|
retry: Riprova
|
|
183
185
|
retry_confirm: Reimpostare il timeout di visibilità e riprovare?
|
|
184
186
|
failed_table:
|
|
@@ -197,6 +199,7 @@ it:
|
|
|
197
199
|
index:
|
|
198
200
|
discard_all: Scarta Tutto
|
|
199
201
|
discard_all_confirm: Scartare tutti i lavori falliti?
|
|
202
|
+
discard_all_enqueued_notice: Scartati %{count} lavori in coda e rilasciati i relativi blocchi.
|
|
200
203
|
retry_all: Riprova Tutto
|
|
201
204
|
retry_all_confirm: Riprova tutti i lavori falliti?
|
|
202
205
|
title: Lavori
|
data/config/locales/ja.yml
CHANGED
|
@@ -179,6 +179,8 @@ ja:
|
|
|
179
179
|
title: キューに入れられたジョブ
|
|
180
180
|
discard: 破棄
|
|
181
181
|
discard_confirm: このメッセージを破棄しますか?
|
|
182
|
+
discard_all: すべて破棄
|
|
183
|
+
discard_all_confirm: キュー内のすべてのジョブを破棄し、ロックを解放しますか?この操作は元に戻せません。
|
|
182
184
|
retry: リトライ
|
|
183
185
|
retry_confirm: 可視性タイムアウトをリセットしてリトライしますか?
|
|
184
186
|
failed_table:
|
|
@@ -197,6 +199,7 @@ ja:
|
|
|
197
199
|
index:
|
|
198
200
|
discard_all: すべて破棄
|
|
199
201
|
discard_all_confirm: すべての失敗したジョブを破棄しますか?
|
|
202
|
+
discard_all_enqueued_notice: "%{count}件のキュー内ジョブを破棄し、ロックを解放しました。"
|
|
200
203
|
retry_all: すべてリトライ
|
|
201
204
|
retry_all_confirm: すべての失敗したジョブをリトライしますか?
|
|
202
205
|
title: ジョブ
|
data/config/locales/nb.yml
CHANGED
|
@@ -179,6 +179,8 @@ nb:
|
|
|
179
179
|
title: Kølagte jobber
|
|
180
180
|
discard: Forkast
|
|
181
181
|
discard_confirm: Forkaste denne meldingen?
|
|
182
|
+
discard_all: Forkast alle
|
|
183
|
+
discard_all_confirm: Forkast alle køede jobber og frigi låsene? Dette kan ikke angres.
|
|
182
184
|
retry: Prøv igjen
|
|
183
185
|
retry_confirm: Tilbakestill synlighetstidsavbrudd og prøv igjen?
|
|
184
186
|
failed_table:
|
|
@@ -197,6 +199,7 @@ nb:
|
|
|
197
199
|
index:
|
|
198
200
|
discard_all: Forkast alle
|
|
199
201
|
discard_all_confirm: Forkast alle mislykkede jobber?
|
|
202
|
+
discard_all_enqueued_notice: Forkastet %{count} køede jobber og frigitt låsene.
|
|
200
203
|
retry_all: Prøv alle på nytt
|
|
201
204
|
retry_all_confirm: Prøv alle mislykkede jobber på nytt?
|
|
202
205
|
title: Jobber
|
data/config/locales/nl.yml
CHANGED
|
@@ -179,6 +179,8 @@ nl:
|
|
|
179
179
|
title: Taken in de wachtrij
|
|
180
180
|
discard: Verwerpen
|
|
181
181
|
discard_confirm: Dit bericht verwerpen?
|
|
182
|
+
discard_all: Alles verwijderen
|
|
183
|
+
discard_all_confirm: Alle taken in de wachtrij verwijderen en hun vergrendelingen vrijgeven? Dit kan niet ongedaan worden gemaakt.
|
|
182
184
|
retry: Opnieuw proberen
|
|
183
185
|
retry_confirm: Zichtbaarheidstimeout resetten en opnieuw proberen?
|
|
184
186
|
failed_table:
|
|
@@ -197,6 +199,7 @@ nl:
|
|
|
197
199
|
index:
|
|
198
200
|
discard_all: Alles Verwijderen
|
|
199
201
|
discard_all_confirm: Alle mislukte taken verwijderen?
|
|
202
|
+
discard_all_enqueued_notice: "%{count} taken in de wachtrij verwijderd en vergrendelingen vrijgegeven."
|
|
200
203
|
retry_all: Alles Opnieuw Proberen
|
|
201
204
|
retry_all_confirm: Alle mislukte taken opnieuw proberen?
|
|
202
205
|
title: Taken
|
data/config/locales/pt.yml
CHANGED
|
@@ -179,6 +179,8 @@ pt:
|
|
|
179
179
|
title: Trabalhos Enfileirados
|
|
180
180
|
discard: Descartar
|
|
181
181
|
discard_confirm: Descartar esta mensagem?
|
|
182
|
+
discard_all: Descartar todos
|
|
183
|
+
discard_all_confirm: Descartar todos os trabalhos na fila e liberar seus bloqueios? Esta ação não pode ser desfeita.
|
|
182
184
|
retry: Tentar novamente
|
|
183
185
|
retry_confirm: Redefinir tempo de visibilidade e tentar novamente?
|
|
184
186
|
failed_table:
|
|
@@ -197,6 +199,7 @@ pt:
|
|
|
197
199
|
index:
|
|
198
200
|
discard_all: Descartar Todos
|
|
199
201
|
discard_all_confirm: Descartar todos os trabalhos falhados?
|
|
202
|
+
discard_all_enqueued_notice: Descartados %{count} trabalhos na fila e bloqueios liberados.
|
|
200
203
|
retry_all: Tentar Novamente Todos
|
|
201
204
|
retry_all_confirm: Tentar novamente todos os trabalhos falhados?
|
|
202
205
|
title: Trabalhos
|
data/config/locales/sv.yml
CHANGED
|
@@ -179,6 +179,8 @@ sv:
|
|
|
179
179
|
title: Köade jobb
|
|
180
180
|
discard: Kassera
|
|
181
181
|
discard_confirm: Kassera detta meddelande?
|
|
182
|
+
discard_all: Kassera alla
|
|
183
|
+
discard_all_confirm: Kassera alla köade jobb och frigör deras lås? Detta kan inte ångras.
|
|
182
184
|
retry: Försök igen
|
|
183
185
|
retry_confirm: Återställ synlighetstimeout och försök igen?
|
|
184
186
|
failed_table:
|
|
@@ -197,6 +199,7 @@ sv:
|
|
|
197
199
|
index:
|
|
198
200
|
discard_all: Kassera alla
|
|
199
201
|
discard_all_confirm: Kassera alla misslyckade jobb?
|
|
202
|
+
discard_all_enqueued_notice: Kasserade %{count} köade jobb och frigjorde deras lås.
|
|
200
203
|
retry_all: Försök igen alla
|
|
201
204
|
retry_all_confirm: Försök igen alla misslyckade jobb?
|
|
202
205
|
title: Jobb
|
data/config/routes.rb
CHANGED
|
@@ -21,6 +21,7 @@ Pgbus::Engine.routes.draw do
|
|
|
21
21
|
collection do
|
|
22
22
|
post :retry_all
|
|
23
23
|
post :discard_all
|
|
24
|
+
post :discard_all_enqueued
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -60,4 +61,9 @@ Pgbus::Engine.routes.draw do
|
|
|
60
61
|
get :stats, to: "stats#show"
|
|
61
62
|
get :insights, to: "insights#show"
|
|
62
63
|
end
|
|
64
|
+
|
|
65
|
+
scope :frontend, controller: :frontends, defaults: { version: Pgbus::VERSION.tr(".", "-") } do
|
|
66
|
+
get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format: "js" }
|
|
67
|
+
get "static/:version/:id", action: :static, as: :frontend_static
|
|
68
|
+
end
|
|
63
69
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
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 AddJobStatsLatencyGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Add enqueue latency and retry count columns to pgbus_job_stats"
|
|
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 "add_job_stats_latency.rb.erb",
|
|
23
|
+
"db/pgbus_migrate/add_pgbus_job_stats_latency.rb"
|
|
24
|
+
else
|
|
25
|
+
migration_template "add_job_stats_latency.rb.erb",
|
|
26
|
+
"db/migrate/add_pgbus_job_stats_latency.rb"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_post_install
|
|
31
|
+
say ""
|
|
32
|
+
say "Pgbus job stats latency columns installed!", :green
|
|
33
|
+
say ""
|
|
34
|
+
say "Next steps:"
|
|
35
|
+
say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
|
|
36
|
+
say " 2. Queue latency and retry metrics are now tracked automatically"
|
|
37
|
+
say " 3. View latency insights at /pgbus/insights"
|
|
38
|
+
say ""
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def migration_version
|
|
44
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def separate_database?
|
|
48
|
+
options[:database].present?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddPgbusJobStatsLatency < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
add_column :pgbus_job_stats, :enqueue_latency_ms, :bigint
|
|
4
|
+
add_column :pgbus_job_stats, :retry_count, :integer, default: 0
|
|
5
|
+
|
|
6
|
+
add_index :pgbus_job_stats, [:queue_name, :created_at],
|
|
7
|
+
name: "idx_pgbus_job_stats_queue_time"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
module Pgbus
|
|
4
6
|
module ActiveJob
|
|
5
7
|
class Executor
|
|
@@ -20,7 +22,7 @@ module Pgbus
|
|
|
20
22
|
signal_concurrency(payload)
|
|
21
23
|
signal_batch_discarded(payload)
|
|
22
24
|
Uniqueness.release_lock(Uniqueness.extract_key(payload))
|
|
23
|
-
record_stat(payload, queue_name, "dead_lettered", execution_start)
|
|
25
|
+
record_stat(payload, queue_name, "dead_lettered", execution_start, message: message)
|
|
24
26
|
return :dead_lettered
|
|
25
27
|
end
|
|
26
28
|
|
|
@@ -56,12 +58,12 @@ module Pgbus
|
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
instrument("pgbus.job_completed", queue: queue_name, job_class: job_class)
|
|
59
|
-
record_stat(payload, queue_name, "success", execution_start)
|
|
61
|
+
record_stat(payload, queue_name, "success", execution_start, message: message)
|
|
60
62
|
:success
|
|
61
63
|
rescue StandardError => e
|
|
62
64
|
handle_failure(message, queue_name, e)
|
|
63
65
|
instrument("pgbus.job_failed", queue: queue_name, job_class: payload&.dig("job_class"), error: e.class.name)
|
|
64
|
-
record_stat(payload, queue_name, "failed", execution_start)
|
|
66
|
+
record_stat(payload, queue_name, "failed", execution_start, message: message)
|
|
65
67
|
# Don't signal concurrency on transient failure — the job will be retried.
|
|
66
68
|
# Semaphore is released only on success or dead-lettering.
|
|
67
69
|
:failed
|
|
@@ -91,20 +93,37 @@ module Pgbus
|
|
|
91
93
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
92
94
|
end
|
|
93
95
|
|
|
94
|
-
def record_stat(payload, queue_name, status, start_time)
|
|
96
|
+
def record_stat(payload, queue_name, status, start_time, message: nil)
|
|
95
97
|
return unless config.stats_enabled
|
|
96
98
|
|
|
97
99
|
duration_ms = ((monotonic_now - start_time) * 1000).round
|
|
100
|
+
enqueue_latency_ms = compute_enqueue_latency(message)
|
|
101
|
+
retry_count = message ? [message.read_ct.to_i - 1, 0].max : 0
|
|
102
|
+
|
|
98
103
|
JobStat.record!(
|
|
99
104
|
job_class: payload&.dig("job_class") || "unknown",
|
|
100
105
|
queue_name: queue_name,
|
|
101
106
|
status: status,
|
|
102
|
-
duration_ms: duration_ms
|
|
107
|
+
duration_ms: duration_ms,
|
|
108
|
+
enqueue_latency_ms: enqueue_latency_ms,
|
|
109
|
+
retry_count: retry_count
|
|
103
110
|
)
|
|
104
111
|
rescue StandardError => e
|
|
105
112
|
Pgbus.logger.debug { "[Pgbus] Stat recording failed: #{e.message}" }
|
|
106
113
|
end
|
|
107
114
|
|
|
115
|
+
def compute_enqueue_latency(message)
|
|
116
|
+
return unless message
|
|
117
|
+
|
|
118
|
+
enqueued_at_str = message.enqueued_at
|
|
119
|
+
return unless enqueued_at_str
|
|
120
|
+
|
|
121
|
+
enqueued_at = Time.parse(enqueued_at_str.to_s)
|
|
122
|
+
[((Time.now.utc - enqueued_at) * 1000).round, 0].max
|
|
123
|
+
rescue ArgumentError, TypeError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
108
127
|
def handle_failure(_message, _queue_name, error)
|
|
109
128
|
Pgbus.logger.error { "[Pgbus] Job failed: #{error.class}: #{error.message}" }
|
|
110
129
|
Pgbus.logger.debug { error.backtrace&.join("\n") }
|
|
@@ -17,10 +17,24 @@ module Pgbus
|
|
|
17
17
|
def enqueue_task(task, run_at:)
|
|
18
18
|
queue = resolve_queue(task)
|
|
19
19
|
|
|
20
|
+
# Check uniqueness lock before enqueuing. If the job class declares
|
|
21
|
+
# ensures_uniqueness, we acquire the lock here so duplicate recurring
|
|
22
|
+
# enqueues are rejected while a previous instance is still queued or running.
|
|
23
|
+
if uniqueness_locked?(task)
|
|
24
|
+
Pgbus.logger.debug do
|
|
25
|
+
"[Pgbus] Recurring task #{task.key} skipped: uniqueness lock held"
|
|
26
|
+
end
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
RecurringExecution.record(task.key, run_at) do
|
|
21
31
|
payload = build_payload(task)
|
|
22
32
|
headers = build_headers(task, run_at)
|
|
23
33
|
|
|
34
|
+
# Inject uniqueness metadata into the payload so the worker knows
|
|
35
|
+
# to release the lock after execution.
|
|
36
|
+
payload = inject_uniqueness_metadata(task, payload)
|
|
37
|
+
|
|
24
38
|
Pgbus.client.ensure_queue(queue)
|
|
25
39
|
Pgbus.client.send_message(queue, payload, headers: headers)
|
|
26
40
|
|
|
@@ -97,6 +111,78 @@ module Pgbus
|
|
|
97
111
|
"pgbus.recurring_schedule" => task.schedule
|
|
98
112
|
}
|
|
99
113
|
end
|
|
114
|
+
|
|
115
|
+
# Check if the job class has ensures_uniqueness and if its lock is currently held.
|
|
116
|
+
# Returns true if the lock is held (skip enqueue), false otherwise.
|
|
117
|
+
def uniqueness_locked?(task)
|
|
118
|
+
return false unless task.class_name
|
|
119
|
+
|
|
120
|
+
job_class = task.class_name.safe_constantize
|
|
121
|
+
return false unless job_class
|
|
122
|
+
return false unless job_class.respond_to?(:pgbus_uniqueness)
|
|
123
|
+
|
|
124
|
+
config = job_class.pgbus_uniqueness
|
|
125
|
+
return false unless config
|
|
126
|
+
return false unless config[:strategy] == :until_executed
|
|
127
|
+
|
|
128
|
+
key = resolve_uniqueness_key(config, task)
|
|
129
|
+
return false unless key
|
|
130
|
+
|
|
131
|
+
# Try to acquire the lock. If it fails, the lock is already held.
|
|
132
|
+
acquired = JobLock.acquire!(
|
|
133
|
+
key,
|
|
134
|
+
job_class: task.class_name,
|
|
135
|
+
job_id: "recurring-#{task.key}",
|
|
136
|
+
state: "queued",
|
|
137
|
+
ttl: config[:lock_ttl]
|
|
138
|
+
)
|
|
139
|
+
# If we acquired it, great — the message will be enqueued with the lock held.
|
|
140
|
+
# If not, a previous instance is still queued/running.
|
|
141
|
+
!acquired
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
Pgbus.logger.warn { "[Pgbus] Uniqueness check failed for #{task.key}: #{e.message}" }
|
|
144
|
+
false # Fail open — allow enqueue if uniqueness check errors
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Resolve the uniqueness key for a recurring task.
|
|
148
|
+
# For no-argument recurring jobs, the key defaults to the class name.
|
|
149
|
+
def resolve_uniqueness_key(config, task)
|
|
150
|
+
key_proc = config[:key]
|
|
151
|
+
args = task.arguments || []
|
|
152
|
+
|
|
153
|
+
if args.empty?
|
|
154
|
+
key_proc.call
|
|
155
|
+
else
|
|
156
|
+
key_proc.call(*args)
|
|
157
|
+
end
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
Pgbus.logger.warn { "[Pgbus] Could not resolve uniqueness key for #{task.key}: #{e.message}" }
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Inject uniqueness metadata into the payload so the executor releases
|
|
164
|
+
# the lock after the job completes.
|
|
165
|
+
# Only inject for :until_executed strategy — :while_executing locks are
|
|
166
|
+
# acquired at execution time by the executor, not by the scheduler.
|
|
167
|
+
def inject_uniqueness_metadata(task, payload)
|
|
168
|
+
return payload unless task.class_name
|
|
169
|
+
|
|
170
|
+
job_class = task.class_name.safe_constantize
|
|
171
|
+
return payload unless job_class.respond_to?(:pgbus_uniqueness)
|
|
172
|
+
|
|
173
|
+
config = job_class.pgbus_uniqueness
|
|
174
|
+
return payload unless config
|
|
175
|
+
return payload unless config[:strategy] == :until_executed
|
|
176
|
+
|
|
177
|
+
key = resolve_uniqueness_key(config, task)
|
|
178
|
+
return payload unless key
|
|
179
|
+
|
|
180
|
+
payload.merge(
|
|
181
|
+
Pgbus::Uniqueness::METADATA_KEY => key,
|
|
182
|
+
Pgbus::Uniqueness::STRATEGY_KEY => config[:strategy].to_s,
|
|
183
|
+
Pgbus::Uniqueness::TTL_KEY => config[:lock_ttl]
|
|
184
|
+
)
|
|
185
|
+
end
|
|
100
186
|
end
|
|
101
187
|
end
|
|
102
188
|
end
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -113,9 +113,31 @@ module Pgbus
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def discard_job(queue_name, msg_id)
|
|
116
|
+
release_lock_for_message(queue_name, msg_id)
|
|
116
117
|
@client.archive_message(queue_name, msg_id.to_i, prefixed: false)
|
|
117
118
|
end
|
|
118
119
|
|
|
120
|
+
def discard_all_enqueued
|
|
121
|
+
dlq_suffix = Pgbus.configuration.dead_letter_queue_suffix
|
|
122
|
+
queues = queues_with_metrics.reject { |q| q[:name].end_with?(dlq_suffix) }
|
|
123
|
+
total = 0
|
|
124
|
+
|
|
125
|
+
queues.each do |q|
|
|
126
|
+
messages = query_queue_messages_raw(q[:name], 10_000, 0)
|
|
127
|
+
next if messages.empty?
|
|
128
|
+
|
|
129
|
+
release_locks_for_messages(messages)
|
|
130
|
+
|
|
131
|
+
ids = messages.map { |m| m[:msg_id].to_i }
|
|
132
|
+
@client.archive_batch(q[:name], ids, prefixed: false)
|
|
133
|
+
total += ids.size
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error discarding enqueued messages from #{q[:name]}: #{e.message}" }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
total
|
|
139
|
+
end
|
|
140
|
+
|
|
119
141
|
# Failed events
|
|
120
142
|
def failed_events(page: 1, per_page: 25)
|
|
121
143
|
offset = (page - 1) * per_page
|
|
@@ -170,6 +192,9 @@ module Pgbus
|
|
|
170
192
|
end
|
|
171
193
|
|
|
172
194
|
def discard_failed_event(id)
|
|
195
|
+
event = failed_event(id)
|
|
196
|
+
release_lock_for_payload(event["payload"]) if event
|
|
197
|
+
|
|
173
198
|
connection.exec_delete(
|
|
174
199
|
"DELETE FROM pgbus_failed_events WHERE id = $1", "Pgbus Delete Failed Event", [id.to_i]
|
|
175
200
|
)
|
|
@@ -207,6 +232,8 @@ module Pgbus
|
|
|
207
232
|
end
|
|
208
233
|
|
|
209
234
|
def discard_all_failed
|
|
235
|
+
release_locks_for_failed_events
|
|
236
|
+
|
|
210
237
|
result = connection.execute("DELETE FROM pgbus_failed_events")
|
|
211
238
|
result.cmd_tuples
|
|
212
239
|
rescue StandardError => e
|
|
@@ -273,6 +300,7 @@ module Pgbus
|
|
|
273
300
|
|
|
274
301
|
def discard_dlq_message(queue_name, msg_id)
|
|
275
302
|
# queue_name here is the full DLQ name (already prefixed)
|
|
303
|
+
release_lock_for_message(queue_name, msg_id)
|
|
276
304
|
@client.delete_message(queue_name, msg_id.to_i, prefixed: false)
|
|
277
305
|
true
|
|
278
306
|
rescue StandardError => e
|
|
@@ -296,6 +324,8 @@ module Pgbus
|
|
|
296
324
|
messages = dlq_messages(page: 1, per_page: 1000)
|
|
297
325
|
return 0 if messages.empty?
|
|
298
326
|
|
|
327
|
+
release_locks_for_messages(messages)
|
|
328
|
+
|
|
299
329
|
# Group by queue for batch delete — one call per DLQ instead of N calls
|
|
300
330
|
messages.group_by { |m| m[:queue_name] }.sum do |queue_name, msgs|
|
|
301
331
|
ids = msgs.map { |m| m[:msg_id].to_i }
|
|
@@ -552,6 +582,20 @@ module Pgbus
|
|
|
552
582
|
[]
|
|
553
583
|
end
|
|
554
584
|
|
|
585
|
+
def latency_trend(minutes: 60)
|
|
586
|
+
JobStat.latency_trend(minutes: minutes)
|
|
587
|
+
rescue StandardError => e
|
|
588
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching latency trend: #{e.message}" }
|
|
589
|
+
[]
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def latency_by_queue(minutes: 60)
|
|
593
|
+
JobStat.avg_latency_by_queue(minutes: minutes)
|
|
594
|
+
rescue StandardError => e
|
|
595
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching latency by queue: #{e.message}" }
|
|
596
|
+
[]
|
|
597
|
+
end
|
|
598
|
+
|
|
555
599
|
# Subscriber registry
|
|
556
600
|
def registered_subscribers
|
|
557
601
|
EventBus::Registry.instance.subscribers.map do |s|
|
|
@@ -719,6 +763,69 @@ module Pgbus
|
|
|
719
763
|
Pgbus.logger.debug { "[Pgbus::Web] Invalid recurring task arguments JSON: #{e.message}" }
|
|
720
764
|
[]
|
|
721
765
|
end
|
|
766
|
+
|
|
767
|
+
# --- Lock cleanup helpers ---
|
|
768
|
+
|
|
769
|
+
# Extract uniqueness key from a queue message and release its lock.
|
|
770
|
+
def release_lock_for_message(queue_name, msg_id)
|
|
771
|
+
row = connection.select_one(
|
|
772
|
+
"SELECT * FROM pgmq.q_#{sanitize_name(queue_name)} WHERE msg_id = $1",
|
|
773
|
+
"Pgbus Job Detail",
|
|
774
|
+
[msg_id.to_i]
|
|
775
|
+
)
|
|
776
|
+
return unless row
|
|
777
|
+
|
|
778
|
+
release_lock_for_payload(row["message"])
|
|
779
|
+
rescue StandardError => e
|
|
780
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error releasing lock for message #{msg_id}: #{e.message}" }
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Extract uniqueness key from a JSON payload string and release its lock.
|
|
784
|
+
def release_lock_for_payload(payload_str)
|
|
785
|
+
return unless payload_str
|
|
786
|
+
|
|
787
|
+
payload = payload_str.is_a?(String) ? JSON.parse(payload_str) : payload_str
|
|
788
|
+
key = payload[Uniqueness::METADATA_KEY]
|
|
789
|
+
JobLock.release!(key) if key
|
|
790
|
+
rescue JSON::ParserError => e
|
|
791
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error parsing payload for lock release: #{e.message}" }
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Extract uniqueness keys from a collection of formatted messages and
|
|
795
|
+
# release all associated locks in a single query.
|
|
796
|
+
def release_locks_for_messages(messages)
|
|
797
|
+
keys = messages.filter_map do |m|
|
|
798
|
+
payload = m[:message]
|
|
799
|
+
next unless payload
|
|
800
|
+
|
|
801
|
+
parsed = payload.is_a?(String) ? JSON.parse(payload) : payload
|
|
802
|
+
parsed[Uniqueness::METADATA_KEY]
|
|
803
|
+
rescue JSON::ParserError
|
|
804
|
+
nil
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
JobLock.where(lock_key: keys).delete_all if keys.any?
|
|
808
|
+
rescue StandardError => e
|
|
809
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error releasing locks for messages: #{e.message}" }
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# Collect uniqueness keys from all failed events and release their locks.
|
|
813
|
+
def release_locks_for_failed_events
|
|
814
|
+
rows = connection.select_all(
|
|
815
|
+
"SELECT payload FROM pgbus_failed_events", "Pgbus Collect Failed Keys"
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
keys = rows.to_a.filter_map do |row|
|
|
819
|
+
payload = JSON.parse(row["payload"])
|
|
820
|
+
payload[Uniqueness::METADATA_KEY]
|
|
821
|
+
rescue JSON::ParserError
|
|
822
|
+
nil
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
JobLock.where(lock_key: keys).delete_all if keys.any?
|
|
826
|
+
rescue StandardError => e
|
|
827
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error releasing locks for failed events: #{e.message}" }
|
|
828
|
+
end
|
|
722
829
|
end
|
|
723
830
|
end
|
|
724
831
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- app/controllers/pgbus/dashboard_controller.rb
|
|
128
128
|
- app/controllers/pgbus/dead_letter_controller.rb
|
|
129
129
|
- app/controllers/pgbus/events_controller.rb
|
|
130
|
+
- app/controllers/pgbus/frontends_controller.rb
|
|
130
131
|
- app/controllers/pgbus/insights_controller.rb
|
|
131
132
|
- app/controllers/pgbus/jobs_controller.rb
|
|
132
133
|
- app/controllers/pgbus/locale_controller.rb
|
|
@@ -135,6 +136,12 @@ files:
|
|
|
135
136
|
- app/controllers/pgbus/processes_controller.rb
|
|
136
137
|
- app/controllers/pgbus/queues_controller.rb
|
|
137
138
|
- app/controllers/pgbus/recurring_tasks_controller.rb
|
|
139
|
+
- app/frontend/pgbus/application.js
|
|
140
|
+
- app/frontend/pgbus/modules/charts.js
|
|
141
|
+
- app/frontend/pgbus/style.css
|
|
142
|
+
- app/frontend/pgbus/tailwind.css
|
|
143
|
+
- app/frontend/pgbus/vendor/apexcharts.js
|
|
144
|
+
- app/frontend/pgbus/vendor/turbo.js
|
|
138
145
|
- app/helpers/pgbus/application_helper.rb
|
|
139
146
|
- app/models/pgbus/application_record.rb
|
|
140
147
|
- app/models/pgbus/batch_entry.rb
|
|
@@ -192,12 +199,14 @@ files:
|
|
|
192
199
|
- lib/active_job/queue_adapters/pgbus_adapter.rb
|
|
193
200
|
- lib/generators/pgbus/add_job_locks_generator.rb
|
|
194
201
|
- lib/generators/pgbus/add_job_stats_generator.rb
|
|
202
|
+
- lib/generators/pgbus/add_job_stats_latency_generator.rb
|
|
195
203
|
- lib/generators/pgbus/add_outbox_generator.rb
|
|
196
204
|
- lib/generators/pgbus/add_queue_states_generator.rb
|
|
197
205
|
- lib/generators/pgbus/add_recurring_generator.rb
|
|
198
206
|
- lib/generators/pgbus/install_generator.rb
|
|
199
207
|
- lib/generators/pgbus/templates/add_job_locks.rb.erb
|
|
200
208
|
- lib/generators/pgbus/templates/add_job_stats.rb.erb
|
|
209
|
+
- lib/generators/pgbus/templates/add_job_stats_latency.rb.erb
|
|
201
210
|
- lib/generators/pgbus/templates/add_outbox.rb.erb
|
|
202
211
|
- lib/generators/pgbus/templates/add_queue_states.rb.erb
|
|
203
212
|
- lib/generators/pgbus/templates/add_recurring_tables.rb.erb
|