pgbus 0.19.2 → 0.19.4

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.
@@ -59,6 +59,7 @@ module Pgbus
59
59
  raise ArgumentError, UNTAGGED_MESSAGE unless Thread.current[:pgbus_batch_id] == @batch.batch_id
60
60
 
61
61
  jobs = self.class.job_instances(jobs)
62
+ Pgbus::ActiveJob::BatchId.capture(jobs, @batch.batch_id)
62
63
  ::ActiveJob.perform_all_later(jobs) if jobs.any?
63
64
  yield if block_given?
64
65
  jobs
data/lib/pgbus/batch.rb CHANGED
@@ -45,6 +45,19 @@ module Pgbus
45
45
  # Thread.current): b.add raises anywhere else, and a plain perform_later
46
46
  # from a thread or fiber the block spawns does not join the batch.
47
47
  #
48
+ # Inside a database transaction the batch stays pending until the
49
+ # transaction commits: a job with enqueue_after_transaction_commit keeps
50
+ # the batch on its instance (b.add, batch.add and perform_later put it
51
+ # there) and counts itself in at commit, before the batch starts. On a
52
+ # rollback no job is sent and, when pgbus shares the application's
53
+ # database, the batch row is gone with the rest (a separate pgbus
54
+ # database commits the row on its own connection). A bare
55
+ # ActiveJob.perform_all_later in the block does not keep it: use b.add.
56
+ # Add jobs that defer: one sent at once, on a connection other than the
57
+ # caller's, can run before the batch commits and is then accounted for
58
+ # only by the stalled-execution sweep. A job instance is one member (one
59
+ # job_id): enqueue it once.
60
+ #
48
61
  # Pgbus::Batch.start(description: "Import") do |b|
49
62
  # b.on_complete ImportDoneJob
50
63
  # b.add(*rows.map { |row| ImportRowJob.new(row.id) })
@@ -124,7 +137,7 @@ module Pgbus
124
137
  end
125
138
  @started = true
126
139
  count_jobs(&)
127
- start_processing
140
+ self.class.send(:after_transactions_commit) { start_processing }
128
141
  self
129
142
  end
130
143
 
@@ -139,6 +152,7 @@ module Pgbus
139
152
  return jobs if jobs.empty? && block.nil?
140
153
 
141
154
  enqueue do
155
+ Pgbus::ActiveJob::BatchId.capture(jobs, batch_id)
142
156
  ::ActiveJob.perform_all_later(jobs) if jobs.any?
143
157
  yield if block
144
158
  end
@@ -473,6 +487,22 @@ module Pgbus
473
487
  finish_if_needed(try_finish!(batch_id))
474
488
  end
475
489
 
490
+ # Run the end-of-block start (or an open batch's re-check) once every
491
+ # open transaction has committed, and at once when none is open
492
+ # (issue #553). Jobs with enqueue_after_transaction_commit register
493
+ # their enqueue as an after_commit callback during the block; Active
494
+ # Record runs a transaction's callbacks in registration order and
495
+ # appends a savepoint's to its parent's on release, so every one of
496
+ # them has counted itself into the still-pending row before this runs.
497
+ # On a rollback none of them runs, and the row (created in the same
498
+ # transaction) is gone. Active Record 7.1 has no
499
+ # after_all_transactions_commit, and Active Job 7.1 never defers.
500
+ def after_transactions_commit(&)
501
+ return yield unless ::ActiveRecord.respond_to?(:after_all_transactions_commit)
502
+
503
+ ::ActiveRecord.after_all_transactions_commit(&)
504
+ end
505
+
476
506
  def finish_if_needed(result)
477
507
  return result unless result&.fetch(:just_finished, false) && result[:record]
478
508
 
@@ -629,8 +659,10 @@ module Pgbus
629
659
  raise AlreadyFinished, "Can't add jobs into an already finished batch" if finished?
630
660
 
631
661
  count_jobs(&)
632
- reload
633
- self.class.send(:finish_if_needed, BatchEntry.check_finished!(batch_id))
662
+ self.class.send(:after_transactions_commit) do
663
+ reload
664
+ self.class.send(:finish_if_needed, BatchEntry.check_finished!(batch_id))
665
+ end
634
666
  self
635
667
  end
