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.
- checksums.yaml +4 -4
- data/app/controllers/pgbus/outbox_controller.rb +10 -0
- data/app/controllers/pgbus/queues_controller.rb +10 -0
- data/app/helpers/pgbus/application_helper.rb +6 -0
- data/app/models/pgbus/outbox_entry.rb +10 -0
- data/app/models/pgbus/queue_state.rb +33 -0
- data/app/views/layouts/pgbus/application.html.erb +1 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +7 -1
- data/app/views/pgbus/outbox/index.html.erb +55 -0
- data/app/views/pgbus/queues/_queues_list.html.erb +15 -1
- data/config/routes.rb +4 -0
- data/lib/generators/pgbus/add_outbox_generator.rb +52 -0
- data/lib/generators/pgbus/add_queue_states_generator.rb +51 -0
- data/lib/generators/pgbus/templates/add_outbox.rb.erb +25 -0
- data/lib/generators/pgbus/templates/add_queue_states.rb.erb +16 -0
- data/lib/pgbus/active_job/adapter.rb +6 -5
- data/lib/pgbus/active_job/executor.rb +22 -5
- data/lib/pgbus/circuit_breaker.rb +112 -0
- data/lib/pgbus/client.rb +140 -49
- data/lib/pgbus/configuration.rb +54 -2
- data/lib/pgbus/dedup_cache.rb +76 -0
- data/lib/pgbus/engine.rb +6 -0
- data/lib/pgbus/event_bus/handler.rb +13 -2
- data/lib/pgbus/outbox/poller.rb +117 -0
- data/lib/pgbus/outbox.rb +30 -0
- data/lib/pgbus/process/dispatcher.rb +46 -0
- data/lib/pgbus/process/heartbeat.rb +3 -1
- data/lib/pgbus/process/lifecycle.rb +111 -0
- data/lib/pgbus/process/supervisor.rb +40 -5
- data/lib/pgbus/process/worker.rb +86 -19
- data/lib/pgbus/rate_counter.rb +81 -0
- data/lib/pgbus/recurring/schedule.rb +1 -1
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +87 -2
- data/lib/pgbus.rb +35 -6
- data/lib/tasks/pgbus_pgmq.rake +5 -3
- metadata +15 -1
|
@@ -11,6 +11,8 @@ module Pgbus
|
|
|
11
11
|
CONCURRENCY_INTERVAL = 300 # Run concurrency cleanup every 5 minutes
|
|
12
12
|
BATCH_CLEANUP_INTERVAL = 3600 # Run batch cleanup every hour
|
|
13
13
|
RECURRING_CLEANUP_INTERVAL = 3600 # Run recurring execution cleanup every hour
|
|
14
|
+
ARCHIVE_COMPACTION_INTERVAL = 3600 # Run archive compaction every hour
|
|
15
|
+
OUTBOX_CLEANUP_INTERVAL = 3600 # Run outbox cleanup every hour
|
|
14
16
|
|
|
15
17
|
attr_reader :config
|
|
16
18
|
|
|
@@ -22,6 +24,8 @@ module Pgbus
|
|
|
22
24
|
@last_concurrency_at = Time.now
|
|
23
25
|
@last_batch_cleanup_at = Time.now
|
|
24
26
|
@last_recurring_cleanup_at = Time.now
|
|
27
|
+
@last_archive_compaction_at = Time.now
|
|
28
|
+
@last_outbox_cleanup_at = Time.now
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
def run
|
|
@@ -64,6 +68,8 @@ module Pgbus
|
|
|
64
68
|
run_if_due(now, :@last_concurrency_at, CONCURRENCY_INTERVAL) { cleanup_concurrency }
|
|
65
69
|
run_if_due(now, :@last_batch_cleanup_at, BATCH_CLEANUP_INTERVAL) { cleanup_batches }
|
|
66
70
|
run_if_due(now, :@last_recurring_cleanup_at, RECURRING_CLEANUP_INTERVAL) { cleanup_recurring_executions }
|
|
71
|
+
run_if_due(now, :@last_archive_compaction_at, archive_compaction_interval) { compact_archives }
|
|
72
|
+
run_if_due(now, :@last_outbox_cleanup_at, OUTBOX_CLEANUP_INTERVAL) { cleanup_outbox }
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
# Only update the timestamp when the block succeeds.
|
|
@@ -121,6 +127,46 @@ module Pgbus
|
|
|
121
127
|
Pgbus.logger.warn { "[Pgbus] Batch cleanup failed: #{e.message}" }
|
|
122
128
|
end
|
|
123
129
|
|
|
130
|
+
def cleanup_outbox
|
|
131
|
+
return unless config.outbox_enabled
|
|
132
|
+
|
|
133
|
+
retention = config.outbox_retention
|
|
134
|
+
return unless retention&.positive?
|
|
135
|
+
|
|
136
|
+
deleted = OutboxEntry.published_before(Time.now.utc - retention).delete_all
|
|
137
|
+
Pgbus.logger.debug { "[Pgbus] Cleaned up #{deleted} published outbox entries" } if deleted.positive?
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
Pgbus.logger.warn { "[Pgbus] Outbox cleanup failed: #{e.message}" }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def archive_compaction_interval
|
|
143
|
+
config.archive_compaction_interval || ARCHIVE_COMPACTION_INTERVAL
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def compact_archives
|
|
147
|
+
retention = config.archive_retention
|
|
148
|
+
return unless retention&.positive?
|
|
149
|
+
|
|
150
|
+
cutoff = Time.now.utc - retention
|
|
151
|
+
batch_size = config.archive_compaction_batch_size || 1000
|
|
152
|
+
prefix = config.queue_prefix
|
|
153
|
+
|
|
154
|
+
conn = config.connects_to ? Pgbus::ApplicationRecord.connection : ActiveRecord::Base.connection
|
|
155
|
+
queue_names = conn.select_values("SELECT queue_name FROM pgmq.meta ORDER BY queue_name")
|
|
156
|
+
|
|
157
|
+
queue_names.each do |full_name|
|
|
158
|
+
next unless full_name.start_with?("#{prefix}_")
|
|
159
|
+
|
|
160
|
+
stripped = full_name.delete_prefix("#{prefix}_")
|
|
161
|
+
deleted = Pgbus.client.purge_archive(stripped, older_than: cutoff, batch_size: batch_size)
|
|
162
|
+
Pgbus.logger.debug { "[Pgbus] Compacted #{deleted} archive entries from #{full_name}" } if deleted.positive?
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
Pgbus.logger.warn { "[Pgbus] Archive compaction failed for #{full_name}: #{e.message}" }
|
|
165
|
+
end
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
Pgbus.logger.warn { "[Pgbus] Archive compaction failed: #{e.message}" }
|
|
168
|
+
end
|
|
169
|
+
|
|
124
170
|
def cleanup_recurring_executions
|
|
125
171
|
retention = config.recurring_execution_retention
|
|
126
172
|
return unless retention&.positive?
|
|
@@ -11,9 +11,10 @@ module Pgbus
|
|
|
11
11
|
|
|
12
12
|
attr_reader :process_entry
|
|
13
13
|
|
|
14
|
-
def initialize(kind:, metadata: {})
|
|
14
|
+
def initialize(kind:, metadata: {}, on_beat: nil)
|
|
15
15
|
@kind = kind
|
|
16
16
|
@metadata = metadata
|
|
17
|
+
@on_beat = on_beat
|
|
17
18
|
@timer = nil
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -31,6 +32,7 @@ module Pgbus
|
|
|
31
32
|
def beat
|
|
32
33
|
return unless @process_id
|
|
33
34
|
|
|
35
|
+
@on_beat&.call
|
|
34
36
|
ProcessEntry.where(id: @process_id).update_all(last_heartbeat_at: Time.current)
|
|
35
37
|
rescue StandardError => e
|
|
36
38
|
Pgbus.logger.warn { "[Pgbus] Heartbeat failed: #{e.message}" }
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module Process
|
|
7
|
+
# Thread-safe worker lifecycle state machine inspired by LavinMQ's QueueState.
|
|
8
|
+
#
|
|
9
|
+
# States:
|
|
10
|
+
# :starting → initial state, setting up
|
|
11
|
+
# :running → actively processing messages
|
|
12
|
+
# :paused → temporarily stopped (manual or circuit breaker)
|
|
13
|
+
# :draining → finishing in-flight work before stopping
|
|
14
|
+
# :stopped → terminal state
|
|
15
|
+
#
|
|
16
|
+
# Transitions:
|
|
17
|
+
# starting → running
|
|
18
|
+
# running → paused | draining | stopped
|
|
19
|
+
# paused → running | draining | stopped
|
|
20
|
+
# draining → stopped
|
|
21
|
+
class Lifecycle
|
|
22
|
+
STATES = %i[starting running paused draining stopped].freeze
|
|
23
|
+
|
|
24
|
+
TRANSITIONS = {
|
|
25
|
+
starting: %i[running stopped],
|
|
26
|
+
running: %i[paused draining stopped],
|
|
27
|
+
paused: %i[running draining stopped],
|
|
28
|
+
draining: %i[stopped],
|
|
29
|
+
stopped: []
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
attr_reader :state
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
@state = :starting
|
|
36
|
+
@mutex = Mutex.new
|
|
37
|
+
@callbacks = Hash.new { |h, k| h[k] = [] }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def transition_to!(new_state)
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
validate_transition!(new_state)
|
|
43
|
+
old_state = @state
|
|
44
|
+
@state = new_state
|
|
45
|
+
fire_callbacks(old_state, new_state)
|
|
46
|
+
new_state
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def transition_to(new_state)
|
|
51
|
+
transition_to!(new_state)
|
|
52
|
+
rescue InvalidTransition
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def on(event, &block)
|
|
57
|
+
@callbacks[event] << block
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def starting?
|
|
61
|
+
@state == :starting
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def running?
|
|
65
|
+
@state == :running
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def paused?
|
|
69
|
+
@state == :paused
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def draining?
|
|
73
|
+
@state == :draining
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def stopped?
|
|
77
|
+
@state == :stopped
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def active?
|
|
81
|
+
running? || paused?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def can_process?
|
|
85
|
+
running?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def terminal?
|
|
89
|
+
stopped?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def validate_transition!(new_state)
|
|
95
|
+
raise ArgumentError, "Unknown state: #{new_state}. Valid states: #{STATES.join(", ")}" unless STATES.include?(new_state)
|
|
96
|
+
|
|
97
|
+
return if TRANSITIONS[@state].include?(new_state)
|
|
98
|
+
|
|
99
|
+
raise InvalidTransition, "Cannot transition from #{@state} to #{new_state}. " \
|
|
100
|
+
"Valid transitions: #{TRANSITIONS[@state].join(", ")}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fire_callbacks(old_state, new_state)
|
|
104
|
+
@callbacks[:"#{old_state}_to_#{new_state}"].each(&:call)
|
|
105
|
+
@callbacks[:any].each { |cb| cb.call(old_state, new_state) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class InvalidTransition < Pgbus::Error; end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -55,6 +55,9 @@ module Pgbus
|
|
|
55
55
|
|
|
56
56
|
# Boot event consumers if configured
|
|
57
57
|
boot_consumers
|
|
58
|
+
|
|
59
|
+
# Boot outbox poller if configured
|
|
60
|
+
boot_outbox_poller
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
def fork_worker(worker_config)
|
|
@@ -63,7 +66,7 @@ module Pgbus
|
|
|
63
66
|
|
|
64
67
|
pid = fork do
|
|
65
68
|
restore_signals
|
|
66
|
-
|
|
69
|
+
setup_child_process
|
|
67
70
|
load_rails_app
|
|
68
71
|
worker = Worker.new(queues: queues, threads: threads, config: config)
|
|
69
72
|
worker.run
|
|
@@ -83,7 +86,7 @@ module Pgbus
|
|
|
83
86
|
def fork_dispatcher
|
|
84
87
|
pid = fork do
|
|
85
88
|
restore_signals
|
|
86
|
-
|
|
89
|
+
setup_child_process
|
|
87
90
|
load_rails_app
|
|
88
91
|
dispatcher = Dispatcher.new(config: config)
|
|
89
92
|
dispatcher.run
|
|
@@ -110,7 +113,7 @@ module Pgbus
|
|
|
110
113
|
def fork_scheduler
|
|
111
114
|
pid = fork do
|
|
112
115
|
restore_signals
|
|
113
|
-
|
|
116
|
+
setup_child_process
|
|
114
117
|
load_rails_app
|
|
115
118
|
load_recurring_config
|
|
116
119
|
scheduler = Recurring::Scheduler.new(config: config)
|
|
@@ -165,7 +168,7 @@ module Pgbus
|
|
|
165
168
|
|
|
166
169
|
pid = fork do
|
|
167
170
|
restore_signals
|
|
168
|
-
|
|
171
|
+
setup_child_process
|
|
169
172
|
load_rails_app
|
|
170
173
|
consumer = Consumer.new(topics: topics, threads: threads, config: config)
|
|
171
174
|
consumer.run
|
|
@@ -182,6 +185,32 @@ module Pgbus
|
|
|
182
185
|
Pgbus.logger.error { "[Pgbus] Fork failed for consumer: #{e.message}" }
|
|
183
186
|
end
|
|
184
187
|
|
|
188
|
+
def boot_outbox_poller
|
|
189
|
+
return unless config.outbox_enabled
|
|
190
|
+
|
|
191
|
+
fork_outbox_poller
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def fork_outbox_poller
|
|
195
|
+
pid = fork do
|
|
196
|
+
restore_signals
|
|
197
|
+
setup_child_process
|
|
198
|
+
load_rails_app
|
|
199
|
+
poller = Outbox::Poller.new(config: config)
|
|
200
|
+
poller.run
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
unless pid
|
|
204
|
+
Pgbus.logger.error { "[Pgbus] Failed to fork outbox poller" }
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@forks[pid] = { type: :outbox_poller }
|
|
209
|
+
Pgbus.logger.info { "[Pgbus] Forked outbox poller pid=#{pid}" }
|
|
210
|
+
rescue Errno::EAGAIN, Errno::ENOMEM => e
|
|
211
|
+
Pgbus.logger.error { "[Pgbus] Fork failed for outbox poller: #{e.message}" }
|
|
212
|
+
end
|
|
213
|
+
|
|
185
214
|
def monitor_loop
|
|
186
215
|
loop do
|
|
187
216
|
break if @shutting_down && @forks.empty?
|
|
@@ -223,6 +252,8 @@ module Pgbus
|
|
|
223
252
|
fork_scheduler
|
|
224
253
|
when :consumer
|
|
225
254
|
fork_consumer(info[:config])
|
|
255
|
+
when :outbox_poller
|
|
256
|
+
fork_outbox_poller
|
|
226
257
|
end
|
|
227
258
|
end
|
|
228
259
|
|
|
@@ -234,7 +265,11 @@ module Pgbus
|
|
|
234
265
|
end
|
|
235
266
|
end
|
|
236
267
|
|
|
237
|
-
def
|
|
268
|
+
def setup_child_process
|
|
269
|
+
# Reset the PGMQ client so this forked process gets a fresh
|
|
270
|
+
# PG::Connection instead of inheriting the parent's (which is
|
|
271
|
+
# in undefined state post-fork and not thread-safe to share).
|
|
272
|
+
Pgbus.reset_client!
|
|
238
273
|
%w[INT TERM QUIT].each do |sig|
|
|
239
274
|
trap(sig) { @shutting_down = true }
|
|
240
275
|
end
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -13,31 +13,44 @@ module Pgbus
|
|
|
13
13
|
@queues = Array(queues)
|
|
14
14
|
@threads = threads
|
|
15
15
|
@config = config
|
|
16
|
-
@
|
|
16
|
+
@lifecycle = Lifecycle.new
|
|
17
17
|
@jobs_processed = Concurrent::AtomicFixnum.new(0)
|
|
18
18
|
@jobs_failed = Concurrent::AtomicFixnum.new(0)
|
|
19
|
+
@in_flight = Concurrent::AtomicFixnum.new(0)
|
|
20
|
+
@rate_counter = RateCounter.new(:processed, :failed, :dequeued)
|
|
19
21
|
@started_at = Time.now
|
|
20
22
|
@executor = Pgbus::ActiveJob::Executor.new
|
|
21
23
|
@pool = Concurrent::FixedThreadPool.new(threads)
|
|
24
|
+
@circuit_breaker = Pgbus::CircuitBreaker.new(config: config)
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def stats
|
|
25
|
-
{
|
|
28
|
+
{
|
|
29
|
+
jobs_processed: @jobs_processed.value,
|
|
30
|
+
jobs_failed: @jobs_failed.value,
|
|
31
|
+
in_flight: @in_flight.value,
|
|
32
|
+
state: @lifecycle.state,
|
|
33
|
+
rates: @rate_counter.rates,
|
|
34
|
+
started_at: @started_at
|
|
35
|
+
}
|
|
26
36
|
end
|
|
27
37
|
|
|
28
38
|
def run
|
|
29
39
|
setup_signals
|
|
30
40
|
start_heartbeat
|
|
31
41
|
resolve_wildcard_queues
|
|
42
|
+
@lifecycle.transition_to!(:running)
|
|
32
43
|
Pgbus.logger.info { "[Pgbus] Worker started: queues=#{queues.join(",")} threads=#{threads} pid=#{::Process.pid}" }
|
|
33
44
|
|
|
34
45
|
loop do
|
|
35
46
|
process_signals
|
|
36
|
-
|
|
37
|
-
break if recycle_needed? && @pool.queue_length.zero?
|
|
47
|
+
check_recycle
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
break if @lifecycle.stopped?
|
|
50
|
+
break if @lifecycle.draining? && @pool.queue_length.zero?
|
|
51
|
+
|
|
52
|
+
claim_and_execute if @lifecycle.can_process?
|
|
53
|
+
interruptible_sleep(config.polling_interval) if @lifecycle.draining? || @lifecycle.paused?
|
|
41
54
|
end
|
|
42
55
|
|
|
43
56
|
shutdown
|
|
@@ -45,12 +58,12 @@ module Pgbus
|
|
|
45
58
|
|
|
46
59
|
def graceful_shutdown
|
|
47
60
|
Pgbus.logger.info { "[Pgbus] Worker shutting down gracefully..." }
|
|
48
|
-
@
|
|
61
|
+
@lifecycle.transition_to(:draining)
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
def immediate_shutdown
|
|
52
65
|
Pgbus.logger.warn { "[Pgbus] Worker shutting down immediately!" }
|
|
53
|
-
@
|
|
66
|
+
@lifecycle.transition_to!(:stopped)
|
|
54
67
|
@pool.kill
|
|
55
68
|
end
|
|
56
69
|
|
|
@@ -60,6 +73,13 @@ module Pgbus
|
|
|
60
73
|
idle = @pool.max_length - @pool.queue_length
|
|
61
74
|
return interruptible_sleep(config.polling_interval) if idle <= 0
|
|
62
75
|
|
|
76
|
+
if config.prefetch_limit
|
|
77
|
+
available = config.prefetch_limit - @in_flight.value
|
|
78
|
+
return interruptible_sleep(config.polling_interval) if available <= 0
|
|
79
|
+
|
|
80
|
+
idle = [idle, available].min
|
|
81
|
+
end
|
|
82
|
+
|
|
63
83
|
tagged_messages = fetch_messages(idle)
|
|
64
84
|
|
|
65
85
|
if tagged_messages.empty?
|
|
@@ -67,21 +87,28 @@ module Pgbus
|
|
|
67
87
|
return
|
|
68
88
|
end
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
@rate_counter.increment(:dequeued, tagged_messages.size)
|
|
91
|
+
tagged_messages.each do |queue_name, message, source_queue|
|
|
92
|
+
@in_flight.increment
|
|
93
|
+
@pool.post { process_message(message, queue_name, source_queue: source_queue) }
|
|
72
94
|
end
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
# Returns an array of [queue_name, message] pairs so we always know
|
|
76
98
|
# which queue each message came from (PGMQ messages don't carry this).
|
|
77
99
|
def fetch_messages(qty)
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
active_queues = queues.reject { |q| @circuit_breaker.paused?(q) }
|
|
101
|
+
return [] if active_queues.empty?
|
|
102
|
+
|
|
103
|
+
if priority_enabled?
|
|
104
|
+
fetch_prioritized(active_queues, qty)
|
|
105
|
+
elsif active_queues.size == 1
|
|
106
|
+
queue = active_queues.first
|
|
80
107
|
messages = Pgbus.client.read_batch(queue, qty: qty) || []
|
|
81
108
|
messages.map { |m| [queue, m] }
|
|
82
109
|
else
|
|
83
|
-
per_queue = [(qty /
|
|
84
|
-
|
|
110
|
+
per_queue = [(qty / active_queues.size.to_f).ceil, 1].max
|
|
111
|
+
active_queues.flat_map do |q|
|
|
85
112
|
(Pgbus.client.read_batch(q, qty: per_queue) || []).map { |m| [q, m] }
|
|
86
113
|
end.first(qty)
|
|
87
114
|
end
|
|
@@ -90,13 +117,45 @@ module Pgbus
|
|
|
90
117
|
[]
|
|
91
118
|
end
|
|
92
119
|
|
|
93
|
-
def
|
|
94
|
-
|
|
120
|
+
def fetch_prioritized(active_queues, qty)
|
|
121
|
+
remaining = qty
|
|
122
|
+
results = []
|
|
123
|
+
|
|
124
|
+
active_queues.each do |q|
|
|
125
|
+
break if remaining <= 0
|
|
126
|
+
|
|
127
|
+
batch = Pgbus.client.read_batch_prioritized(q, qty: remaining)
|
|
128
|
+
batch.each do |physical_queue, message|
|
|
129
|
+
results << [q, message, physical_queue]
|
|
130
|
+
end
|
|
131
|
+
remaining -= batch.size
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
results
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def priority_enabled?
|
|
138
|
+
config.priority_levels && config.priority_levels > 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def process_message(message, queue_name, source_queue: nil)
|
|
142
|
+
result = @executor.execute(message, queue_name, source_queue: source_queue)
|
|
95
143
|
@jobs_processed.increment
|
|
96
|
-
@
|
|
144
|
+
@rate_counter.increment(:processed)
|
|
145
|
+
if result == :failed
|
|
146
|
+
@jobs_failed.increment
|
|
147
|
+
@rate_counter.increment(:failed)
|
|
148
|
+
@circuit_breaker.record_failure(queue_name)
|
|
149
|
+
else
|
|
150
|
+
@circuit_breaker.record_success(queue_name)
|
|
151
|
+
end
|
|
97
152
|
rescue StandardError => e
|
|
98
153
|
@jobs_failed.increment
|
|
154
|
+
@rate_counter.increment(:failed)
|
|
155
|
+
@circuit_breaker.record_failure(queue_name)
|
|
99
156
|
Pgbus.logger.error { "[Pgbus] Unhandled error processing message: #{e.message}" }
|
|
157
|
+
ensure
|
|
158
|
+
@in_flight.decrement
|
|
100
159
|
end
|
|
101
160
|
|
|
102
161
|
# Resolve "*" to all non-DLQ queues from pgmq.meta, stripping the prefix.
|
|
@@ -107,7 +166,8 @@ module Pgbus
|
|
|
107
166
|
dlq_suffix = config.dead_letter_queue_suffix
|
|
108
167
|
prefix = "#{config.queue_prefix}_"
|
|
109
168
|
|
|
110
|
-
|
|
169
|
+
conn = Pgbus.configuration.connects_to ? Pgbus::ApplicationRecord.connection : ActiveRecord::Base.connection
|
|
170
|
+
all_queues = conn.select_values("SELECT queue_name FROM pgmq.meta ORDER BY queue_name")
|
|
111
171
|
resolved = all_queues
|
|
112
172
|
.reject { |q| q.end_with?(dlq_suffix) }
|
|
113
173
|
.map { |q| q.delete_prefix(prefix) }
|
|
@@ -124,6 +184,12 @@ module Pgbus
|
|
|
124
184
|
@queues = [config.default_queue]
|
|
125
185
|
end
|
|
126
186
|
|
|
187
|
+
def check_recycle
|
|
188
|
+
return unless @lifecycle.running? && recycle_needed?
|
|
189
|
+
|
|
190
|
+
@lifecycle.transition_to(:draining)
|
|
191
|
+
end
|
|
192
|
+
|
|
127
193
|
def recycle_needed?
|
|
128
194
|
exceeded_max_jobs? || exceeded_max_memory? || exceeded_max_lifetime?
|
|
129
195
|
end
|
|
@@ -164,7 +230,8 @@ module Pgbus
|
|
|
164
230
|
def start_heartbeat
|
|
165
231
|
@heartbeat = Heartbeat.new(
|
|
166
232
|
kind: "worker",
|
|
167
|
-
metadata: { queues: queues, threads: threads, pid: ::Process.pid }
|
|
233
|
+
metadata: { queues: queues, threads: threads, pid: ::Process.pid },
|
|
234
|
+
on_beat: -> { @rate_counter.snapshot! }
|
|
168
235
|
)
|
|
169
236
|
@heartbeat.start
|
|
170
237
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
# Thread-safe rate counter inspired by LavinMQ's rate_stats macro.
|
|
7
|
+
# Tracks absolute counts via AtomicFixnum and computes rates as
|
|
8
|
+
# deltas between periodic snapshots.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# counter = RateCounter.new(:enqueued, :dequeued, :failed)
|
|
12
|
+
# counter.increment(:enqueued)
|
|
13
|
+
# counter.snapshot! # call periodically (e.g. every 5s)
|
|
14
|
+
# counter.rate(:enqueued) # => msgs/s since last snapshot
|
|
15
|
+
# counter.rates # => { enqueued: 12.4, dequeued: 10.1, failed: 0.2 }
|
|
16
|
+
class RateCounter
|
|
17
|
+
attr_reader :names
|
|
18
|
+
|
|
19
|
+
def initialize(*names)
|
|
20
|
+
@names = names.map(&:to_sym)
|
|
21
|
+
@counters = {}
|
|
22
|
+
@last_values = {}
|
|
23
|
+
@rates = {}
|
|
24
|
+
@last_snapshot_at = monotonic_now
|
|
25
|
+
|
|
26
|
+
@names.each do |name|
|
|
27
|
+
@counters[name] = Concurrent::AtomicFixnum.new(0)
|
|
28
|
+
@last_values[name] = 0
|
|
29
|
+
@rates[name] = 0.0
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def increment(name, delta = 1)
|
|
34
|
+
@counters.fetch(name).increment(delta)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def count(name)
|
|
38
|
+
@counters.fetch(name).value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rate(name)
|
|
42
|
+
@rates.fetch(name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def rates
|
|
46
|
+
@names.to_h { |name| [name, @rates[name]] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def counts
|
|
50
|
+
@names.to_h { |name| [name, @counters[name].value] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def snapshot!
|
|
54
|
+
now = monotonic_now
|
|
55
|
+
elapsed = now - @last_snapshot_at
|
|
56
|
+
return if elapsed <= 0
|
|
57
|
+
|
|
58
|
+
@names.each do |name|
|
|
59
|
+
current = @counters[name].value
|
|
60
|
+
delta = current - @last_values[name]
|
|
61
|
+
@rates[name] = (delta / elapsed).round(1)
|
|
62
|
+
@last_values[name] = current
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@last_snapshot_at = now
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_h
|
|
69
|
+
{
|
|
70
|
+
counts: counts,
|
|
71
|
+
rates: rates
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def monotonic_now
|
|
78
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/pgbus/version.rb
CHANGED