pgbus 0.1.3 → 0.1.5

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/pgbus/outbox_controller.rb +10 -0
  3. data/app/controllers/pgbus/queues_controller.rb +10 -0
  4. data/app/helpers/pgbus/application_helper.rb +6 -0
  5. data/app/models/pgbus/outbox_entry.rb +10 -0
  6. data/app/models/pgbus/queue_state.rb +33 -0
  7. data/app/views/layouts/pgbus/application.html.erb +1 -0
  8. data/app/views/pgbus/dashboard/_stats_cards.html.erb +7 -1
  9. data/app/views/pgbus/outbox/index.html.erb +55 -0
  10. data/app/views/pgbus/queues/_queues_list.html.erb +15 -1
  11. data/config/routes.rb +4 -0
  12. data/lib/generators/pgbus/add_outbox_generator.rb +52 -0
  13. data/lib/generators/pgbus/add_queue_states_generator.rb +51 -0
  14. data/lib/generators/pgbus/templates/add_outbox.rb.erb +25 -0
  15. data/lib/generators/pgbus/templates/add_queue_states.rb.erb +16 -0
  16. data/lib/pgbus/active_job/adapter.rb +6 -5
  17. data/lib/pgbus/active_job/executor.rb +22 -5
  18. data/lib/pgbus/circuit_breaker.rb +112 -0
  19. data/lib/pgbus/client.rb +140 -49
  20. data/lib/pgbus/configuration.rb +54 -2
  21. data/lib/pgbus/dedup_cache.rb +76 -0
  22. data/lib/pgbus/engine.rb +6 -0
  23. data/lib/pgbus/event_bus/handler.rb +13 -2
  24. data/lib/pgbus/outbox/poller.rb +117 -0
  25. data/lib/pgbus/outbox.rb +30 -0
  26. data/lib/pgbus/process/dispatcher.rb +46 -0
  27. data/lib/pgbus/process/heartbeat.rb +3 -1
  28. data/lib/pgbus/process/lifecycle.rb +111 -0
  29. data/lib/pgbus/process/supervisor.rb +40 -5
  30. data/lib/pgbus/process/worker.rb +86 -19
  31. data/lib/pgbus/rate_counter.rb +81 -0
  32. data/lib/pgbus/recurring/schedule.rb +1 -1
  33. data/lib/pgbus/version.rb +1 -1
  34. data/lib/pgbus/web/data_source.rb +87 -2
  35. data/lib/pgbus.rb +35 -6
  36. data/lib/tasks/pgbus_pgmq.rake +5 -3
  37. metadata +15 -1
@@ -7,6 +7,8 @@ module Pgbus
7
7
  class DataSource
8
8
  def initialize(client: Pgbus.client)
9
9
  @client = client
10
+ @last_throughput_snapshot = nil
11
+ @last_throughput_at = nil
10
12
  end
11
13
 
12
14
  # Dashboard summary
@@ -17,6 +19,8 @@ module Pgbus
17
19
  dlq_suffix = Pgbus.configuration.dead_letter_queue_suffix
18
20
  dlq_depth = queues.select { |q| q[:name].end_with?(dlq_suffix) }.sum { |q| q[:queue_length] }
19
21
 
22
+ throughput = compute_throughput(queues)
23
+
20
24
  {
21
25
  total_queues: queues.size,
22
26
  total_depth: total_depth,
@@ -24,7 +28,8 @@ module Pgbus
24
28
  active_processes: processes.count,
25
29
  failed_count: failed_events_count,
26
30
  dlq_depth: dlq_depth,
27
- recurring_count: recurring_tasks_count
31
+ recurring_count: recurring_tasks_count,
32
+ throughput_rate: throughput
28
33
  }
29
34
  end
30
35
 
@@ -33,7 +38,10 @@ module Pgbus
33
38
  # different connection lifecycle than the worker processes).
34
39
  def queues_with_metrics
35
40
  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
41
+ paused_queues = paused_queue_names
42
+ queue_names.map { |name| queue_metrics_via_sql(name) }.compact.map do |q|
43
+ q.merge(paused: paused_queues.include?(logical_queue_name(q[:name])))
44
+ end
37
45
  rescue StandardError => e
