pgbus 0.1.5 → 0.1.7

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +326 -11
  3. data/app/controllers/pgbus/api/insights_controller.rb +16 -0
  4. data/app/controllers/pgbus/insights_controller.rb +10 -0
  5. data/app/controllers/pgbus/locks_controller.rb +9 -0
  6. data/app/helpers/pgbus/application_helper.rb +28 -0
  7. data/app/models/pgbus/job_lock.rb +82 -0
  8. data/app/models/pgbus/job_stat.rb +94 -0
  9. data/app/views/layouts/pgbus/application.html.erb +31 -8
  10. data/app/views/pgbus/dashboard/_processes_table.html.erb +6 -6
  11. data/app/views/pgbus/dashboard/_queues_table.html.erb +6 -6
  12. data/app/views/pgbus/dashboard/_recent_failures.html.erb +5 -5
  13. data/app/views/pgbus/dashboard/_stats_cards.html.erb +20 -20
  14. data/app/views/pgbus/dashboard/show.html.erb +1 -1
  15. data/app/views/pgbus/dead_letter/_messages_table.html.erb +12 -12
  16. data/app/views/pgbus/dead_letter/index.html.erb +1 -1
  17. data/app/views/pgbus/dead_letter/show.html.erb +10 -10
  18. data/app/views/pgbus/events/index.html.erb +15 -15
  19. data/app/views/pgbus/events/show.html.erb +5 -5
  20. data/app/views/pgbus/insights/show.html.erb +161 -0
  21. data/app/views/pgbus/jobs/_enqueued_table.html.erb +13 -13
  22. data/app/views/pgbus/jobs/_failed_table.html.erb +7 -7
  23. data/app/views/pgbus/jobs/index.html.erb +1 -1
  24. data/app/views/pgbus/jobs/show.html.erb +10 -10
  25. data/app/views/pgbus/locks/index.html.erb +53 -0
  26. data/app/views/pgbus/outbox/index.html.erb +12 -12
  27. data/app/views/pgbus/processes/_processes_table.html.erb +6 -6
  28. data/app/views/pgbus/processes/index.html.erb +1 -1
  29. data/app/views/pgbus/queues/_queues_list.html.erb +5 -5
  30. data/app/views/pgbus/queues/index.html.erb +1 -1
  31. data/app/views/pgbus/queues/show.html.erb +7 -7
  32. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +6 -6
  33. data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
  34. data/app/views/pgbus/recurring_tasks/show.html.erb +22 -22
  35. data/config/routes.rb +3 -0
  36. data/lib/generators/pgbus/add_job_locks_generator.rb +52 -0
  37. data/lib/generators/pgbus/add_job_stats_generator.rb +52 -0
  38. data/lib/generators/pgbus/add_outbox_generator.rb +1 -1
  39. data/lib/generators/pgbus/add_queue_states_generator.rb +1 -1
  40. data/lib/generators/pgbus/add_recurring_generator.rb +1 -1
  41. data/lib/generators/pgbus/install_generator.rb +1 -1
  42. data/lib/generators/pgbus/templates/add_job_locks.rb.erb +21 -0
  43. data/lib/generators/pgbus/templates/add_job_stats.rb.erb +18 -0
  44. data/lib/generators/pgbus/upgrade_pgmq_generator.rb +1 -1
  45. data/lib/pgbus/active_job/adapter.rb +58 -4
  46. data/lib/pgbus/active_job/executor.rb +45 -0
  47. data/lib/pgbus/client.rb +8 -22
  48. data/lib/pgbus/configuration.rb +6 -0
  49. data/lib/pgbus/engine.rb +1 -0
  50. data/lib/pgbus/process/consumer_priority.rb +64 -0
  51. data/lib/pgbus/process/dispatcher.rb +29 -0
  52. data/lib/pgbus/process/queue_lock.rb +87 -0
  53. data/lib/pgbus/process/supervisor.rb +6 -1
  54. data/lib/pgbus/process/wake_signal.rb +53 -0
  55. data/lib/pgbus/process/worker.rb +36 -6
  56. data/lib/pgbus/queue_factory.rb +62 -0
  57. data/lib/pgbus/uniqueness.rb +169 -0
  58. data/lib/pgbus/version.rb +1 -1
  59. data/lib/pgbus/web/data_source.rb +49 -0
  60. data/lib/pgbus.rb +1 -0
  61. metadata +17 -1
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Pgbus
6
+ module Process
7
+ # Wake signal inspired by LavinMQ's BoolChannel pattern.
8
+ # Replaces polling-based coordination with instant state-change signaling.
9
+ #
10
+ # IMPORTANT: Single-waiter only. The +wait+ method calls @event.reset
11
+ # immediately after waking, which means concurrent waiters may miss
12
+ # notifications. Callers must ensure only one thread calls +wait+ at
13
+ # a time. In pgbus this is guaranteed because each Worker has exactly
14
+ # one main loop thread that calls +wait+, while +notify!+ can be
15
+ # called from any thread (signal handlers, lifecycle transitions).
16
+ #
17
+ # Usage:
18
+ # signal = WakeSignal.new
19
+ # # In worker thread (single waiter):
20
+ # signal.wait(timeout: 5) # blocks until signaled or timeout
21
+ # # In another thread (e.g. signal handler, lifecycle transition):
22
+ # signal.notify! # wakes the waiting thread
23
+ class WakeSignal
24
+ def initialize
25
+ @event = Concurrent::Event.new
26
+ end
27
+
28
+ # Block until +notify!+ is called or timeout expires.
29
+ # Returns true if signaled, false if timed out.
30
+ # Resets the event after waking — only safe with a single waiter.
31
+ def wait(timeout: nil)
32
+ result = @event.wait(timeout)
33
+ @event.reset
34
+ result
35
+ end
36
+
37
+ # Wake all waiting threads immediately.
38
+ def notify!
39
+ @event.set
40
+ end
41
+
42
+ # Check if a notification is pending without blocking.
43
+ def pending?
44
+ @event.set?
45
+ end
46
+
47
+ # Clear the pending notification.
48
+ def reset!
49
+ @event.reset
50
+ end
51
+ end
52
+ end
53
+ end
@@ -9,10 +9,13 @@ module Pgbus
9
9
 