636
668
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ class Configuration
5
+ # The settings a non-main Ractor reads, as one Ractor-shareable value
6
+ # (issue #527). The configuration itself stays on the main Ractor: it holds
7
+ # the logger and the app's callables. Pgbus.finalize! and Pgbus.configure
8
+ # publish a snapshot of it as Pgbus.configuration_snapshot, which any
9
+ # Ractor can read.
10
+ #
11
+ # Only what pgbus's Ractor-safe code reads is here: the queue_prefix, for
12
+ # the stream-key budget (Streams::Key). A setting joins when such code
13
+ # needs it.
14
+ Snapshot = Data.define(:queue_prefix) do
15
+ # A shareable snapshot of +configuration+. The configuration's own
16
+ # values are copied, never frozen in place. Nil when a setting is not
17
+ # plain data (Shareable.data?), which only a configuration that skipped
18
+ # validation can hold: the caller keeps the snapshot it has.
19
+ def self.of(configuration)
20
+ prefix = configuration.queue_prefix
21
+ return unless Shareable.data?(prefix)
22
+
23
+ new(queue_prefix: Shareable.freeze_copy(prefix))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pgbus/integrations/appsignal/span_subscriber"
4
+
5
+ module Pgbus
6
+ module Integrations
7
+ module Appsignal
8
+ # The `<Handler>#handle` transaction around pgbus.event_processed, and
9
+ # the fallback for an error pgbus.event_failed reports from outside it
10
+ # (an event that could not be built never reaches the span).
11
+ class EventSpan < SpanSubscriber
12
+ private
13
+
14
+ def frame_key = :pgbus_appsignal_event_span
15
+
16
+ def attached_key = :pgbus_appsignal_event_span_error
17
+
18
+ def describe(transaction, payload)
19
+ transaction.set_action_if_nil("#{payload[:handler] || "UnknownHandler"}#handle")
20
+ apply_queue_start(transaction, payload[:published_at])
21
+ transaction.add_tags(handler_tags(payload))
22
+ end
23
+
24
+ # The processed count is taken at finish whether or not the handler
25
+ # raised, as Metrics::Subscriber does: a raised handler counts as
26
+ # processed and, through pgbus.event_failed, as failed.
27
+ def finished(payload, duration)
28
+ tags = { handler: payload[:handler], routing_key: payload[:routing_key] }
29
+ ::Appsignal.add_distribution_value("pgbus_event_duration_ms", duration, tags)
30
+ ::Appsignal.increment_counter("pgbus_event_count", 1, tags.merge(status: "processed"))
31
+ end
32
+
33
+ def handler_tags(payload)
34
+ {
35
+ "handler" => payload[:handler],
36
+ "routing_key" => payload[:routing_key],
37
+ "attempts" => payload[:read_ct]
38
+ }.compact
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pgbus/integrations/appsignal/span_subscriber"
4
+
5
+ module Pgbus
6
+ module Integrations
7
+ module Appsignal
8
+ # The `<JobClass>#perform` transaction around pgbus.executor.execute
9
+ # (deserialize + perform + archive), and the fallback for an error
10
+ # pgbus.job_failed reports from outside it.
11
+ class JobSpan < SpanSubscriber
12
+ private
13
+
14
+ def frame_key = :pgbus_appsignal_job_span
15
+
16
+ def attached_key = :pgbus_appsignal_job_span_error
17
+
18
+ def describe(transaction, payload)
19
+ transaction.set_action_if_nil("#{payload[:job_class] || "UnknownJob"}#perform")
20
+ apply_queue_start(transaction, payload[:enqueued_at])
21
+ transaction.add_tags(job_tags(payload))
22
+ transaction.add_params_if_nil { { arguments: payload[:arguments] } } if payload.key?(:arguments)
23
+ end
24
+
25
+ def finished(payload, duration)
26
+ ::Appsignal.add_distribution_value(
27
+ "pgbus_job_duration_ms",
28
+ duration,
29
+ { queue: payload[:queue], job_class: payload[:job_class] }
30
+ )
31
+ end
32
+
33
+ def job_tags(payload)
34
+ tags = {
35
+ "queue" => payload[:queue],
36
+ "job_class" => payload[:job_class],
37
+ "attempts" => payload[:read_ct]
38
+ }
39
+ tags["active_job_id"] = payload[:job_id] if payload[:job_id]
40
+ tags["provider_job_id"] = payload[:provider_job_id] if payload[:provider_job_id]
41
+ tags["request_id"] = payload[:provider_job_id] || payload[:job_id]
42
+ tags.compact
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Pgbus
6
+ module Integrations
7
+ module Appsignal
8
+ # The AppSignal transaction of one unit of work: a job (JobSpan, on
9
+ # pgbus.executor.execute) or an event handler (EventSpan, on
10
+ # pgbus.event_processed).
11
+ #
12
+ # An ActiveSupport::Notifications listener that responds to #start and
13
+ # #finish is called on both sides of the instrumented block (the Evented
14
+ # subscriber). A finish-only block subscriber runs after the block has
15
+ # returned, so the transaction it opened was never current while the job
16
+ # ran: job code had nothing to attach to, and pgbus.job_failed, fired
17
+ # later, found no transaction to put its error on (issue #552). Here
18
+ # #start opens the transaction before the block runs, so job and handler
19
+ # code, and the Rails error reporter, find it current; #finish records
20
+ # the exception the block raised (the instrumenter puts it in
21
+ # payload[:exception_object]) and completes the transaction.
22
+ #
23
+ # Like AppSignal's own ActiveJob hook, a transaction that is already
24
+ # current is reused and left to its owner to complete.
25
+ #
26
+ # The frame lives in Thread.current[], which is fiber-local, so start and
27
+ # finish pair up per job under execution_mode: :async, the same way
28
+ # AppSignal keeps its current transaction. A span opened inside another
29
+ # of its kind (a handler calling another handler's #process) keeps the
30
+ # enclosing frame and restores it at finish, so the outer span still
31
+ # completes its transaction. An instance holds no state of its own.
32
+ # Nothing an AppSignal call raises reaches the job: it is logged at warn
33
+ # level.
34
+ #
35
+ # Subclasses define #frame_key and #attached_key (the fiber-local slots),
36
+ # #describe(transaction, payload) (action, queue start, tags) and
37
+ # #finished(payload, duration_ms) (the span's metrics).
38
+ class SpanSubscriber
39
+ BACKGROUND_JOB = "background_job"
40
+
41
+ # owned: this span created the transaction, so it completes it.
42
+ # previous: the enclosing span's frame, restored at finish.
43
+ Frame = Struct.new(:started_at, :owned, :previous)
44
+
45
+ def start(_name, _id, payload)
46
+ frame = Thread.current[frame_key] = Frame.new(monotonic_now, false, Thread.current[frame_key])
47
+ guard do
48
+ frame.owned = !::Appsignal::Transaction.current?
49
+ transaction = frame.owned ? ::Appsignal::Transaction.create(BACKGROUND_JOB) : ::Appsignal::Transaction.current
50
+ describe(transaction, payload)
51
+ end
52
+ end
53
+
54
+ def finish(_name, _id, payload)
55
+ frame = Thread.current[frame_key]
56
+ guard do
57
+ Thread.current[attached_key] = attach(payload[:exception_object])
58
+ finished(payload, monotonic_now - frame.started_at)
59
+ end
60
+ ensure
61
+ guard do
62
+ Thread.current[frame_key] = frame.previous
63
+ ::Appsignal::Transaction.complete_current! if frame.owned
64
+ end
65
+ end
66
+
67
+ # pgbus.job_failed and pgbus.event_failed fire after #finish. An error
68
+ # the span recorded is already on its transaction. One raised before
69
+ # the span opened (a message body that is not JSON, an event that
70
+ # cannot be built, a dead-letter move that failed) had no transaction,
71
+ # so it is reported on one of its own, named and tagged like the span's
72
+ # (or added to a transaction that is current, which is left as it is).
73
+ def report_unattached(error, payload)
74
+ attached = Thread.current[attached_key]
75
+ Thread.current[attached_key] = nil
76
+ return if error.nil? || error.equal?(attached)
77
+ return ::Appsignal::Transaction.current.set_error(error) if ::Appsignal::Transaction.current?
78
+
79
+ ::Appsignal.report_error(error) do |transaction|
80
+ transaction.set_namespace(BACKGROUND_JOB)
81
+ describe(transaction, payload)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ # Returns the exception it recorded, nil when there was none.
88
+ def attach(error)
89
+ return unless error && ::Appsignal::Transaction.current?
90
+
91
+ ::Appsignal::Transaction.current.set_error(error)
92
+ error
93
+ end
94
+
95
+ # AppSignal expects queue-start as Unix epoch milliseconds. Pgbus
96
+ # carries it as either an ISO-8601 String or a Time — both happen
97
+ # in practice (executor passes the JSON string, handler passes a
98
+ # parsed Time).
99
+ def apply_queue_start(transaction, value)
100
+ return unless value
101
+
102
+ millis =
103
+ case value
104
+ when Time
105
+ (value.to_f * 1_000).to_i
106
+ when String
107
+ (Time.parse(value).to_f * 1_000).to_i
108
+ when Numeric
109
+ value.to_i
110
+ end
111
+ transaction.set_queue_start(millis) if millis
112
+ rescue ArgumentError
113
+ # Unparseable timestamp — skip rather than blow up.
114
+ end
115
+
116
+ def guard
117
+ yield
118
+ rescue StandardError => e
119
+ Pgbus.logger.warn { "[Pgbus::AppSignal] subscriber error: #{e.class}: #{e.message}" }
120
+ end
121
+
122
+ def monotonic_now
123
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :float_millisecond)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "time"
3
+ require "pgbus/integrations/appsignal/job_span"
4
+ require "pgbus/integrations/appsignal/event_span"
4
5
 
