pgbus 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +15 -0
- data/app/controllers/pgbus/dead_letter_controller.rb +17 -0
- data/app/controllers/pgbus/jobs_controller.rb +36 -0
- data/app/controllers/pgbus/locks_controller.rb +25 -0
- data/app/controllers/pgbus/recurring_tasks_controller.rb +5 -3
- data/app/frontend/pgbus/application.js +58 -1
- data/app/models/pgbus/job_lock.rb +16 -8
- data/app/models/pgbus/uniqueness_key.rb +36 -0
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +22 -2
- data/app/views/pgbus/dead_letter/index.html.erb +9 -1
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +36 -6
- data/app/views/pgbus/jobs/_failed_table.html.erb +35 -4
- data/app/views/pgbus/locks/index.html.erb +53 -28
- data/app/views/pgbus/queues/show.html.erb +58 -21
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +2 -1
- data/config/locales/da.yml +21 -9
- data/config/locales/de.yml +21 -9
- data/config/locales/en.yml +51 -9
- data/config/locales/es.yml +21 -9
- data/config/locales/fi.yml +21 -9
- data/config/locales/fr.yml +21 -9
- data/config/locales/it.yml +21 -9
- data/config/locales/ja.yml +21 -9
- data/config/locales/nb.yml +21 -9
- data/config/locales/nl.yml +21 -9
- data/config/locales/pt.yml +21 -9
- data/config/locales/sv.yml +21 -9
- data/config/routes.rb +12 -1
- data/lib/generators/pgbus/migrate_job_locks_generator.rb +56 -0
- data/lib/generators/pgbus/templates/add_uniqueness_keys.rb.erb +13 -0
- data/lib/generators/pgbus/templates/migrate_job_locks_to_uniqueness_keys.rb.erb +33 -0
- data/lib/pgbus/active_job/executor.rb +34 -20
- data/lib/pgbus/client.rb +18 -2
- data/lib/pgbus/process/dispatcher.rb +33 -10
- data/lib/pgbus/process/worker.rb +4 -1
- data/lib/pgbus/recurring/schedule.rb +38 -35
- data/lib/pgbus/stat_buffer.rb +107 -0
- data/lib/pgbus/uniqueness.rb +24 -39
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +49 -18
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a25921e6a7a0ac72023501978b6dec0b38645a18b45cece73bf15ddbc655dae8
|
|
4
|
+
data.tar.gz: 810980a58f382aad948b660a9102cdb6ea3a3af57c3768f1205c3582b0705003
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 597fbc986e88b2c37339156de5f21a576d50f45bab5f72343dc3eaed02b730d35dce777285f65b63f3264f4a91d76fa0b7a001635a6c103b08ef95454c6dc71f
|
|
7
|
+
data.tar.gz: 2a305826eb3b9b0618e64c99e0724c2daf020047933e83d231a35866d8e8b1e334a45c45c03a7fbef577b8dd2243686dc4e351595a671a8d8cd1a63f940602ed
|
data/Rakefile
CHANGED
|
@@ -176,4 +176,19 @@ task :release, %i[version force] do |_t, args|
|
|
|
176
176
|
puts " • Upload assets to the release"
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
+
namespace :dummy do
|
|
180
|
+
desc "Start dummy app with stub data for dashboard QA (PORT=3003, no database required)"
|
|
181
|
+
task :server do
|
|
182
|
+
port = ENV.fetch("PORT", "3003")
|
|
183
|
+
ENV["PGBUS_STUB_DATA"] = "1"
|
|
184
|
+
ENV["RAILS_ENV"] = "development"
|
|
185
|
+
|
|
186
|
+
puts "\n\e[32m→ Starting dummy app with stub data at http://localhost:#{port}/pgbus\e[0m"
|
|
187
|
+
puts " Dashboard: http://localhost:#{port}/pgbus"
|
|
188
|
+
puts " Using stub data source (no database needed)"
|
|
189
|
+
puts " Press Ctrl+C to stop\n\n"
|
|
190
|
+
sh("bundle exec puma spec/dummy/config.ru -p #{port}")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
179
194
|
task default: %i[spec rubocop]
|
|
@@ -46,5 +46,22 @@ module Pgbus
|
|
|
46
46
|
count = data_source.discard_all_dlq
|
|
47
47
|
redirect_to dead_letter_index_path, notice: "Discarded #{count} DLQ messages."
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
def discard_selected
|
|
51
|
+
selections = Array(params[:messages]).reject { |s| s[:queue_name].blank? || s[:msg_id].blank? }
|
|
52
|
+
if selections.empty?
|
|
53
|
+
redirect_to dead_letter_index_path, alert: t("pgbus.dead_letter.index.none_selected")
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
count = 0
|
|
58
|
+
selections.each do |sel|
|
|
59
|
+
queue_name = sel[:queue_name].to_s
|
|
60
|
+
next unless queue_name.end_with?(Pgbus.configuration.dead_letter_queue_suffix)
|
|
61
|
+
|
|
62
|
+
count += 1 if data_source.discard_dlq_message(queue_name, sel[:msg_id])
|
|
63
|
+
end
|
|
64
|
+
redirect_to dead_letter_index_path, notice: t("pgbus.dead_letter.index.discarded_selected", count: count)
|
|
65
|
+
end
|
|
49
66
|
end
|
|
50
67
|
end
|
|
@@ -49,5 +49,41 @@ module Pgbus
|
|
|
49
49
|
count = data_source.discard_all_enqueued
|
|
50
50
|
redirect_to jobs_path, notice: t("pgbus.jobs.index.discard_all_enqueued_notice", count: count)
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
def discard_selected_failed
|
|
54
|
+
ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
|
|
55
|
+
if ids.empty?
|
|
56
|
+
redirect_to jobs_path, alert: t("pgbus.jobs.index.none_selected")
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
count = 0
|
|
61
|
+
ids.each do |id|
|
|
62
|
+
count += 1 if data_source.discard_failed_event(id)
|
|
63
|
+
end
|
|
64
|
+
redirect_to jobs_path, notice: t("pgbus.jobs.index.discarded_selected", count: count)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def discard_selected_enqueued
|
|
68
|
+
selections = Array(params[:messages]).filter_map do |s|
|
|
69
|
+
next unless s.respond_to?(:[])
|
|
70
|
+
|
|
71
|
+
queue_name = s[:queue_name]
|
|
72
|
+
msg_id = s[:msg_id]
|
|
73
|
+
next if queue_name.blank? || msg_id.blank?
|
|
74
|
+
|
|
75
|
+
{ queue_name: queue_name, msg_id: msg_id }
|
|
76
|
+
end
|
|
77
|
+
if selections.empty?
|
|
78
|
+
redirect_to jobs_path, alert: t("pgbus.jobs.index.none_selected")
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
count = 0
|
|
83
|
+
selections.each do |sel|
|
|
84
|
+
count += 1 if data_source.discard_job(sel[:queue_name], sel[:msg_id])
|
|
85
|
+
end
|
|
86
|
+
redirect_to jobs_path, notice: t("pgbus.jobs.index.discarded_selected", count: count)
|
|
87
|
+
end
|
|
52
88
|
end
|
|
53
89
|
end
|
|
@@ -5,5 +5,30 @@ module Pgbus
|
|
|
5
5
|
def index
|
|
6
6
|
@locks = data_source.job_locks
|
|
7
7
|
end
|
|
8
|
+
|
|
9
|
+
def discard
|
|
10
|
+
count = data_source.discard_lock(params[:id])
|
|
11
|
+
if count.positive?
|
|
12
|
+
redirect_to locks_path, notice: t("pgbus.locks.index.lock_discarded")
|
|
13
|
+
else
|
|
14
|
+
redirect_to locks_path, alert: t("pgbus.locks.index.lock_discard_failed")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def discard_selected
|
|
19
|
+
keys = Array(params[:lock_keys]).reject(&:blank?)
|
|
20
|
+
if keys.empty?
|
|
21
|
+
redirect_to locks_path, alert: t("pgbus.locks.index.none_selected")
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
count = data_source.discard_locks(keys)
|
|
26
|
+
redirect_to locks_path, notice: t("pgbus.locks.index.locks_discarded", count: count)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def discard_all
|
|
30
|
+
count = data_source.discard_all_locks
|
|
31
|
+
redirect_to locks_path, notice: t("pgbus.locks.index.all_locks_discarded", count: count)
|
|
32
|
+
end
|
|
8
33
|
end
|
|
9
34
|
end
|
|
@@ -18,10 +18,12 @@ module Pgbus
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def toggle
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
result = data_source.toggle_recurring_task(params[:id])
|
|
22
|
+
if result
|
|
23
|
+
message = result == :enabled ? t("pgbus.recurring_tasks.toggle.enabled") : t("pgbus.recurring_tasks.toggle.disabled")
|
|
24
|
+
redirect_to pgbus.recurring_tasks_path, notice: message
|
|
23
25
|
else
|
|
24
|
-
redirect_to pgbus.recurring_tasks_path, alert: "
|
|
26
|
+
redirect_to pgbus.recurring_tasks_path, alert: t("pgbus.recurring_tasks.toggle.failed")
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -69,12 +69,69 @@ function renderFlashToasts() {
|
|
|
69
69
|
renderFlashToasts();
|
|
70
70
|
document.addEventListener("turbo:load", renderFlashToasts);
|
|
71
71
|
|
|
72
|
-
// --
|
|
72
|
+
// -- Bulk checkbox selection --
|
|
73
|
+
function initBulkSelect() {
|
|
74
|
+
document.querySelectorAll("[data-bulk-select-all]").forEach(selectAll => {
|
|
75
|
+
if (selectAll.dataset.bulkInitialized === "true") return;
|
|
76
|
+
selectAll.dataset.bulkInitialized = "true";
|
|
77
|
+
|
|
78
|
+
const scope = selectAll.closest("[data-bulk-scope]") || document;
|
|
79
|
+
const checkboxes = () => scope.querySelectorAll("input[data-bulk-item]");
|
|
80
|
+
// Look for bulk-actions in the parent page container (outside the table scope)
|
|
81
|
+
const pageScope = scope.closest("[data-bulk-page]") || scope.closest("turbo-frame") || scope.parentElement?.closest("div") || document;
|
|
82
|
+
const countEl = pageScope.querySelector("[data-bulk-count]");
|
|
83
|
+
const actions = pageScope.querySelector("[data-bulk-actions]");
|
|
84
|
+
|
|
85
|
+
function updateUI() {
|
|
86
|
+
const checked = scope.querySelectorAll("input[data-bulk-item]:checked");
|
|
87
|
+
if (countEl) countEl.textContent = checked.length;
|
|
88
|
+
if (actions) actions.classList.toggle("hidden", checked.length === 0);
|
|
89
|
+
|
|
90
|
+
const all = checkboxes();
|
|
91
|
+
selectAll.checked = all.length > 0 && checked.length === all.length;
|
|
92
|
+
selectAll.indeterminate = checked.length > 0 && checked.length < all.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let inSelectAll = false;
|
|
96
|
+
selectAll.addEventListener("change", () => {
|
|
97
|
+
inSelectAll = true;
|
|
98
|
+
checkboxes().forEach(cb => {
|
|
99
|
+
cb.checked = selectAll.checked;
|
|
100
|
+
// Dispatch change event so inline onchange handlers fire (e.g., hidden input sync)
|
|
101
|
+
cb.dispatchEvent(new Event("change", { bubbles: false }));
|
|
102
|
+
});
|
|
103
|
+
inSelectAll = false;
|
|
104
|
+
updateUI();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
scope.addEventListener("change", (e) => {
|
|
108
|
+
if (e.target.matches("input[data-bulk-item]")) updateUI();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
initBulkSelect();
|
|
114
|
+
document.addEventListener("turbo:load", initBulkSelect);
|
|
115
|
+
document.addEventListener("turbo:frame-load", initBulkSelect);
|
|
116
|
+
|
|
117
|
+
// -- Auto-refresh (pauses when user is interacting) --
|
|
73
118
|
const refreshInterval = parseInt(document.body?.dataset.pgbusRefreshInterval || "0", 10);
|
|
74
119
|
if (refreshInterval > 0) {
|
|
75
120
|
let timer;
|
|
121
|
+
|
|
122
|
+
function hasUserInteraction() {
|
|
123
|
+
// Pause when any checkbox is checked
|
|
124
|
+
const checked = document.querySelector("input[data-bulk-item]:checked, input[data-bulk-select-all]:checked");
|
|
125
|
+
if (checked) return true;
|
|
126
|
+
// Pause when any job detail row is expanded
|
|
127
|
+
const openDetails = document.querySelector("turbo-frame[data-auto-refresh] details[open]");
|
|
128
|
+
if (openDetails) return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
76
132
|
function refreshFrames() {
|
|
77
133
|
if (document.hidden) return;
|
|
134
|
+
if (hasUserInteraction()) return;
|
|
78
135
|
document.querySelectorAll("turbo-frame[data-auto-refresh]")
|
|
79
136
|
.forEach(frame => {
|
|
80
137
|
try {
|
|
@@ -62,23 +62,31 @@ module Pgbus
|
|
|
62
62
|
!result.nil?
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# Reap orphaned locks
|
|
66
|
-
#
|
|
67
|
-
# Returns the number of orphaned locks released.
|
|
68
|
-
# Reap orphaned locks by matching (pid, hostname) against live process entries.
|
|
69
|
-
# A lock is orphaned if no healthy process exists with the same pid AND hostname.
|
|
65
|
+
# Reap orphaned locks whose owner is no longer alive, plus stale queued
|
|
66
|
+
# locks that were never claimed by a worker.
|
|
67
|
+
# Returns the total number of orphaned locks released.
|
|
70
68
|
def self.reap_orphaned!
|
|
69
|
+
reaped = 0
|
|
70
|
+
|
|
71
|
+
# 1. Executing locks whose owner process has no healthy heartbeat
|
|
71
72
|
alive_workers = ProcessEntry
|
|
72
73
|
.where("last_heartbeat_at >= ?", Time.current - Process::Heartbeat::ALIVE_THRESHOLD)
|
|
73
74
|
.pluck(:pid, :hostname)
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
orphaned_executing = executing.select do |lock|
|
|
76
77
|
alive_workers.none? { |pid, hostname| pid == lock.owner_pid && hostname == lock.owner_hostname }
|
|
77
78
|
end
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
reaped += where(id: orphaned_executing.map(&:id)).delete_all if orphaned_executing.any?
|
|
81
|
+
|
|
82
|
+
# 2. Queued locks older than the visibility timeout that were never
|
|
83
|
+
# claimed. These are left behind when enqueue fails after lock
|
|
84
|
+
# acquisition (e.g. network error, process crash).
|
|
85
|
+
threshold = Pgbus.configuration.visibility_timeout
|
|
86
|
+
stale_queued = queued_locks.where("locked_at < ?", Time.current - threshold)
|
|
87
|
+
reaped += stale_queued.delete_all if stale_queued.exists?
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
reaped
|
|
82
90
|
end
|
|
83
91
|
|
|
84
92
|
# Last-resort cleanup: delete locks whose expires_at has passed.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class UniquenessKey < BusRecord
|
|
5
|
+
self.table_name = "pgbus_uniqueness_keys"
|
|
6
|
+
self.primary_key = "lock_key"
|
|
7
|
+
|
|
8
|
+
# Atomically try to acquire a uniqueness lock via INSERT ... ON CONFLICT.
|
|
9
|
+
# PostgreSQL's unique index on lock_key guarantees at most one caller wins.
|
|
10
|
+
# Returns true if acquired (row inserted), false if already locked.
|
|
11
|
+
def self.acquire!(lock_key, queue_name:, msg_id:) # rubocop:disable Naming/PredicateMethod
|
|
12
|
+
connection.exec_query(
|
|
13
|
+
"INSERT INTO #{table_name} (lock_key, queue_name, msg_id) " \
|
|
14
|
+
"VALUES ($1, $2, $3) ON CONFLICT (lock_key) DO NOTHING RETURNING lock_key",
|
|
15
|
+
"UniquenessKey Acquire", [lock_key, queue_name, msg_id]
|
|
16
|
+
).rows.any?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Release a uniqueness lock after job completion or DLQ.
|
|
20
|
+
def self.release!(lock_key)
|
|
21
|
+
connection.exec_delete(
|
|
22
|
+
"DELETE FROM #{table_name} WHERE lock_key = $1",
|
|
23
|
+
"UniquenessKey Release", [lock_key]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check if a key is currently locked.
|
|
28
|
+
def self.locked?(lock_key)
|
|
29
|
+
result = connection.select_value(
|
|
30
|
+
"SELECT 1 FROM #{table_name} WHERE lock_key = $1 LIMIT 1",
|
|
31
|
+
"UniquenessKey Check", [lock_key]
|
|
32
|
+
)
|
|
33
|
+
!result.nil?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
<turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= pgbus.dead_letter_index_path(frame: 'list') %>">
|
|
2
|
-
|
|
2
|
+
<%# Bulk form outside table to avoid nested form issues %>
|
|
3
|
+
<form id="bulk-discard-dlq-form" action="<%= pgbus.discard_selected_dead_letter_index_path %>" method="post" data-turbo-frame="_top" class="hidden">
|
|
4
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
5
|
+
</form>
|
|
6
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="dlq">
|
|
3
7
|
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
4
8
|
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
5
9
|
<tr>
|
|
10
|
+
<% if @messages.any? %>
|
|
11
|
+
<th class="w-10 px-4 py-3">
|
|
12
|
+
<input type="checkbox" data-bulk-select-all
|
|
13
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
|
|
14
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
15
|
+
</th>
|
|
16
|
+
<% end %>
|
|
6
17
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.id") %></th>
|
|
7
18
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.job_class") %></th>
|
|
8
19
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.source_queue") %></th>
|
|
@@ -16,6 +27,15 @@
|
|
|
16
27
|
<% dlq_suffix = Pgbus.configuration.dead_letter_queue_suffix %>
|
|
17
28
|
<% source_queue = m[:queue_name].to_s.delete_suffix(dlq_suffix) %>
|
|
18
29
|
<tr>
|
|
30
|
+
<td class="w-10 px-4 py-3 align-top">
|
|
31
|
+
<input type="checkbox" data-bulk-item
|
|
32
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_row", id: m[:msg_id]) %>"
|
|
33
|
+
data-queue-name="<%= m[:queue_name] %>" data-msg-id="<%= m[:msg_id] %>"
|
|
34
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 mt-1"
|
|
35
|
+
onchange="this.nextElementSibling.disabled = !this.checked; this.nextElementSibling.nextElementSibling.disabled = !this.checked">
|
|
36
|
+
<input type="hidden" name="messages[][queue_name]" value="<%= m[:queue_name] %>" form="bulk-discard-dlq-form" disabled>
|
|
37
|
+
<input type="hidden" name="messages[][msg_id]" value="<%= m[:msg_id] %>" form="bulk-discard-dlq-form" disabled>
|
|
38
|
+
</td>
|
|
19
39
|
<td colspan="5" class="p-0">
|
|
20
40
|
<details class="group">
|
|
21
41
|
<summary class="flex cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900 list-none">
|
|
@@ -69,7 +89,7 @@
|
|
|
69
89
|
</tr>
|
|
70
90
|
<% end %>
|
|
71
91
|
<% if @messages.empty? %>
|
|
72
|
-
<tr><td colspan="
|
|
92
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dead_letter.messages_table.empty") %></td></tr>
|
|
73
93
|
<% end %>
|
|
74
94
|
</tbody>
|
|
75
95
|
</table>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
<div class="flex items-center justify-between mb-6">
|
|
2
2
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.dead_letter.index.title") %></h1>
|
|
3
3
|
<% if @messages.any? %>
|
|
4
|
-
<div class="flex space-x-2">
|
|
4
|
+
<div class="flex items-center space-x-2">
|
|
5
|
+
<div data-bulk-actions class="hidden flex items-center space-x-2">
|
|
6
|
+
<span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
|
|
7
|
+
<button type="submit" form="bulk-discard-dlq-form"
|
|
8
|
+
class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
9
|
+
data-turbo-confirm="<%= t("pgbus.dead_letter.index.discard_selected_confirm") %>">
|
|
10
|
+
<%= t("pgbus.dead_letter.index.discard_selected") %>
|
|
11
|
+
</button>
|
|
12
|
+
</div>
|
|
5
13
|
<%= button_to t("pgbus.dead_letter.index.retry_all"), pgbus.retry_all_dead_letter_index_path, method: :post,
|
|
6
14
|
class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500",
|
|
7
15
|
data: { turbo_confirm: t("pgbus.dead_letter.index.retry_all_confirm") } %>
|
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
<turbo-frame id="jobs-enqueued" data-auto-refresh data-src="<%= pgbus.jobs_path(request.query_parameters.merge(frame: 'enqueued')) %>">
|
|
2
|
-
<div>
|
|
2
|
+
<div data-bulk-page>
|
|
3
3
|
<div class="flex items-center justify-between mb-3">
|
|
4
4
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.jobs.enqueued_table.title") %></h2>
|
|
5
5
|
<% if @jobs.any? %>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
<div class="flex items-center space-x-2">
|
|
7
|
+
<div data-bulk-actions class="hidden flex items-center space-x-2">
|
|
8
|
+
<span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
|
|
9
|
+
<button type="submit" form="bulk-discard-enqueued-form"
|
|
10
|
+
class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
11
|
+
data-turbo-confirm="<%= t("pgbus.jobs.index.discard_selected_confirm") %>">
|
|
12
|
+
<%= t("pgbus.jobs.index.discard_selected") %>
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
<%= button_to t("pgbus.jobs.enqueued_table.discard_all"), pgbus.discard_all_enqueued_jobs_path, method: :post,
|
|
16
|
+
class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
|
|
17
|
+
data: { turbo_confirm: t("pgbus.jobs.enqueued_table.discard_all_confirm"), turbo_frame: "_top" } %>
|
|
18
|
+
</div>
|
|
9
19
|
<% end %>
|
|
10
20
|
</div>
|
|
11
|
-
|
|
21
|
+
<%# Bulk form outside table to avoid nested form issues %>
|
|
22
|
+
<form id="bulk-discard-enqueued-form" action="<%= pgbus.discard_selected_enqueued_jobs_path %>" method="post" data-turbo-frame="_top" class="hidden">
|
|
23
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
24
|
+
</form>
|
|
25
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="enqueued">
|
|
12
26
|
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
13
27
|
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
14
28
|
<tr>
|
|
29
|
+
<% if @jobs.any? %>
|
|
30
|
+
<th class="w-10 px-4 py-3">
|
|
31
|
+
<input type="checkbox" data-bulk-select-all
|
|
32
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
|
|
33
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
34
|
+
</th>
|
|
35
|
+
<% end %>
|
|
15
36
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.id") %></th>
|
|
16
37
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.job_class") %></th>
|
|
17
38
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.queue") %></th>
|
|
@@ -23,6 +44,15 @@
|
|
|
23
44
|
<% @jobs.each do |j| %>
|
|
24
45
|
<% payload = pgbus_parse_message(j[:message]) %>
|
|
25
46
|
<tr>
|
|
47
|
+
<td class="w-10 px-4 py-3 align-top">
|
|
48
|
+
<input type="checkbox" data-bulk-item
|
|
49
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_row", id: j[:msg_id]) %>"
|
|
50
|
+
data-queue-name="<%= j[:queue_name] %>" data-msg-id="<%= j[:msg_id] %>"
|
|
51
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 mt-1"
|
|
52
|
+
onchange="this.nextElementSibling.disabled = !this.checked; this.nextElementSibling.nextElementSibling.disabled = !this.checked">
|
|
53
|
+
<input type="hidden" name="messages[][queue_name]" value="<%= j[:queue_name] %>" form="bulk-discard-enqueued-form" disabled>
|
|
54
|
+
<input type="hidden" name="messages[][msg_id]" value="<%= j[:msg_id] %>" form="bulk-discard-enqueued-form" disabled>
|
|
55
|
+
</td>
|
|
26
56
|
<td colspan="5" class="p-0">
|
|
27
57
|
<details class="group">
|
|
28
58
|
<summary class="flex cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900 list-none">
|
|
@@ -80,7 +110,7 @@
|
|
|
80
110
|
</tr>
|
|
81
111
|
<% end %>
|
|
82
112
|
<% if @jobs.empty? %>
|
|
83
|
-
<tr><td colspan="
|
|
113
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.jobs.enqueued_table.empty") %></td></tr>
|
|
84
114
|
<% end %>
|
|
85
115
|
</tbody>
|
|
86
116
|
</table>
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
<turbo-frame id="jobs-failed" data-auto-refresh data-src="<%= pgbus.jobs_path(frame: 'failed') %>">
|
|
2
|
-
<div class="mb-8">
|
|
3
|
-
<
|
|
4
|
-
|
|
2
|
+
<div class="mb-8" data-bulk-page>
|
|
3
|
+
<div class="flex items-center justify-between mb-3">
|
|
4
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.jobs.failed_table.title") %></h2>
|
|
5
|
+
<% if @failed.any? %>
|
|
6
|
+
<div class="flex items-center space-x-2">
|
|
7
|
+
<div data-bulk-actions class="hidden flex items-center space-x-2">
|
|
8
|
+
<span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
|
|
9
|
+
<button type="submit" form="bulk-discard-failed-form"
|
|
10
|
+
class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
11
|
+
data-turbo-confirm="<%= t("pgbus.jobs.index.discard_selected_confirm") %>">
|
|
12
|
+
<%= t("pgbus.jobs.index.discard_selected") %>
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
<%# Bulk form outside table to avoid nested form issues %>
|
|
19
|
+
<form id="bulk-discard-failed-form" action="<%= pgbus.discard_selected_failed_jobs_path %>" method="post" data-turbo-frame="_top" class="hidden">
|
|
20
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
21
|
+
</form>
|
|
22
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="failed">
|
|
5
23
|
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
6
24
|
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
7
25
|
<tr>
|
|
26
|
+
<% if @failed.any? %>
|
|
27
|
+
<th class="w-10 px-4 py-3">
|
|
28
|
+
<input type="checkbox" data-bulk-select-all
|
|
29
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
|
|
30
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
31
|
+
</th>
|
|
32
|
+
<% end %>
|
|
8
33
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.id") %></th>
|
|
9
34
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.queue") %></th>
|
|
10
35
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.error") %></th>
|
|
@@ -16,6 +41,12 @@
|
|
|
16
41
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
17
42
|
<% @failed.each do |f| %>
|
|
18
43
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
|
|
44
|
+
<td class="w-10 px-4 py-3">
|
|
45
|
+
<input type="checkbox" name="ids[]" value="<%= f["id"] %>" form="bulk-discard-failed-form"
|
|
46
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_row", id: f["id"]) %>"
|
|
47
|
+
data-bulk-item
|
|
48
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
49
|
+
</td>
|
|
19
50
|
<td data-label="ID" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white">
|
|
20
51
|
<%= link_to f["id"], pgbus.job_path(f["id"]), class: "text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
|
|
21
52
|
</td>
|
|
@@ -36,7 +67,7 @@
|
|
|
36
67
|
</tr>
|
|
37
68
|
<% end %>
|
|
38
69
|
<% if @failed.empty? %>
|
|
39
|
-
<tr><td colspan="
|
|
70
|
+
<tr><td colspan="7" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.jobs.failed_table.empty") %></td></tr>
|
|
40
71
|
<% end %>
|
|
41
72
|
</tbody>
|
|
42
73
|
</table>
|
|
@@ -1,52 +1,77 @@
|
|
|
1
|
-
<div class="mb-6">
|
|
2
|
-
<
|
|
3
|
-
|
|
1
|
+
<div class="flex items-center justify-between mb-6">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.locks.index.title") %></h1>
|
|
4
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.description") %></p>
|
|
5
|
+
</div>
|
|
6
|
+
<% if @locks.any? %>
|
|
7
|
+
<div class="flex items-center space-x-2">
|
|
8
|
+
<div data-bulk-actions class="hidden flex items-center space-x-2">
|
|
9
|
+
<span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
|
|
10
|
+
<button type="submit" form="bulk-discard-locks-form"
|
|
11
|
+
class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
12
|
+
data-turbo-confirm="<%= t("pgbus.locks.index.discard_selected_confirm") %>">
|
|
13
|
+
<%= t("pgbus.locks.index.discard_selected") %>
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
16
|
+
<%= button_to t("pgbus.locks.index.discard_all"), pgbus.discard_all_locks_path, method: :post,
|
|
17
|
+
class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
|
|
18
|
+
data: { turbo_confirm: t("pgbus.locks.index.discard_all_confirm") } %>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
4
21
|
</div>
|
|
5
22
|
|
|
6
|
-
|
|
23
|
+
<%# Bulk form lives outside the table to avoid nested form issues with button_to %>
|
|
24
|
+
<form id="bulk-discard-locks-form" action="<%= pgbus.discard_selected_locks_path %>" method="post" data-turbo-frame="_top" class="hidden">
|
|
25
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
26
|
+
</form>
|
|
27
|
+
|
|
28
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="locks">
|
|
7
29
|
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
8
30
|
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
9
31
|
<tr>
|
|
32
|
+
<% if @locks.any? %>
|
|
33
|
+
<th class="w-10 px-4 py-3">
|
|
34
|
+
<input type="checkbox" data-bulk-select-all
|
|
35
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
|
|
36
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
37
|
+
</th>
|
|
38
|
+
<% end %>
|
|
10
39
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.lock_key") %></th>
|
|
11
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.
|
|
12
|
-
<th class="px-4 py-3 text-
|
|
13
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.owner") %></th>
|
|
40
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.queue_name", default: "Queue") %></th>
|
|
41
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.msg_id", default: "Message ID") %></th>
|
|
14
42
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.locks.index.headers.age") %></th>
|
|
15
|
-
|
|
43
|
+
<% if @locks.any? %>
|
|
44
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"></th>
|
|
45
|
+
<% end %>
|
|
16
46
|
</tr>
|
|
17
47
|
</thead>
|
|
18
48
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
19
49
|
<% @locks.each do |lock| %>
|
|
20
50
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
21
|
-
<td
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<% else %>
|
|
27
|
-
<span class="inline-flex items-center rounded-full bg-yellow-100 dark:bg-yellow-900/50 px-2.5 py-0.5 text-xs font-medium text-yellow-800 dark:text-yellow-300"><%= t("pgbus.locks.index.queued") %></span>
|
|
28
|
-
<% end %>
|
|
29
|
-
</td>
|
|
30
|
-
<td data-label="Owner" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
|
31
|
-
<% if lock[:owner_pid] %>
|
|
32
|
-
<span class="font-mono"><%= lock[:owner_pid] %></span>
|
|
33
|
-
<% if lock[:owner_hostname] %>
|
|
34
|
-
<span class="text-xs text-gray-400 dark:text-gray-500">@<%= lock[:owner_hostname] %></span>
|
|
35
|
-
<% end %>
|
|
36
|
-
<% else %>
|
|
37
|
-
<span class="text-gray-400 dark:text-gray-500">—</span>
|
|
38
|
-
<% end %>
|
|
51
|
+
<td class="w-10 px-4 py-3">
|
|
52
|
+
<input type="checkbox" name="lock_keys[]" value="<%= lock[:lock_key] %>"
|
|
53
|
+
aria-label="<%= t("pgbus.helpers.bulk_select_row", id: lock[:lock_key]) %>"
|
|
54
|
+
form="bulk-discard-locks-form" data-bulk-item
|
|
55
|
+
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
39
56
|
</td>
|
|
57
|
+
<td data-label="Lock Key" class="px-4 py-3 text-sm font-mono text-gray-700 dark:text-gray-300 max-w-xs truncate"><%= lock[:lock_key] %></td>
|
|
58
|
+
<td data-label="Queue" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300"><%= lock[:queue_name] %></td>
|
|
59
|
+
<td data-label="Message ID" class="px-4 py-3 text-sm text-right font-mono text-gray-500 dark:text-gray-400"><%= lock[:msg_id] %></td>
|
|
40
60
|
<td data-label="Age" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
|
|
41
61
|
<% if lock[:age_seconds] %>
|
|
42
62
|
<%= pgbus_duration(lock[:age_seconds]) %>
|
|
43
63
|
<% end %>
|
|
44
64
|
</td>
|
|
45
|
-
<td
|
|
65
|
+
<td class="px-4 py-3 text-sm text-right">
|
|
66
|
+
<%= button_to t("pgbus.locks.index.discard"), pgbus.discard_lock_path(lock[:lock_key]),
|
|
67
|
+
method: :post,
|
|
68
|
+
class: "text-xs text-red-600 hover:text-red-800 font-medium",
|
|
69
|
+
data: { turbo_confirm: t("pgbus.locks.index.discard_confirm"), turbo_frame: "_top" } %>
|
|
70
|
+
</td>
|
|
46
71
|
</tr>
|
|
47
72
|
<% end %>
|
|
48
73
|
<% if @locks.empty? %>
|
|
49
|
-
<tr><td colspan="
|
|
74
|
+
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.locks.index.empty") %></td></tr>
|
|
50
75
|
<% end %>
|
|
51
76
|
</tbody>
|
|
52
77
|
</table>
|