10
10
  attr_reader :queues, :threads, :config
11
11
 
12
- def initialize(queues:, threads: 5, config: Pgbus.configuration)
12
+ def initialize(queues:, threads: 5, config: Pgbus.configuration,
13
+ single_active_consumer: false, consumer_priority: 0)
13
14
  @queues = Array(queues)
14
15
  @threads = threads
15
16
  @config = config
17
+ @single_active_consumer = single_active_consumer
18
+ @consumer_priority = consumer_priority
16
19
  @lifecycle = Lifecycle.new
17
20
  @jobs_processed = Concurrent::AtomicFixnum.new(0)
18
21
  @jobs_failed = Concurrent::AtomicFixnum.new(0)
@@ -22,6 +25,8 @@ module Pgbus
22
25
  @executor = Pgbus::ActiveJob::Executor.new
23
26
  @pool = Concurrent::FixedThreadPool.new(threads)
24
27
  @circuit_breaker = Pgbus::CircuitBreaker.new(config: config)
28
+ @queue_lock = QueueLock.new if @single_active_consumer
29
+ @wake_signal = WakeSignal.new
25
30
  end
26
31
 
27
32
  def stats
@@ -30,6 +35,9 @@ module Pgbus
30
35
  jobs_failed: @jobs_failed.value,
31
36
  in_flight: @in_flight.value,
32
37
  state: @lifecycle.state,
38
+ consumer_priority: @consumer_priority,
39
+ single_active_consumer: @single_active_consumer,
40
+ locked_queues: @queue_lock&.held_queues || [],
33
41
  rates: @rate_counter.rates,
34
42
  started_at: @started_at
35
43
  }
@@ -50,7 +58,7 @@ module Pgbus
50
58
  break if @lifecycle.draining? && @pool.queue_length.zero?
51
59
 
52
60
  claim_and_execute if @lifecycle.can_process?
53
- interruptible_sleep(config.polling_interval) if @lifecycle.draining? || @lifecycle.paused?
61
+ @wake_signal.wait(timeout: config.polling_interval) if @lifecycle.draining? || @lifecycle.paused?
54
62
  end