5
6
  module Pgbus
6
7
  module Integrations
@@ -10,16 +11,23 @@ module Pgbus
10
11
  #
11
12
  # Job and event-handler events open a BACKGROUND_JOB transaction so they
12
13
  # appear under AppSignal's "Performance > Background jobs" view, with
13
- # action `<JobClass>#perform` or `<HandlerClass>#handle`. All other
14
- # events are reported as counters or distributions only.
14
+ # action `<JobClass>#perform` or `<HandlerClass>#handle`. The transaction
15
+ # is opened when pgbus.executor.execute / pgbus.event_processed starts and
16
+ # completed when it finishes (JobSpan, EventSpan), so it is current while the
17
+ # job or handler runs and carries the error the execution raised. All
18
+ # other events are reported as counters or distributions only.
15
19
  #
16
20
  # All metric names are prefixed `pgbus_`. Tag keys avoid high-cardinality
17
21
  # values (no msg_id, no event_id) so AppSignal's metric storage stays
18
22
  # efficient.
19
23
  module Subscriber
20
- BACKGROUND_JOB = "background_job"
21
24
  METRIC_PREFIX = "pgbus_"
22
- private_constant :BACKGROUND_JOB, :METRIC_PREFIX
25
+
26
+ # Built once, at load: a span keeps its state in a fiber-local frame,
27
+ # never in itself.
28
+ JOB_SPAN = JobSpan.new.freeze
29
+ EVENT_SPAN = EventSpan.new.freeze
30
+ private_constant :METRIC_PREFIX, :JOB_SPAN, :EVENT_SPAN
23
31
 
