pgbus 0.1.5 → 0.1.6
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/README.md +326 -11
- data/app/controllers/pgbus/api/insights_controller.rb +16 -0
- data/app/controllers/pgbus/insights_controller.rb +10 -0
- data/app/controllers/pgbus/locks_controller.rb +9 -0
- data/app/helpers/pgbus/application_helper.rb +28 -0
- data/app/models/pgbus/job_lock.rb +82 -0
- data/app/models/pgbus/job_stat.rb +94 -0
- data/app/views/layouts/pgbus/application.html.erb +32 -8
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +20 -20
- data/app/views/pgbus/insights/show.html.erb +161 -0
- data/app/views/pgbus/locks/index.html.erb +53 -0
- data/config/routes.rb +3 -0
- data/lib/generators/pgbus/add_job_locks_generator.rb +52 -0
- data/lib/generators/pgbus/add_job_stats_generator.rb +52 -0
- data/lib/generators/pgbus/add_outbox_generator.rb +1 -1
- data/lib/generators/pgbus/add_queue_states_generator.rb +1 -1
- data/lib/generators/pgbus/add_recurring_generator.rb +1 -1
- data/lib/generators/pgbus/install_generator.rb +1 -1
- data/lib/generators/pgbus/templates/add_job_locks.rb.erb +21 -0
- data/lib/generators/pgbus/templates/add_job_stats.rb.erb +18 -0
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +1 -1
- data/lib/pgbus/active_job/adapter.rb +58 -4
- data/lib/pgbus/active_job/executor.rb +45 -0
- data/lib/pgbus/client.rb +8 -22
- data/lib/pgbus/configuration.rb +6 -0
- data/lib/pgbus/engine.rb +1 -0
- data/lib/pgbus/process/consumer_priority.rb +64 -0
- data/lib/pgbus/process/dispatcher.rb +29 -0
- data/lib/pgbus/process/queue_lock.rb +87 -0
- data/lib/pgbus/process/supervisor.rb +6 -1
- data/lib/pgbus/process/wake_signal.rb +53 -0
- data/lib/pgbus/process/worker.rb +36 -6
- data/lib/pgbus/queue_factory.rb +62 -0
- data/lib/pgbus/uniqueness.rb +169 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +49 -0
- data/lib/pgbus.rb +1 -0
- metadata +17 -1
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" class="h-full
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Pgbus Dashboard</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = { darkMode: 'class' };
|
|
10
|
+
// Restore dark mode preference
|
|
11
|
+
if (localStorage.getItem('pgbus-dark') === 'true' ||
|
|
12
|
+
(!localStorage.getItem('pgbus-dark') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
13
|
+
document.documentElement.classList.add('dark');
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
8
16
|
<script type="module">
|
|
9
17
|
import * as Turbo from "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8/dist/turbo.es2017.esm.js";
|
|
10
18
|
|
|
11
19
|
<% if Pgbus.configuration.web_live_updates %>
|
|
12
|
-
// Auto-refresh turbo frames, pausing when tab is hidden
|
|
13
20
|
const interval = <%= Pgbus.configuration.web_refresh_interval %>;
|
|
14
21
|
if (interval > 0) {
|
|
15
22
|
let timer;
|
|
@@ -24,12 +31,18 @@
|
|
|
24
31
|
start();
|
|
25
32
|
}
|
|
26
33
|
<% end %>
|
|
34
|
+
|
|
35
|
+
// Dark mode toggle
|
|
36
|
+
window.toggleDarkMode = function() {
|
|
37
|
+
const isDark = document.documentElement.classList.toggle('dark');
|
|
38
|
+
localStorage.setItem('pgbus-dark', isDark);
|
|
39
|
+
};
|
|
27
40
|
</script>
|
|
28
41
|
</head>
|
|
29
|
-
<body class="h-full">
|
|
42
|
+
<body class="h-full bg-gray-50 dark:bg-gray-950 transition-colors">
|
|
30
43
|
<div class="min-h-full">
|
|
31
44
|
<!-- Top nav -->
|
|
32
|
-
<nav class="bg-gray-900">
|
|
45
|
+
<nav class="bg-gray-900 dark:bg-gray-950 border-b border-gray-800">
|
|
33
46
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
34
47
|
<div class="flex h-14 items-center justify-between">
|
|
35
48
|
<div class="flex items-center space-x-8">
|
|
@@ -47,8 +60,19 @@
|
|
|
47
60
|
<%= pgbus_nav_link "Events", pgbus.events_path %>
|
|
48
61
|
<%= pgbus_nav_link "DLQ", pgbus.dead_letter_index_path %>
|
|
49
62
|
<%= pgbus_nav_link "Outbox", pgbus.outbox_index_path %>
|
|
63
|
+
<%= pgbus_nav_link "Locks", pgbus.locks_path %>
|
|
64
|
+
<%= pgbus_nav_link "Insights", pgbus.insights_path %>
|
|
50
65
|
</div>
|
|
51
66
|
</div>
|
|
67
|
+
|
|
68
|
+
<button onclick="toggleDarkMode()" class="rounded-md p-2 text-gray-400 hover:text-white focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-900" aria-label="Toggle dark mode">
|
|
69
|
+
<svg class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
|
|
70
|
+
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
|
|
71
|
+
</svg>
|
|
72
|
+
<svg class="h-5 w-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
|
|
73
|
+
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
52
76
|
</div>
|
|
53
77
|
</div>
|
|
54
78
|
</nav>
|
|
@@ -56,15 +80,15 @@
|
|
|
56
80
|
<!-- Flash messages -->
|
|
57
81
|
<% if notice %>
|
|
58
82
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
|
|
59
|
-
<div class="rounded-md bg-green-50 p-3">
|
|
60
|
-
<p class="text-sm text-green-800"><%= notice %></p>
|
|
83
|
+
<div class="rounded-md bg-green-50 dark:bg-green-900/30 p-3">
|
|
84
|
+
<p class="text-sm text-green-800 dark:text-green-300"><%= notice %></p>
|
|
61
85
|
</div>
|
|
62
86
|
</div>
|
|
63
87
|
<% end %>
|
|
64
88
|
<% if alert %>
|
|
65
89
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
|
|
66
|
-
<div class="rounded-md bg-red-50 p-3">
|
|
67
|
-
<p class="text-sm text-red-800"><%= alert %></p>
|
|
90
|
+
<div class="rounded-md bg-red-50 dark:bg-red-900/30 p-3">
|
|
91
|
+
<p class="text-sm text-red-800 dark:text-red-300"><%= alert %></p>
|
|
68
92
|
</div>
|
|
69
93
|
</div>
|
|
70
94
|
<% end %>
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
<turbo-frame id="dashboard-stats" data-auto-refresh src="<%= pgbus.root_path(frame: 'stats') %>">
|
|
2
2
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-6 mb-8">
|
|
3
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
4
|
-
<p class="text-sm font-medium text-gray-500">Queues</p>
|
|
5
|
-
<p class="mt-1 text-3xl font-semibold text-gray-900"><%= @stats[:total_queues] %></p>
|
|
3
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
4
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Queues</p>
|
|
5
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:total_queues] %></p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
9
|
-
<p class="text-sm font-medium text-gray-500">Enqueued</p>
|
|
10
|
-
<p class="mt-1 text-3xl font-semibold text-gray-900"><%= pgbus_number(@stats[:total_depth]) %></p>
|
|
11
|
-
<p class="text-xs text-gray-400"><%= pgbus_number(@stats[:total_visible]) %> visible</p>
|
|
8
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
9
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Enqueued</p>
|
|
10
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@stats[:total_depth]) %></p>
|
|
11
|
+
<p class="text-xs text-gray-400 dark:text-gray-500"><%= pgbus_number(@stats[:total_visible]) %> visible</p>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
15
|
-
<p class="text-sm font-medium text-gray-500">Processes</p>
|
|
16
|
-
<p class="mt-1 text-3xl font-semibold <%= @stats[:active_processes] > 0 ? 'text-green-600' : 'text-gray-400' %>">
|
|
14
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
15
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Processes</p>
|
|
16
|
+
<p class="mt-1 text-3xl font-semibold <%= @stats[:active_processes] > 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-400' %>">
|
|
17
17
|
<%= @stats[:active_processes] %>
|
|
18
18
|
</p>
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
22
|
-
<p class="text-sm font-medium text-gray-500">Recurring</p>
|
|
23
|
-
<p class="mt-1 text-3xl font-semibold text-gray-900"><%= @stats[:recurring_count] %></p>
|
|
21
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
22
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Recurring</p>
|
|
23
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:recurring_count] %></p>
|
|
24
24
|
<p class="text-xs text-gray-400">
|
|
25
|
-
<%= link_to "View tasks", pgbus.recurring_tasks_path, class: "text-blue-500 hover:text-blue-700" %>
|
|
25
|
+
<%= link_to "View tasks", pgbus.recurring_tasks_path, class: "text-blue-500 hover:text-blue-700 dark:text-blue-400" %>
|
|
26
26
|
</p>
|
|
27
27
|
</div>
|
|
28
28
|
|
|
29
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
30
|
-
<p class="text-sm font-medium text-gray-500">Failed / DLQ</p>
|
|
31
|
-
<p class="mt-1 text-3xl font-semibold <%= (@stats[:failed_count] + @stats[:dlq_depth]) > 0 ? 'text-red-600' : 'text-gray-900' %>">
|
|
29
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
30
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Failed / DLQ</p>
|
|
31
|
+
<p class="mt-1 text-3xl font-semibold <%= (@stats[:failed_count] + @stats[:dlq_depth]) > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white' %>">
|
|
32
32
|
<%= @stats[:failed_count] %> / <%= @stats[:dlq_depth] %>
|
|
33
33
|
</p>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
|
-
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
37
|
-
<p class="text-sm font-medium text-gray-500">Throughput</p>
|
|
38
|
-
<p class="mt-1 text-3xl font-semibold text-gray-900"><%= @stats[:throughput_rate] %></p>
|
|
36
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
37
|
+
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Throughput</p>
|
|
38
|
+
<p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:throughput_rate] %></p>
|
|
39
39
|
<p class="text-xs text-gray-400">msgs/s</p>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Insights</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Job performance metrics for the last hour</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<!-- Summary cards -->
|
|
7
|
+
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6 mb-8">
|
|
8
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
9
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Total Jobs</dt>
|
|
10
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@summary[:total]) %></dd>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
13
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Succeeded</dt>
|
|
14
|
+
<dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400"><%= pgbus_number(@summary[:success]) %></dd>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
17
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Failed</dt>
|
|
18
|
+
<dd class="mt-1 text-2xl font-semibold text-red-600 dark:text-red-400"><%= pgbus_number(@summary[:failed]) %></dd>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
21
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Dead Lettered</dt>
|
|
22
|
+
<dd class="mt-1 text-2xl font-semibold text-orange-600 dark:text-orange-400"><%= pgbus_number(@summary[:dead_lettered]) %></dd>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
25
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Avg Duration</dt>
|
|
26
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:avg_duration_ms]) %></dd>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
29
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Max Duration</dt>
|
|
30
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:max_duration_ms]) %></dd>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Charts -->
|
|
35
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
36
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
37
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">Throughput (jobs/min)</h3>
|
|
38
|
+
<div id="throughput-chart" style="height: 280px;"></div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
41
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">Status Distribution</h3>
|
|
42
|
+
<div id="status-chart" style="height: 280px;"></div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Slowest job classes -->
|
|
47
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
48
|
+
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
49
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">Slowest Job Classes (avg duration)</h3>
|
|
50
|
+
</div>
|
|
51
|
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
52
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
53
|
+
<tr>
|
|
54
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Job Class</th>
|
|
55
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Count</th>
|
|
56
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Avg</th>
|
|
57
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Max</th>
|
|
58
|
+
</tr>
|
|
59
|
+
</thead>
|
|
60
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
61
|
+
<% @slowest.each do |row| %>
|
|
62
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
63
|
+
<td class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:job_class] %></td>
|
|
64
|
+
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
|
|
65
|
+
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
|
|
66
|
+
<td class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:max_ms]) %></td>
|
|
67
|
+
</tr>
|
|
68
|
+
<% end %>
|
|
69
|
+
<% if @slowest.empty? %>
|
|
70
|
+
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500">No job stats yet</td></tr>
|
|
71
|
+
<% end %>
|
|
72
|
+
</tbody>
|
|
73
|
+
</table>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<script src="https://cdn.jsdelivr.net/npm/apexcharts@4"></script>
|
|
77
|
+
<script>
|
|
78
|
+
let throughputChart, statusChart;
|
|
79
|
+
|
|
80
|
+
function getThemeColors() {
|
|
81
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
82
|
+
return {
|
|
83
|
+
isDark,
|
|
84
|
+
text: isDark ? '#9ca3af' : '#6b7280',
|
|
85
|
+
grid: isDark ? '#374151' : '#e5e7eb',
|
|
86
|
+
tooltip: isDark ? 'dark' : 'light',
|
|
87
|
+
dataLabel: isDark ? '#fff' : '#000'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderCharts(data) {
|
|
92
|
+
const t = getThemeColors();
|
|
93
|
+
|
|
94
|
+
// Destroy existing charts before re-rendering
|
|
95
|
+
if (throughputChart) throughputChart.destroy();
|
|
96
|
+
if (statusChart) statusChart.destroy();
|
|
97
|
+
|
|
98
|
+
// Throughput chart
|
|
99
|
+
const throughputData = data.throughput.map(p => ({
|
|
100
|
+
x: new Date(p.time).getTime(), y: p.count
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
throughputChart = new ApexCharts(document.querySelector('#throughput-chart'), {
|
|
104
|
+
series: [{ name: 'Jobs/min', data: throughputData }],
|
|
105
|
+
chart: { type: 'area', height: 280, toolbar: { show: false }, background: 'transparent', foreColor: t.text },
|
|
106
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
107
|
+
fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.4, opacityTo: 0.05, stops: [0, 100] } },
|
|
108
|
+
colors: ['#6366f1'],
|
|
109
|
+
xaxis: { type: 'datetime', labels: { style: { colors: t.text } } },
|
|
110
|
+
yaxis: { labels: { style: { colors: t.text } } },
|
|
111
|
+
grid: { borderColor: t.grid },
|
|
112
|
+
tooltip: { theme: t.tooltip },
|
|
113
|
+
dataLabels: { enabled: false }
|
|
114
|
+
});
|
|
115
|
+
throughputChart.render();
|
|
116
|
+
|
|
117
|
+
// Status distribution chart
|
|
118
|
+
const statusLabels = Object.keys(data.status_counts);
|
|
119
|
+
const statusValues = Object.values(data.status_counts);
|
|
120
|
+
const statusColors = statusLabels.map(s => {
|
|
121
|
+
if (s === 'success') return '#10b981';
|
|
122
|
+
if (s === 'failed') return '#ef4444';
|
|
123
|
+
if (s === 'dead_lettered') return '#f97316';
|
|
124
|
+
return '#6b7280';
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (statusLabels.length > 0) {
|
|
128
|
+
statusChart = new ApexCharts(document.querySelector('#status-chart'), {
|
|
129
|
+
series: statusValues, labels: statusLabels,
|
|
130
|
+
chart: { type: 'donut', height: 280, background: 'transparent', foreColor: t.text },
|
|
131
|
+
colors: statusColors,
|
|
132
|
+
legend: { position: 'bottom', labels: { colors: t.text } },
|
|
133
|
+
plotOptions: { pie: { donut: { size: '60%' } } },
|
|
134
|
+
dataLabels: { style: { colors: [t.dataLabel] } },
|
|
135
|
+
tooltip: { theme: t.tooltip }
|
|
136
|
+
});
|
|
137
|
+
statusChart.render();
|
|
138
|
+
} else {
|
|
139
|
+
document.querySelector('#status-chart').innerHTML =
|
|
140
|
+
'<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24">No data yet</p>';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fetch data and render
|
|
145
|
+
let chartData = null;
|
|
146
|
+
fetch('<%= pgbus.api_insights_path %>')
|
|
147
|
+
.then(r => r.json())
|
|
148
|
+
.then(data => { chartData = data; renderCharts(data); })
|
|
149
|
+
.catch(() => {
|
|
150
|
+
const msg = '<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24">Failed to load chart data</p>';
|
|
151
|
+
document.querySelector('#throughput-chart').innerHTML = msg;
|
|
152
|
+
document.querySelector('#status-chart').innerHTML = msg;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Re-render charts when dark mode toggles
|
|
156
|
+
const origToggle = window.toggleDarkMode;
|
|
157
|
+
window.toggleDarkMode = function() {
|
|
158
|
+
origToggle();
|
|
159
|
+
if (chartData) renderCharts(chartData);
|
|
160
|
+
};
|
|
161
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Job Locks</h1>
|
|
3
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Active uniqueness locks preventing duplicate job execution</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
7
|
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
8
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Lock Key</th>
|
|
11
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Job Class</th>
|
|
12
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">State</th>
|
|
13
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Owner</th>
|
|
14
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Age</th>
|
|
15
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Expires</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
19
|
+
<% @locks.each do |lock| %>
|
|
20
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
21
|
+
<td class="px-4 py-3 text-sm font-mono text-gray-700 dark:text-gray-300 max-w-xs truncate"><%= lock[:lock_key] %></td>
|
|
22
|
+
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300"><%= lock[:job_class] %></td>
|
|
23
|
+
<td class="px-4 py-3 text-sm">
|
|
24
|
+
<% if lock[:state] == "executing" %>
|
|
25
|
+
<span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:text-blue-300">Executing</span>
|
|
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">Queued</span>
|
|
28
|
+
<% end %>
|
|
29
|
+
</td>
|
|
30
|
+
<td 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 %>
|
|
39
|
+
</td>
|
|
40
|
+
<td class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
|
|
41
|
+
<% if lock[:age_seconds] %>
|
|
42
|
+
<%= pgbus_duration(lock[:age_seconds]) %>
|
|
43
|
+
<% end %>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= pgbus_time_ago_future(lock[:expires_at]) %></td>
|
|
46
|
+
</tr>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if @locks.empty? %>
|
|
49
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500">No active locks</td></tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -49,8 +49,11 @@ Pgbus::Engine.routes.draw do
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
resources :outbox, only: [:index], controller: "outbox"
|
|
52
|
+
resources :locks, only: [:index]
|
|
53
|
+
resource :insights, only: [:show], controller: "insights"
|
|
52
54
|
|
|
53
55
|
namespace :api do
|
|
54
56
|
get :stats, to: "stats#show"
|
|
57
|
+
get :insights, to: "insights#show"
|
|
55
58
|
end
|
|
56
59
|
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 AddJobLocksGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Add job locks table for uniqueness guarantees"
|
|
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_locks.rb.erb",
|
|
23
|
+
"db/pgbus_migrate/add_pgbus_job_locks.rb"
|
|
24
|
+
else
|
|
25
|
+
migration_template "add_job_locks.rb.erb",
|
|
26
|
+
"db/migrate/add_pgbus_job_locks.rb"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_post_install
|
|
31
|
+
say ""
|
|
32
|
+
say "Pgbus job locks table installed!", :green
|
|
33
|
+
say ""
|
|
34
|
+
say "Next steps:"
|
|
35
|
+
say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
|
|
36
|
+
say " 2. Add `ensures_uniqueness` to your job classes (DSL is auto-included)"
|
|
37
|
+
say " 3. Restart pgbus: bin/pgbus start"
|
|
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,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 AddJobStatsGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Add job stats table for dashboard insights and performance tracking"
|
|
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.rb.erb",
|
|
23
|
+
"db/pgbus_migrate/add_pgbus_job_stats.rb"
|
|
24
|
+
else
|
|
25
|
+
migration_template "add_job_stats.rb.erb",
|
|
26
|
+
"db/migrate/add_pgbus_job_stats.rb"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_post_install
|
|
31
|
+
say ""
|
|
32
|
+
say "Pgbus job stats table installed!", :green
|
|
33
|
+
say ""
|
|
34
|
+
say "Next steps:"
|
|
35
|
+
say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
|
|
36
|
+
say " 2. Stats collection is enabled by default"
|
|
37
|
+
say " 3. View 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
|
|
@@ -17,7 +17,7 @@ module Pgbus
|
|
|
17
17
|
default: nil,
|
|
18
18
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def create_migration_file
|
|
21
21
|
if separate_database?
|
|
22
22
|
migration_template "add_outbox.rb.erb",
|
|
23
23
|
"db/pgbus_migrate/add_pgbus_outbox.rb"
|
|
@@ -17,7 +17,7 @@ module Pgbus
|
|
|
17
17
|
default: nil,
|
|
18
18
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def create_migration_file
|
|
21
21
|
if separate_database?
|
|
22
22
|
migration_template "add_queue_states.rb.erb",
|
|
23
23
|
"db/pgbus_migrate/add_pgbus_queue_states.rb"
|
|
@@ -17,7 +17,7 @@ module Pgbus
|
|
|
17
17
|
default: nil,
|
|
18
18
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def create_migration_file
|
|
21
21
|
if separate_database?
|
|
22
22
|
migration_template "add_recurring_tables.rb.erb",
|
|
23
23
|
"db/pgbus_migrate/add_pgbus_recurring_tables.rb"
|
|
@@ -24,7 +24,7 @@ module Pgbus
|
|
|
24
24
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus). " \
|
|
25
25
|
"Migrations go to db/pgbus_migrate/ and schema to db/pgbus_schema.rb"
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def create_migration_file
|
|
28
28
|
if separate_database?
|
|
29
29
|
migration_template "migration.rb.erb",
|
|
30
30
|
"db/pgbus_migrate/create_pgbus_tables.rb"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class AddPgbusJobLocks < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :pgbus_job_locks do |t|
|
|
4
|
+
t.string :lock_key, null: false
|
|
5
|
+
t.string :job_class, null: false
|
|
6
|
+
t.string :job_id
|
|
7
|
+
t.string :state, null: false, default: "queued"
|
|
8
|
+
t.integer :owner_pid
|
|
9
|
+
t.string :owner_hostname
|
|
10
|
+
t.datetime :locked_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
11
|
+
t.datetime :expires_at, null: false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :pgbus_job_locks, :lock_key,
|
|
15
|
+
unique: true, name: "idx_pgbus_job_locks_key"
|
|
16
|
+
add_index :pgbus_job_locks, :expires_at,
|
|
17
|
+
name: "idx_pgbus_job_locks_expires"
|
|
18
|
+
add_index :pgbus_job_locks, [:state, :owner_pid],
|
|
19
|
+
name: "idx_pgbus_job_locks_reaper"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class AddPgbusJobStats < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :pgbus_job_stats do |t|
|
|
4
|
+
t.string :job_class, null: false
|
|
5
|
+
t.string :queue_name, null: false
|
|
6
|
+
t.string :status, null: false
|
|
7
|
+
t.integer :duration_ms, null: false, default: 0
|
|
8
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :pgbus_job_stats, :created_at,
|
|
12
|
+
name: "idx_pgbus_job_stats_time"
|
|
13
|
+
add_index :pgbus_job_stats, [:job_class, :created_at],
|
|
14
|
+
name: "idx_pgbus_job_stats_class_time"
|
|
15
|
+
add_index :pgbus_job_stats, [:status, :created_at],
|
|
16
|
+
name: "idx_pgbus_job_stats_status_time"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -17,7 +17,7 @@ module Pgbus
|
|
|
17
17
|
default: nil,
|
|
18
18
|
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def create_migration_file
|
|
21
21
|
if separate_database?
|
|
22
22
|
migration_template "upgrade_pgmq.rb.erb",
|
|
23
23
|
"db/pgbus_migrate/upgrade_pgmq_to_v#{target_version_slug}.rb"
|