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.
- checksums.yaml +4 -4
- data/README.md +326 -11
- data/app/controllers/pgbus/api/insights_controller.rb +16 -0
- data/app/controllers/pgbus/insights_controller.rb +10 -0
- data/app/controllers/pgbus/locks_controller.rb +9 -0
- data/app/helpers/pgbus/application_helper.rb +28 -0
- data/app/models/pgbus/job_lock.rb +82 -0
- data/app/models/pgbus/job_stat.rb +94 -0
- data/app/views/layouts/pgbus/application.html.erb +31 -8
- data/app/views/pgbus/dashboard/_processes_table.html.erb +6 -6
- data/app/views/pgbus/dashboard/_queues_table.html.erb +6 -6
- data/app/views/pgbus/dashboard/_recent_failures.html.erb +5 -5
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +20 -20
- data/app/views/pgbus/dashboard/show.html.erb +1 -1
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +12 -12
- data/app/views/pgbus/dead_letter/index.html.erb +1 -1
- data/app/views/pgbus/dead_letter/show.html.erb +10 -10
- data/app/views/pgbus/events/index.html.erb +15 -15
- data/app/views/pgbus/events/show.html.erb +5 -5
- data/app/views/pgbus/insights/show.html.erb +161 -0
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +13 -13
- data/app/views/pgbus/jobs/_failed_table.html.erb +7 -7
- data/app/views/pgbus/jobs/index.html.erb +1 -1
- data/app/views/pgbus/jobs/show.html.erb +10 -10
- data/app/views/pgbus/locks/index.html.erb +53 -0
- data/app/views/pgbus/outbox/index.html.erb +12 -12
- data/app/views/pgbus/processes/_processes_table.html.erb +6 -6
- data/app/views/pgbus/processes/index.html.erb +1 -1
- data/app/views/pgbus/queues/_queues_list.html.erb +5 -5
- data/app/views/pgbus/queues/index.html.erb +1 -1
- data/app/views/pgbus/queues/show.html.erb +7 -7
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +6 -6
- data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
- data/app/views/pgbus/recurring_tasks/show.html.erb +22 -22
- data/config/routes.rb +3 -0
- data/lib/generators/pgbus/add_job_locks_generator.rb +52 -0
- data/lib/generators/pgbus/add_job_stats_generator.rb +52 -0
- data/lib/generators/pgbus/add_outbox_generator.rb +1 -1
- data/lib/generators/pgbus/add_queue_states_generator.rb +1 -1
- data/lib/generators/pgbus/add_recurring_generator.rb +1 -1
- data/lib/generators/pgbus/install_generator.rb +1 -1
- data/lib/generators/pgbus/templates/add_job_locks.rb.erb +21 -0
- data/lib/generators/pgbus/templates/add_job_stats.rb.erb +18 -0
- data/lib/generators/pgbus/upgrade_pgmq_generator.rb +1 -1
- data/lib/pgbus/active_job/adapter.rb +58 -4
- data/lib/pgbus/active_job/executor.rb +45 -0
- data/lib/pgbus/client.rb +8 -22
- data/lib/pgbus/configuration.rb +6 -0
- data/lib/pgbus/engine.rb +1 -0
- data/lib/pgbus/process/consumer_priority.rb +64 -0
- data/lib/pgbus/process/dispatcher.rb +29 -0
- data/lib/pgbus/process/queue_lock.rb +87 -0
- data/lib/pgbus/process/supervisor.rb +6 -1
- data/lib/pgbus/process/wake_signal.rb +53 -0
- data/lib/pgbus/process/worker.rb +36 -6
- data/lib/pgbus/queue_factory.rb +62 -0
- data/lib/pgbus/uniqueness.rb +169 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +49 -0
- data/lib/pgbus.rb +1 -0
- 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
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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
|
@@ -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
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.
|
|
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
|