24
32
  # Tracked so we can detach in reset! (used by specs). Installed once
25
33
  # at boot (the engine's after_initialize); @install_lock makes the
@@ -54,11 +62,11 @@ module Pgbus
54
62
 
55
63
  def subscribe_all
56
64
  [
57
- subscribe("pgbus.executor.execute") { |event| on_executor_execute(event) },
65
+ ActiveSupport::Notifications.subscribe("pgbus.executor.execute", JOB_SPAN),
58
66
  subscribe("pgbus.job_completed") { |event| on_job_completed(event) },
59
67
  subscribe("pgbus.job_failed") { |event| on_job_failed(event) },
60
68
  subscribe("pgbus.job_dead_lettered") { |event| on_job_dead_lettered(event) },
61
- subscribe("pgbus.event_processed") { |event| on_event_processed(event) },
69
+ ActiveSupport::Notifications.subscribe("pgbus.event_processed", EVENT_SPAN),
62
70
  subscribe("pgbus.event_failed") { |event| on_event_failed(event) },
63
71
  subscribe("pgbus.event_dead_lettered") { |event| on_event_dead_lettered(event) },
64
72
  subscribe("pgbus.client.send_message") { |event| on_send_message(event) },
@@ -98,22 +106,6 @@ module Pgbus
98
106
 
99
107
  # ── Job execution ───────────────────────────────────────────────
100
108
 
101
- def on_executor_execute(event)
102
- payload = event.payload
103
- transaction = ::Appsignal::Transaction.create(BACKGROUND_JOB)
104
- transaction.set_action_if_nil("#{payload[:job_class] || "UnknownJob"}#perform")
105
- apply_queue_start(transaction, payload[:enqueued_at])
106
- transaction.add_tags(job_tags(payload))
107
- transaction.add_params_if_nil { { arguments: payload[:arguments] } }
108
- ::Appsignal.add_distribution_value(
109
- "#{METRIC_PREFIX}job_duration_ms",
110
- event.duration,
111
- { queue: payload[:queue], job_class: payload[:job_class] }
112
- )
113
- ensure
114
- ::Appsignal::Transaction.complete_current!
115
- end
116
-
117
109
  def on_job_completed(event)
118
110
  payload = event.payload
119
111
  ::Appsignal.increment_counter(
@@ -123,15 +115,18 @@ module Pgbus
123
115
  )
124
116
  end
125
117
 
118
+ # The counter has its own guard so a raise there cannot keep the
119
+ # error from being reported.
126
120
  def on_job_failed(event)
127
121
  payload = event.payload
128
- ::Appsignal.increment_counter(
129
- "#{METRIC_PREFIX}queue_job_count",
130
- 1,
131
- { queue: payload[:queue], job_class: payload[:job_class], status: "failed" }
132
- )
133
- err = payload[:exception_object]
134
- ::Appsignal.set_error(err) if err && ::Appsignal.respond_to?(:set_error)
122
+ safely do
123
+ ::Appsignal.increment_counter(
124
+ "#{METRIC_PREFIX}queue_job_count",
125
+ 1,
126
+ { queue: payload[:queue], job_class: payload[:job_class], status: "failed" }
127
+ )
128
+ end
129
+ JOB_SPAN.report_unattached(payload[:exception_object], payload)
135
130
  end
136
131
 
137
132
  def on_job_dead_lettered(event)
@@ -145,40 +140,23 @@ module Pgbus
145
140
 
146
141
  # ── Event handler ───────────────────────────────────────────────
147
142
 
148
- def on_event_processed(event)
149
- payload = event.payload
150
- transaction = ::Appsignal::Transaction.create(BACKGROUND_JOB)
151
- transaction.set_action_if_nil("#{payload[:handler] || "UnknownHandler"}#handle")
152
- apply_queue_start(transaction, payload[:published_at])
153
- transaction.add_tags(handler_tags(payload))
154
- ::Appsignal.add_distribution_value(
155
- "#{METRIC_PREFIX}event_duration_ms",
156
- event.duration,
157
- { handler: payload[:handler], routing_key: payload[:routing_key] }
158
- )
159
- ::Appsignal.increment_counter(
160
- "#{METRIC_PREFIX}event_count",
161
- 1,
162
- { handler: payload[:handler], routing_key: payload[:routing_key], status: "processed" }
163
- )
164
- ensure
165
- ::Appsignal::Transaction.complete_current!
166
- end
167
-
143
+ # The counter has its own guard so a raise there cannot keep the
144
+ # error from being reported.
168
145
  def on_event_failed(event)
169
146
  payload = event.payload
170
- ::Appsignal.increment_counter(
171
- "#{METRIC_PREFIX}event_count",
172
- 1,
173
- { handler: payload[:handler], routing_key: payload[:routing_key], status: "failed" }
174
- )
175
- err = payload[:exception_object]
176
- ::Appsignal.set_error(err) if err && ::Appsignal.respond_to?(:set_error)
147
+ safely do
148
+ ::Appsignal.increment_counter(
149
+ "#{METRIC_PREFIX}event_count",
150
+ 1,
151
+ { handler: payload[:handler], routing_key: payload[:routing_key], status: "failed" }
152
+ )
153
+ end
154
+ EVENT_SPAN.report_unattached(payload[:exception_object], payload)
177
155
  end
178
156
 
179
157
  # A counter only, like on_job_dead_lettered: nothing is raised at
180
- # move time, and the handler's error already went out through
181
- # pgbus.event_failed on the attempt that raised (issue #543). A body
158
+ # move time, and the handler's error already went out on the
159
+ # attempt that raised (issues #543, #552). A body
182
160
  # that is not JSON leaves handler and routing_key nil; compact drops
183
161
  # those tags, as Metrics::Subscriber does.
184
162
  def on_event_dead_lettered(event)
@@ -295,49 +273,6 @@ module Pgbus
295
273
  { reason: payload[:reason], kind: "consumer" }
296
274
  )
