pgbus 0.3.3 → 0.3.4
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/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/frontend/pgbus/application.js +45 -0
- 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/config/locales/da.yml +3 -7
- data/config/locales/de.yml +3 -7
- data/config/locales/en.yml +33 -7
- data/config/locales/es.yml +3 -7
- data/config/locales/fi.yml +3 -7
- data/config/locales/fr.yml +3 -7
- data/config/locales/it.yml +3 -7
- data/config/locales/ja.yml +3 -7
- data/config/locales/nb.yml +3 -7
- data/config/locales/nl.yml +3 -7
- data/config/locales/pt.yml +3 -7
- data/config/locales/sv.yml +3 -7
- 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 +92 -0
- data/lib/pgbus/uniqueness.rb +24 -39
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +46 -15
- 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: 35114da580e6620d0e519cd3d7bf4340728273a2e945eb30d228464d8b8dcc2c
|
|
4
|
+
data.tar.gz: ce2d743e27e02e05b2d94a7632403670cbeb5bf305be1fab0b39823cb87ce4e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f917b1f6c754fd12b687d714653e87a8766156498c468a9708b05aa26e51d0f852de3e0b6a9445b1f02f2569d18bd28e35c76cba40adcc09b089fa4bb2b7093
|
|
7
|
+
data.tar.gz: 1f54ebbd27831d925d2b73de1ac763461d1712e965cf79f1dc20c1c6cc9a99c1955cb777072a0aeee0b6ef4c8d0423bcb063b67cd6291faf3510c4efadda8069
|
|
@@ -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
|
|
@@ -69,6 +69,51 @@ function renderFlashToasts() {
|
|
|
69
69
|
renderFlashToasts();
|
|
70
70
|
document.addEventListener("turbo:load", renderFlashToasts);
|
|
71
71
|
|
|
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
|
+
|
|
72
117
|
// -- Auto-refresh --
|
|
73
118
|
const refreshInterval = parseInt(document.body?.dataset.pgbusRefreshInterval || "0", 10);
|
|
74
119
|
if (refreshInterval > 0) {
|
|
@@ -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>
|
data/config/locales/da.yml
CHANGED
|
@@ -239,16 +239,12 @@ da:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Aktive unikke låse forhindrer duplikeret jobudførelse
|
|
241
241
|
empty: Ingen aktive låse
|
|
242
|
-
executing: Udfører
|
|
243
242
|
headers:
|
|
244
243
|
age: Alder
|
|
245
|
-
expires: Udløber
|
|
246
|
-
job_class: Jobklasse
|
|
247
244
|
lock_key: Låsenøgle
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Joblåse
|
|
245
|
+
msg_id: Besked-ID
|
|
246
|
+
queue_name: Kø
|
|
247
|
+
title: Unikhedsnøgler
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Transaktionelle udbakke-poster, der venter på publicering til PGMQ
|
data/config/locales/de.yml
CHANGED
|
@@ -239,16 +239,12 @@ de:
|
|
|
239
239
|
index:
|
|
240
240
|
description: Aktive Einzigartigkeitssperren verhindern doppelte Auftragserstellung
|
|
241
241
|
empty: Keine aktiven Sperren
|
|
242
|
-
executing: Ausführen
|
|
243
242
|
headers:
|
|
244
243
|
age: Alter
|
|
245
|
-
expires: Läuft ab
|
|
246
|
-
job_class: Auftragsklasse
|
|
247
244
|
lock_key: Sperrschlüssel
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
title: Auftragssperren
|
|
245
|
+
msg_id: Nachrichten-ID
|
|
246
|
+
queue_name: Warteschlange
|
|
247
|
+
title: Eindeutigkeitsschlüssel
|
|
252
248
|
outbox:
|
|
253
249
|
index:
|
|
254
250
|
description: Transaktionale Postausgangseinträge ausstehend zur Veröffentlichung an PGMQ
|