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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -3
  3. data/Rakefile +98 -1
  4. data/app/controllers/pgbus/application_controller.rb +8 -0
  5. data/app/controllers/pgbus/recurring_tasks_controller.rb +36 -0
  6. data/app/helpers/pgbus/application_helper.rb +41 -0
  7. data/app/models/pgbus/application_record.rb +7 -0
  8. data/app/models/pgbus/batch_entry.rb +31 -0
  9. data/app/models/pgbus/blocked_execution.rb +40 -0
  10. data/app/models/pgbus/process_entry.rb +9 -0
  11. data/app/models/pgbus/processed_event.rb +9 -0
  12. data/app/models/pgbus/recurring_execution.rb +33 -0
  13. data/app/models/pgbus/recurring_task.rb +42 -0
  14. data/app/models/pgbus/semaphore.rb +29 -0
  15. data/app/views/layouts/pgbus/application.html.erb +1 -0
  16. data/app/views/pgbus/dashboard/_stats_cards.html.erb +9 -1
  17. data/app/views/pgbus/dead_letter/_messages_table.html.erb +55 -18
  18. data/app/views/pgbus/jobs/_enqueued_table.html.erb +46 -8
  19. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +79 -0
  20. data/app/views/pgbus/recurring_tasks/index.html.erb +6 -0
  21. data/app/views/pgbus/recurring_tasks/show.html.erb +122 -0
  22. data/config/routes.rb +7 -0
  23. data/lib/active_job/queue_adapters/pgbus_adapter.rb +29 -0
  24. data/lib/generators/pgbus/add_recurring_generator.rb +56 -0
  25. data/lib/generators/pgbus/install_generator.rb +76 -2
  26. data/lib/generators/pgbus/templates/add_recurring_tables.rb.erb +31 -0
  27. data/lib/generators/pgbus/templates/migration.rb.erb +72 -4
  28. data/lib/generators/pgbus/templates/recurring.yml.erb +40 -0
  29. data/lib/generators/pgbus/templates/upgrade_pgmq.rb.erb +30 -0
  30. data/lib/generators/pgbus/upgrade_pgmq_generator.rb +60 -0
  31. data/lib/pgbus/active_job/adapter.rb +3 -6
  32. data/lib/pgbus/active_job/executor.rb +26 -12
  33. data/lib/pgbus/batch.rb +65 -72
  34. data/lib/pgbus/cli.rb +11 -16
  35. data/lib/pgbus/client.rb +32 -15
  36. data/lib/pgbus/concurrency/blocked_execution.rb +32 -37
  37. data/lib/pgbus/concurrency/semaphore.rb +11 -39
  38. data/lib/pgbus/concurrency.rb +10 -2
  39. data/lib/pgbus/configuration.rb +48 -0
  40. data/lib/pgbus/engine.rb +19 -1
  41. data/lib/pgbus/event_bus/handler.rb +10 -23
  42. data/lib/pgbus/instrumentation.rb +29 -0
  43. data/lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql +2123 -0
  44. data/lib/pgbus/pgmq_schema.rb +159 -0
  45. data/lib/pgbus/process/consumer.rb +17 -9
  46. data/lib/pgbus/process/dispatcher.rb +33 -41
  47. data/lib/pgbus/process/heartbeat.rb +15 -23
  48. data/lib/pgbus/process/signal_handler.rb +23 -1
  49. data/lib/pgbus/process/supervisor.rb +79 -2
  50. data/lib/pgbus/process/worker.rb +42 -13
  51. data/lib/pgbus/recurring/already_recorded.rb +7 -0
  52. data/lib/pgbus/recurring/command_job.rb +28 -0
  53. data/lib/pgbus/recurring/config_loader.rb +35 -0
  54. data/lib/pgbus/recurring/schedule.rb +102 -0
  55. data/lib/pgbus/recurring/scheduler.rb +102 -0
  56. data/lib/pgbus/recurring/task.rb +111 -0
  57. data/lib/pgbus/serializer.rb +16 -6
  58. data/lib/pgbus/version.rb +1 -1
  59. data/lib/pgbus/web/data_source.rb +217 -36
  60. data/lib/pgbus.rb +8 -0
  61. data/lib/tasks/pgbus_pgmq.rake +62 -0
  62. metadata +51 -24
  63. data/.bun-version +0 -1
  64. data/.claude/commands/architect.md +0 -100
  65. data/.claude/commands/github-review-comments.md +0 -237
  66. data/.claude/commands/lfg.md +0 -271
  67. data/.claude/commands/review-pr.md +0 -69
  68. data/.claude/commands/security.md +0 -122
  69. data/.claude/commands/tdd.md +0 -148
  70. data/.claude/rules/agents.md +0 -49
  71. data/.claude/rules/coding-style.md +0 -91
  72. data/.claude/rules/git-workflow.md +0 -56
  73. data/.claude/rules/performance.md +0 -73
  74. data/.claude/rules/testing.md +0 -67
  75. data/CLAUDE.md +0 -80
  76. data/CODE_OF_CONDUCT.md +0 -10
  77. data/bun.lock +0 -18
  78. data/docs/README.md +0 -28
  79. data/docs/switch_from_good_job.md +0 -279
  80. data/docs/switch_from_sidekiq.md +0 -226
  81. data/docs/switch_from_solid_queue.md +0 -247
  82. data/package.json +0 -9
  83. 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
- metrics = @client.metrics || []
33
- Array(metrics).map { |m| format_metrics(m) }
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.debug { "[Pgbus::Web] Error fetching queue metrics: #{e.message}" }
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
- m = @client.metrics(name)
41
- m ? format_metrics(m) : nil
45
+ queue_metrics_via_sql(name)
42
46
  rescue StandardError => e
43
- Pgbus.logger.debug { "[Pgbus::Web] Error fetching queue detail for #{name}: #{e.message}" }
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(full_name)} WHERE msg_id = $1",
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
- full_name = Pgbus.configuration.queue_name(queue_name)
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.execute("DELETE FROM pgbus_failed_events WHERE id = #{id.to_i}")
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.execute("DELETE FROM pgbus_failed_events WHERE id = #{id.to_i}")
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
- connection.select_all("SELECT * FROM pgbus_failed_events").each do |event|
149
- payload = JSON.parse(event["payload"])
150
- headers = event["headers"]
151
- headers = JSON.parse(headers) if headers.is_a?(String)
152
-
153
- connection.transaction do
154
- @client.send_message(event["queue_name"], payload, headers: headers)
155
- connection.execute("DELETE FROM pgbus_failed_events WHERE id = #{event["id"].to_i}")
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
- ActiveRecord::Base.connection
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
- full_name = Pgbus.configuration.queue_name(name)
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 format_metrics(m)
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: m.queue_name.to_s,
363
- queue_length: m.queue_length.to_i,
364
- queue_visible_length: m.queue_visible_length.to_i,
365
- oldest_msg_age_sec: m.oldest_msg_age_sec&.to_i,
366
- newest_msg_age_sec: m.newest_msg_age_sec&.to_i,
367
- total_messages: m.total_messages.to_i
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.0.1
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
- - bun.lock
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
- - package.json
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: 4.0.6
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.