297
275
  end
298
-
299
- # ── Helpers ─────────────────────────────────────────────────────
300
-
301
- # AppSignal expects queue-start as Unix epoch milliseconds. Pgbus
302
- # carries it as either an ISO-8601 String or a Time — both happen
303
- # in practice (executor passes the JSON string, handler passes a
304
- # parsed Time).
305
- def apply_queue_start(transaction, value)
306
- return unless value
307
-
308
- millis =
309
- case value
310
- when Time
311
- (value.to_f * 1_000).to_i
312
- when String
313
- (Time.parse(value).to_f * 1_000).to_i
314
- when Numeric
315
- value.to_i
316
- end
317
- transaction.set_queue_start(millis) if millis
318
- rescue ArgumentError
319
- # Unparseable timestamp — skip rather than blow up.
320
- end
321
-
322
- def job_tags(payload)
323
- tags = {
324
- "queue" => payload[:queue],
325
- "job_class" => payload[:job_class],
326
- "attempts" => payload[:read_ct]
327
- }
328
- tags["active_job_id"] = payload[:job_id] if payload[:job_id]
329
- tags["provider_job_id"] = payload[:provider_job_id] if payload[:provider_job_id]
330
- tags["request_id"] = payload[:provider_job_id] || payload[:job_id]
331
- tags.compact
332
- end
333
-
334
- def handler_tags(payload)
335
- {
336
- "handler" => payload[:handler],
337
- "routing_key" => payload[:routing_key],
338
- "attempts" => payload[:read_ct]
339
- }.compact
340
- end
341
276
  end
