pgbus 0.16.7 → 0.17.1
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/CHANGELOG.md +14 -0
- data/README.md +21 -1
- data/Rakefile +10 -169
- data/lib/generators/pgbus/templates/upgrade_pgmq.rb.erb +22 -0
- data/lib/pgbus/active_job/executor.rb +20 -11
- data/lib/pgbus/client.rb +3 -0
- data/lib/pgbus/configuration.rb +46 -0
- data/lib/pgbus/doctor.rb +29 -1
- data/lib/pgbus/failed_event_recorder.rb +17 -0
- data/lib/pgbus/pgmq_schema/fixups/pgmq_v1.13.0.sql +49 -0
- data/lib/pgbus/pgmq_schema/pgmq_v1.13.0.sql +2193 -0
- data/lib/pgbus/pgmq_schema.rb +41 -3
- data/lib/pgbus/process/claim_buffer.rb +115 -0
- data/lib/pgbus/process/consumer.rb +41 -14
- data/lib/pgbus/process/supervisor.rb +3 -1
- data/lib/pgbus/process/worker.rb +48 -19
- data/lib/pgbus/ruby_jit.rb +21 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/visibility_heartbeat.rb +38 -3
- metadata +5 -1
data/lib/pgbus/pgmq_schema.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Pgbus
|
|
|
14
14
|
class VersionNotFoundError < Pgbus::Error; end
|
|
15
15
|
|
|
16
16
|
SCHEMA_DIR = File.expand_path("pgmq_schema", __dir__).freeze
|
|
17
|
+
FIXUPS_DIR = File.join(SCHEMA_DIR, "fixups").freeze
|
|
17
18
|
|
|
18
19
|
class << self
|
|
19
20
|
# Returns the latest vendored PGMQ version string.
|
|
@@ -23,9 +24,37 @@ module Pgbus
|
|
|
23
24
|
|
|
24
25
|
# Returns sorted list of all vendored PGMQ versions.
|
|
25
26
|
def available_versions
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
versions_in(SCHEMA_DIR)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Versions that ship a table fixup, in version order.
|
|
31
|
+
#
|
|
32
|
+
# An upgrade drops every pgmq function and composite type and re-runs the
|
|
33
|
+
# target version's schema, which recreates both — but never touches an
|
|
34
|
+
# existing table. An upstream hop that ALTERs one (1.13.0 moves
|
|
35
|
+
# partitioned queues' msg_id from GENERATED ALWAYS to BY DEFAULT) needs
|
|
36
|
+
# its own step, and that is what these files are.
|
|
37
|
+
def fixup_versions
|
|
38
|
+
versions_in(FIXUPS_DIR)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Concatenated fixups for every version in (after, upto], in version
|
|
42
|
+
# order. Returns "" when the hop crosses none.
|
|
43
|
+
#
|
|
44
|
+
# @param after [String, nil] the installed version; nil (no recorded
|
|
45
|
+
# version) applies every fixup up to the target, which is safe because
|
|
46
|
+
# each one is idempotent.
|
|
47
|
+
# @param upto [String] the version being upgraded to
|
|
48
|
+
def fixups_sql(after:, upto: latest_version)
|
|
49
|
+
ceiling = Gem::Version.new(upto)
|
|
50
|
+
floor = after && Gem::Version.new(after)
|
|
51
|
+
|
|
52
|
+
applicable = fixup_versions.select do |version|
|
|
53
|
+
candidate = Gem::Version.new(version)
|
|
54
|
+
candidate <= ceiling && (floor.nil? || candidate > floor)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
applicable.map { |version| File.read(File.join(FIXUPS_DIR, "pgmq_v#{version}.sql")) }.join("\n")
|
|
29
58
|
end
|
|
30
59
|
|
|
31
60
|
# Returns the filesystem path to the vendored SQL file for a given version.
|
|
@@ -177,6 +206,15 @@ module Pgbus
|
|
|
177
206
|
|
|
178
207
|
private
|
|
179
208
|
|
|
209
|
+
# Vendored versions discovered in a directory, in version order. Schema
|
|
210
|
+
# files and fixups share this so the two can never drift into different
|
|
211
|
+
# discovery rules.
|
|
212
|
+
def versions_in(dir)
|
|
213
|
+
Dir.glob(File.join(dir, "pgmq_v*.sql"))
|
|
214
|
+
.map { |f| File.basename(f).match(/pgmq_v(.+)\.sql/)[1] }
|
|
215
|
+
.sort_by { |v| Gem::Version.new(v) }
|
|
216
|
+
end
|
|
217
|
+
|
|
180
218
|
# Strips extension-specific blocks (pg_extension_config_dump, pg_depend checks)
|
|
181
219
|
# that only work when pgmq is installed as an extension.
|
|
182
220
|
def strip_extension_only_blocks(sql)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Process
|
|
5
|
+
# The read-ahead buffer a Worker or Consumer owns (issue #486).
|
|
6
|
+
#
|
|
7
|
+
# Under database latency the claim loop used to issue one read round trip
|
|
8
|
+
# per job: once the pool is full, slots free one at a time and each loop
|
|
9
|
+
# step read `qty = free slots` = 1. With `read_ahead = N` the loop claims
|
|
10
|
+
# up to N messages beyond its free slots and parks them here, so one read
|
|
11
|
+
# returns a batch and the pool never waits on the reader.
|
|
12
|
+
#
|
|
13
|
+
# Surplus claims cannot live inside the execution pool: AsyncPool#post
|
|
14
|
+
# raises at zero capacity and a queued block could not be handed back on
|
|
15
|
+
# drain. So the buffer is drained into the pool only while it has a free
|
|
16
|
+
# slot, and returned to the queue (vt: 0) on drain, recycle or shutdown.
|
|
17
|
+
#
|
|
18
|
+
# Every buffered message is kept invisible by a VisibilityHeartbeat hold
|
|
19
|
+
# from claim until the run starts, under the executor's key
|
|
20
|
+
# (`source_queue || queue_name`, `prefixed: source_queue.nil?`), so the
|
|
21
|
+
# run's own tracking takes over the same entry with no gap. The hold does
|
|
22
|
+
# not know the job class, so a job that opted out with
|
|
23
|
+
# `pgbus_visibility_heartbeat false` is still held while it waits here:
|
|
24
|
+
# the opt-out covers its run, not the wait. With the heartbeat disabled
|
|
25
|
+
# globally there is no hold at all (Configuration#validate! warns).
|
|
26
|
+
#
|
|
27
|
+
# Only ever touched from the owning process's loop thread, so no mutex.
|
|
28
|
+
class ClaimBuffer
|
|
29
|
+
Claim = Struct.new(:queue_name, :message, :source_queue, :hold)
|
|
30
|
+
|
|
31
|
+
HOLD_LABEL = "(read-ahead)"
|
|
32
|
+
|
|
33
|
+
def initialize(config: Pgbus.configuration)
|
|
34
|
+
@config = config
|
|
35
|
+
@claims = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def size
|
|
39
|
+
@claims.size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def empty?
|
|
43
|
+
@claims.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def push(queue_name, message, source_queue = nil, client: Pgbus.client)
|
|
47
|
+
hold = VisibilityHeartbeat.hold(
|
|
48
|
+
client: client, queue_name: source_queue || queue_name, prefixed: source_queue.nil?,
|
|
49
|
+
msg_id: message.msg_id, job_class: HOLD_LABEL, config: @config
|
|
50
|
+
)
|
|
51
|
+
@claims << Claim.new(queue_name, message, source_queue, hold)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def shift
|
|
56
|
+
@claims.shift
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Yields the oldest claims, one per free pool slot, and returns how many.
|
|
60
|
+
# Capacity is re-read before every claim: the block's post takes a slot.
|
|
61
|
+
def drain_into(pool)
|
|
62
|
+
drained = 0
|
|
63
|
+
while !@claims.empty? && pool.available_capacity.positive?
|
|
64
|
+
yield @claims.shift
|
|
65
|
+
drained += 1
|
|
66
|
+
end
|
|
67
|
+
drained
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# How many messages the next read should claim: enough to fill the free
|
|
71
|
+
# slots and top the buffer back up to `read_ahead`, never more than
|
|
72
|
+
# `prefetch_room` (prefetch_limit minus everything already claimed).
|
|
73
|
+
# With read_ahead 0 and an empty buffer this is `free_slots`: the
|
|
74
|
+
# pre-#486 qty.
|
|
75
|
+
def deficit(free_slots:, read_ahead:, prefetch_room: nil)
|
|
76
|
+
want = free_slots + read_ahead - @claims.size
|
|
77
|
+
want = [want, prefetch_room].min if prefetch_room
|
|
78
|
+
want.clamp(0..)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Hand every buffered message back to the queue right away instead of
|
|
82
|
+
# leaving it invisible until its timeout. Their read_ct has already been
|
|
83
|
+
# bumped, the same cost a crash or a stale claim pays. A failed return
|
|
84
|
+
# is logged and skipped: the hold is already dropped, so the message
|
|
85
|
+
# reappears when its current timeout runs out. Returns the count. One
|
|
86
|
+
# claim at a time, so #size stays true while the returns are written.
|
|
87
|
+
def return_all!(client: Pgbus.client)
|
|
88
|
+
returned = 0
|
|
89
|
+
while (claim = @claims.shift)
|
|
90
|
+
return_claim(claim, client)
|
|
91
|
+
returned += 1
|
|
92
|
+
end
|
|
93
|
+
returned
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# The hold goes first: a heartbeat tick that still held the entry could
|
|
99
|
+
# otherwise write its extension after the vt: 0 and hide the message
|
|
100
|
+
# again for a full visibility_timeout. (VisibilityHeartbeat#extend!
|
|
101
|
+
# re-checks registration, which shrinks the remaining window to the
|
|
102
|
+
# tick's own check-then-write.)
|
|
103
|
+
def return_claim(claim, client)
|
|
104
|
+
VisibilityHeartbeat.release(claim.hold)
|
|
105
|
+
queue = claim.source_queue || claim.queue_name
|
|
106
|
+
client.set_visibility_timeout(queue, claim.message.msg_id.to_i, vt: 0, prefixed: claim.source_queue.nil?)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
Pgbus.logger.warn do
|
|
109
|
+
"[Pgbus] Could not return read-ahead message msg_id=#{claim.message.msg_id} queue=#{queue}: " \
|
|
110
|
+
"#{e.class}: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -7,7 +7,7 @@ module Pgbus
|
|
|
7
7
|
class Consumer
|
|
8
8
|
include SignalHandler
|
|
9
9
|
|
|
10
|
-
attr_reader :topics, :threads, :config, :execution_mode,
|
|
10
|
+
attr_reader :topics, :threads, :config, :execution_mode, :read_ahead,
|
|
11
11
|
:queue_names, :wake_signal, :notify_retry_backoff, :circuit_breaker
|
|
12
12
|
# notify_listener is writable so tests can simulate a start_notify_listener
|
|
13
13
|
# success from inside a stub (production sets it in start_notify_listener).
|
|
@@ -30,6 +30,11 @@ module Pgbus
|
|
|
30
30
|
@jobs_processed.value
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Messages claimed by read-ahead and not yet handed to the pool (#486).
|
|
34
|
+
def buffered
|
|
35
|
+
@claim_buffer.size
|
|
36
|
+
end
|
|
37
|
+
|
|
33
38
|
# Seed the processed-job counter. Used by tests to drive the recycle
|
|
34
39
|
# thresholds without running thousands of real jobs; production only ever
|
|
35
40
|
# increments it via the AtomicFixnum during message handling.
|
|
@@ -60,7 +65,7 @@ module Pgbus
|
|
|
60
65
|
queue_names: nil, liveness_pipe: nil, stat_buffer: :default,
|
|
61
66
|
notify_listener: nil, notify_retry_at: 0.0,
|
|
62
67
|
notify_retry_backoff: NOTIFY_RETRY_BASE_SECONDS,
|
|
63
|
-
started_at_monotonic: nil, wake_pipe: nil)
|
|
68
|
+
started_at_monotonic: nil, wake_pipe: nil, read_ahead: nil)
|
|
64
69
|
@topics = Array(topics)
|
|
65
70
|
@threads = threads
|
|
66
71
|
@config = config
|
|
@@ -68,6 +73,8 @@ module Pgbus
|
|
|
68
73
|
@shutting_down = false
|
|
69
74
|
@recycling = false
|
|
70
75
|
@jobs_processed = Concurrent::AtomicFixnum.new(0)
|
|
76
|
+
@read_ahead = read_ahead || config.read_ahead
|
|
77
|
+
@claim_buffer = ClaimBuffer.new(config: config)
|
|
71
78
|
@loop_tick_at = Concurrent::AtomicReference.new(nil)
|
|
72
79
|
@started_at_monotonic = started_at_monotonic || monotonic_now
|
|
73
80
|
@wake_signal = WakeSignal.new
|
|
@@ -134,7 +141,12 @@ module Pgbus
|
|
|
134
141
|
check_recycle
|
|
135
142
|
ensure_notify_listener
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
if @shutting_down
|
|
145
|
+
# No drain loop here: buffered claims go straight back to the
|
|
146
|
+
# queue; shutdown then waits for the ones already running.
|
|
147
|
+
@claim_buffer.return_all!(client: Pgbus.client) unless @claim_buffer.empty?
|
|
148
|
+
break
|
|
149
|
+
end
|
|
138
150
|
|
|
139
151
|
consume
|
|
140
152
|
@stat_buffer&.flush_if_due
|
|
@@ -171,20 +183,35 @@ module Pgbus
|
|
|
171
183
|
@queue_names = @registry.queue_names_for_topics(topics)
|
|
172
184
|
end
|
|
173
185
|
|
|
186
|
+
# Same loop step as Worker#claim_and_execute (issue #486): feed
|
|
187
|
+
# buffered claims to free slots, read the deficit, feed again, and wait
|
|
188
|
+
# only when the step moved nothing.
|
|
174
189
|
def consume
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
drained = drain_claim_buffer
|
|
191
|
+
claimed = claim_deficit
|
|
192
|
+
drained += drain_claim_buffer
|
|
193
|
+
@wake_signal.wait(timeout: wake_timeout) if drained.zero? && claimed.zero?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The hold is released before handle_message's own heartbeat tracking
|
|
197
|
+
# registers the same key, so the message is never untracked in between.
|
|
198
|
+
def drain_claim_buffer
|
|
199
|
+
@claim_buffer.drain_into(@pool) do |claim|
|
|
200
|
+
@pool.post do
|
|
201
|
+
VisibilityHeartbeat.release(claim.hold)
|
|
202
|
+
handle_message(claim.message, claim.queue_name)
|
|
203
|
+
end
|
|
183
204
|
end
|
|
205
|
+
end
|
|
184
206
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
def claim_deficit
|
|
208
|
+
want = @claim_buffer.deficit(free_slots: @pool.available_capacity, read_ahead: @read_ahead)
|
|
209
|
+
return 0 if want.zero?
|
|
210
|
+
|
|
211
|
+
tagged_messages = fetch_messages(want)
|
|
212
|
+
client = Pgbus.client
|
|
213
|
+
tagged_messages.each { |queue_name, message| @claim_buffer.push(queue_name, message, client: client) }
|
|
214
|
+
tagged_messages.size
|
|
188
215
|
end
|
|
189
216
|
|
|
190
217
|
# Returns an array of [queue_name, message] pairs. Queues whose circuit
|
|
@@ -171,7 +171,7 @@ module Pgbus
|
|
|
171
171
|
private_constant :ROLE_FLAGS
|
|
172
172
|
|
|
173
173
|
def log_boot_banner
|
|
174
|
-
Pgbus.logger.info { "[Pgbus] boot: pgbus #{Pgbus::VERSION} pid=#{::Process.pid}" }
|
|
174
|
+
Pgbus.logger.info { "[Pgbus] boot: pgbus #{Pgbus::VERSION} pid=#{::Process.pid} jit=#{RubyJit.label}" }
|
|
175
175
|
Pgbus.logger.info do
|
|
176
176
|
"[Pgbus] boot: connection=#{redacted_connection_target} pool=#{banner_field { config.resolved_pool_size }}"
|
|
177
177
|
end
|
|
@@ -330,6 +330,7 @@ module Pgbus
|
|
|
330
330
|
queues: queues, threads: threads, config: config,
|
|
331
331
|
single_active_consumer: single_active, consumer_priority: priority,
|
|
332
332
|
execution_mode: exec_mode, group_mode: grp_mode,
|
|
333
|
+
read_ahead: config.read_ahead_for(worker_config),
|
|
333
334
|
liveness_pipe: liveness_writer, wake_pipe: wake_reader
|
|
334
335
|
)
|
|
335
336
|
worker.run
|
|
@@ -498,6 +499,7 @@ module Pgbus
|
|
|
498
499
|
setup_child_process
|
|
499
500
|
load_rails_app
|
|
500
501
|
consumer = Consumer.new(topics: topics, threads: threads, config: config,
|
|
502
|
+
read_ahead: config.read_ahead_for(consumer_config),
|
|
501
503
|
liveness_pipe: liveness_writer, wake_pipe: wake_reader)
|
|
502
504
|
consumer.run
|
|
503
505
|
end
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Pgbus
|
|
|
7
7
|
class Worker
|
|
8
8
|
include SignalHandler
|
|
9
9
|
|
|
10
|
-
attr_reader :queues, :threads, :config, :execution_mode,
|
|
10
|
+
attr_reader :queues, :threads, :config, :execution_mode, :read_ahead,
|
|
11
11
|
:rate_counter, :wake_signal, :restore_streak, :lifecycle
|
|
12
12
|
# stat_buffer is writable so a test can swap in a buffer double after
|
|
13
13
|
# construction to assert graceful_shutdown / check_recycle flush it. The
|
|
@@ -35,7 +35,7 @@ module Pgbus
|
|
|
35
35
|
rate_counter: nil, wake_signal: nil, stat_buffer: :default,
|
|
36
36
|
notify_listener: nil, notify_retry_at: 0.0,
|
|
37
37
|
notify_retry_backoff: NOTIFY_RETRY_BASE_SECONDS,
|
|
38
|
-
started_at_monotonic: nil, wake_pipe: nil)
|
|
38
|
+
started_at_monotonic: nil, wake_pipe: nil, read_ahead: nil)
|
|
39
39
|
@queues = Array(queues)
|
|
40
40
|
@initial_queues = @queues.dup.freeze
|
|
41
41
|
@wildcard = @queues.include?("*")
|
|
@@ -65,7 +65,10 @@ module Pgbus
|
|
|
65
65
|
@last_wildcard_resolve = nil
|
|
66
66
|
@jobs_processed = Concurrent::AtomicFixnum.new(0)
|
|
67
67
|
@jobs_failed = Concurrent::AtomicFixnum.new(0)
|
|
68
|
+
# in_flight counts every claimed message: buffered and running.
|
|
68
69
|
@in_flight = Concurrent::AtomicFixnum.new(0)
|
|
70
|
+
@read_ahead = read_ahead || config.read_ahead
|
|
71
|
+
@claim_buffer = ClaimBuffer.new(config: config)
|
|
69
72
|
@loop_tick_at = Concurrent::AtomicReference.new(nil)
|
|
70
73
|
@rate_counter = rate_counter || RateCounter.new(:processed, :failed, :dequeued)
|
|
71
74
|
@started_at = Time.current
|
|
@@ -119,6 +122,7 @@ module Pgbus
|
|
|
119
122
|
jobs_processed: @jobs_processed.value,
|
|
120
123
|
jobs_failed: @jobs_failed.value,
|
|
121
124
|
in_flight: @in_flight.value,
|
|
125
|
+
buffered: @claim_buffer.size,
|
|
122
126
|
state: @lifecycle.state,
|
|
123
127
|
execution_mode: @execution_mode,
|
|
124
128
|
consumer_priority: @consumer_priority,
|
|
@@ -184,6 +188,9 @@ module Pgbus
|
|
|
184
188
|
check_recycle
|
|
185
189
|
refresh_wildcard_queues
|
|
186
190
|
ensure_notify_listener
|
|
191
|
+
# Draining, paused or stopped: claims nobody will run here go back
|
|
192
|
+
# to the queue now rather than when their timeout runs out.
|
|
193
|
+
return_claims unless @lifecycle.can_process?
|
|
187
194
|
|
|
188
195
|
break if @lifecycle.stopped?
|
|
189
196
|
# quiesced? (all slots free), not idle? (any slot free) — exiting
|
|
@@ -240,32 +247,53 @@ module Pgbus
|
|
|
240
247
|
|
|
241
248
|
private
|
|
242
249
|
|
|
250
|
+
# One loop step (issue #486): feed buffered claims to free slots, read
|
|
251
|
+
# the deficit (free slots + read_ahead - buffered, capped by
|
|
252
|
+
# prefetch_limit), feed again. With read_ahead 0 the buffer never
|
|
253
|
+
# outlives the step and qty is the free slot count, as before. Waits
|
|
254
|
+
# only when the step moved nothing, so a full pool with a topped-up
|
|
255
|
+
# buffer sleeps until a slot frees (the pool's on_state_change wakes it).
|
|
243
256
|
def claim_and_execute
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
drained = drain_claim_buffer
|
|
258
|
+
claimed = claim_deficit
|
|
259
|
+
drained += drain_claim_buffer
|
|
260
|
+
@wake_signal.wait(timeout: wake_timeout) if drained.zero? && claimed.zero?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# The hold is released before the executor's own heartbeat tracking
|
|
264
|
+
# registers the same key, so the message is never untracked in between.
|
|
265
|
+
def drain_claim_buffer
|
|
266
|
+
@claim_buffer.drain_into(@pool) do |claim|
|
|
267
|
+
@pool.post do
|
|
268
|
+
VisibilityHeartbeat.release(claim.hold)
|
|
269
|
+
process_message(claim.message, claim.queue_name, source_queue: claim.source_queue)
|
|
270
|
+
end
|
|
254
271
|
end
|
|
272
|
+
end
|
|
255
273
|
|
|
256
|
-
|
|
274
|
+
def claim_deficit
|
|
275
|
+
prefetch_room = config.prefetch_limit && (config.prefetch_limit - @in_flight.value)
|
|
276
|
+
want = @claim_buffer.deficit(free_slots: @pool.available_capacity, read_ahead: @read_ahead,
|
|
277
|
+
prefetch_room: prefetch_room)
|
|
278
|
+
return 0 if want.zero?
|
|
257
279
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return
|
|
261
|
-
end
|
|
280
|
+
tagged_messages = fetch_messages(want)
|
|
281
|
+
return 0 if tagged_messages.empty?
|
|
262
282
|
|
|
263
283
|
@rate_counter.increment(:dequeued, tagged_messages.size)
|
|
284
|
+
client = Pgbus.client
|
|
264
285
|
tagged_messages.each do |queue_name, message, source_queue|
|
|
265
286
|
detect_zombie(queue_name, message)
|
|
266
287
|
@in_flight.increment
|
|
267
|
-
@
|
|
288
|
+
@claim_buffer.push(queue_name, message, source_queue, client: client)
|
|
268
289
|
end
|
|
290
|
+
tagged_messages.size
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def return_claims
|
|
294
|
+
return if @claim_buffer.empty?
|
|
295
|
+
|
|
296
|
+
@in_flight.decrement(@claim_buffer.return_all!(client: Pgbus.client))
|
|
269
297
|
end
|
|
270
298
|
|
|
271
299
|
# Returns an array of [queue_name, message] pairs so we always know
|
|
@@ -831,7 +859,8 @@ module Pgbus
|
|
|
831
859
|
"rates" => @rate_counter.rates.transform_keys(&:to_s),
|
|
832
860
|
"jobs_processed" => @jobs_processed.value,
|
|
833
861
|
"jobs_failed" => @jobs_failed.value,
|
|
834
|
-
"in_flight" => @in_flight.value
|
|
862
|
+
"in_flight" => @in_flight.value,
|
|
863
|
+
"buffered" => @claim_buffer.size
|
|
835
864
|
}
|
|
836
865
|
end
|
|
837
866
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# Reports which Ruby JIT this process runs under (issue #484). Read-only:
|
|
5
|
+
# Rails owns turning YJIT on (config.yjit), pgbus only makes the state
|
|
6
|
+
# visible in the boot log and `pgbus doctor`.
|
|
7
|
+
module RubyJit
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def label
|
|
11
|
+
return "yjit" if yjit_available? && RubyVM::YJIT.enabled?
|
|
12
|
+
return "zjit" if defined?(RubyVM::ZJIT) && RubyVM::ZJIT.enabled?
|
|
13
|
+
|
|
14
|
+
"none"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def yjit_available?
|
|
18
|
+
defined?(RubyVM::YJIT) ? true : false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -30,8 +30,9 @@ module Pgbus
|
|
|
30
30
|
#
|
|
31
31
|
# The ninth member takes the struct out of the 80-byte slot it used to fit
|
|
32
32
|
# (measured: 80 → 160). That is paid at most once per in-flight message, so
|
|
33
|
-
# the whole table is bounded by the execution pool's capacity
|
|
34
|
-
#
|
|
33
|
+
# the whole table is bounded by the execution pool's capacity plus the
|
|
34
|
+
# read-ahead buffer (threads + read_ahead per process, issue #486) — a
|
|
35
|
+
# handful of entries per process, not one per enqueued job.
|
|
35
36
|
Entry = Struct.new(:client, :queue_name, :prefixed, :msg_id, :job_class, :extended_at, :extensions,
|
|
36
37
|
:concurrency, :on_beat, keyword_init: true)
|
|
37
38
|
|
|
@@ -81,6 +82,25 @@ module Pgbus
|
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
# Keep a claimed-but-not-started message invisible until #release.
|
|
86
|
+
# Used by Process::ClaimBuffer for read-ahead (issue #486): a buffered
|
|
87
|
+
# message waits for a free slot and must not be redelivered meanwhile.
|
|
88
|
+
# Same key as #track, so the run's own tracking takes over after the
|
|
89
|
+
# hold is released. Returns the entry, or nil when the heartbeat is off.
|
|
90
|
+
def hold(client:, queue_name:, msg_id:, prefixed: true, job_class: nil, config: Pgbus.configuration)
|
|
91
|
+
return unless config.visibility_heartbeat
|
|
92
|
+
|
|
93
|
+
entry = Entry.new(client: client, queue_name: queue_name, prefixed: prefixed, msg_id: msg_id.to_i,
|
|
94
|
+
job_class: job_class, extended_at: monotonic_now, extensions: 0)
|
|
95
|
+
register(entry, config)
|
|
96
|
+
entry
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Drop a #hold. A nil entry (heartbeat off) is a no-op.
|
|
100
|
+
def release(entry)
|
|
101
|
+
unregister(entry) if entry
|
|
102
|
+
end
|
|
103
|
+
|
|
84
104
|
# Extend every tracked message whose last extension is older than the
|
|
85
105
|
# heartbeat interval. Public so tests and callers without the thread
|
|
86
106
|
# can drive it.
|
|
@@ -127,11 +147,22 @@ module Pgbus
|
|
|
127
147
|
end
|
|
128
148
|
end
|
|
129
149
|
|
|
150
|
+
# Only the entry that is registered under the key: a stale hold released
|
|
151
|
+
# after #track re-registered the same message must not drop the running
|
|
152
|
+
# job's entry (issue #486).
|
|
130
153
|
def unregister(entry)
|
|
131
|
-
synchronize
|
|
154
|
+
synchronize do
|
|
155
|
+
key = key_for(entry)
|
|
156
|
+
entries.delete(key) if entries[key].equal?(entry)
|
|
157
|
+
end
|
|
132
158
|
end
|
|
133
159
|
|
|
134
160
|
def extend!(entry, now:, config:)
|
|
161
|
+
# tick! picked this entry under the mutex but runs here without it: if
|
|
162
|
+
# the entry was released meanwhile (a read-ahead claim handed back with
|
|
163
|
+
# vt: 0, a job that finished), extending it would undo that.
|
|
164
|
+
return unless registered?(entry)
|
|
165
|
+
|
|
135
166
|
vt = config.visibility_timeout
|
|
136
167
|
entry.client.set_visibility_timeout(entry.queue_name, entry.msg_id, vt: vt, prefixed: entry.prefixed)
|
|
137
168
|
entry.extended_at = now
|
|
@@ -218,6 +249,10 @@ module Pgbus
|
|
|
218
249
|
@entries ||= {}
|
|
219
250
|
end
|
|
220
251
|
|
|
252
|
+
def registered?(entry)
|
|
253
|
+
synchronize { entries[key_for(entry)].equal?(entry) }
|
|
254
|
+
end
|
|
255
|
+
|
|
221
256
|
def key_for(entry)
|
|
222
257
|
[entry.queue_name, entry.msg_id]
|
|
223
258
|
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.
|
|
4
|
+
version: 0.17.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -343,9 +343,12 @@ files:
|
|
|
343
343
|
- lib/pgbus/outbox.rb
|
|
344
344
|
- lib/pgbus/outbox/poller.rb
|
|
345
345
|
- lib/pgbus/pgmq_schema.rb
|
|
346
|
+
- lib/pgbus/pgmq_schema/fixups/pgmq_v1.13.0.sql
|
|
346
347
|
- lib/pgbus/pgmq_schema/pgmq_v1.11.0.sql
|
|
347
348
|
- lib/pgbus/pgmq_schema/pgmq_v1.11.1.sql
|
|
348
349
|
- lib/pgbus/pgmq_schema/pgmq_v1.12.0.sql
|
|
350
|
+
- lib/pgbus/pgmq_schema/pgmq_v1.13.0.sql
|
|
351
|
+
- lib/pgbus/process/claim_buffer.rb
|
|
349
352
|
- lib/pgbus/process/consumer.rb
|
|
350
353
|
- lib/pgbus/process/consumer_priority.rb
|
|
351
354
|
- lib/pgbus/process/dispatcher.rb
|
|
@@ -374,6 +377,7 @@ files:
|
|
|
374
377
|
- lib/pgbus/recurring/scheduler.rb
|
|
375
378
|
- lib/pgbus/recurring/task.rb
|
|
376
379
|
- lib/pgbus/retry_backoff.rb
|
|
380
|
+
- lib/pgbus/ruby_jit.rb
|
|
377
381
|
- lib/pgbus/serializer.rb
|
|
378
382
|
- lib/pgbus/stat_buffer.rb
|
|
379
383
|
- lib/pgbus/streams.rb
|