38
46
  Pgbus.logger.error { "[Pgbus::Web] Error fetching queue metrics: #{e.class}: #{e.message}" }
39
47
  []
@@ -52,6 +60,24 @@ module Pgbus
52
60
  @client.purge_queue(name)
53
61
  end
54
62
 
63
+ def pause_queue(name, reason: nil)
64
+ QueueState.pause!(logical_queue_name(name), reason: reason)
65
+ rescue StandardError => e
66
+ Pgbus.logger.error { "[Pgbus::Web] Error pausing queue #{name}: #{e.message}" }
67
+ end
68
+
69
+ def resume_queue(name)
70
+ QueueState.resume!(logical_queue_name(name))
71
+ rescue StandardError => e
72
+ Pgbus.logger.error { "[Pgbus::Web] Error resuming queue #{name}: #{e.message}" }
73
+ end
74
+
75
+ def queue_paused?(name)
76
+ QueueState.paused?(logical_queue_name(name))
77
+ rescue StandardError
78
+ false
79
+ end
80
+
55
81
  # Jobs (messages in queue tables)
56
82
  def jobs(queue_name: nil, page: 1, per_page: 25)
57
83
  offset = (page - 1) * per_page
@@ -451,6 +477,26 @@ module Pgbus
451
477
  0
452
478
  end
453
479
 
480
+ # Outbox
481
+ def outbox_stats
482
+ {
483
+ unpublished: OutboxEntry.unpublished.count,
484
+ total: OutboxEntry.count,
485
+ oldest_unpublished_age: oldest_unpublished_age
486
+ }
487
+ rescue StandardError => e
488
+ Pgbus.logger.debug { "[Pgbus::Web] Error fetching outbox stats: #{e.message}" }
489
+ { unpublished: 0, total: 0, oldest_unpublished_age: nil }
490
+ end
491
+
492
+ def outbox_entries(page: 1, per_page: 25)
493
+ offset = (page - 1) * per_page
494
+ OutboxEntry.order(id: :desc).limit(per_page).offset(offset).to_a
495
+ rescue StandardError => e
496
+ Pgbus.logger.debug { "[Pgbus::Web] Error fetching outbox entries: #{e.message}" }
497
+ []
498
+ end
499
+
454
500
  # Subscriber registry
455
501
  def registered_subscribers
456
502
  EventBus::Registry.instance.subscribers.map do |s|
@@ -571,6 +617,45 @@ module Pgbus
571
617
  sanitized
572
618
  end
573
619
 
620
+ def compute_throughput(queues)
621
+ current_totals = queues.sum { |q| q[:total_messages] }
622
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
623
+
624
+ if @last_throughput_snapshot && @last_throughput_at
625
+ elapsed = now - @last_throughput_at
626
+ delta = current_totals - @last_throughput_snapshot
627
+ rate = elapsed.positive? ? (delta / elapsed).round(1) : 0.0
628
+ end
629
+
630
+ @last_throughput_snapshot = current_totals
631
+ @last_throughput_at = now
632
+
633
+ rate || 0.0
634
+ rescue StandardError
635
+ 0.0
636
+ end
637
+
638
+ def logical_queue_name(name)
639
+ name
640
+ .delete_prefix("#{Pgbus.configuration.queue_prefix}_")
641
+ .sub(/_p\d+\z/, "")
642
+ end
643
+
644
+ def paused_queue_names
645
+ QueueState.paused.pluck(:queue_name)
646
+ rescue StandardError
647
+ []
648
+ end
649
+
650
+ def oldest_unpublished_age
651
+ oldest = OutboxEntry.unpublished.order(:id).pick(:created_at)
652
+ return nil unless oldest
653
+
654
+ (Time.now - oldest).to_i
655
+ rescue StandardError
656
+ nil
657
+ end
658
+
574
659
  def parse_arguments(args)
575
660
  case args
576
661
  when Array then args
data/lib/pgbus.rb CHANGED
@@ -18,16 +18,33 @@ module Pgbus
18
18
  loader.inflector.inflect("pgbus" => "Pgbus", "cli" => "CLI", "dsl" => "DSL")