342
277
  end
343
278
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Pgbus
4
4
  # Deep-frozen, Ractor-shareable copies of plain data, for the settings
5
- # Configuration#finalize! freezes at boot (issue #527).
5
+ # Configuration#finalize! freezes at boot and the snapshot non-main Ractors
6
+ # read (issue #527).
6
7
  module Shareable
7
8
  # Leaves that are plain data. Modules and classes count too: they are
8
9
  # shareable by nature.
@@ -39,6 +40,15 @@ module Pgbus
39
40
  end
40
41
  end
41
42
 
43
+ # True on the main Ractor, the only one that can read Pgbus.configuration,
44
+ # and wherever the engine has no Ractors. Ractor.main? is Ruby 3.4+.
45
+ def main_ractor?
46
+ return true unless defined?(::Ractor)
47
+ return ::Ractor.main? if ::Ractor.respond_to?(:main?)
48
+
49
+ ::Ractor.current.equal?(::Ractor.main)
50
+ end
51
+
42
52
  # A deep-frozen copy of plain data (see data?), Ractor-shareable where the
43
53
  # engine has Ractors. The caller's own objects are copied, never frozen in
44
54
  # place.
@@ -105,7 +105,7 @@ module Pgbus
105
105
  raise ArgumentError,
106
106
  "stream_key #{key.inspect} is #{key.length} chars, " \
107
107
  "exceeds pgbus budget of #{budget} " \
108
- "(queue_prefix=#{Pgbus.configuration.queue_prefix.inspect}, " \
108
+ "(queue_prefix=#{queue_prefix.inspect}, " \
109
109
  "pgbus_max_queue_name_length=#{QueueNameValidator::MAX_QUEUE_NAME_LENGTH}). " \
110
110
  "Shorten the streamables or use Pgbus::Streams::Streamable on the model."
111
111
  end
@@ -182,10 +182,19 @@ module Pgbus
182
182
  # override `config.queue_prefix` get the correct budget
183
183
  # automatically.
184
184
  def queue_name_budget
185
- QueueNameValidator::MAX_QUEUE_NAME_LENGTH -
186
- Pgbus.configuration.queue_prefix.length - 1
185
+ QueueNameValidator::MAX_QUEUE_NAME_LENGTH - queue_prefix.length - 1
187
186
  end
188
187
 
188
+ # The live configuration's queue_prefix on the main Ractor. A non-main
189
+ # Ractor cannot read Pgbus.configuration (it holds the logger and the
190
+ # app's callables), so there it is the published snapshot's: the
191
+ # prefix as the last Pgbus.finalize! or Pgbus.configure left it.
192
+ def queue_prefix
193
+ config = Shareable.main_ractor? ? Pgbus.configuration : Pgbus.configuration_snapshot
194
+ config.queue_prefix
195
+ end
196
+ private_class_method :queue_prefix
197
+
189
198
  # Raises if a normalized fragment contains the `:` separator.
190
199
  # Kept private-ish via module_function so the guard is shared
191
200
  # between stream_key and any future composer without becoming
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.19.2"
4
+ VERSION = "0.19.4"
5
5
  end