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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/pgbus/api/insights_controller.rb +6 -1
  3. data/app/controllers/pgbus/frontends_controller.rb +68 -0
  4. data/app/controllers/pgbus/insights_controller.rb +2 -0
  5. data/app/controllers/pgbus/jobs_controller.rb +5 -0
  6. data/app/frontend/pgbus/application.js +90 -0
  7. data/app/frontend/pgbus/modules/charts.js +106 -0
  8. data/app/frontend/pgbus/style.css +2 -0
  9. data/app/frontend/pgbus/tailwind.css +64 -0
  10. data/app/frontend/pgbus/vendor/apexcharts.js +38 -0
  11. data/app/frontend/pgbus/vendor/turbo.js +6696 -0
  12. data/app/models/pgbus/job_stat.rb +85 -13
  13. data/app/views/layouts/pgbus/application.html.erb +20 -141
  14. data/app/views/pgbus/insights/show.html.erb +86 -80
  15. data/app/views/pgbus/jobs/_enqueued_table.html.erb +8 -1
  16. data/config/locales/da.yml +3 -0
  17. data/config/locales/de.yml +3 -0
  18. data/config/locales/en.yml +19 -0
  19. data/config/locales/es.yml +3 -0
  20. data/config/locales/fi.yml +3 -0
  21. data/config/locales/fr.yml +3 -0
  22. data/config/locales/it.yml +3 -0
  23. data/config/locales/ja.yml +3 -0
  24. data/config/locales/nb.yml +3 -0
  25. data/config/locales/nl.yml +3 -0
  26. data/config/locales/pt.yml +3 -0
  27. data/config/locales/sv.yml +3 -0
  28. data/config/routes.rb +6 -0
  29. data/lib/generators/pgbus/add_job_stats_latency_generator.rb +52 -0
  30. data/lib/generators/pgbus/templates/add_job_stats_latency.rb.erb +9 -0
  31. data/lib/pgbus/active_job/executor.rb +24 -5
  32. data/lib/pgbus/recurring/schedule.rb +86 -0
  33. data/lib/pgbus/version.rb +1 -1
  34. data/lib/pgbus/web/data_source.rb +107 -0
  35. metadata +10 -1
@@ -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
@@ -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
@@ -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: ジョブ
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.2.9"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -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.2.9
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