19
19
  loader.ignore("#{__dir__}/generators")
20
20
  loader.ignore("#{__dir__}/active_job")
21
- # Register app/models for non-Rails usage (specs, standalone).
22
- # When Rails is running, the Engine handles autoloading app/models.
23
- unless defined?(Rails::Engine)
24
- models_dir = File.expand_path("../app/models", __dir__)
25
- loader.push_dir(models_dir) if File.directory?(models_dir)
26
- end
27
21
  loader
28
22
  end
29
23
  end
30
24
 
25
+ # Separate loader for app/models used only in non-Rails contexts (specs,
26
+ # standalone scripts). When the Engine boots, Rails' autoloader takes over
27
+ # app/models and this loader is torn down to avoid conflicts.
28
+ def models_loader
29
+ models_dir = File.expand_path("../app/models", __dir__)
30
+ return nil unless File.directory?(models_dir)
31
+
32
+ @models_loader ||= begin
33
+ loader = Zeitwerk::Loader.new
34
+ loader.tag = "pgbus-models"
35
+ loader.push_dir(models_dir)
36
+ loader.setup
37
+ loader
38
+ end
39
+ end
40
+
41
+ def teardown_models_loader!
42
+ return unless @models_loader
43
+
44
+ @models_loader.unregister
45
+ @models_loader = nil
46
+ end
47
+
31
48
  def configuration
32
49
  @configuration ||= Configuration.new
33
50
  end
@@ -46,12 +63,24 @@ module Pgbus
46
63
  @configuration = nil
47
64
  end
48
65
 
66
+ # Discard the inherited PGMQ client after fork.
67
+ # Do NOT call close — the parent's @pgmq_mutex is in undefined
68
+ # state post-fork and attempting to acquire it can deadlock.
69
+ # The next call to Pgbus.client will lazily create a fresh one.
70
+ def reset_client!
71
+ @client = nil
72
+ end
73
+
49
74
  def logger
50
75
  configuration.logger
51
76
  end
52
77
  end
53
78
 
54
79
  loader.setup
80
+
81
+ # In non-Rails contexts, set up model autoloading via a separate loader.
82
+ # This is torn down by the Engine initializer when Rails boots.
83
+ models_loader unless defined?(Rails::Engine)
55
84
  end
56
85
 
57
86
  require "active_job/queue_adapters/pgbus_adapter" if defined?(ActiveJob)
@@ -12,8 +12,10 @@ namespace :pgbus do
12
12
  latest = Pgbus::PgmqSchema.latest_version
13
13
  puts "Vendored version: #{latest}"
14
14
 