55
63
 
56
64
  shutdown
@@ -59,23 +67,27 @@ module Pgbus
59
67
  def graceful_shutdown
60
68
  Pgbus.logger.info { "[Pgbus] Worker shutting down gracefully..." }
61
69
  @lifecycle.transition_to(:draining)
70
+ @wake_signal.notify!
62
71
  end
63
72
 
64
73
  def immediate_shutdown
65
74
  Pgbus.logger.warn { "[Pgbus] Worker shutting down immediately!" }
66
75
  @lifecycle.transition_to!(:stopped)
76
+ @wake_signal.notify!
67
77
  @pool.kill
68
78
  end
69
79
 
70
80
  private
71
81
 
72
82
  def claim_and_execute
83
+ poll_interval = effective_polling_interval
84
+
73
85
  idle = @pool.max_length - @pool.queue_length
74
- return interruptible_sleep(config.polling_interval) if idle <= 0
86
+ return @wake_signal.wait(timeout: poll_interval) if idle <= 0
75
87
 
76
88
  if config.prefetch_limit
77
89
  available = config.prefetch_limit - @in_flight.value
78
- return interruptible_sleep(config.polling_interval) if available <= 0
90
+ return @wake_signal.wait(timeout: poll_interval) if available <= 0
79
91
 
80
92
  idle = [idle, available].min
81
93
  end
@@ -83,7 +95,7 @@ module Pgbus
83
95
  tagged_messages = fetch_messages(idle)
84
96
 
85
97
  if tagged_messages.empty?
86
- interruptible_sleep(config.polling_interval)
98
+ @wake_signal.wait(timeout: poll_interval)
87
99
  return
88
100
  end
89
101
 
@@ -98,6 +110,7 @@ module Pgbus
98
110
  # which queue each message came from (PGMQ messages don't carry this).
99
111
  def fetch_messages(qty)
100
112
  active_queues = queues.reject { |q| @circuit_breaker.paused?(q) }
113
+ active_queues = active_queues.select { |q| @queue_lock.try_lock(q) } if @single_active_consumer
101
114
  return [] if active_queues.empty?
102
115
 
103
116
  if priority_enabled?
@@ -188,6 +201,7 @@ module Pgbus
188
201
  return unless @lifecycle.running? && recycle_needed?
189
202
 
190
203
  @lifecycle.transition_to(:draining)
204
+ @wake_signal.notify!
191
205
  end
192
206
 
193
207
  def recycle_needed?
@@ -227,10 +241,25 @@ module Pgbus
227
241
  end
228
242
  end
229
243
 
244
+ def effective_polling_interval
245
+ return config.polling_interval if @consumer_priority.zero?
246
+
247
+ ConsumerPriority.effective_polling_interval(
248
+ base_interval: config.polling_interval,
249
+ my_priority: @consumer_priority,
250
+ max_priority: ConsumerPriority.max_active_priority(queues, ::Process.pid)
251
+ )
252
+ rescue StandardError
253
+ config.polling_interval
254
+ end
255
+
230
256
  def start_heartbeat
231
257
  @heartbeat = Heartbeat.new(
232
258
  kind: "worker",
233
- metadata: { queues: queues, threads: threads, pid: ::Process.pid },
259
+ metadata: {
260
+ queues: queues, threads: threads, pid: ::Process.pid,
261
+ consumer_priority: @consumer_priority
262
+ },
234
263
  on_beat: -> { @rate_counter.snapshot! }
235
264
  )
236
265
  @heartbeat.start
@@ -240,6 +269,7 @@ module Pgbus
240
269
  Pgbus.logger.info { "[Pgbus] Worker draining thread pool..." }
241
270
  @pool.shutdown
242
271
  @pool.wait_for_termination(30)
272
+ @queue_lock&.unlock_all
243
273
  @heartbeat&.stop
244
274
  restore_signals
