pgbus 0.19.3 → 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 +2 -0
- data/README.md +8 -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/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/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5167e34401cdf1e4d452969bd1ebebd569e145bc559f62ea7d3913156e02ccf
|
|
4
|
+
data.tar.gz: 2f47167e2c3abc31d02482117db5cf0cf3d0c8b2e3413351864493782033503b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 936892796bbb42eb143e61bb6ae1e9f3ac4f1056127ddd96d1dd59638ba87f2c01b4c5e6fa26fa6a3b32e8d19b359070dceba20afd4ce91dd76edbbb98062efa
|
|
7
|
+
data.tar.gz: 30cfff6ead30c7b35d3454c56b41793b9f984c1283073c414e1e83b357325d5ab930160ecfa035b3cb9a6f2e1f1d832172d7684f8cdb467aa0354daa84273eaa
|
data/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
### Fixed
|
|
15
15
|
|
|
16
|
+
- **`Pgbus.batch` inside a database transaction counts the jobs that enqueue after commit, and waits for them.** A job with Active Job's `enqueue_after_transaction_commit` added inside an open transaction is enqueued only when the transaction commits, after the block has returned and its thread tag is gone. The batch counted nothing, finished empty when the block returned and sent its `on_complete` (and `on_success`) callback before any member job existed; the jobs then ran outside the batch. Even a rolled-back transaction sent the callback. Now `b.add`, `batch.add` and `perform_later` record the batch on the job instance (`enqueue_batch_id`, transient: never serialized, cleared once the enqueue is done), the adapter tags and counts each job from it at the real enqueue, and the batch starts processing (or an open batch re-checks for finish) through `ActiveRecord.after_all_transactions_commit`, after every deferred enqueue has counted itself in. On a rollback nothing is sent, no callback fires and the batch row is rolled back with the transaction. A deferred first attempt discarded at enqueue (uniqueness or concurrency `on_conflict: :discard`) is uncounted as before, and a `retry_on` re-enqueue still re-joins without a second count. Outside a transaction nothing changes, including the queries a batch runs. Known gaps: a bare `ActiveJob.perform_all_later` inside the block does not record the batch (use `b.add`); a `batch.add` inside a transaction raises `AlreadyFinished` when the transaction commits, not at the `add` line, if the batch finished in the meantime; a batch row on a separate `connects_to` database is not part of the caller's transaction. Refs #553.
|
|
17
|
+
- **AppSignal: the job's transaction is current while the job runs, and carries its error (issue #552).** The AppSignal subscriber opened the `background_job` transaction from a finish-only notification callback, after the job had run, and completed it in the same call. `pgbus.job_failed` fired after that, so its `Appsignal.set_error` found no current transaction and reported nothing: a job whose class no longer exists (`ActiveJob::UnknownJobClassError`) or whose arguments cannot be deserialized retried and dead-lettered with nothing in AppSignal but the counters, an error raised inside `perform` arrived only as an action-less sample from the Rails error reporter, and job code calling `Appsignal.add_tags`, `set_error` or `instrument` attached to nothing. Event handlers had the same shape. The subscriber now listens to `pgbus.executor.execute` and `pgbus.event_processed` on both sides (`start` and `finish`): the `<JobClass>#perform` / `<Handler>#handle` transaction opens before the job or handler runs, is current for the whole execution (deserialize, perform, archive), records the exception the execution raised and completes at the end, so an error raised inside `perform` lands on it once, with its duration and queue start. A failure raised before that span opens (a message body that is not JSON, an event that cannot be built, a dead-letter move that failed) is reported from `pgbus.job_failed` / `pgbus.event_failed` on a transaction of its own with the same action and tags; an error the span already recorded is not reported twice. A transaction that is already current is reused and left to its owner to complete, as AppSignal's own ActiveJob hook does, and a span inside another of its kind restores the outer one when it finishes. Like that hook, the span records any exception that ends the execution, so a job interrupted between `perform` and the archive (an `Async::Stop` on shutdown under `execution_mode: :async`, an `Interrupt`) now shows up as an error sample too. The counters and the `pgbus_job_duration_ms` / `pgbus_event_duration_ms` distributions are recorded as before, once per execution. Refs #552.
|
|
16
18
|
- **The dashboard's confirm and alert dialogs open centred again (issue #529).** The engine's stylesheet is built with Tailwind 4, whose preflight sets `margin: 0` on every element. A modal `<dialog>` keeps the browser's `inset: 0` but lost the `margin: auto` that centres it inside that inset, so every Pause, Purge and Delete confirmation opened in the top-left corner. Both dialogs now carry `m-auto`. The confirm dialog's Confirm button was white on yellow (1.91:1). It now uses the dark-on-yellow warning colours (`bg-yellow-400 text-yellow-950`), and the axe gate audits an open confirm dialog in light and dark mode. Refs #529.
|
|
17
19
|
|
|
18
20
|
## [0.19.0] - 2026-10-16
|
data/README.md
CHANGED
|
@@ -917,13 +917,19 @@ By default, Pgbus logs caught exceptions and continues. To route them to your AP
|
|
|
917
917
|
```ruby
|
|
918
918
|
Pgbus.configure do |c|
|
|
919
919
|
c.error_reporters << ->(ex, ctx) {
|
|
920
|
-
Appsignal.
|
|
920
|
+
Appsignal.report_error(ex) do |t|
|
|
921
|
+
t.set_namespace("background_job") if ctx[:job_class]
|
|
922
|
+
t.set_action("#{ctx[:job_class]}#perform") if ctx[:job_class]
|
|
923
|
+
t.add_tags(ctx)
|
|
924
|
+
end
|
|
921
925
|
}
|
|
922
926
|
end
|
|
923
927
|
```
|
|
924
928
|
|
|
925
929
|
Each reporter receives `(exception, context_hash)`. The context hash includes keys like `action`, `queue`, `job_class`, and `msg_id` depending on the call site. Reporters that accept a third argument also receive the Pgbus configuration object.
|
|
926
930
|
|
|
931
|
+
Use `Appsignal.report_error`, not `Appsignal.set_error`: a reporter may run from a rescue where no AppSignal transaction is current, and there `set_error` does nothing. `report_error` works whether a transaction is current or not. With the [AppSignal integration](#appsignal-integration) installed, job and handler errors already reach AppSignal on their own transaction, so a reporter like the one above is only needed for the other rescue paths. It does not report those job errors a second time: AppSignal marks an exception once it is on a transaction (`Transaction#add_error`) and does not add a marked exception to another one.
|
|
932
|
+
|
|
927
933
|
Reporters are wired into all critical rescue paths: job execution failures, worker fetch/process errors, dispatcher maintenance, supervisor fork failures, circuit breaker trips, outbox publish errors, and failed event recording. Non-critical paths (dashboard queries, stat recording) remain log-only.
|
|
928
934
|
|
|
929
935
|
`ErrorReporter.report` is guaranteed to never raise — if a reporter or the logger itself throws, the error is swallowed silently. This preserves fault-tolerance invariants at every rescue site.
|
|
@@ -951,7 +957,7 @@ If you upgraded from 0.9.x and previously did `rescue ArgumentError` around `Pgb
|
|
|
951
957
|
|
|
952
958
|
When the `appsignal` gem is loaded in your app, Pgbus auto-installs a subscriber and a minutely probe that report into AppSignal:
|
|
953
959
|
|
|
954
|
-
- **Background-job transactions** for every ActiveJob run and every event-bus handler invocation. Action names follow the AppSignal convention: `MyJob#perform`, `MyHandler#handle`. Tags include `queue`, `job_class`/`handler`, `routing_key`, `attempts`, and the `active_job_id` / `provider_job_id`. `enqueued_at` becomes the AppSignal `queue_start` timestamp so "time on queue" shows up correctly in the timeline.
|
|
960
|
+
- **Background-job transactions** for every ActiveJob run and every event-bus handler invocation. Action names follow the AppSignal convention: `MyJob#perform`, `MyHandler#handle`. Tags include `queue`, `job_class`/`handler`, `routing_key`, `attempts`, and the `active_job_id` / `provider_job_id`. `enqueued_at` becomes the AppSignal `queue_start` timestamp so "time on queue" shows up correctly in the timeline. The transaction is current for the whole execution (deserialize, `perform`/`handle`, archive), so `Appsignal.add_tags`, `Appsignal.set_error` and `Appsignal.instrument` called from job or handler code attach to it, and the error an execution raised is reported on it, also when it was raised before `perform` (an unknown job class, arguments that cannot be deserialized).
|
|
955
961
|
- **Custom counters and distributions** for sends, reads, broadcasts, outbox publishes, recurring scheduling, and worker recycles. All metric names are prefixed `pgbus_`.
|
|
956
962
|
- **A minutely probe** that gauges queue depth (visible vs total), oldest message age per queue, DLQ depth, failed events count, dead-tuple totals, MVCC horizon age, active processes, and stream connection estimates.
|
|
957
963
|
|
|
@@ -11,14 +11,16 @@ module Pgbus
|
|
|
11
11
|
payload_hash = Concurrency.inject_metadata(active_job, payload_hash)
|
|
12
12
|
payload_hash = Uniqueness.inject_metadata(active_job, payload_hash)
|
|
13
13
|
payload_hash = FairShare.inject_metadata(active_job, payload_hash)
|
|
14
|
-
payload_hash = inject_batch_metadata(payload_hash, active_job
|
|
14
|
+
payload_hash = inject_batch_metadata(payload_hash, active_job)
|
|
15
15
|
|
|
16
16
|
if uniqueness_rejected?(active_job, payload_hash, queue: queue)
|
|
17
|
-
uncount_batch_job(payload_hash)
|
|
17
|
+
uncount_batch_job(payload_hash, active_job)
|
|
18
18
|
return active_job
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
enqueue_with_concurrency(active_job, queue, payload_hash)
|
|
22
|
+
ensure
|
|
23
|
+
forget_enqueue_batch_id(active_job)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def enqueue_at(active_job, timestamp)
|
|
@@ -27,15 +29,17 @@ module Pgbus
|
|
|
27
29
|
payload_hash = Concurrency.inject_metadata(active_job, payload_hash)
|
|
28
30
|
payload_hash = Uniqueness.inject_metadata(active_job, payload_hash)
|
|
29
31
|
payload_hash = FairShare.inject_metadata(active_job, payload_hash)
|
|
30
|
-
payload_hash = inject_batch_metadata(payload_hash, active_job
|
|
32
|
+
payload_hash = inject_batch_metadata(payload_hash, active_job)
|
|
31
33
|
delay = [(timestamp - Time.current.to_f).ceil, 0].max
|
|
32
34
|
|
|
33
35
|
if uniqueness_rejected?(active_job, payload_hash, queue: queue)
|
|
34
|
-
uncount_batch_job(payload_hash)
|
|
36
|
+
uncount_batch_job(payload_hash, active_job)
|
|
35
37
|
return active_job
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
enqueue_with_concurrency(active_job, queue, payload_hash, delay: delay)
|
|
41
|
+
ensure
|
|
42
|
+
forget_enqueue_batch_id(active_job)
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
def enqueue_all(active_jobs)
|
|
@@ -114,7 +118,7 @@ module Pgbus
|
|
|
114
118
|
Batch.backfill_execution(payload_hash, msg_id, physical_queue(queue, priority)) if msg_id
|
|
115
119
|
# A retry re-enqueue that is now live (sent, or parked as a blocked
|
|
116
120
|
# execution) must stop the original attempt from signalling completion.
|
|
117
|
-
Batch.note_retry_reenqueued(payload_hash["job_id"]) if (msg_id || blocked) && retry_retagged?(payload_hash)
|
|
121
|
+
Batch.note_retry_reenqueued(payload_hash["job_id"]) if (msg_id || blocked) && retry_retagged?(payload_hash, active_job)
|
|
118
122
|
uniqueness_key = Thread.current[:pgbus_acquired_uniqueness_key]
|
|
119
123
|
UniquenessKey.clear_bind_stamp!(uniqueness_key) if uniqueness_key
|
|
120
124
|
Thread.current[:pgbus_acquired_uniqueness_key] = nil
|
|
@@ -131,7 +135,7 @@ module Pgbus
|
|
|
131
135
|
# database error from the slot upsert or the park is not ambiguous.
|
|
132
136
|
if msg_id.nil? && !(sending && ambiguous_delivery?(e))
|
|
133
137
|
rollback_acquired_uniqueness_lock
|
|
134
|
-
uncount_batch_job(payload_hash)
|
|
138
|
+
uncount_batch_job(payload_hash, active_job)
|
|
135
139
|
else
|
|
136
140
|
# Drop the thread-local so a later discard on this thread cannot
|
|
137
141
|
# release that job's uniqueness lock, but do not DELETE the
|
|
@@ -210,7 +214,7 @@ module Pgbus
|
|
|
210
214
|
# acquired earlier in this enqueue (no executor will release it), and
|
|
211
215
|
# uncount it from its batch so completion is not waited on forever.
|
|
212
216
|
rollback_acquired_uniqueness_lock
|
|
213
|
-
uncount_batch_job(payload_hash)
|
|
217
|
+
uncount_batch_job(payload_hash, active_job)
|
|
214
218
|
when :raise
|
|
215
219
|
raise ConcurrencyLimitExceeded, "Concurrency limit reached for key: #{key}"
|
|
216
220
|
end
|
|
@@ -283,25 +287,39 @@ module Pgbus
|
|
|
283
287
|
|
|
284
288
|
# Reverses inject_batch_metadata for a job discarded at enqueue time
|
|
285
289
|
# (uniqueness duplicate or concurrency :discard conflict): the message is
|
|
286
|
-
# never sent, so it can never signal completion. Only applies
|
|
287
|
-
#
|
|
288
|
-
def uncount_batch_job(payload_hash)
|
|
290
|
+
# never sent, so it can never signal completion. Only applies to a job
|
|
291
|
+
# counted in as a member (see #member_batch_id).
|
|
292
|
+
def uncount_batch_job(payload_hash, active_job)
|
|
289
293
|
batch_id = payload_hash[Batch::METADATA_KEY]
|
|
290
294
|
return unless batch_id
|
|
291
295
|
|
|
292
|
-
if batch_id ==
|
|
296
|
+
if batch_id == member_batch_id(active_job)
|
|
293
297
|
Batch.untrack_enqueue(payload_hash)
|
|
294
|
-
elsif retry_retagged?(payload_hash)
|
|
298
|
+
elsif retry_retagged?(payload_hash, active_job)
|
|
295
299
|
# The retry never became live; the original attempt's row and its
|
|
296
300
|
# normal completion signal stand.
|
|
297
301
|
Batch.forget_retry_reenqueued(payload_hash["job_id"])
|
|
298
302
|
end
|
|
299
303
|
end
|
|
300
304
|
|
|
301
|
-
# Tagged for a batch
|
|
302
|
-
#
|
|
303
|
-
def retry_retagged?(payload_hash)
|
|
304
|
-
payload_hash[Batch::METADATA_KEY] &&
|
|
305
|
+
# Tagged for a batch the job was neither added to nor enqueued inside
|
|
306
|
+
# the block of — only a retry_on re-enqueue gets there (issue #424).
|
|
307
|
+
def retry_retagged?(payload_hash, active_job)
|
|
308
|
+
payload_hash[Batch::METADATA_KEY] && member_batch_id(active_job).nil?
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# The batch a job joins as a new member: the one it was added to, kept
|
|
312
|
+
# on the instance so it survives an enqueue deferred to after commit
|
|
313
|
+
# (issue #553), else the Batch#enqueue block active on this thread. The
|
|
314
|
+
# instance wins: a deferred enqueue can run while another batch's
|
|
315
|
+
# block is open on the committing thread.
|
|
316
|
+
def member_batch_id(active_job)
|
|
317
|
+
(active_job.enqueue_batch_id if active_job.respond_to?(:enqueue_batch_id)) ||
|
|
318
|
+
Thread.current[:pgbus_batch_id]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def forget_enqueue_batch_id(active_job)
|
|
322
|
+
active_job.enqueue_batch_id = nil if active_job.respond_to?(:enqueue_batch_id=)
|
|
305
323
|
end
|
|
306
324
|
|
|
307
325
|
# A retry_on re-enqueue of a job that is already a batch member: same
|
|
@@ -316,18 +334,17 @@ module Pgbus
|
|
|
316
334
|
active_job.batch_id
|
|
317
335
|
end
|
|
318
336
|
|
|
319
|
-
# Tag the payload with the
|
|
320
|
-
# increment + execution row, before the send — Batch.track_enqueue).
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
batch_id = Thread.current[:pgbus_batch_id]
|
|
337
|
+
# Tag the payload with the job's batch and count it in (guarded
|
|
338
|
+
# increment + execution row, before the send — Batch.track_enqueue).
|
|
339
|
+
def inject_batch_metadata(payload_hash, active_job)
|
|
340
|
+
batch_id = member_batch_id(active_job)
|
|
324
341
|
if batch_id
|
|
325
342
|
tagged = payload_hash.merge(Batch::METADATA_KEY => batch_id)
|
|
326
|
-
Batch.track_enqueue(tagged)
|
|
343
|
+
Batch.track_enqueue(tagged)
|
|
327
344
|
return tagged
|
|
328
345
|
end
|
|
329
346
|
|
|
330
|
-
retry_batch_id =
|
|
347
|
+
retry_batch_id = retry_batch_id_for(active_job)
|
|
331
348
|
return payload_hash unless retry_batch_id
|
|
332
349
|
|
|
333
350
|
tagged = payload_hash.merge(Batch::METADATA_KEY => retry_batch_id)
|
|
@@ -335,14 +352,20 @@ module Pgbus
|
|
|
335
352
|
tagged
|
|
336
353
|
end
|
|
337
354
|
|
|
355
|
+
# Only tags: the bulk is counted once per batch below. A retry re-enqueue
|
|
356
|
+
# never takes the bulk path (retry_on enqueues one job at a time).
|
|
357
|
+
def tag_bulk_member(payload_hash, active_job)
|
|
358
|
+
batch_id = member_batch_id(active_job)
|
|
359
|
+
batch_id ? payload_hash.merge(Batch::METADATA_KEY => batch_id) : payload_hash
|
|
360
|
+
end
|
|
361
|
+
|
|
338
362
|
def enqueue_immediate(queue, jobs, priority: nil)
|
|
339
363
|
return if jobs.empty?
|
|
340
364
|
|
|
341
365
|
payloads = jobs.map do |j|
|
|
342
|
-
|
|
366
|
+
tag_bulk_member(FairShare.inject_metadata(j, Serializer.serialize_job_hash(j)), j)
|
|
343
367
|
end
|
|
344
|
-
|
|
345
|
-
Batch.track_enqueue(payloads) if Thread.current[:pgbus_batch_id]
|
|
368
|
+
track_bulk(payloads)
|
|
346
369
|
physical = physical_queue(queue, priority)
|
|
347
370
|
msg_ids = nil
|
|
348
371
|
msg_ids = Pgbus.client.send_batch(queue, payloads, priority: priority)
|
|
@@ -361,6 +384,24 @@ module Pgbus
|
|
|
361
384
|
rescue Pgbus::SchemaNotReady => e
|
|
362
385
|
Pgbus.logger.error { "[Pgbus] #{e.message}" }
|
|
363
386
|
raise
|
|
387
|
+
ensure
|
|
388
|
+
jobs.each { |j| forget_enqueue_batch_id(j) }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# One guarded increment per batch for the whole bulk, not one per job.
|
|
392
|
+
# When a later batch refuses (finished), the batches already counted are
|
|
393
|
+
# uncounted before the error goes up: nothing has been sent yet.
|
|
394
|
+
def track_bulk(payloads)
|
|
395
|
+
tracked = []
|
|
396
|
+
payloads.select { |p| p[Batch::METADATA_KEY] }
|
|
397
|
+
.group_by { |p| p[Batch::METADATA_KEY] }
|
|
398
|
+
.each_value do |members|
|
|
399
|
+
Batch.track_enqueue(members)
|
|
400
|
+
tracked.concat(members)
|
|
401
|
+
end
|
|
402
|
+
rescue StandardError
|
|
403
|
+
tracked.each { |payload| Batch.untrack_enqueue(payload) }
|
|
404
|
+
raise
|
|
364
405
|
end
|
|
365
406
|
|
|
366
407
|
def scheduled_in_future?(job)
|
|
@@ -16,11 +16,33 @@ module Pgbus
|
|
|
16
16
|
# Both round-trip through +serialize+/+deserialize+, and both are omitted
|
|
17
17
|
# from the serialized hash when unset — an unbatched job's payload is
|
|
18
18
|
# byte-for-byte what it was before this mixin existed.
|
|
19
|
+
#
|
|
20
|
+
# A third, transient id, +enqueue_batch_id+, is the batch this instance
|
|
21
|
+
# was *added* to. +b.add+ / +batch.add+ set it, and +enqueue+ copies the
|
|
22
|
+
# block's tag onto it, before Active Job decides whether to defer the
|
|
23
|
+
# enqueue to after commit (+enqueue_after_transaction_commit+). The
|
|
24
|
+
# deferred enqueue keeps the instance, so the adapter still finds the
|
|
25
|
+
# batch once the block's thread tag is gone (issue #553). It is never
|
|
26
|
+
# serialized and the adapter clears it once the enqueue is done.
|
|
19
27
|
module BatchId
|
|
20
28
|
extend ActiveSupport::Concern
|
|
21
29
|
|
|
22
30
|
included do
|
|
23
|
-
attr_accessor :batch_id, :callback_batch_id
|
|
31
|
+
attr_accessor :batch_id, :callback_batch_id, :enqueue_batch_id
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Mark +jobs+ as added to +batch_id+ (instances without this mixin are
|
|
35
|
+
# left alone; they join through the block's thread tag only).
|
|
36
|
+
def self.capture(jobs, batch_id)
|
|
37
|
+
jobs.each { |job| job.enqueue_batch_id = batch_id if job.respond_to?(:enqueue_batch_id=) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Plain assignment, not ||=: an id left by an add whose enqueue never
|
|
41
|
+
# ran (rolled back, or aborted by a before_enqueue) must not follow the
|
|
42
|
+
# instance into this one. b.add / batch.add never come through here.
|
|
43
|
+
def enqueue(...)
|
|
44
|
+
self.enqueue_batch_id = Thread.current[:pgbus_batch_id]
|
|
45
|
+
super
|
|
24
46
|
end
|
|
25
47
|
|
|
26
48
|
def serialize
|
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,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/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.19.
|
|
4
|
+
version: 0.19.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -364,7 +364,10 @@ files:
|
|
|
364
364
|
- lib/pgbus/integrations/appsignal/dashboards/pgbus_health.json
|
|
365
365
|
- lib/pgbus/integrations/appsignal/dashboards/pgbus_streams.json
|
|
366
366
|
- lib/pgbus/integrations/appsignal/dashboards/pgbus_throughput.json
|
|
367
|
+
- lib/pgbus/integrations/appsignal/event_span.rb
|
|
368
|
+
- lib/pgbus/integrations/appsignal/job_span.rb
|
|
367
369
|
- lib/pgbus/integrations/appsignal/probe.rb
|
|
370
|
+
- lib/pgbus/integrations/appsignal/span_subscriber.rb
|
|
368
371
|
- lib/pgbus/integrations/appsignal/subscriber.rb
|
|
369
372
|
- lib/pgbus/log_formatter.rb
|
|
370
373
|
- lib/pgbus/mcp.rb
|