pgbus 0.0.1 → 0.1.2
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 +3 -6
- data/lib/pgbus/active_job/executor.rb +26 -12
- data/lib/pgbus/batch.rb +65 -72
- data/lib/pgbus/cli.rb +11 -16
- data/lib/pgbus/client.rb +32 -15
- 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 +48 -0
- data/lib/pgbus/engine.rb +19 -1
- data/lib/pgbus/event_bus/handler.rb +10 -23
- 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 +17 -9
- data/lib/pgbus/process/dispatcher.rb +33 -41
- data/lib/pgbus/process/heartbeat.rb +15 -23
- data/lib/pgbus/process/signal_handler.rb +23 -1
- data/lib/pgbus/process/supervisor.rb +79 -2
- data/lib/pgbus/process/worker.rb +42 -13
- data/lib/pgbus/recurring/already_recorded.rb +7 -0
- data/lib/pgbus/recurring/command_job.rb +28 -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 +16 -6
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +217 -36
- 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
|
@@ -23,24 +23,28 @@ module Pgbus
|
|
|
23
23
|
total_visible: total_visible,
|
|
24
24
|
active_processes: processes.count,
|
|
25
25
|
failed_count: failed_events_count,
|
|
26
|
-
dlq_depth: dlq_depth
|
|
26
|
+
dlq_depth: dlq_depth,
|
|
27
|
+
recurring_count: recurring_tasks_count
|
|
27
28
|
}
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
# Queues
|
|
31
|
+
# Queues — query via ActiveRecord for reliability in web processes
|
|
32
|
+
# (avoids PGMQ client connection issues when the web server uses a
|
|
33
|
+
# different connection lifecycle than the worker processes).
|
|
31
34
|
def queues_with_metrics
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
queue_names = connection.select_values("SELECT queue_name FROM pgmq.meta ORDER BY queue_name")
|
|
36
|
+
queue_names.map { |name| queue_metrics_via_sql(name) }.compact
|
|
34
37
|
rescue StandardError => e
|
|
35
|
-
Pgbus.logger.
|
|
38
|
+
Pgbus.logger.error { "[Pgbus::Web] Error fetching queue metrics: #{e.class}: #{e.message}" }
|
|
36
39
|
[]
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
# name is the full PGMQ queue name (e.g. "pgbus_default") as returned
|
|
43
|
+
# by queues_with_metrics. No prefix is added.
|
|
39
44
|
def queue_detail(name)
|
|
40
|
-
|
|
41
|
-
m ? format_metrics(m) : nil
|
|
45
|
+
queue_metrics_via_sql(name)
|
|
42
46
|
rescue StandardError => e
|
|
43
|
-
Pgbus.logger.
|
|
47
|
+
Pgbus.logger.error { "[Pgbus::Web] Error fetching queue detail for #{name}: #{e.class}: #{e.message}" }
|
|
44
48
|
nil
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -63,9 +67,8 @@ module Pgbus
|
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
def job_detail(queue_name, msg_id)
|
|
66
|
-
full_name = Pgbus.configuration.queue_name(queue_name)
|
|
67
70
|
row = connection.select_one(
|
|
68
|
-
"SELECT * FROM pgmq.q_#{sanitize_name(
|
|
71
|
+
"SELECT * FROM pgmq.q_#{sanitize_name(queue_name)} WHERE msg_id = $1",
|
|
69
72
|
"Pgbus Job Detail",
|
|
70
73
|
[msg_id.to_i]
|
|
71
74
|
)
|
|
@@ -76,8 +79,7 @@ module Pgbus
|
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def retry_job(queue_name, msg_id)
|
|
79
|
-
|
|
80
|
-
@client.set_visibility_timeout(full_name, msg_id.to_i, vt: 0)
|
|
82
|
+
@client.set_visibility_timeout(queue_name, msg_id.to_i, vt: 0)
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
def discard_job(queue_name, msg_id)
|
|
@@ -127,7 +129,9 @@ module Pgbus
|
|
|
127
129
|
|
|
128
130
|
connection.transaction do
|
|
129
131
|
@client.send_message(event["queue_name"], payload, headers: headers)
|
|
130
|
-
connection.
|
|
132
|
+
connection.exec_delete(
|
|
133
|
+
"DELETE FROM pgbus_failed_events WHERE id = $1", "Pgbus Delete Failed Event", [id.to_i]
|
|
134
|
+
)
|
|
131
135
|
end
|
|
132
136
|
true
|
|
133
137
|
rescue StandardError => e
|
|
@@ -136,7 +140,9 @@ module Pgbus
|
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
def discard_failed_event(id)
|
|
139
|
-
connection.
|
|
143
|
+
connection.exec_delete(
|
|
144
|
+
"DELETE FROM pgbus_failed_events WHERE id = $1", "Pgbus Delete Failed Event", [id.to_i]
|
|
145
|
+
)
|
|
140
146
|
true
|
|
141
147
|
rescue StandardError => e
|
|
142
148
|
Pgbus.logger.debug { "[Pgbus::Web] Error discarding failed event #{id}: #{e.message}" }
|
|
@@ -145,18 +151,27 @@ module Pgbus
|
|
|
145
151
|
|
|
146
152
|
def retry_all_failed
|
|
147
153
|
count = 0
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
loop do
|
|
155
|
+
batch = connection.select_all(
|
|
156
|
+
"SELECT * FROM pgbus_failed_events ORDER BY id LIMIT 100", "Pgbus Retry Batch"
|
|
157
|
+
).to_a
|
|
158
|
+
break if batch.empty?
|
|
159
|
+
|
|
160
|
+
batch.each do |event|
|
|
161
|
+
payload = JSON.parse(event["payload"])
|
|
162
|
+
headers = event["headers"]
|
|
163
|
+
headers = JSON.parse(headers) if headers.is_a?(String)
|
|
164
|
+
|
|
165
|
+
connection.transaction do
|
|
166
|
+
@client.send_message(event["queue_name"], payload, headers: headers)
|
|
167
|
+
connection.exec_delete(
|
|
168
|
+
"DELETE FROM pgbus_failed_events WHERE id = $1", "Pgbus Delete Failed Event", [event["id"].to_i]
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
count += 1
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
Pgbus.logger.error { "[Pgbus::Web] Failed to retry event #{event["id"]}: #{e.message}" }
|
|
156
174
|
end
|
|
157
|
-
count += 1
|
|
158
|
-
rescue StandardError => e
|
|
159
|
-
Pgbus.logger.error { "[Pgbus::Web] Failed to retry event #{event["id"]}: #{e.message}" }
|
|
160
175
|
end
|
|
161
176
|
count
|
|
162
177
|
end
|
|
@@ -315,6 +330,127 @@ module Pgbus
|
|
|
315
330
|
false
|
|
316
331
|
end
|
|
317
332
|
|
|
333
|
+
# Recurring tasks
|
|
334
|
+
def recurring_tasks
|
|
335
|
+
records = RecurringTask.order(:key).to_a
|
|
336
|
+
last_runs = RecurringExecution
|
|
337
|
+
.where(task_key: records.map(&:key))
|
|
338
|
+
.select("task_key, MAX(run_at) AS run_at")
|
|
339
|
+
.group(:task_key)
|
|
340
|
+
.index_by(&:task_key)
|
|
341
|
+
|
|
342
|
+
records.map do |record|
|
|
343
|
+
last_exec = last_runs[record.key]
|
|
344
|
+
task = Recurring::Task.from_configuration(record.key,
|
|
345
|
+
class: record.class_name,
|
|
346
|
+
command: record.command,
|
|
347
|
+
schedule: record.schedule,
|
|
348
|
+
queue: record.queue_name,
|
|
349
|
+
args: parse_arguments(record.arguments),
|
|
350
|
+
priority: record.priority,
|
|
351
|
+
description: record.description)
|
|
352
|
+
|
|
353
|
+
{
|
|
354
|
+
id: record.id,
|
|
355
|
+
key: record.key,
|
|
356
|
+
class_name: record.class_name,
|
|
357
|
+
command: record.command,
|
|
358
|
+
schedule: record.schedule,
|
|
359
|
+
human_schedule: task.human_schedule,
|
|
360
|
+
queue_name: record.queue_name,
|
|
361
|
+
priority: record.priority,
|
|
362
|
+
description: record.description,
|
|
363
|
+
enabled: record.enabled,
|
|
364
|
+
static: record.static,
|
|
365
|
+
next_run_at: task.next_time,
|
|
366
|
+
last_run_at: last_exec&.run_at,
|
|
367
|
+
created_at: record.created_at,
|
|
368
|
+
updated_at: record.updated_at
|
|
369
|
+
}
|
|
370
|
+
end
|
|
371
|
+
rescue StandardError => e
|
|
372
|
+
Pgbus.logger.error { "[Pgbus::Web] Error fetching recurring tasks: #{e.class}: #{e.message}" }
|
|
373
|
+
[]
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def recurring_task(id)
|
|
377
|
+
record = RecurringTask.find_by(id: id)
|
|
378
|
+
return nil unless record
|
|
379
|
+
|
|
380
|
+
task = Recurring::Task.from_configuration(record.key,
|
|
381
|
+
class: record.class_name,
|
|
382
|
+
command: record.command,
|
|
383
|
+
schedule: record.schedule,
|
|
384
|
+
queue: record.queue_name,
|
|
385
|
+
args: parse_arguments(record.arguments),
|
|
386
|
+
priority: record.priority,
|
|
387
|
+
description: record.description)
|
|
388
|
+
|
|
389
|
+
executions = RecurringExecution.for_task(record.key).recent(25).map do |exec|
|
|
390
|
+
{ run_at: exec.run_at, created_at: exec.created_at }
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
{
|
|
394
|
+
id: record.id,
|
|
395
|
+
key: record.key,
|
|
396
|
+
class_name: record.class_name,
|
|
397
|
+
command: record.command,
|
|
398
|
+
schedule: record.schedule,
|
|
399
|
+
human_schedule: task.human_schedule,
|
|
400
|
+
queue_name: record.queue_name,
|
|
401
|
+
arguments: parse_arguments(record.arguments),
|
|
402
|
+
priority: record.priority,
|
|
403
|
+
description: record.description,
|
|
404
|
+
enabled: record.enabled,
|
|
405
|
+
static: record.static,
|
|
406
|
+
next_run_at: task.next_time,
|
|
407
|
+
executions: executions,
|
|
408
|
+
created_at: record.created_at,
|
|
409
|
+
updated_at: record.updated_at
|
|
410
|
+
}
|
|
411
|
+
rescue StandardError => e
|
|
412
|
+
Pgbus.logger.error { "[Pgbus::Web] Error fetching recurring task #{id}: #{e.class}: #{e.message}" }
|
|
413
|
+
nil
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def toggle_recurring_task(id)
|
|
417
|
+
record = RecurringTask.find_by(id: id)
|
|
418
|
+
return false unless record
|
|
419
|
+
|
|
420
|
+
record.update!(enabled: !record.enabled)
|
|
421
|
+
true
|
|
422
|
+
rescue StandardError => e
|
|
423
|
+
Pgbus.logger.error { "[Pgbus::Web] Error toggling recurring task #{id}: #{e.message}" }
|
|
424
|
+
false
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def enqueue_recurring_task_now(id)
|
|
428
|
+
record = RecurringTask.find_by(id: id)
|
|
429
|
+
return false unless record
|
|
430
|
+
|
|
431
|
+
task = Recurring::Task.from_configuration(record.key,
|
|
432
|
+
class: record.class_name,
|
|
433
|
+
command: record.command,
|
|
434
|
+
schedule: record.schedule,
|
|
435
|
+
queue: record.queue_name,
|
|
436
|
+
args: parse_arguments(record.arguments),
|
|
437
|
+
priority: record.priority)
|
|
438
|
+
|
|
439
|
+
schedule = Recurring::Schedule.new(config: Pgbus.configuration)
|
|
440
|
+
schedule.enqueue_task(task, run_at: Time.now.utc)
|
|
441
|
+
true
|
|
442
|
+
rescue StandardError => e
|
|
443
|
+
Pgbus.logger.error { "[Pgbus::Web] Error enqueuing recurring task #{id}: #{e.message}" }
|
|
444
|
+
false
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def recurring_tasks_count
|
|
448
|
+
RecurringTask.count
|
|
449
|
+
rescue StandardError => e
|
|
450
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error counting recurring tasks: #{e.message}" }
|
|
451
|
+
0
|
|
452
|
+
end
|
|
453
|
+
|
|
318
454
|
# Subscriber registry
|
|
319
455
|
def registered_subscribers
|
|
320
456
|
EventBus::Registry.instance.subscribers.map do |s|
|
|
@@ -328,12 +464,12 @@ module Pgbus
|
|
|
328
464
|
private
|
|
329
465
|
|
|
330
466
|
def connection
|
|
331
|
-
|
|
467
|
+
Pgbus::ApplicationRecord.connection
|
|
332
468
|
end
|
|
333
469
|
|
|
470
|
+
# name is the full PGMQ queue name (already prefixed)
|
|
334
471
|
def query_queue_messages(name, limit, offset)
|
|
335
|
-
|
|
336
|
-
query_queue_messages_raw(full_name, limit, offset).map { |m| m.merge(queue: name) }
|
|
472
|
+
query_queue_messages_raw(name, limit, offset).map { |m| m.merge(queue: name) }
|
|
337
473
|
end
|
|
338
474
|
|
|
339
475
|
def query_queue_messages_raw(full_name, limit, offset)
|
|
@@ -357,15 +493,45 @@ module Pgbus
|
|
|
357
493
|
messages.sort_by { |m| -m[:msg_id].to_i }.slice(offset, limit) || []
|
|
358
494
|
end
|
|
359
495
|
|
|
360
|
-
def
|
|
496
|
+
def queue_metrics_via_sql(queue_name)
|
|
497
|
+
qtable = "q_#{sanitize_name(queue_name)}"
|
|
498
|
+
seq_name = "#{qtable}_msg_id_seq"
|
|
499
|
+
|
|
500
|
+
row = connection.select_one(<<~SQL, "Pgbus Queue Metrics")
|
|
501
|
+
WITH q_summary AS (
|
|
502
|
+
SELECT
|
|
503
|
+
count(*) AS queue_length,
|
|
504
|
+
count(CASE WHEN vt <= NOW() THEN 1 END) AS queue_visible_length,
|
|
505
|
+
EXTRACT(epoch FROM (NOW() - max(enqueued_at)))::int AS newest_msg_age_sec,
|
|
506
|
+
EXTRACT(epoch FROM (NOW() - min(enqueued_at)))::int AS oldest_msg_age_sec
|
|
507
|
+
FROM pgmq.#{qtable}
|
|
508
|
+
),
|
|
509
|
+
all_metrics AS (
|
|
510
|
+
SELECT CASE WHEN is_called THEN last_value ELSE 0 END AS total_messages
|
|
511
|
+
FROM pgmq.#{seq_name}
|
|
512
|
+
)
|
|
513
|
+
SELECT
|
|
514
|
+
q_summary.queue_length,
|
|
515
|
+
q_summary.queue_visible_length,
|
|
516
|
+
q_summary.newest_msg_age_sec,
|
|
517
|
+
q_summary.oldest_msg_age_sec,
|
|
518
|
+
all_metrics.total_messages
|
|
519
|
+
FROM q_summary, all_metrics
|
|
520
|
+
SQL
|
|
521
|
+
|
|
522
|
+
return nil unless row
|
|
523
|
+
|
|
361
524
|
{
|
|
362
|
-
name:
|
|
363
|
-
queue_length:
|
|
364
|
-
queue_visible_length:
|
|
365
|
-
oldest_msg_age_sec:
|
|
366
|
-
newest_msg_age_sec:
|
|
367
|
-
total_messages:
|
|
525
|
+
name: queue_name,
|
|
526
|
+
queue_length: row["queue_length"].to_i,
|
|
527
|
+
queue_visible_length: row["queue_visible_length"].to_i,
|
|
528
|
+
oldest_msg_age_sec: row["oldest_msg_age_sec"]&.to_i,
|
|
529
|
+
newest_msg_age_sec: row["newest_msg_age_sec"]&.to_i,
|
|
530
|
+
total_messages: row["total_messages"].to_i
|
|
368
531
|
}
|
|
532
|
+
rescue StandardError => e
|
|
533
|
+
Pgbus.logger.error { "[Pgbus::Web] Error fetching metrics for #{queue_name}: #{e.class}: #{e.message}" }
|
|
534
|
+
nil
|
|
369
535
|
end
|
|
370
536
|
|
|
371
537
|
def format_message(row, queue_name)
|
|
@@ -399,7 +565,22 @@ module Pgbus
|
|
|
399
565
|
end
|
|
400
566
|
|
|
401
567
|
def sanitize_name(name)
|
|
402
|
-
name.gsub(/[^a-zA-Z0-9_]/, "")
|
|
568
|
+
sanitized = name.gsub(/[^a-zA-Z0-9_]/, "")
|
|
569
|
+
raise ArgumentError, "Invalid queue name: #{name.inspect}" if sanitized.empty?
|
|
570
|
+
|
|
571
|
+
sanitized
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def parse_arguments(args)
|
|
575
|
+
case args
|
|
576
|
+
when Array then args
|
|
577
|
+
when String then JSON.parse(args)
|
|
578
|
+
when NilClass then []
|
|
579
|
+
else Array(args)
|
|
580
|
+
end
|
|
581
|
+
rescue JSON::ParserError => e
|
|
582
|
+
Pgbus.logger.debug { "[Pgbus::Web] Invalid recurring task arguments JSON: #{e.message}" }
|
|
583
|
+
[]
|
|
403
584
|
end
|
|
404
585
|
end
|
|
405
586
|
end
|
data/lib/pgbus.rb
CHANGED
|
@@ -16,6 +16,13 @@ module Pgbus
|
|
|
16
16
|
loader = Zeitwerk::Loader.for_gem
|
|
17
17
|
loader.inflector.inflect("pgbus" => "Pgbus", "cli" => "CLI", "dsl" => "DSL")
|
|
18
18
|
loader.ignore("#{__dir__}/generators")
|
|
19
|
+
loader.ignore("#{__dir__}/active_job")
|
|
20
|
+
# Register app/models for non-Rails usage (specs, standalone).
|
|
21
|
+
# When Rails is running, the Engine handles autoloading app/models.
|
|
22
|
+
unless defined?(Rails::Engine)
|
|
23
|
+
models_dir = File.expand_path("../app/models", __dir__)
|
|
24
|
+
loader.push_dir(models_dir) if File.directory?(models_dir)
|
|
25
|
+
end
|
|
19
26
|
loader
|
|
20
27
|
end
|
|
21
28
|
end
|
|
@@ -46,4 +53,5 @@ module Pgbus
|
|
|
46
53
|
loader.setup
|
|
47
54
|
end
|
|
48
55
|
|
|
56
|
+
require "active_job/queue_adapters/pgbus_adapter" if defined?(ActiveJob)
|
|
49
57
|
require "pgbus/engine" if defined?(Rails::Engine)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :pgbus do
|
|
4
|
+
namespace :pgmq do
|
|
5
|
+
desc "Show current PGMQ schema status (installed version, available updates)"
|
|
6
|
+
task status: :environment do
|
|
7
|
+
require "pgbus/pgmq_schema"
|
|
8
|
+
|
|
9
|
+
puts "PGMQ Schema Status"
|
|
10
|
+
puts "=" * 40
|
|
11
|
+
|
|
12
|
+
latest = Pgbus::PgmqSchema.latest_version
|
|
13
|
+
puts "Vendored version: #{latest}"
|
|
14
|
+
|
|
15
|
+
if ActiveRecord::Base.connection.table_exists?("pgbus_pgmq_schema_versions")
|
|
16
|
+
row = ActiveRecord::Base.connection.select_one(
|
|
17
|
+
"SELECT version, install_method, installed_at FROM pgbus_pgmq_schema_versions ORDER BY installed_at DESC LIMIT 1"
|
|
18
|
+
)
|
|
19
|
+
if row
|
|
20
|
+
puts "Installed version: #{row["version"]}"
|
|
21
|
+
puts "Install method: #{row["install_method"]}"
|
|
22
|
+
puts "Installed at: #{row["installed_at"]}"
|
|
23
|
+
|
|
24
|
+
puts ""
|
|
25
|
+
if Gem::Version.new(row["version"]) < Gem::Version.new(latest)
|
|
26
|
+
puts "Update available! Run: rails generate pgbus:upgrade_pgmq"
|
|
27
|
+
else
|
|
28
|
+
puts "Schema is up to date."
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
puts "Installed version: unknown (no records in tracking table)"
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
# Check if pgmq schema exists at all
|
|
35
|
+
schema_exists = ActiveRecord::Base.connection.select_value(
|
|
36
|
+
"SELECT 1 FROM information_schema.schemata WHERE schema_name = 'pgmq'"
|
|
37
|
+
)
|
|
38
|
+
if schema_exists
|
|
39
|
+
puts "Installed version: unknown (installed before version tracking)"
|
|
40
|
+
puts ""
|
|
41
|
+
puts "Run: rails generate pgbus:upgrade_pgmq"
|
|
42
|
+
puts "This will add version tracking and ensure latest functions."
|
|
43
|
+
else
|
|
44
|
+
puts "PGMQ is not installed."
|
|
45
|
+
puts ""
|
|
46
|
+
puts "Run: rails generate pgbus:install"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "Show available vendored PGMQ versions"
|
|
52
|
+
task versions: :environment do
|
|
53
|
+
require "pgbus/pgmq_schema"
|
|
54
|
+
|
|
55
|
+
puts "Available vendored PGMQ versions:"
|
|
56
|
+
Pgbus::PgmqSchema.available_versions.each do |v|
|
|
57
|
+
marker = v == Pgbus::PgmqSchema.latest_version ? " (latest)" : ""
|
|
58
|
+
puts " #{v}#{marker}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -23,6 +23,26 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: fugit
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.11'
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: 1.11.1
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - "~>"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.11'
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 1.11.1
|
|
26
46
|
- !ruby/object:Gem::Dependency
|
|
27
47
|
name: pgmq-ruby
|
|
28
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -83,21 +103,7 @@ executables:
|
|
|
83
103
|
extensions: []
|
|
84
104
|
extra_rdoc_files: []
|
|
85
105
|
files:
|
|
86
|
-
- ".bun-version"
|
|
87
|
-
- ".claude/commands/architect.md"
|
|
88
|
-
- ".claude/commands/github-review-comments.md"
|
|
89
|
-
- ".claude/commands/lfg.md"
|
|
90
|
-
- ".claude/commands/review-pr.md"
|
|
91
|
-
- ".claude/commands/security.md"
|
|
92
|
-
- ".claude/commands/tdd.md"
|
|
93
|
-
- ".claude/rules/agents.md"
|
|
94
|
-
- ".claude/rules/coding-style.md"
|
|
95
|
-
- ".claude/rules/git-workflow.md"
|
|
96
|
-
- ".claude/rules/performance.md"
|
|
97
|
-
- ".claude/rules/testing.md"
|
|
98
106
|
- CHANGELOG.md
|
|
99
|
-
- CLAUDE.md
|
|
100
|
-
- CODE_OF_CONDUCT.md
|
|
101
107
|
- LICENSE.txt
|
|
102
108
|
- README.md
|
|
103
109
|
- Rakefile
|
|
@@ -109,7 +115,16 @@ files:
|
|
|
109
115
|
- app/controllers/pgbus/jobs_controller.rb
|
|
110
116
|
- app/controllers/pgbus/processes_controller.rb
|
|
111
117
|
- app/controllers/pgbus/queues_controller.rb
|
|
118
|
+
- app/controllers/pgbus/recurring_tasks_controller.rb
|
|
112
119
|
- app/helpers/pgbus/application_helper.rb
|
|
120
|
+
- app/models/pgbus/application_record.rb
|
|
121
|
+
- app/models/pgbus/batch_entry.rb
|
|
122
|
+
- app/models/pgbus/blocked_execution.rb
|
|
123
|
+
- app/models/pgbus/process_entry.rb
|
|
124
|
+
- app/models/pgbus/processed_event.rb
|
|
125
|
+
- app/models/pgbus/recurring_execution.rb
|
|
126
|
+
- app/models/pgbus/recurring_task.rb
|
|
127
|
+
- app/models/pgbus/semaphore.rb
|
|
113
128
|
- app/views/layouts/pgbus/application.html.erb
|
|
114
129
|
- app/views/pgbus/dashboard/_processes_table.html.erb
|
|
115
130
|
- app/views/pgbus/dashboard/_queues_table.html.erb
|
|
@@ -130,17 +145,21 @@ files:
|
|
|
130
145
|
- app/views/pgbus/queues/_queues_list.html.erb
|
|
131
146
|
- app/views/pgbus/queues/index.html.erb
|
|
132
147
|
- app/views/pgbus/queues/show.html.erb
|
|
133
|
-
-
|
|
148
|
+
- app/views/pgbus/recurring_tasks/_tasks_table.html.erb
|
|
149
|
+
- app/views/pgbus/recurring_tasks/index.html.erb
|
|
150
|
+
- app/views/pgbus/recurring_tasks/show.html.erb
|
|
134
151
|
- config/routes.rb
|
|
135
|
-
- docs/README.md
|
|
136
|
-
- docs/switch_from_good_job.md
|
|
137
|
-
- docs/switch_from_sidekiq.md
|
|
138
|
-
- docs/switch_from_solid_queue.md
|
|
139
152
|
- exe/pgbus
|
|
153
|
+
- lib/active_job/queue_adapters/pgbus_adapter.rb
|
|
154
|
+
- lib/generators/pgbus/add_recurring_generator.rb
|
|
140
155
|
- lib/generators/pgbus/install_generator.rb
|
|
156
|
+
- lib/generators/pgbus/templates/add_recurring_tables.rb.erb
|
|
141
157
|
- lib/generators/pgbus/templates/migration.rb.erb
|
|
142
158
|
- lib/generators/pgbus/templates/pgbus.yml.erb
|
|
143
159
|
- lib/generators/pgbus/templates/pgbus_binstub.erb
|
|
160
|
+
- lib/generators/pgbus/templates/recurring.yml.erb
|
|
161
|
+
- lib/generators/pgbus/templates/upgrade_pgmq.rb.erb
|
|
162
|
+
- lib/generators/pgbus/upgrade_pgmq_generator.rb
|
|
144
163
|
- lib/pgbus.rb
|
|
145
164
|
- lib/pgbus/active_job/adapter.rb
|
|
146
165
|
- lib/pgbus/active_job/executor.rb
|
|
@@ -158,24 +177,32 @@ files:
|
|
|
158
177
|
- lib/pgbus/event_bus/publisher.rb
|
|
159
178
|
- lib/pgbus/event_bus/registry.rb
|
|
160
179
|
- lib/pgbus/event_bus/subscriber.rb
|
|
180
|
+
- lib/pgbus/instrumentation.rb
|
|
181
|
+
- lib/pgbus/pgmq_schema.rb
|
|
182
|
+
- lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql
|
|
161
183
|
- lib/pgbus/process/consumer.rb
|
|
162
184
|
- lib/pgbus/process/dispatcher.rb
|
|
163
185
|
- lib/pgbus/process/heartbeat.rb
|
|
164
186
|
- lib/pgbus/process/signal_handler.rb
|
|
165
187
|
- lib/pgbus/process/supervisor.rb
|
|
166
188
|
- lib/pgbus/process/worker.rb
|
|
189
|
+
- lib/pgbus/recurring/already_recorded.rb
|
|
190
|
+
- lib/pgbus/recurring/command_job.rb
|
|
191
|
+
- lib/pgbus/recurring/config_loader.rb
|
|
192
|
+
- lib/pgbus/recurring/schedule.rb
|
|
193
|
+
- lib/pgbus/recurring/scheduler.rb
|
|
194
|
+
- lib/pgbus/recurring/task.rb
|
|
167
195
|
- lib/pgbus/serializer.rb
|
|
168
196
|
- lib/pgbus/version.rb
|
|
169
197
|
- lib/pgbus/web/authentication.rb
|
|
170
198
|
- lib/pgbus/web/data_source.rb
|
|
171
|
-
-
|
|
172
|
-
- sig/pgbus.rbs
|
|
199
|
+
- lib/tasks/pgbus_pgmq.rake
|
|
173
200
|
homepage: https://github.com/mhenrixon/pgbus
|
|
174
201
|
licenses:
|
|
175
202
|
- MIT
|
|
176
203
|
metadata:
|
|
177
204
|
homepage_uri: https://github.com/mhenrixon/pgbus
|
|
178
|
-
source_code_uri: https://github.com/mhenrixon/pgbus
|
|
205
|
+
source_code_uri: https://github.com/mhenrixon/pgbus/tree/main
|
|
179
206
|
changelog_uri: https://github.com/mhenrixon/pgbus/blob/main/CHANGELOG.md
|
|
180
207
|
rubygems_mfa_required: 'true'
|
|
181
208
|
rdoc_options: []
|
|
@@ -192,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
192
219
|
- !ruby/object:Gem::Version
|
|
193
220
|
version: '0'
|
|
194
221
|
requirements: []
|
|
195
|
-
rubygems_version:
|
|
222
|
+
rubygems_version: 3.6.9
|
|
196
223
|
specification_version: 4
|
|
197
224
|
summary: PostgreSQL-native job processing and event bus for Rails, built on PGMQ
|
|
198
225
|
test_files: []
|
data/.bun-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.3.11
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Coordinates development across pgbus layers. Use when planning multi-layer features, orchestrating implementation order, or designing new subsystems."
|
|
3
|
-
model: claude-opus-4-6
|
|
4
|
-
argument-hint: "feature or task to coordinate"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Pgbus Architect Mode
|
|
8
|
-
|
|
9
|
-
You are now in **Architect Mode** - coordinating development across all pgbus layers.
|
|
10
|
-
|
|
11
|
-
## Why This Skill Exists
|
|
12
|
-
|
|
13
|
-
Pgbus spans multiple layers (PGMQ transport, ActiveJob adapter, event bus, process model, dashboard). Without coordination, developers tackle layers in the wrong order, miss integration points, or create inconsistent implementations.
|
|
14
|
-
|
|
15
|
-
## Pgbus Architecture Layers
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
Layer 6: Dashboard (Web UI) app/controllers/pgbus/, app/views/pgbus/
|
|
19
|
-
Layer 5: CLI lib/pgbus/cli.rb
|
|
20
|
-
Layer 4: Process Model lib/pgbus/process/ (supervisor, worker, dispatcher, consumer)
|
|
21
|
-
Layer 3: Event Bus lib/pgbus/event_bus/ (publisher, subscriber, registry, handler)
|
|
22
|
-
Layer 2: ActiveJob Adapter lib/pgbus/active_job/ (adapter, executor)
|
|
23
|
-
Layer 1: Client lib/pgbus/client.rb (PGMQ wrapper)
|
|
24
|
-
Layer 0: Configuration lib/pgbus/configuration.rb, config_loader.rb
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Typical Implementation Flow
|
|
28
|
-
|
|
29
|
-
1. **Configuration** - Add new config options if needed
|
|
30
|
-
2. **Client** - Add PGMQ operations to the client wrapper
|
|
31
|
-
3. **Adapter / Event Bus** - Build the feature's core logic
|
|
32
|
-
4. **Process Model** - Add worker/consumer support
|
|
33
|
-
5. **Dashboard** - Expose in the web UI via DataSource
|
|
34
|
-
6. **Tests** - Coverage across all touched layers
|
|
35
|
-
|
|
36
|
-
## When to Delegate vs. Do Directly
|
|
37
|
-
|
|
38
|
-
**Delegate when**:
|
|
39
|
-
- Multiple files across a layer need changes
|
|
40
|
-
- Deep domain expertise is needed (e.g., PGMQ internals)
|
|
41
|
-
- Work is clearly scoped to one layer
|
|
42
|
-
|
|
43
|
-
**Handle directly when**:
|
|
44
|
-
- Simple, single-file changes
|
|
45
|
-
- Cross-cutting concerns affecting multiple layers
|
|
46
|
-
- Quick fixes or minor adjustments
|
|
47
|
-
|
|
48
|
-
## Decision Guidelines
|
|
49
|
-
|
|
50
|
-
| Decision | Use When |
|
|
51
|
-
|----------|----------|
|
|
52
|
-
| New config option | Feature needs user-configurable behavior |
|
|
53
|
-
| New Client method | New PGMQ operation needed |
|
|
54
|
-
| New event handler | Business event needs processing |
|
|
55
|
-
| New worker type | Different processing pattern needed |
|
|
56
|
-
| Dashboard page | Users need visibility into new feature |
|
|
57
|
-
| Migration change | New metadata table or PGMQ queue |
|
|
58
|
-
|
|
59
|
-
## Integration Points
|
|
60
|
-
|
|
61
|
-
| When working on... | Also consider... |
|
|
62
|
-
|-------------------|------------------|
|
|
63
|
-
| Client changes | Worker/consumer that calls it |
|
|
64
|
-
| New queue type | Dead letter queue, dashboard visibility |
|
|
65
|
-
| Event bus feature | Idempotency table, subscriber registry |
|
|
66
|
-
| Process model | Heartbeat, supervisor restart logic |
|
|
67
|
-
| Dashboard | DataSource queries, authentication |
|
|
68
|
-
| Configuration | Config loader YAML, generator template |
|
|
69
|
-
|
|
70
|
-
## Common Mistakes to Avoid
|
|
71
|
-
|
|
72
|
-
| Wrong | Right |
|
|
73
|
-
|-------|-------|
|
|
74
|
-
| Start with dashboard | Start with client/adapter layer |
|
|
75
|
-
| Skip configuration | Make it configurable from the start |
|
|
76
|
-
| Direct PGMQ calls | Go through Client wrapper |
|
|
77
|
-
| Forget DLQ | Every queue needs a dead letter strategy |
|
|
78
|
-
| Skip tests | TDD -- tests first at every layer |
|
|
79
|
-
| Monolith methods | Small files, focused classes |
|
|
80
|
-
|
|
81
|
-
## Verification Checklist
|
|
82
|
-
|
|
83
|
-
- [ ] Implementation order planned (bottom-up)
|
|
84
|
-
- [ ] Dependencies between layers identified
|
|
85
|
-
- [ ] PGMQ operations go through Client
|
|
86
|
-
- [ ] Configuration is extensible
|
|
87
|
-
- [ ] Dashboard exposes new data via DataSource
|
|
88
|
-
- [ ] Tests cover all touched layers
|
|
89
|
-
- [ ] `bundle exec rubocop` passes
|
|
90
|
-
- [ ] `bundle exec rspec` passes
|
|
91
|
-
|
|
92
|
-
## Handoff
|
|
93
|
-
|
|
94
|
-
When complete, summarize:
|
|
95
|
-
- Implementation plan with layer order
|
|
96
|
-
- Files to create/modify per layer
|
|
97
|
-
- Integration points identified
|
|
98
|
-
- Architectural decisions made
|
|
99
|
-
|
|
100
|
-
Now, coordinate pgbus development with this architectural perspective.
|