245
275
  Pgbus.logger.info { "[Pgbus] Worker stopped. Processed: #{@jobs_processed.value}, Failed: #{@jobs_failed.value}" }
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ # Dispatches queue operations based on queue type (standard vs priority).
5
+ # Replaces conditional `priority_enabled?` checks scattered through Client
6
+ # with a single strategy object selected at initialization.
7
+ #
8
+ # Inspired by LavinMQ's QueueFactory which dispatches queue creation by
9
+ # type: standard, durable, priority, stream, delayed.
10
+ module QueueFactory
11
+ def self.for(config)
12
+ if config.priority_levels && config.priority_levels > 1
13
+ PriorityStrategy.new(config)
14
+ else
15
+ StandardStrategy.new(config)
16
+ end
17
+ end
18
+
19
+ # Standard single-queue strategy: one PGMQ queue per logical name.
20
+ class StandardStrategy
21
+ def initialize(config)
22
+ @config = config
23
+ end
24
+
25
+ def physical_queue_names(name)
26
+ [@config.queue_name(name)]
27
+ end
28
+
29
+ def target_queue(name, _priority)
30
+ @config.queue_name(name)
31
+ end
32
+
33
+ def priority?
34
+ false
35
+ end
36
+ end
37
+
38
+ # Priority sub-queue strategy: N PGMQ queues per logical name (_p0.._pN).
39
+ class PriorityStrategy
40
+ def initialize(config)
41
+ @config = config
42
+ end
43
+
44
+ def physical_queue_names(name)
45
+ @config.priority_queue_names(name)
46
+ end
47
+
48
+ def target_queue(name, priority)
49
+ level = if priority
50
+ priority.clamp(0, @config.priority_levels - 1)
51
+ else
52
+ @config.default_priority.clamp(0, @config.priority_levels - 1)
53
+ end
54
+ @config.priority_queue_name(name, level)
55
+ end
56
+
57
+ def priority?
58
+ true
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "socket"
5
+
6
+ module Pgbus
7
+ # Job uniqueness guarantees: prevent duplicate jobs from running concurrently.
8
+ #
9
+ # Unlike concurrency limits (which allow N concurrent jobs for the same key),
10
+ # uniqueness ensures AT MOST ONE job with a given key exists in the system
11
+ # at any time — from enqueue through completion.
12
+ #
13
+ # Lock lifecycle:
14
+ # 1. Enqueue: lock acquired (state: queued), no owner yet
15
+ # 2. Execution start: lock transitions to (state: executing, owner_pid: PID)
16
+ # 3. Completion/DLQ: lock released (row deleted)
17
+ # 4. Crash recovery: reaper detects orphaned locks by cross-referencing
18
+ # owner_pid against pgbus_processes heartbeats
19
+ # 5. Last resort: expires_at TTL (default 24h) handles the case where
20
+ # even the reaper can't run (entire supervisor dead)
21
+ #
22
+ # Strategies:
23
+ # :until_executed — Lock acquired at enqueue, held through execution, released on
24
+ # completion or DLQ. Prevents duplicate enqueue AND duplicate execution.
25
+ #
26
+ # :while_executing — Lock acquired at execution start, released on completion.
27
+ # Allows duplicate enqueue (multiple copies in queue) but only one
28
+ # executes at a time.
29
+ #
30
+ # Usage:
31
+ # class ImportOrderJob < ApplicationJob
32
+ # ensures_uniqueness strategy: :until_executed,
33
+ # key: ->(order_id) { "import-order-#{order_id}" },
34
+ # on_conflict: :reject
35
+ #
36
+ # def perform(order_id)
37
+ # # Only one instance of this job per order_id can exist at a time
38
+ # end
39
+ # end
40
+ module Uniqueness
41
+ extend ActiveSupport::Concern
42
+
43
+ METADATA_KEY = "pgbus_uniqueness_key"
44
+ STRATEGY_KEY = "pgbus_uniqueness_strategy"
45
+ TTL_KEY = "pgbus_uniqueness_lock_ttl"
46
+
47
+ # 24 hours — last-resort fallback only. The reaper handles normal crash recovery.
48
+ DEFAULT_LOCK_TTL = 24 * 60 * 60
49
+
50
+ VALID_STRATEGIES = %i[until_executed while_executing].freeze
51
+ VALID_CONFLICTS = %i[reject discard log].freeze
52
+
53
+ class_methods do
54
+ def ensures_uniqueness(strategy: :until_executed, key: nil, lock_ttl: DEFAULT_LOCK_TTL, on_conflict: :reject)
55
+ raise ArgumentError, "strategy must be one of: #{VALID_STRATEGIES.join(", ")}" unless VALID_STRATEGIES.include?(strategy)
56
+ raise ArgumentError, "on_conflict must be one of: #{VALID_CONFLICTS.join(", ")}" unless VALID_CONFLICTS.include?(on_conflict)
57
+
58
+ valid_ttl = lock_ttl.is_a?(Numeric) || (defined?(ActiveSupport::Duration) && lock_ttl.is_a?(ActiveSupport::Duration))
59
+ raise ArgumentError, "lock_ttl must be a positive number or Duration" unless valid_ttl && lock_ttl.positive?
60
+ raise ArgumentError, "key must be callable (Proc or lambda)" if key && !key.respond_to?(:call)
61
+
62
+ @pgbus_uniqueness = {
63
+ strategy: strategy,
64
+ key: key || ->(*) { name },
65
+ lock_ttl: lock_ttl,
66
+ on_conflict: on_conflict
67
+ }.freeze
68
+ end
69
+
70
+ def pgbus_uniqueness
71
+ @pgbus_uniqueness
72
+ end
73
+ end
74
+
75
+ class << self
76
+ def resolve_key(active_job)
77
+ config = uniqueness_config(active_job)
78
+ return nil unless config
79
+
80
+ args = active_job.arguments
81
+ last = args.last
82
+ if last.is_a?(Hash) && last.each_key.all?(Symbol)
83
+ config[:key].call(*args[...-1], **last)
84
+ else
85
+ config[:key].call(*args)
86
+ end
87
+ end
88
+
89
+ def inject_metadata(active_job, payload_hash)
90
+ config = uniqueness_config(active_job)
91
+ return payload_hash unless config
92
+
93
+ key = resolve_key(active_job)
94
+ return payload_hash unless key
95
+
96
+ payload_hash.merge(
97
+ METADATA_KEY => key,
98
+ STRATEGY_KEY => config[:strategy].to_s,
99
+ TTL_KEY => config[:lock_ttl]
100
+ )
101
+ end
102
+
103
+ def extract_key(payload)
104
+ payload&.dig(METADATA_KEY)
105
+ end
106
+
107
+ def extract_strategy(payload)
108
+ payload&.dig(STRATEGY_KEY)&.to_sym
109
+ end
110
+
111
+ def uniqueness_config(active_job)
112
+ return nil unless active_job.class.respond_to?(:pgbus_uniqueness)
113
+
114
+ active_job.class.pgbus_uniqueness
115
+ end
116
+
117
+ # Acquire the uniqueness lock at enqueue time (:until_executed only).
118
+ # Lock state: queued, no owner_pid yet.
119
+ # Returns :acquired or :locked.
120
+ def acquire_enqueue_lock(key, active_job)
121
+ config = uniqueness_config(active_job)
122
+ return :no_lock unless config
123
+ return :no_lock unless config[:strategy] == :until_executed
124
+
125
+ acquired = JobLock.acquire!(
126
+ key,
127
+ job_class: active_job.class.name,
128
+ job_id: active_job.job_id,
129
+ state: "queued",
130
+ ttl: config[:lock_ttl]
131
+ )
132
+ acquired ? :acquired : :locked
133
+ end
134
+
135
+ # Transition a queued lock to executing state when the worker picks it up.
136
+ # Called for :until_executed jobs at execution start.
137
+ def claim_for_execution!(key, ttl:)
138
+ JobLock.claim_for_execution!(key, owner_pid: ::Process.pid, owner_hostname: Socket.gethostname, ttl: ttl)
139
+ end
140
+
141
+ # Acquire the uniqueness lock at execution time (:while_executing only).
142
+ # Lock state: executing with owner_pid.
143
+ # Returns true if acquired, false if another instance is running.
144
+ def acquire_execution_lock(key, payload)
145
+ strategy = extract_strategy(payload)
146
+ return true unless strategy == :while_executing
147
+
148
+ ttl = payload[TTL_KEY] || DEFAULT_LOCK_TTL
149
+
150
+ JobLock.acquire!(
151
+ key,
152
+ job_class: payload["job_class"],
153
+ job_id: payload["job_id"],
154
+ state: "executing",
155
+ owner_pid: ::Process.pid,
156
+ owner_hostname: Socket.gethostname,
157
+ ttl: ttl
158
+ )
159
+ end
160
+
161
+ # Release the uniqueness lock after execution completes.
162
+ def release_lock(key)
163
+ return unless key
164
+
165
+ JobLock.release!(key)
166
+ end
167
+ end
168
+ end
169
+ end
data/lib/pgbus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.7"
5
5
  end