15
- if ActiveRecord::Base.connection.table_exists?("pgbus_pgmq_schema_versions")
16
- row = ActiveRecord::Base.connection.select_one(
15
+ conn = Pgbus.configuration.connects_to ? Pgbus::ApplicationRecord.connection : ActiveRecord::Base.connection
16
+
17
+ if conn.table_exists?("pgbus_pgmq_schema_versions")
18
+ row = conn.select_one(
17
19
  "SELECT version, install_method, installed_at FROM pgbus_pgmq_schema_versions ORDER BY installed_at DESC LIMIT 1"
18
20
  )
19
21
  if row
@@ -32,7 +34,7 @@ namespace :pgbus do
32
34
  end
33
35
  else
34
36
  # Check if pgmq schema exists at all
35
- schema_exists = ActiveRecord::Base.connection.select_value(
37
+ schema_exists = conn.select_value(
36
38
  "SELECT 1 FROM information_schema.schemata WHERE schema_name = 'pgmq'"
37
39
  )
38
40
  if schema_exists
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.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -113,6 +113,7 @@ files:
113
113
  - app/controllers/pgbus/dead_letter_controller.rb
114
114
  - app/controllers/pgbus/events_controller.rb
115
115
  - app/controllers/pgbus/jobs_controller.rb
116
+ - app/controllers/pgbus/outbox_controller.rb
116
117
  - app/controllers/pgbus/processes_controller.rb
117
118
  - app/controllers/pgbus/queues_controller.rb
118
119
  - app/controllers/pgbus/recurring_tasks_controller.rb
@@ -120,8 +121,10 @@ files:
120
121
  - app/models/pgbus/application_record.rb
121
122
  - app/models/pgbus/batch_entry.rb
122
123
  - app/models/pgbus/blocked_execution.rb
124
+ - app/models/pgbus/outbox_entry.rb
123
125
  - app/models/pgbus/process_entry.rb
124
126
  - app/models/pgbus/processed_event.rb
127
+ - app/models/pgbus/queue_state.rb
125
128
  - app/models/pgbus/recurring_execution.rb
126
129
  - app/models/pgbus/recurring_task.rb
127
130
  - app/models/pgbus/semaphore.rb
@@ -140,6 +143,7 @@ files:
140
143
  - app/views/pgbus/jobs/_failed_table.html.erb
141
144
  - app/views/pgbus/jobs/index.html.erb
142
145
  - app/views/pgbus/jobs/show.html.erb
146
+ - app/views/pgbus/outbox/index.html.erb
143
147
  - app/views/pgbus/processes/_processes_table.html.erb
144
148
  - app/views/pgbus/processes/index.html.erb
145
149
  - app/views/pgbus/queues/_queues_list.html.erb
@@ -151,8 +155,12 @@ files:
151
155
  - config/routes.rb
152
156
  - exe/pgbus
153
157
  - lib/active_job/queue_adapters/pgbus_adapter.rb
158
+ - lib/generators/pgbus/add_outbox_generator.rb
159
+ - lib/generators/pgbus/add_queue_states_generator.rb
154
160
  - lib/generators/pgbus/add_recurring_generator.rb
155
161
  - lib/generators/pgbus/install_generator.rb
162
+ - lib/generators/pgbus/templates/add_outbox.rb.erb
163
+ - lib/generators/pgbus/templates/add_queue_states.rb.erb
156
164
  - lib/generators/pgbus/templates/add_recurring_tables.rb.erb
157
165
  - lib/generators/pgbus/templates/migration.rb.erb
158
166
  - lib/generators/pgbus/templates/pgbus.yml.erb
@@ -164,6 +172,7 @@ files:
164
172
  - lib/pgbus/active_job/adapter.rb
165
173
  - lib/pgbus/active_job/executor.rb
166
174
  - lib/pgbus/batch.rb
175
+ - lib/pgbus/circuit_breaker.rb
167
176
  - lib/pgbus/cli.rb
168
177
  - lib/pgbus/client.rb
169
178
  - lib/pgbus/concurrency.rb
@@ -171,6 +180,7 @@ files:
171
180
  - lib/pgbus/concurrency/semaphore.rb
172
181
  - lib/pgbus/config_loader.rb
173
182
  - lib/pgbus/configuration.rb
183
+ - lib/pgbus/dedup_cache.rb
174
184
  - lib/pgbus/engine.rb
175
185
  - lib/pgbus/event.rb
176
186
  - lib/pgbus/event_bus/handler.rb
@@ -178,14 +188,18 @@ files:
178
188
  - lib/pgbus/event_bus/registry.rb
179
189
  - lib/pgbus/event_bus/subscriber.rb
180
190
  - lib/pgbus/instrumentation.rb
191
+ - lib/pgbus/outbox.rb
192
+ - lib/pgbus/outbox/poller.rb
181
193
  - lib/pgbus/pgmq_schema.rb
182
194
  - lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql
183
195
  - lib/pgbus/process/consumer.rb
184
196
  - lib/pgbus/process/dispatcher.rb
185
197
  - lib/pgbus/process/heartbeat.rb
198
+ - lib/pgbus/process/lifecycle.rb
186
199
  - lib/pgbus/process/signal_handler.rb
187
200
  - lib/pgbus/process/supervisor.rb
188
201
  - lib/pgbus/process/worker.rb
202
+ - lib/pgbus/rate_counter.rb
189
203
  - lib/pgbus/recurring/already_recorded.rb
190
204
  - lib/pgbus/recurring/command_job.rb
191
205
  - lib/pgbus/recurring/config_loader.rb