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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +9 -3
- data/app/frontend/pgbus/application.js +4 -3
- data/app/frontend/pgbus/application.min.js +1 -1
- data/app/frontend/pgbus/application.min.js.map +1 -1
- data/app/frontend/pgbus/style.css +1 -1
- data/app/views/layouts/pgbus/application.html.erb +2 -2
- data/lib/pgbus/active_job/adapter.rb +67 -26
- data/lib/pgbus/active_job/batch_id.rb +23 -1
- data/lib/pgbus/batch/builder.rb +1 -0
- data/lib/pgbus/batch.rb +35 -3
- data/lib/pgbus/configuration/snapshot.rb +27 -0
- data/lib/pgbus/integrations/appsignal/event_span.rb +43 -0
- data/lib/pgbus/integrations/appsignal/job_span.rb +47 -0
- data/lib/pgbus/integrations/appsignal/span_subscriber.rb +128 -0
- data/lib/pgbus/integrations/appsignal/subscriber.rb +37 -102
- data/lib/pgbus/shareable.rb +11 -1
- data/lib/pgbus/streams/key.rb +12 -3
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus.rb +31 -6
- data/sig/pgbus/configuration/snapshot.rbs +12 -0
- data/sig/pgbus.rbs +1 -0
- metadata +6 -1
data/lib/pgbus/batch/builder.rb
CHANGED
|
@@ -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
|
-
|
|
633
|
-
|
|
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 "
|
|
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`.
|
|
14
|
-
#
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
181
|
-
#
|
|
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
|
data/lib/pgbus/shareable.rb
CHANGED
|
@@ -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
|
|
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.
|
data/lib/pgbus/streams/key.rb
CHANGED
|
@@ -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=#{
|
|
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