@@ -497,6 +497,55 @@ module Pgbus
497
497
  []
498
498
  end
499
499
 
500
+ # Job locks
501
+ def job_locks
502
+ JobLock.order(locked_at: :desc).limit(100).map do |lock|
503
+ {
504
+ lock_key: lock.lock_key,
505
+ job_class: lock.job_class,
506
+ job_id: lock.job_id,
507
+ state: lock.state,
508
+ owner_pid: lock.owner_pid,
509
+ owner_hostname: lock.owner_hostname,
510
+ locked_at: lock.locked_at,
511
+ expires_at: lock.expires_at,
512
+ age_seconds: lock.locked_at ? (Time.current - lock.locked_at).to_i : nil
513
+ }
514
+ end
515
+ rescue StandardError => e
516
+ Pgbus.logger.debug { "[Pgbus::Web] Error fetching job locks: #{e.message}" }
517
+ []
518
+ end
519
+
520
+ # Job stats
521
+ def job_stats_summary(minutes: 60)
522
+ JobStat.summary(minutes: minutes)
523
+ rescue StandardError => e
524
+ Pgbus.logger.debug { "[Pgbus::Web] Error fetching job stats summary: #{e.message}" }
525
+ { total: 0, success: 0, failed: 0, dead_lettered: 0, avg_duration_ms: 0, max_duration_ms: 0 }
526
+ end
527
+
528
+ def job_throughput(minutes: 60)
529
+ JobStat.throughput(minutes: minutes).map { |time, count| { time: time, count: count } }
530
+ rescue StandardError => e
531
+ Pgbus.logger.debug { "[Pgbus::Web] Error fetching throughput: #{e.message}" }
532
+ []
533
+ end
534
+
535
+ def job_status_counts(minutes: 60)
536
+ JobStat.status_counts(minutes: minutes)
537
+ rescue StandardError => e
538
+ Pgbus.logger.debug { "[Pgbus::Web] Error fetching status counts: #{e.message}" }
539
+ {}
540
+ end
541
+
542
+ def slowest_job_classes(limit: 10, minutes: 60)
543
+ JobStat.slowest_classes(limit: limit, minutes: minutes)
544
+ rescue StandardError => e
545
+ Pgbus.logger.debug { "[Pgbus::Web] Error fetching slowest classes: #{e.message}" }
546
+ []
547
+ end
548
+
500
549
  # Subscriber registry
