pgbus 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +37 -3
- data/Rakefile +98 -1
- data/app/controllers/pgbus/application_controller.rb +8 -0
- data/app/controllers/pgbus/recurring_tasks_controller.rb +36 -0
- data/app/helpers/pgbus/application_helper.rb +41 -0
- data/app/models/pgbus/application_record.rb +7 -0
- data/app/models/pgbus/batch_entry.rb +31 -0
- data/app/models/pgbus/blocked_execution.rb +40 -0
- data/app/models/pgbus/process_entry.rb +9 -0
- data/app/models/pgbus/processed_event.rb +9 -0
- data/app/models/pgbus/recurring_execution.rb +33 -0
- data/app/models/pgbus/recurring_task.rb +42 -0
- data/app/models/pgbus/semaphore.rb +29 -0
- data/app/views/layouts/pgbus/application.html.erb +1 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +9 -1
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +55 -18
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +46 -8
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +79 -0
- data/app/views/pgbus/recurring_tasks/index.html.erb +6 -0
- data/app/views/pgbus/recurring_tasks/show.html.erb +122 -0
- data/config/routes.rb +7 -0
- data/lib/active_job/queue_adapters/pgbus_adapter.rb +29 -0
- data/lib/generators/pgbus/add_recurring_generator.rb +56 -0
- data/lib/generators/pgbus/install_generator.rb +76 -2
- data/lib/generators/pgbus/templates/add_recurring_tables.rb.erb +31 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +72 -4
- data/lib/generators/pgbus/templates/recurring.yml.erb +40 -0
- data/lib/generators/pgbus/templates/upgrade_pgmq.rb.erb +30 -0
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +60 -0
- data/lib/pgbus/active_job/adapter.rb +0 -3
- data/lib/pgbus/active_job/executor.rb +27 -12
- data/lib/pgbus/batch.rb +60 -69
- data/lib/pgbus/cli.rb +11 -16
- data/lib/pgbus/client.rb +25 -7
- data/lib/pgbus/concurrency/blocked_execution.rb +32 -37
- data/lib/pgbus/concurrency/semaphore.rb +11 -39
- data/lib/pgbus/concurrency.rb +10 -2
- data/lib/pgbus/configuration.rb +33 -0
- data/lib/pgbus/engine.rb +19 -1
- data/lib/pgbus/event_bus/handler.rb +4 -14
- data/lib/pgbus/instrumentation.rb +29 -0
- data/lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql +2123 -0
- data/lib/pgbus/pgmq_schema.rb +159 -0
- data/lib/pgbus/process/consumer.rb +8 -9
- data/lib/pgbus/process/dispatcher.rb +26 -24
- data/lib/pgbus/process/heartbeat.rb +15 -23
- data/lib/pgbus/process/signal_handler.rb +23 -1
- data/lib/pgbus/process/supervisor.rb +51 -2
- data/lib/pgbus/process/worker.rb +37 -9
- data/lib/pgbus/recurring/already_recorded.rb +7 -0
- data/lib/pgbus/recurring/command_job.rb +16 -0
- data/lib/pgbus/recurring/config_loader.rb +35 -0
- data/lib/pgbus/recurring/schedule.rb +102 -0
- data/lib/pgbus/recurring/scheduler.rb +102 -0
- data/lib/pgbus/recurring/task.rb +111 -0
- data/lib/pgbus/serializer.rb +10 -6
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +187 -22
- data/lib/pgbus.rb +8 -0
- data/lib/tasks/pgbus_pgmq.rake +62 -0
- metadata +51 -24
- data/.bun-version +0 -1
- data/.claude/commands/architect.md +0 -100
- data/.claude/commands/github-review-comments.md +0 -237
- data/.claude/commands/lfg.md +0 -271
- data/.claude/commands/review-pr.md +0 -69
- data/.claude/commands/security.md +0 -122
- data/.claude/commands/tdd.md +0 -148
- data/.claude/rules/agents.md +0 -49
- data/.claude/rules/coding-style.md +0 -91
- data/.claude/rules/git-workflow.md +0 -56
- data/.claude/rules/performance.md +0 -73
- data/.claude/rules/testing.md +0 -67
- data/CLAUDE.md +0 -80
- data/CODE_OF_CONDUCT.md +0 -10
- data/bun.lock +0 -18
- data/docs/README.md +0 -28
- data/docs/switch_from_good_job.md +0 -279
- data/docs/switch_from_sidekiq.md +0 -226
- data/docs/switch_from_solid_queue.md +0 -247
- data/package.json +0 -9
- data/sig/pgbus.rbs +0 -4
|
@@ -6,21 +6,59 @@
|
|
|
6
6
|
<thead class="bg-gray-50">
|
|
7
7
|
<tr>
|
|
8
8
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">ID</th>
|
|
9
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Job Class</th>
|
|
9
10
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
|
|
10
11
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Enqueued</th>
|
|
11
12
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Reads</th>
|
|
12
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Payload</th>
|
|
13
13
|
</tr>
|
|
14
14
|
</thead>
|
|
15
15
|
<tbody class="divide-y divide-gray-100">
|
|
16
16
|
<% @jobs.each do |j| %>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<td class="
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
<% payload = pgbus_parse_message(j[:message]) %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td colspan="5" class="p-0">
|
|
20
|
+
<details class="group">
|
|
21
|
+
<summary class="flex cursor-pointer hover:bg-gray-50 list-none">
|
|
22
|
+
<span class="w-16 shrink-0 px-4 py-3 text-sm font-mono text-gray-900"><%= j[:msg_id] %></span>
|
|
23
|
+
<span class="flex-1 px-4 py-3 text-sm font-medium text-gray-900"><%= payload["job_class"] || "—" %></span>
|
|
24
|
+
<span class="w-40 shrink-0 px-4 py-3 text-sm text-gray-700"><%= j[:queue_name] %></span>
|
|
25
|
+
<span class="w-28 shrink-0 px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(j[:enqueued_at]) %></span>
|
|
26
|
+
<span class="w-16 shrink-0 px-4 py-3 text-sm text-gray-500"><%= j[:read_ct] %></span>
|
|
27
|
+
</summary>
|
|
28
|
+
<div class="px-4 pb-4 bg-gray-50 border-t border-gray-100">
|
|
29
|
+
<div class="flex items-center mt-3 mb-3">
|
|
30
|
+
<span class="text-xs font-mono text-gray-400">Job ID: <%= payload["job_id"] %></span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="grid grid-cols-2 gap-4 mb-3">
|
|
33
|
+
<div>
|
|
34
|
+
<span class="text-xs font-medium text-gray-500">Arguments</span>
|
|
35
|
+
<pre class="text-xs text-gray-700 bg-white rounded p-2 mt-1 overflow-x-auto max-h-40"><%= JSON.pretty_generate(payload["arguments"] || []) rescue "—" %></pre>
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<span class="text-xs font-medium text-gray-500">Metadata</span>
|
|
39
|
+
<div class="text-xs text-gray-600 bg-white rounded p-2 mt-1 space-y-1">
|
|
40
|
+
<% if payload["queue_name"] %><p><strong>Queue:</strong> <%= payload["queue_name"] %></p><% end %>
|
|
41
|
+
<% if payload["priority"] %><p><strong>Priority:</strong> <%= payload["priority"] %></p><% end %>
|
|
42
|
+
<% if payload["locale"] %><p><strong>Locale:</strong> <%= payload["locale"] %></p><% end %>
|
|
43
|
+
<% if payload["timezone"] %><p><strong>Timezone:</strong> <%= payload["timezone"] %></p><% end %>
|
|
44
|
+
<% if payload["scheduled_at"] %><p><strong>Scheduled:</strong> <%= payload["scheduled_at"] %></p><% end %>
|
|
45
|
+
<% if j[:vt] %><p><strong>Visible at:</strong> <%= j[:vt] %></p><% end %>
|
|
46
|
+
<% if j[:last_read_at] %><p><strong>Last read:</strong> <%= j[:last_read_at] %></p><% end %>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<details class="mt-2">
|
|
51
|
+
<summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700">Full JSON payload</summary>
|
|
52
|
+
<pre class="text-xs text-gray-600 bg-white rounded p-2 mt-1 overflow-x-auto max-h-96"><%= JSON.pretty_generate(payload) rescue j[:message] %></pre>
|
|
53
|
+
</details>
|
|
54
|
+
<% if j[:headers] %>
|
|
55
|
+
<details class="mt-2">
|
|
56
|
+
<summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700">Headers</summary>
|
|
57
|
+
<pre class="text-xs text-gray-600 bg-white rounded p-2 mt-1 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(j[:headers])) rescue j[:headers] %></pre>
|
|
58
|
+
</details>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
</details>
|
|
24
62
|
</td>
|
|
25
63
|
</tr>
|
|
26
64
|
<% end %>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<turbo-frame id="recurring-tasks" data-auto-refresh src="<%= pgbus.recurring_tasks_path(frame: 'recurring_tasks') %>">
|
|
2
|
+
<div class="rounded-lg bg-white shadow ring-1 ring-gray-200">
|
|
3
|
+
<% if @recurring_tasks.empty? %>
|
|
4
|
+
<div class="p-8 text-center text-gray-500">
|
|
5
|
+
<p class="text-lg font-medium">No recurring tasks configured</p>
|
|
6
|
+
<p class="mt-1 text-sm">Add tasks to <code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs">config/recurring.yml</code></p>
|
|
7
|
+
</div>
|
|
8
|
+
<% else %>
|
|
9
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
10
|
+
<thead class="bg-gray-50">
|
|
11
|
+
<tr>
|
|
12
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Task</th>
|
|
13
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Schedule</th>
|
|
14
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Queue</th>
|
|
15
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Last Run</th>
|
|
16
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Next Run</th>
|
|
17
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
18
|
+
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody class="divide-y divide-gray-200">
|
|
22
|
+
<% @recurring_tasks.each do |task| %>
|
|
23
|
+
<tr class="hover:bg-gray-50">
|
|
24
|
+
<td class="px-4 py-3">
|
|
25
|
+
<div>
|
|
26
|
+
<%= link_to task[:key], pgbus.recurring_task_path(task[:id]),
|
|
27
|
+
class: "text-sm font-medium text-blue-600 hover:text-blue-800" %>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="text-xs text-gray-500"><%= task[:class_name] || task[:command]&.truncate(40) %></div>
|
|
30
|
+
<% if task[:description] %>
|
|
31
|
+
<div class="text-xs text-gray-400 mt-0.5"><%= task[:description] %></div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</td>
|
|
34
|
+
<td class="px-4 py-3">
|
|
35
|
+
<div class="text-sm text-gray-900"><%= task[:schedule] %></div>
|
|
36
|
+
<% if task[:human_schedule] && task[:human_schedule] != task[:schedule] %>
|
|
37
|
+
<div class="text-xs text-gray-400"><%= task[:human_schedule] %></div>
|
|
38
|
+
<% end %>
|
|
39
|
+
</td>
|
|
40
|
+
<td class="px-4 py-3 text-sm text-gray-600">
|
|
41
|
+
<%= task[:queue_name] || "default" %>
|
|
42
|
+
</td>
|
|
43
|
+
<td class="px-4 py-3 text-sm text-gray-600">
|
|
44
|
+
<%= task[:last_run_at] ? pgbus_time_ago(task[:last_run_at]) : "Never" %>
|
|
45
|
+
</td>
|
|
46
|
+
<td class="px-4 py-3 text-sm text-gray-600">
|
|
47
|
+
<% if task[:enabled] && task[:next_run_at] %>
|
|
48
|
+
<%= pgbus_time_ago_future(task[:next_run_at]) %>
|
|
49
|
+
<% else %>
|
|
50
|
+
<span class="text-gray-400">—</span>
|
|
51
|
+
<% end %>
|
|
52
|
+
</td>
|
|
53
|
+
<td class="px-4 py-3">
|
|
54
|
+
<% if task[:enabled] %>
|
|
55
|
+
<%= pgbus_recurring_health_badge(task) %>
|
|
56
|
+
<% else %>
|
|
57
|
+
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">
|
|
58
|
+
Disabled
|
|
59
|
+
</span>
|
|
60
|
+
<% end %>
|
|
61
|
+
</td>
|
|
62
|
+
<td class="px-4 py-3 text-right space-x-1">
|
|
63
|
+
<%= button_to task[:enabled] ? "Disable" : "Enable",
|
|
64
|
+
pgbus.toggle_recurring_task_path(task[:id]),
|
|
65
|
+
class: "inline-flex items-center rounded px-2 py-1 text-xs font-medium " \
|
|
66
|
+
"#{task[:enabled] ? 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200' : 'bg-green-100 text-green-700 hover:bg-green-200'}" %>
|
|
67
|
+
<% if task[:enabled] %>
|
|
68
|
+
<%= button_to "Run Now",
|
|
69
|
+
pgbus.enqueue_recurring_task_path(task[:id]),
|
|
70
|
+
class: "inline-flex items-center rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 hover:bg-blue-200" %>
|
|
71
|
+
<% end %>
|
|
72
|
+
</td>
|
|
73
|
+
</tr>
|
|
74
|
+
<% end %>
|
|
75
|
+
</tbody>
|
|
76
|
+
</table>
|
|
77
|
+
<% end %>
|
|
78
|
+
</div>
|
|
79
|
+
</turbo-frame>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<div class="flex items-center justify-between mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Recurring Tasks</h1>
|
|
3
|
+
<span class="text-sm text-gray-500"><%= @recurring_tasks.size %> task(s) configured</span>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "pgbus/recurring_tasks/tasks_table" %>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="flex items-center justify-between">
|
|
3
|
+
<div>
|
|
4
|
+
<h1 class="text-2xl font-bold text-gray-900"><%= @task[:key] %></h1>
|
|
5
|
+
<p class="mt-1 text-sm text-gray-500"><%= @task[:class_name] || @task[:command] %></p>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="flex space-x-2">
|
|
8
|
+
<%= button_to @task[:enabled] ? "Disable" : "Enable",
|
|
9
|
+
pgbus.toggle_recurring_task_path(@task[:id]),
|
|
10
|
+
class: "inline-flex items-center rounded-md px-3 py-2 text-sm font-medium " \
|
|
11
|
+
"#{@task[:enabled] ? 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200' : 'bg-green-100 text-green-700 hover:bg-green-200'}" %>
|
|
12
|
+
<% if @task[:enabled] %>
|
|
13
|
+
<%= button_to "Run Now",
|
|
14
|
+
pgbus.enqueue_recurring_task_path(@task[:id]),
|
|
15
|
+
class: "inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700" %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to "Back", pgbus.recurring_tasks_path,
|
|
18
|
+
class: "inline-flex items-center rounded-md bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200" %>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
24
|
+
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
25
|
+
<p class="text-sm font-medium text-gray-500">Schedule</p>
|
|
26
|
+
<p class="mt-1 text-lg font-semibold text-gray-900"><%= @task[:schedule] %></p>
|
|
27
|
+
<% if @task[:human_schedule] && @task[:human_schedule] != @task[:schedule] %>
|
|
28
|
+
<p class="text-xs text-gray-400"><%= @task[:human_schedule] %></p>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
33
|
+
<p class="text-sm font-medium text-gray-500">Next Run</p>
|
|
34
|
+
<% if @task[:enabled] && @task[:next_run_at] %>
|
|
35
|
+
<p class="mt-1 text-lg font-semibold text-gray-900"><%= pgbus_time_ago_future(@task[:next_run_at]) %></p>
|
|
36
|
+
<p class="text-xs text-gray-400"><%= @task[:next_run_at]&.strftime("%Y-%m-%d %H:%M:%S %Z") %></p>
|
|
37
|
+
<% else %>
|
|
38
|
+
<p class="mt-1 text-lg font-semibold text-gray-400">—</p>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
|
|
43
|
+
<p class="text-sm font-medium text-gray-500">Status</p>
|
|
44
|
+
<p class="mt-1"><%= @task[:enabled] ? pgbus_recurring_health_badge(@task) : raw('<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">Disabled</span>') %></p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="rounded-lg bg-white shadow ring-1 ring-gray-200 mb-8">
|
|
49
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
50
|
+
<h2 class="text-lg font-medium text-gray-900">Configuration</h2>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="p-4">
|
|
53
|
+
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
54
|
+
<div>
|
|
55
|
+
<dt class="text-sm font-medium text-gray-500">Job Class</dt>
|
|
56
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @task[:class_name] || "—" %></dd>
|
|
57
|
+
</div>
|
|
58
|
+
<% if @task[:command] %>
|
|
59
|
+
<div>
|
|
60
|
+
<dt class="text-sm font-medium text-gray-500">Command</dt>
|
|
61
|
+
<dd class="mt-1 text-sm text-gray-900 font-mono text-xs"><%= @task[:command] %></dd>
|
|
62
|
+
</div>
|
|
63
|
+
<% end %>
|
|
64
|
+
<div>
|
|
65
|
+
<dt class="text-sm font-medium text-gray-500">Queue</dt>
|
|
66
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @task[:queue_name] || "default" %></dd>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<dt class="text-sm font-medium text-gray-500">Priority</dt>
|
|
70
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @task[:priority] %></dd>
|
|
71
|
+
</div>
|
|
72
|
+
<% if @task[:arguments]&.any? %>
|
|
73
|
+
<div class="sm:col-span-2">
|
|
74
|
+
<dt class="text-sm font-medium text-gray-500">Arguments</dt>
|
|
75
|
+
<dd class="mt-1 text-sm text-gray-900 font-mono text-xs"><%= @task[:arguments].inspect %></dd>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
<% if @task[:description] %>
|
|
79
|
+
<div class="sm:col-span-2">
|
|
80
|
+
<dt class="text-sm font-medium text-gray-500">Description</dt>
|
|
81
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @task[:description] %></dd>
|
|
82
|
+
</div>
|
|
83
|
+
<% end %>
|
|
84
|
+
<div>
|
|
85
|
+
<dt class="text-sm font-medium text-gray-500">Source</dt>
|
|
86
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @task[:static] ? "Config file" : "Dynamic" %></dd>
|
|
87
|
+
</div>
|
|
88
|
+
</dl>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="rounded-lg bg-white shadow ring-1 ring-gray-200">
|
|
93
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
94
|
+
<h2 class="text-lg font-medium text-gray-900">Recent Executions</h2>
|
|
95
|
+
</div>
|
|
96
|
+
<% if @task[:executions]&.any? %>
|
|
97
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
98
|
+
<thead class="bg-gray-50">
|
|
99
|
+
<tr>
|
|
100
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Scheduled For</th>
|
|
101
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Enqueued At</th>
|
|
102
|
+
</tr>
|
|
103
|
+
</thead>
|
|
104
|
+
<tbody class="divide-y divide-gray-200">
|
|
105
|
+
<% @task[:executions].each do |exec| %>
|
|
106
|
+
<tr class="hover:bg-gray-50">
|
|
107
|
+
<td class="px-4 py-3 text-sm text-gray-900">
|
|
108
|
+
<%= exec[:run_at]&.strftime("%Y-%m-%d %H:%M:%S %Z") %>
|
|
109
|
+
</td>
|
|
110
|
+
<td class="px-4 py-3 text-sm text-gray-600">
|
|
111
|
+
<%= exec[:created_at] ? pgbus_time_ago(exec[:created_at]) : "—" %>
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
<% end %>
|
|
115
|
+
</tbody>
|
|
116
|
+
</table>
|
|
117
|
+
<% else %>
|
|
118
|
+
<div class="p-8 text-center text-gray-500">
|
|
119
|
+
<p>No executions recorded yet</p>
|
|
120
|
+
</div>
|
|
121
|
+
<% end %>
|
|
122
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -20,6 +20,13 @@ Pgbus::Engine.routes.draw do
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
resources :recurring_tasks, only: %i[index show] do
|
|
24
|
+
member do
|
|
25
|
+
post :toggle
|
|
26
|
+
post :enqueue
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
resources :processes, only: [:index]
|
|
24
31
|
|
|
25
32
|
resources :events, only: %i[index show] do
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pgbus" unless defined?(Pgbus)
|
|
4
|
+
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module QueueAdapters
|
|
7
|
+
# Adapter for Rails ActiveJob integration with Pgbus.
|
|
8
|
+
#
|
|
9
|
+
# This class lives in the ActiveJob::QueueAdapters namespace so that
|
|
10
|
+
# Rails can find it via const_get("PgbusAdapter") — the standard
|
|
11
|
+
# lookup mechanism used across all Rails versions (7.1+).
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# config.active_job.queue_adapter = :pgbus
|
|
15
|
+
class PgbusAdapter
|
|
16
|
+
delegate :enqueue, :enqueue_at, :enqueue_all, to: :adapter
|
|
17
|
+
|
|
18
|
+
def enqueue_after_transaction_commit?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def adapter
|
|
25
|
+
@adapter ||= Pgbus::ActiveJob::Adapter.new
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
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 AddRecurringGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Add recurring jobs tables to an existing Pgbus installation"
|
|
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
|
|
21
|
+
if separate_database?
|
|
22
|
+
migration_template "add_recurring_tables.rb.erb",
|
|
23
|
+
"db/pgbus_migrate/add_pgbus_recurring_tables.rb"
|
|
24
|
+
else
|
|
25
|
+
migration_template "add_recurring_tables.rb.erb",
|
|
26
|
+
"db/migrate/add_pgbus_recurring_tables.rb"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_recurring_config
|
|
31
|
+
template "recurring.yml.erb", "config/recurring.yml"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def display_post_install
|
|
35
|
+
say ""
|
|
36
|
+
say "Pgbus recurring jobs installed!", :green
|
|
37
|
+
say ""
|
|
38
|
+
say "Next steps:"
|
|
39
|
+
say " 1. Run: rails db:migrate#{":#{options[:database]}" if separate_database?}"
|
|
40
|
+
say " 2. Edit config/recurring.yml to define your recurring tasks"
|
|
41
|
+
say " 3. Restart pgbus: bin/pgbus start"
|
|
42
|
+
say ""
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def migration_version
|
|
48
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def separate_database?
|
|
52
|
+
options[:database].present?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -12,8 +12,26 @@ module Pgbus
|
|
|
12
12
|
|
|
13
13
|
desc "Install Pgbus: create migration, config file, and binstub"
|
|
14
14
|
|
|
15
|
+
class_option :pgmq_schema_mode,
|
|
16
|
+
type: :string,
|
|
17
|
+
default: "auto",
|
|
18
|
+
desc: "PGMQ install mode: auto (try extension, fallback embedded), " \
|
|
19
|
+
"extension (require ext), embedded (no ext)"
|
|
20
|
+
|
|
21
|
+
class_option :database,
|
|
22
|
+
type: :string,
|
|
23
|
+
default: nil,
|
|
24
|
+
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus). " \
|
|
25
|
+
"Migrations go to db/pgbus_migrate/ and schema to db/pgbus_schema.rb"
|
|
26
|
+
|
|
15
27
|
def create_migration
|
|
16
|
-
|
|
28
|
+
if separate_database?
|
|
29
|
+
migration_template "migration.rb.erb",
|
|
30
|
+
"db/pgbus_migrate/create_pgbus_tables.rb"
|
|
31
|
+
else
|
|
32
|
+
migration_template "migration.rb.erb",
|
|
33
|
+
"db/migrate/create_pgbus_tables.rb"
|
|
34
|
+
end
|
|
17
35
|
end
|
|
18
36
|
|
|
19
37
|
def create_config_file
|
|
@@ -35,12 +53,56 @@ module Pgbus
|
|
|
35
53
|
after: "class Application < Rails::Application\n"
|
|
36
54
|
end
|
|
37
55
|
|
|
56
|
+
def configure_separate_database
|
|
57
|
+
return unless separate_database?
|
|
58
|
+
|
|
59
|
+
db_config = <<~YAML
|
|
60
|
+
|
|
61
|
+
# Pgbus separate database (added by pgbus:install --database=#{database_name})
|
|
62
|
+
# Uncomment and configure for your environment:
|
|
63
|
+
# #{database_name}:
|
|
64
|
+
# primary:
|
|
65
|
+
# <<: *default
|
|
66
|
+
# database: #{Rails.application.class.module_parent_name.underscore}_#{database_name}_<%= Rails.env %>
|
|
67
|
+
# migrations_paths: db/pgbus_migrate
|
|
68
|
+
YAML
|
|
69
|
+
|
|
70
|
+
say ""
|
|
71
|
+
say "Separate database mode enabled!", :yellow
|
|
72
|
+
say "Add the following to your config/database.yml:", :yellow
|
|
73
|
+
say db_config, :cyan
|
|
74
|
+
say ""
|
|
75
|
+
say "Then add to config/application.rb or an initializer:", :yellow
|
|
76
|
+
say " Pgbus.configure { |c| c.connects_to = { database: { writing: :#{database_name} } } }", :cyan
|
|
77
|
+
say ""
|
|
78
|
+
end
|
|
79
|
+
|
|
38
80
|
def display_post_install
|
|
39
81
|
say ""
|
|
40
82
|
say "Pgbus installed successfully!", :green
|
|
83
|
+
say ""
|
|
84
|
+
say "PGMQ schema mode: #{pgmq_schema_mode}", :yellow
|
|
85
|
+
case pgmq_schema_mode
|
|
86
|
+
when "auto"
|
|
87
|
+
say " The migration will try the pgmq extension first."
|
|
88
|
+
say " If unavailable, it falls back to embedded SQL (no extension needed)."
|
|
89
|
+
when "extension"
|
|
90
|
+
say " The migration requires the pgmq PostgreSQL extension."
|
|
91
|
+
say " Install it: CREATE EXTENSION pgmq;"
|
|
92
|
+
when "embedded"
|
|
93
|
+
say " The migration uses embedded SQL — no pgmq extension needed."
|
|
94
|
+
say " PGMQ #{Pgbus::PgmqSchema.latest_version} schema will be created directly."
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if separate_database?
|
|
98
|
+
say ""
|
|
99
|
+
say "Migrations path: db/pgbus_migrate/", :yellow
|
|
100
|
+
say "Schema dump: db/pgbus_schema.rb", :yellow
|
|
101
|
+
end
|
|
102
|
+
|
|
41
103
|
say ""
|
|
42
104
|
say "Next steps:"
|
|
43
|
-
say " 1. Run: rails db:migrate"
|
|
105
|
+
say " 1. Run: rails db:migrate#{":#{database_name}" if separate_database?}"
|
|
44
106
|
say " 2. Edit config/pgbus.yml to configure workers"
|
|
45
107
|
say " 3. Start processing: bin/pgbus start"
|
|
46
108
|
say ""
|
|
@@ -51,6 +113,18 @@ module Pgbus
|
|
|
51
113
|
def migration_version
|
|
52
114
|
"[#{ActiveRecord::Migration.current_version}]"
|
|
53
115
|
end
|
|
116
|
+
|
|
117
|
+
def pgmq_schema_mode
|
|
118
|
+
options[:pgmq_schema_mode]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def database_name
|
|
122
|
+
options[:database]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def separate_database?
|
|
126
|
+
database_name.present?
|
|
127
|
+
end
|
|
54
128
|
end
|
|
55
129
|
end
|
|
56
130
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class AddPgbusRecurringTables < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :pgbus_recurring_tasks do |t|
|
|
4
|
+
t.string :key, null: false
|
|
5
|
+
t.string :class_name
|
|
6
|
+
t.string :command, limit: 2048
|
|
7
|
+
t.string :schedule, null: false
|
|
8
|
+
t.text :arguments
|
|
9
|
+
t.string :queue_name
|
|
10
|
+
t.integer :priority, default: 0
|
|
11
|
+
t.boolean :static, null: false, default: true
|
|
12
|
+
t.boolean :enabled, null: false, default: true
|
|
13
|
+
t.text :description
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :pgbus_recurring_tasks, :key,
|
|
18
|
+
unique: true, name: "idx_pgbus_recurring_tasks_key"
|
|
19
|
+
|
|
20
|
+
create_table :pgbus_recurring_executions do |t|
|
|
21
|
+
t.string :task_key, null: false
|
|
22
|
+
t.datetime :run_at, null: false
|
|
23
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
add_index :pgbus_recurring_executions, [:task_key, :run_at],
|
|
27
|
+
unique: true, name: "idx_pgbus_recurring_executions_dedup"
|
|
28
|
+
add_index :pgbus_recurring_executions, :run_at,
|
|
29
|
+
name: "idx_pgbus_recurring_executions_cleanup"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
|
|
2
2
|
def up
|
|
3
|
-
# Install PGMQ
|
|
4
|
-
|
|
3
|
+
# Install PGMQ schema based on pgmq_schema_mode configuration.
|
|
4
|
+
#
|
|
5
|
+
# Modes:
|
|
6
|
+
# :auto - Try extension first, fall back to embedded SQL (default)
|
|
7
|
+
# :extension - Require the pgmq PostgreSQL extension
|
|
8
|
+
# :embedded - Use vendored SQL, no extension needed
|
|
9
|
+
pgmq_schema_mode = :<%= pgmq_schema_mode %>
|
|
10
|
+
|
|
11
|
+
case pgmq_schema_mode
|
|
12
|
+
when :extension
|
|
13
|
+
enable_extension "pgmq"
|
|
14
|
+
execute Pgbus::PgmqSchema.version_tracking_extension_sql
|
|
15
|
+
when :embedded
|
|
16
|
+
execute Pgbus::PgmqSchema.install_sql
|
|
17
|
+
execute Pgbus::PgmqSchema.version_tracking_sql
|
|
18
|
+
when :auto
|
|
19
|
+
if extension_available?("pgmq")
|
|
20
|
+
enable_extension "pgmq"
|
|
21
|
+
execute Pgbus::PgmqSchema.version_tracking_extension_sql
|
|
22
|
+
else
|
|
23
|
+
execute Pgbus::PgmqSchema.install_sql
|
|
24
|
+
execute Pgbus::PgmqSchema.version_tracking_sql
|
|
25
|
+
end
|
|
26
|
+
end
|
|
5
27
|
|
|
6
28
|
# Idempotent event processing deduplication
|
|
7
29
|
create_table :pgbus_processed_events do |t|
|
|
@@ -93,6 +115,36 @@ class InstallPgbus < ActiveRecord::Migration<%= migration_version %>
|
|
|
93
115
|
add_index :pgbus_batches, :status,
|
|
94
116
|
name: "idx_pgbus_batches_status"
|
|
95
117
|
|
|
118
|
+
# Recurring task definitions (synced from config/recurring.yml)
|
|
119
|
+
create_table :pgbus_recurring_tasks do |t|
|
|
120
|
+
t.string :key, null: false
|
|
121
|
+
t.string :class_name
|
|
122
|
+
t.string :command, limit: 2048
|
|
123
|
+
t.string :schedule, null: false
|
|
124
|
+
t.text :arguments
|
|
125
|
+
t.string :queue_name
|
|
126
|
+
t.integer :priority, default: 0
|
|
127
|
+
t.boolean :static, null: false, default: true
|
|
128
|
+
t.boolean :enabled, null: false, default: true
|
|
129
|
+
t.text :description
|
|
130
|
+
t.timestamps
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
add_index :pgbus_recurring_tasks, :key,
|
|
134
|
+
unique: true, name: "idx_pgbus_recurring_tasks_key"
|
|
135
|
+
|
|
136
|
+
# Recurring execution deduplication (prevents double-enqueue)
|
|
137
|
+
create_table :pgbus_recurring_executions do |t|
|
|
138
|
+
t.string :task_key, null: false
|
|
139
|
+
t.datetime :run_at, null: false
|
|
140
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
add_index :pgbus_recurring_executions, [:task_key, :run_at],
|
|
144
|
+
unique: true, name: "idx_pgbus_recurring_executions_dedup"
|
|
145
|
+
add_index :pgbus_recurring_executions, :run_at,
|
|
146
|
+
name: "idx_pgbus_recurring_executions_cleanup"
|
|
147
|
+
|
|
96
148
|
# Create default queues via PGMQ
|
|
97
149
|
execute "SELECT pgmq.create('pgbus_default')"
|
|
98
150
|
execute "SELECT pgmq.create('pgbus_default_dlq')"
|
|
@@ -102,13 +154,29 @@ class InstallPgbus < ActiveRecord::Migration<%= migration_version %>
|
|
|
102
154
|
execute "SELECT pgmq.drop_queue('pgbus_default_dlq')"
|
|
103
155
|
execute "SELECT pgmq.drop_queue('pgbus_default')"
|
|
104
156
|
|
|
157
|
+
drop_table :pgbus_recurring_executions
|
|
158
|
+
drop_table :pgbus_recurring_tasks
|
|
105
159
|
drop_table :pgbus_batches
|
|
106
160
|
drop_table :pgbus_blocked_executions
|
|
107
161
|
drop_table :pgbus_semaphores
|
|
108
162
|
drop_table :pgbus_failed_events
|
|
109
163
|
drop_table :pgbus_processes
|
|
110
164
|
drop_table :pgbus_processed_events
|
|
165
|
+
drop_table :pgbus_pgmq_schema_versions
|
|
166
|
+
|
|
167
|
+
# Drop PGMQ schema (functions, types, tables)
|
|
168
|
+
execute "DROP SCHEMA IF EXISTS pgmq CASCADE"
|
|
169
|
+
|
|
170
|
+
# Also try to disable extension in case it was installed that way
|
|
171
|
+
disable_extension "pgmq" if extension_available?("pgmq")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
111
175
|
|
|
112
|
-
|
|
176
|
+
def extension_available?(name)
|
|
177
|
+
result = execute("SELECT 1 FROM pg_available_extensions WHERE name = '#{name}' LIMIT 1")
|
|
178
|
+
result.any?
|
|
179
|
+
rescue ActiveRecord::StatementInvalid
|
|
180
|
+
false
|
|
113
181
|
end
|
|
114
182
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Pgbus Recurring Tasks Configuration
|
|
2
|
+
#
|
|
3
|
+
# Compatible with Solid Queue's recurring.yml format for easy migration.
|
|
4
|
+
# Supports environment-scoped or flat configuration.
|
|
5
|
+
#
|
|
6
|
+
# Format:
|
|
7
|
+
# task_key:
|
|
8
|
+
# class: JobClassName # ActiveJob class to run (or use command:)
|
|
9
|
+
# schedule: "0 * * * *" # Cron expression or natural language
|
|
10
|
+
# queue: default # Optional: queue name (default: pgbus default queue)
|
|
11
|
+
# args: [arg1, arg2] # Optional: job arguments
|
|
12
|
+
# priority: 0 # Optional: numeric priority
|
|
13
|
+
# description: "text" # Optional: human-readable description
|
|
14
|
+
#
|
|
15
|
+
# Schedule examples:
|
|
16
|
+
# "0 2 * * *" → Every day at 2:00 AM
|
|
17
|
+
# "*/5 * * * *" → Every 5 minutes
|
|
18
|
+
# "every hour" → Every hour at :00
|
|
19
|
+
# "every day at 2am" → Daily at 2:00 AM
|
|
20
|
+
# "@daily" → Daily at midnight
|
|
21
|
+
# "0 9 * * mon-fri" → Weekdays at 9:00 AM
|
|
22
|
+
#
|
|
23
|
+
# Command-based tasks (no job class needed):
|
|
24
|
+
# cleanup_old_records:
|
|
25
|
+
# command: "OldRecord.where('created_at < ?', 30.days.ago).delete_all"
|
|
26
|
+
# schedule: every day at 3am
|
|
27
|
+
#
|
|
28
|
+
# Examples:
|
|
29
|
+
#
|
|
30
|
+
# production:
|
|
31
|
+
# periodic_cleanup:
|
|
32
|
+
# class: CleanupJob
|
|
33
|
+
# queue: maintenance
|
|
34
|
+
# args: [1000, { batch_size: 500 }]
|
|
35
|
+
# schedule: every hour
|
|
36
|
+
#
|
|
37
|
+
# daily_report:
|
|
38
|
+
# class: DailyReportJob
|
|
39
|
+
# schedule: "0 8 * * mon-fri"
|
|
40
|
+
# description: Generate daily business report
|