501
550
  def registered_subscribers
502
551
  EventBus::Registry.instance.subscribers.map do |s|
data/lib/pgbus.rb CHANGED
@@ -9,6 +9,7 @@ module Pgbus
9
9
  class QueueNotFoundError < Error; end
10
10
  class DeadLetterError < Error; end
11
11
  class ConcurrencyLimitExceeded < Error; end
12
+ class JobNotUnique < Error; end
12
13
  class SchemaNotReady < Error; end
13
14
 
14
15
  class << self
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.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -107,12 +107,15 @@ files:
107
107
  - LICENSE.txt
108
108
  - README.md
109
109
  - Rakefile
110
+ - app/controllers/pgbus/api/insights_controller.rb
110
111
  - app/controllers/pgbus/api/stats_controller.rb
111
112
  - app/controllers/pgbus/application_controller.rb
112
113
  - app/controllers/pgbus/dashboard_controller.rb
113
114
  - app/controllers/pgbus/dead_letter_controller.rb
114
115
  - app/controllers/pgbus/events_controller.rb
116
+ - app/controllers/pgbus/insights_controller.rb
115
117
  - app/controllers/pgbus/jobs_controller.rb
118
+ - app/controllers/pgbus/locks_controller.rb
116
119
  - app/controllers/pgbus/outbox_controller.rb
117
120
  - app/controllers/pgbus/processes_controller.rb
118
121
  - app/controllers/pgbus/queues_controller.rb
@@ -121,6 +124,8 @@ files:
121
124
  - app/models/pgbus/application_record.rb
122
125
  - app/models/pgbus/batch_entry.rb
123
126
  - app/models/pgbus/blocked_execution.rb
127
+ - app/models/pgbus/job_lock.rb
128
+ - app/models/pgbus/job_stat.rb
124
129
  - app/models/pgbus/outbox_entry.rb
125
130
  - app/models/pgbus/process_entry.rb
126
131
  - app/models/pgbus/processed_event.rb
@@ -139,10 +144,12 @@ files:
139
144
  - app/views/pgbus/dead_letter/show.html.erb
140
145
  - app/views/pgbus/events/index.html.erb
141
146
  - app/views/pgbus/events/show.html.erb
147
+ - app/views/pgbus/insights/show.html.erb
142
148
  - app/views/pgbus/jobs/_enqueued_table.html.erb
143
149
  - app/views/pgbus/jobs/_failed_table.html.erb
144
150
  - app/views/pgbus/jobs/index.html.erb
145
151
  - app/views/pgbus/jobs/show.html.erb
152
+ - app/views/pgbus/locks/index.html.erb
146
153
  - app/views/pgbus/outbox/index.html.erb
147
154
  - app/views/pgbus/processes/_processes_table.html.erb
148
155
  - app/views/pgbus/processes/index.html.erb
@@ -155,10 +162,14 @@ files:
155
162
  - config/routes.rb
156
163
  - exe/pgbus
157
164
  - lib/active_job/queue_adapters/pgbus_adapter.rb
165
+ - lib/generators/pgbus/add_job_locks_generator.rb
166
+ - lib/generators/pgbus/add_job_stats_generator.rb
158
167
  - lib/generators/pgbus/add_outbox_generator.rb
159
168
  - lib/generators/pgbus/add_queue_states_generator.rb
160
169
  - lib/generators/pgbus/add_recurring_generator.rb
161
170
  - lib/generators/pgbus/install_generator.rb
171
+ - lib/generators/pgbus/templates/add_job_locks.rb.erb
172
+ - lib/generators/pgbus/templates/add_job_stats.rb.erb
162
173
  - lib/generators/pgbus/templates/add_outbox.rb.erb
163
174
  - lib/generators/pgbus/templates/add_queue_states.rb.erb
164
175
  - lib/generators/pgbus/templates/add_recurring_tables.rb.erb
@@ -193,12 +204,16 @@ files:
193
204
  - lib/pgbus/pgmq_schema.rb
194
205
  - lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql
195
206
  - lib/pgbus/process/consumer.rb
207
+ - lib/pgbus/process/consumer_priority.rb
196
208
  - lib/pgbus/process/dispatcher.rb
197
209
  - lib/pgbus/process/heartbeat.rb
198
210
  - lib/pgbus/process/lifecycle.rb
211
+ - lib/pgbus/process/queue_lock.rb
199
212
  - lib/pgbus/process/signal_handler.rb
200
213
  - lib/pgbus/process/supervisor.rb
214
+ - lib/pgbus/process/wake_signal.rb
201
215
  - lib/pgbus/process/worker.rb
216
+ - lib/pgbus/queue_factory.rb
202
217
  - lib/pgbus/rate_counter.rb
203
218
  - lib/pgbus/recurring/already_recorded.rb
204
219
  - lib/pgbus/recurring/command_job.rb
@@ -207,6 +222,7 @@ files:
207
222
  - lib/pgbus/recurring/scheduler.rb
208
223
  - lib/pgbus/recurring/task.rb
209
224
  - lib/pgbus/serializer.rb
225
+ - lib/pgbus/uniqueness.rb
210
226
  - lib/pgbus/version.rb
211
227
  - lib/pgbus/web/authentication.rb
212
228
  - lib/pgbus/web/data_source.rb