pgbus 0.19.1 → 0.19.2
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 +6 -0
- data/README.md +21 -27
- data/config/locales/en.yml +1 -1
- data/lib/generators/pgbus/add_batch_callback_jobs_generator.rb +4 -1
- data/lib/pgbus/active_job/batch_id.rb +10 -4
- data/lib/pgbus/batch/builder.rb +92 -0
- data/lib/pgbus/batch.rb +97 -22
- data/lib/pgbus/generators/migration_detector.rb +1 -1
- data/lib/pgbus/instrumentation.rb +11 -0
- data/lib/pgbus/integrations/appsignal/subscriber.rb +15 -0
- data/lib/pgbus/metrics/subscriber.rb +12 -0
- data/lib/pgbus/process/consumer.rb +56 -3
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus.rb +11 -0
- data/sig/pgbus.rbs +9 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb2ec8b622a127040f7ec0d98d514397b66fe2bda51d910bdf7109aee1cb9201
|
|
4
|
+
data.tar.gz: be53e0ecc3dc3700cd8daa769cf067190244dc4b957d1c8e022adb2515f695df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65ad9a9f4bf9a3bdb9263a0603178370595b2b552336c4a74dacfb5d5575cf14f75ec1e59c2dbcec3c148bd0d5e6a8782fbf47bca89e5d0eb68ba5504b74743c
|
|
7
|
+
data.tar.gz: a3389974026f5626913e1038c39cfb099f8f982dcd5df968ad7ba09e0b6da747a3e6a41b4823ea843415558d3bd576db7922836e3556c43c94ae56c48f2ff529
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
### Added
|
|
4
4
|
|
|
5
5
|
- **Deploy tasks: one-time work owed by a release, run once the fleet has rolled over.** Declare a backfill, data fix or cache warm in `config/deploy_tasks.yml` (the shape of `recurring.yml` minus `schedule:`, plus `version:` and `every_deploy:`) or `config.deploy_tasks`; the scheduler enqueues it once per `(key, version)` (or once per revision for an `every_deploy:` task) after every live pgbus process reports the new app revision and a worker on it is alive, so it never runs on old code during the overlap window and two schedulers never both fire it. It is not exactly-once: a failure between the enqueue and recording the run can repeat a task after a scheduler restart, and a scheduler killed between claiming and enqueueing loses that arm, so deploy tasks must be idempotent. Every pgbus process stamps the revision (`config.app_revision`, `PGBUS_REVISION`, `DASH_VERSION`, `KAMAL_VERSION` or the `REVISION` file) into its heartbeat. The dashboard gets a "Deploy tasks" page with the rollover status, Run now and Enable/Disable; run history lives in `pgbus_deploy_task_runs` (`deploy_task_run_retention`, default 90 days); `pgbus.deploy_task.enqueue` is instrumented and the MCP server gains `pgbus_deploy_tasks`. Set up with `rails generate pgbus:add_deploy_tasks` (`pgbus:update` detects the missing tables). Refs #536.
|
|
6
|
+
- **`pgbus.event_dead_lettered`: the event consumer says when it dead-letters an event.** A job that exhausted `max_retries` emitted `pgbus.job_dead_lettered`; an event moved to `<queue>_dlq` the same way emitted nothing, so a pager, an error tracker or a metrics counter had no push signal for dead events. `Process::Consumer` now emits `pgbus.event_dead_lettered` once per dead-lettered event, after `move_to_dead_letter` returns and the failed-event row is cleared, with `queue_name` (logical), `routing_key`, `handler` (the class that owns the queue in this process, or nil), `event_id`, `read_ct`, `msg_id` and `error` (the class of the last recorded failure, or nil). `handler` is nil unless exactly one subscriber owns the queue. A body that is not a JSON object (unparseable text, or a jsonb scalar or array written straight to the queue) is still dead-lettered, with `routing_key`, `handler` and `event_id` nil, and its `error` is the consumer's own failure to read it (`JSON::ParserError`, `NoMethodError` or `TypeError`): no handler ran. A move that raises emits nothing. A subscriber that raises is logged at debug level and does not change the outcome: the message stays dead-lettered, with one `dead_lettered` stat, no `failed` stat and no circuit-breaker failure. The AppSignal and metrics subscribers count it as `pgbus_event_count` with `status: "dead_lettered"`, tagged `handler` and `routing_key`. `pgbus.job_dead_lettered` and the consumer's success path are unchanged: the envelope is parsed a second time only on the dead-letter branch. Refs #543.
|
|
7
|
+
- **feat(batches): `Pgbus.batch do |b| … end`, a builder for starting a batch.** `b.on_complete`, `b.on_success` and `b.on_failure` take an ActiveJob class or a configured instance; `b.add(*jobs)` takes job instances (each keeps its own `.set(wait:, queue:, priority:)`) and sends them in one bulk enqueue. The batch row exists, and a `uniqueness_key` is held, before the block runs, and the batch stays pending until the block returns, so the calls can come in any order and a job that finishes while the block is still adding cannot finish the batch early. Every `perform_later` inside the block joins the batch, as inside `Batch#enqueue`. With `on_conflict: :discard` or `:log` and the key held, the block does not run. Inside a job, `batch(description: …) { |b| … }` opens a new, independent batch; `batch` without a block still reads the job's own batch. A member job adds siblings with `batch.add(NextJob.new(id))`, the open-batch form of `batch.enqueue { … }`. `Pgbus::Batch.start` is the same as `Pgbus.batch`, and `Pgbus::Batch.new` + `#enqueue` stay as the lower-level API. Refs #538.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **The batch completion callback is `on_complete`.** `Pgbus::Batch.new(on_complete:)`, the builder's `b.on_complete` and the `#on_complete` reader replace `on_finish`, which keeps working as a deprecated alias until 1.0 and logs one warning per process. The database columns stay `on_finish_class` / `on_finish_job` until the 1.0 schema cleanup, so batches created before upgrading still fire, and the `pgbus.batch_finished` instrumentation event keeps its name. The dashboard's batch page labels the callback "On Complete". Refs #538.
|
|
6
12
|
|
|
7
13
|
## [0.19.0] - 2026-10-16
|
|
8
14
|
|
data/README.md
CHANGED
|
@@ -801,28 +801,27 @@ How Pgbus integrates with your application's transactions and tracks groups of r
|
|
|
801
801
|
Coordinate groups of jobs with callbacks when all complete:
|
|
802
802
|
|
|
803
803
|
```ruby
|
|
804
|
-
batch = Pgbus
|
|
805
|
-
|
|
806
|
-
on_success
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
properties: { initiated_by: current_user.id }
|
|
810
|
-
)
|
|
811
|
-
|
|
812
|
-
batch.enqueue do
|
|
813
|
-
users.each { |user| ImportUserJob.perform_later(user.id) }
|
|
804
|
+
batch = Pgbus.batch(description: "Import users", properties: { initiated_by: current_user.id }) do |b|
|
|
805
|
+
b.on_complete BatchFinishedJob
|
|
806
|
+
b.on_success BatchSucceededJob
|
|
807
|
+
b.on_failure BatchFailedJob
|
|
808
|
+
b.add(*users.map { |user| ImportUserJob.new(user.id) })
|
|
814
809
|
end
|
|
815
810
|
```
|
|
816
811
|
|
|
812
|
+
`b.add` takes job instances (each keeps its own `.set(wait:, queue:)`) and sends them in one bulk enqueue. The batch row exists before the block runs and stays pending until it returns, so a job that finishes while the block is still adding cannot finish the batch early. Set callbacks first: if the block raises after an `add`, the added jobs still run and the batch finishes later with only the callbacks set by then. A block that goes `config.batch_stall_threshold` (default 5 minutes) without adding a job is started by the stalled-batch sweep, so keep slow work outside it. Callbacks are job classes or configured instances, never blocks: they are stored on the batch row.
|
|
813
|
+
|
|
814
|
+
Inside a job, `batch { |b| ... }` opens a new batch the same way, and a job that is already a member adds siblings with `batch.add(NextJob.new(id))`. `Pgbus::Batch.new(on_complete: ...)` plus `batch.enqueue { ... }` remains the lower-level form.
|
|
815
|
+
|
|
817
816
|
#### Callbacks
|
|
818
817
|
|
|
819
818
|
| Callback | Fired when |
|
|
820
819
|
|----------|------------|
|
|
821
|
-
| `
|
|
822
|
-
| `on_success` | All jobs completed successfully (zero
|
|
823
|
-
| `
|
|
820
|
+
| `on_complete` | All jobs completed (success or failure). `on_finish` is a deprecated alias until 1.0 |
|
|
821
|
+
| `on_success` | All jobs completed successfully (zero failed) |
|
|
822
|
+
| `on_failure` | At least one job was dead-lettered. `on_discard` is a deprecated alias until 1.0 |
|
|
824
823
|
|
|
825
|
-
Callback jobs receive the batch `properties` hash as their argument:
|
|
824
|
+
Callback jobs given as a bare class receive the batch `properties` hash as their argument:
|
|
826
825
|
|
|
827
826
|
```ruby
|
|
828
827
|
class BatchFinishedJob < ApplicationJob
|
|
@@ -838,23 +837,18 @@ end
|
|
|
838
837
|
`ensures_uniqueness` protects one job. A multi-job run — a nightly import that fans out into hundreds of chunk jobs, a pipeline whose jobs enqueue the next stage — has no single job that lives for the whole run, so a lock on the entry job releases the moment that job finishes and the next scheduler tick starts a second run on top of the first. Give the batch the lock instead:
|
|
839
838
|
|
|
840
839
|
```ruby
|
|
841
|
-
batch
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
on_success: ExportPricesJob
|
|
845
|
-
)
|
|
846
|
-
|
|
847
|
-
batch.enqueue do
|
|
848
|
-
ImportChunkJob.perform_later("products", 0)
|
|
840
|
+
Pgbus.batch(uniqueness_key: "perfecta-daily", on_conflict: :discard) do |b| # :reject (raise AlreadyRunning), :discard, :log
|
|
841
|
+
b.on_success ExportPricesJob
|
|
842
|
+
b.add(ImportChunkJob.new("products", 0))
|
|
849
843
|
end
|
|
850
844
|
```
|
|
851
845
|
|
|
852
|
-
The lock is taken at `enqueue` and held until the batch finishes — through every open-batch `batch.
|
|
846
|
+
The lock is taken at the top of the block (or at `enqueue`) and held until the batch finishes — through every open-batch `batch.add` from inside a running job — then released in the same single-winner finish that fires the callbacks, so completion, the stalled-batch sweep and cleanup all release it. While it is held, another `Pgbus.batch(uniqueness_key: "perfecta-daily") { ... }` raises `Pgbus::Batch::AlreadyRunning` (`:reject`), or skips its block and returns a batch that answers `discarded?` (`:discard`, `:log`). The row lives in `pgbus_uniqueness_keys` as `batch:<key>`, so it never collides with a job's key and shows up in the dashboard like any other lock; the reaper judges it by the batch's status, never by queue contents.
|
|
853
847
|
|
|
854
848
|
#### How batches work
|
|
855
849
|
|
|
856
|
-
1. `
|
|
857
|
-
2.
|
|
850
|
+
1. The top of the `Pgbus.batch` block (or the first `batch.enqueue`) creates a tracking row in `pgbus_batches` with `status: "pending"`
|
|
851
|
+
2. Each job enqueued in the block is tagged with the `pgbus_batch_id` in its payload
|
|
858
852
|
3. After each job completes or is dead-lettered, the executor atomically updates the batch counters
|
|
859
853
|
4. When `completed_jobs + discarded_jobs == total_jobs`, the batch status flips to `"finished"` and callback jobs are enqueued
|
|
860
854
|
5. The dispatcher cleans up finished batches older than 7 days
|
|
@@ -1017,7 +1011,7 @@ c.metrics_backend = MyOpenTelemetryBackend.new
|
|
|
1017
1011
|
| `pgbus_queue_job_count` | counter | `queue`, `job_class`, `status` (`processed`/`failed`/`dead_lettered`) |
|
|
1018
1012
|
| `pgbus_visibility_extended` | counter | `queue`, `job_class` — one per re-armed visibility timeout; a class listed here outlives `visibility_timeout` |
|
|
1019
1013
|
| `pgbus_job_duration_ms` | histogram | `queue`, `job_class` |
|
|
1020
|
-
| `pgbus_event_count` | counter | `handler`, `routing_key`, `status` |
|
|
1014
|
+
| `pgbus_event_count` | counter | `handler`, `routing_key`, `status` (`processed`/`failed`/`skipped`/`dead_lettered`), plus `reason` on `skipped` only (`completed`/`cached`/`owned`) |
|
|
1021
1015
|
| `pgbus_event_duration_ms` | histogram | `handler`, `routing_key` |
|
|
1022
1016
|
| `pgbus_messages_sent` / `pgbus_messages_read` | counter | `queue` |
|
|
1023
1017
|
| `pgbus_stream_broadcast_count` | counter | `stream`, `deferred` |
|
|
@@ -1052,7 +1046,7 @@ ActiveSupport::Notifications.subscribe(/^pgbus\./) do |name, start, finish, _id,
|
|
|
1052
1046
|
end
|
|
1053
1047
|
```
|
|
1054
1048
|
|
|
1055
|
-
Metrics-relevant events emitted (the subset the metrics subscriber maps; see `lib/pgbus/instrumentation.rb` for the full `pgbus.*` catalog): `pgbus.executor.execute`, `pgbus.job_completed`, `pgbus.job_failed`, `pgbus.job_dead_lettered`, `pgbus.event_processed`, `pgbus.event_failed`, `pgbus.client.send_message`, `pgbus.client.send_batch`, `pgbus.client.read_batch`, `pgbus.client.pool`, `pgbus.stream.broadcast`, `pgbus.outbox.publish`, `pgbus.recurring.enqueue`, `pgbus.worker.recycle`, `pgbus.consumer.recycle`. Payload keys are documented in `lib/pgbus/instrumentation.rb`.
|
|
1049
|
+
Metrics-relevant events emitted (the subset the metrics subscriber maps; see `lib/pgbus/instrumentation.rb` for the full `pgbus.*` catalog): `pgbus.executor.execute`, `pgbus.job_completed`, `pgbus.job_failed`, `pgbus.job_dead_lettered`, `pgbus.event_processed`, `pgbus.event_failed`, `pgbus.event_dead_lettered`, `pgbus.event_skipped`, `pgbus.job_visibility_extended`, `pgbus.client.send_message`, `pgbus.client.send_batch`, `pgbus.client.read_batch`, `pgbus.client.pool`, `pgbus.stream.broadcast`, `pgbus.outbox.publish`, `pgbus.recurring.enqueue`, `pgbus.worker.recycle`, `pgbus.consumer.recycle`. Payload keys are documented in `lib/pgbus/instrumentation.rb`.
|
|
1056
1050
|
|
|
1057
1051
|
### Connection pool metrics
|
|
1058
1052
|
|
data/config/locales/en.yml
CHANGED
|
@@ -33,7 +33,10 @@ module Pgbus
|
|
|
33
33
|
say " 2. Restart pgbus: bin/pgbus start"
|
|
34
34
|
say ""
|
|
35
35
|
say "You can now pass a configured ActiveJob instance to a batch:"
|
|
36
|
-
say " Pgbus
|
|
36
|
+
say " Pgbus.batch do |b|"
|
|
37
|
+
say " b.on_complete ReportJob.new.set(queue: :critical, wait: 5.minutes)"
|
|
38
|
+
say " b.add(*rows.map { |row| ImportRowJob.new(row.id) })"
|
|
39
|
+
say " end"
|
|
37
40
|
say ""
|
|
38
41
|
end
|
|
39
42
|
|
|
@@ -8,9 +8,9 @@ module Pgbus
|
|
|
8
8
|
#
|
|
9
9
|
# * +batch_id+ — the batch this job is a *member* of. Assigned by the
|
|
10
10
|
# executor from the payload's +pgbus_batch_id+ before +perform+, so a
|
|
11
|
-
# running job can call +batch.
|
|
11
|
+
# running job can call +batch.add+ to add siblings.
|
|
12
12
|
# * +callback_batch_id+ — the batch this job *reports on*. Set on
|
|
13
|
-
# +
|
|
13
|
+
# +on_complete+/+on_success+/+on_failure+ jobs at fire time. A callback is
|
|
14
14
|
# never a member of the batch it reports on, so its +batch_id+ is nil.
|
|
15
15
|
#
|
|
16
16
|
# Both round-trip through +serialize+/+deserialize+, and both are omitted
|
|
@@ -36,8 +36,14 @@ module Pgbus
|
|
|
36
36
|
self.callback_batch_id = job_data["callback_batch_id"]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
|
|
39
|
+
# Without a block: the batch this job reports on, or is a member of; nil
|
|
40
|
+
# when neither. With a block: open a new, independent batch through
|
|
41
|
+
# Pgbus.batch (this job's own membership is untouched).
|
|
42
|
+
#
|
|
43
|
+
# batch(description: "Import") { |b| b.add(*rows.map { |r| RowJob.new(r.id) }) }
|
|
44
|
+
def batch(**options, &block)
|
|
45
|
+
return Pgbus.batch(**options, &block) if block
|
|
46
|
+
raise ArgumentError, "batch(...) with options needs a block: batch(description: ...) { |b| ... }" if options.any?
|
|
41
47
|
return @batch if defined?(@batch)
|
|
42
48
|
|
|
43
49
|
id = callback_batch_id || batch_id
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class Batch
|
|
5
|
+
# What Pgbus.batch (and +batch { |b| ... }+ inside a job) yields: callback
|
|
6
|
+
# setters and +add+ on a batch that is open for the length of the block.
|
|
7
|
+
#
|
|
8
|
+
# Pgbus.batch(description: "Import #{source.id}") do |b|
|
|
9
|
+
# b.on_complete ImportDoneJob.new.set(queue: :critical)
|
|
10
|
+
# b.on_failure ImportFailedJob
|
|
11
|
+
# b.add(*source.rows.map { |row| ImportRowJob.new(row.id) })
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# The batch row exists (and a uniqueness key is held) before the block
|
|
15
|
+
# runs, and it stays pending until the block returns, so the order of the
|
|
16
|
+
# calls does not matter and a job that completes during the block cannot
|
|
17
|
+
# finish the batch early. Once the block has returned the builder is
|
|
18
|
+
# closed: add more jobs through +batch.add+ on the returned batch.
|
|
19
|
+
class Builder
|
|
20
|
+
CLOSED_MESSAGE = "the batch builder only works inside its Pgbus.batch block; " \
|
|
21
|
+
"add more jobs later with batch.add on the batch Pgbus.batch returned"
|
|
22
|
+
UNTAGGED_MESSAGE = "b.add ran on a thread or fiber outside its Pgbus.batch block (or inside another " \
|
|
23
|
+
"batch's block), where its jobs would not join the batch; call b.add from the block itself"
|
|
24
|
+
|
|
25
|
+
# Flatten +jobs+ and check that each one is an ActiveJob instance.
|
|
26
|
+
def self.job_instances(jobs)
|
|
27
|
+
jobs = jobs.flatten
|
|
28
|
+
invalid = jobs.grep_v(::ActiveJob::Base)
|
|
29
|
+
return jobs if invalid.empty?
|
|
30
|
+
|
|
31
|
+
raise ArgumentError,
|
|
32
|
+
"a batch takes job instances (MyJob.new(args), optionally .set(wait:, queue:)), got #{invalid.first.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(batch)
|
|
36
|
+
@batch = batch
|
|
37
|
+
@closed = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_complete(job = nil, &) = set_callback(:on_complete, job, &)
|
|
41
|
+
|
|
42
|
+
def on_success(job = nil, &) = set_callback(:on_success, job, &)
|
|
43
|
+
|
|
44
|
+
def on_failure(job = nil, &) = set_callback(:on_failure, job, &)
|
|
45
|
+
|
|
46
|
+
# Deprecated alias of #on_complete until 1.0.
|
|
47
|
+
def on_finish(job = nil, &)
|
|
48
|
+
Batch.warn_on_finish_deprecated
|
|
49
|
+
set_callback(:on_complete, job, &)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Enqueue job instances into the batch with one bulk send (and anything
|
|
53
|
+
# the block enqueues). Returns the jobs. Membership is the batch tag the
|
|
54
|
+
# block holds (Thread.current, so fiber-local), so an add from another
|
|
55
|
+
# thread or fiber, or from inside a nested batch's block, raises rather
|
|
56
|
+
# than send jobs that would not be counted.
|
|
57
|
+
def add(*jobs)
|
|
58
|
+
raise ArgumentError, CLOSED_MESSAGE if @closed
|
|
59
|
+
raise ArgumentError, UNTAGGED_MESSAGE unless Thread.current[:pgbus_batch_id] == @batch.batch_id
|
|
60
|
+
|
|
61
|
+
jobs = self.class.job_instances(jobs)
|
|
62
|
+
::ActiveJob.perform_all_later(jobs) if jobs.any?
|
|
63
|
+
yield if block_given?
|
|
64
|
+
jobs
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Called by Batch.start when the block returns or raises.
|
|
68
|
+
def close
|
|
69
|
+
@closed = true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def set_callback(slot, job, &block)
|
|
75
|
+
raise ArgumentError, CLOSED_MESSAGE if @closed
|
|
76
|
+
|
|
77
|
+
unless block.nil? && callback_job?(job)
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"#{slot} takes an ActiveJob class or a configured instance (MyJob.new.set(queue: :critical)), " \
|
|
80
|
+
"not #{block ? "a block" : job.inspect}: callbacks are stored on the batch row"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@batch.send(:write_callback, slot, job)
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def callback_job?(job)
|
|
88
|
+
(job.is_a?(Class) && job < ::ActiveJob::Base) || job.is_a?(::ActiveJob::Base)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/pgbus/batch.rb
CHANGED
|
@@ -21,21 +21,57 @@ module Pgbus
|
|
|
21
21
|
LOCK_QUEUE_PREFIX = "batch:"
|
|
22
22
|
VALID_CONFLICTS = %i[reject discard log].freeze
|
|
23
23
|
|
|
24
|
+
# The callback slots, each mapped to the prefix of its pgbus_batches
|
|
25
|
+
# columns. on_complete keeps the on_finish_* columns until the 1.0 schema
|
|
26
|
+
# cleanup, so rows written before the rename still fire.
|
|
27
|
+
CALLBACK_COLUMNS = { on_complete: "on_finish", on_success: "on_success", on_failure: "on_failure" }.freeze
|
|
28
|
+
|
|
24
29
|
attr_reader :batch_id, :properties, :description,
|
|
25
|
-
:
|
|
30
|
+
:on_complete, :on_success, :on_failure,
|
|
26
31
|
:uniqueness_key, :on_conflict
|
|
27
32
|
|
|
33
|
+
# Deprecated alias of #on_complete until 1.0.
|
|
34
|
+
def on_finish = on_complete
|
|
35
|
+
|
|
28
36
|
def on_discard
|
|
29
37
|
on_failure
|
|
30
38
|
end
|
|
31
39
|
|
|
40
|
+
# Open a batch, yield a Pgbus::Batch::Builder, and start the batch when
|
|
41
|
+
# the block returns. Every job the block enqueues (through +b.add+ or a
|
|
42
|
+
# plain +perform_later+) joins the batch, which stays pending, and so
|
|
43
|
+
# cannot finish, until the block is done. Keywords go to Batch.new.
|
|
44
|
+
# Membership is the block's own thread or fiber (the tag lives in
|
|
45
|
+
# Thread.current): b.add raises anywhere else, and a plain perform_later
|
|
46
|
+
# from a thread or fiber the block spawns does not join the batch.
|
|
47
|
+
#
|
|
48
|
+
# Pgbus::Batch.start(description: "Import") do |b|
|
|
49
|
+
# b.on_complete ImportDoneJob
|
|
50
|
+
# b.add(*rows.map { |row| ImportRowJob.new(row.id) })
|
|
51
|
+
# end
|
|
52
|
+
def self.start(**, &block)
|
|
53
|
+
raise ArgumentError, "Pgbus.batch needs a block: Pgbus.batch { |b| b.add(...) }" unless block
|
|
54
|
+
|
|
55
|
+
batch = new(**)
|
|
56
|
+
builder = Builder.new(batch)
|
|
57
|
+
begin
|
|
58
|
+
batch.enqueue { yield builder }
|
|
59
|
+
ensure
|
|
60
|
+
builder.close
|
|
61
|
+
end
|
|
62
|
+
batch
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param on_complete [Class, ActiveJob::Base, nil] fired when every job
|
|
66
|
+
# reached a terminal state (+on_finish:+ is a deprecated alias)
|
|
32
67
|
# @param uniqueness_key [String, nil] at most one unfinished batch with
|
|
33
68
|
# this key may exist; see #enqueue for what happens to the next one
|
|
34
69
|
# @param on_conflict [Symbol] :reject (raise AlreadyRunning), :discard
|
|
35
70
|
# (skip the block silently) or :log (skip the block, warn)
|
|
36
|
-
def initialize(
|
|
37
|
-
uniqueness_key: nil, on_conflict: :reject)
|
|
71
|
+
def initialize(on_complete: nil, on_finish: nil, on_success: nil, on_discard: nil, on_failure: nil,
|
|
72
|
+
description: nil, properties: {}, uniqueness_key: nil, on_conflict: :reject)
|
|
38
73
|
raise ArgumentError, "pass on_failure: only — on_discard: is a deprecated alias" if on_discard && on_failure
|
|
74
|
+
raise ArgumentError, "pass on_complete: only — on_finish: is a deprecated alias" if on_finish && on_complete
|
|
39
75
|
unless VALID_CONFLICTS.include?(on_conflict)
|
|
40
76
|
raise ArgumentError, "on_conflict must be one of #{VALID_CONFLICTS.join(", ")}, got #{on_conflict.inspect}"
|
|
41
77
|
end
|
|
@@ -45,9 +81,10 @@ module Pgbus
|
|
|
45
81
|
"[Pgbus] Batch on_discard: is deprecated and will be removed in 1.0 — use on_failure: instead"
|
|
46
82
|
end
|
|
47
83
|
end
|
|
84
|
+
self.class.warn_on_finish_deprecated if on_finish
|
|
48
85
|
|
|
49
86
|
@batch_id = SecureRandom.uuid
|
|
50
|
-
@
|
|
87
|
+
@on_complete = on_complete || on_finish
|
|
51
88
|
@on_success = on_success
|
|
52
89
|
@on_failure = on_failure || on_discard
|
|
53
90
|
@description = description
|
|
@@ -91,6 +128,23 @@ module Pgbus
|
|
|
91
128
|
self
|
|
92
129
|
end
|
|
93
130
|
|
|
131
|
+
# Enqueue ActiveJob instances (and anything the block enqueues) into this
|
|
132
|
+
# batch: +enqueue { ActiveJob.perform_all_later(jobs) }+. Instances keep
|
|
133
|
+
# their own +.set(queue:, wait:, priority:)+ options. On a started batch
|
|
134
|
+
# this is an open-batch add, so a job running inside the batch can call
|
|
135
|
+
# +batch.add(NextJob.new(id))+. Returns the jobs; on a unique batch that
|
|
136
|
+
# another run discarded (#discarded?) nothing was sent.
|
|
137
|
+
def add(*jobs, &block)
|
|
138
|
+
jobs = Builder.job_instances(jobs)
|
|
139
|
+
return jobs if jobs.empty? && block.nil?
|
|
140
|
+
|
|
141
|
+
enqueue do
|
|
142
|
+
::ActiveJob.perform_all_later(jobs) if jobs.any?
|
|
143
|
+
yield if block
|
|
144
|
+
end
|
|
145
|
+
jobs
|
|
146
|
+
end
|
|
147
|
+
|
|
94
148
|
# --- readers on a live batch ---------------------------------------
|
|
95
149
|
|
|
96
150
|
def status = record&.status
|
|
@@ -210,11 +264,22 @@ module Pgbus
|
|
|
210
264
|
end
|
|
211
265
|
|
|
212
266
|
# Schema-probe memos (a true answer is kept for the life of the process)
|
|
213
|
-
# and the warn-once
|
|
267
|
+
# and the warn-once flags, read and written under @schema_lock (issue
|
|
214
268
|
# #527). The probes run outside it: a duplicate probe on a race is
|
|
215
269
|
# harmless, a lock held across a query is not.
|
|
216
270
|
@schema_lock = Mutex.new
|
|
217
271
|
|
|
272
|
+
# on_finish: is in most batch calls written before the rename (issue
|
|
273
|
+
# #538), so it warns once per process rather than on every batch.
|
|
274
|
+
def self.warn_on_finish_deprecated
|
|
275
|
+
first = @schema_lock.synchronize { @warned_on_finish ? false : (@warned_on_finish = true) }
|
|
276
|
+
return unless first
|
|
277
|
+
|
|
278
|
+
Pgbus.logger.warn do
|
|
279
|
+
"[Pgbus] Batch on_finish is deprecated and will be removed in 1.0 — use on_complete instead"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
218
283
|
def self.executions_migrated?
|
|
219
284
|
return true if @schema_lock.synchronize { @executions_migrated }
|
|
220
285
|
|
|
@@ -243,6 +308,7 @@ module Pgbus
|
|
|
243
308
|
@executions_migrated = nil
|
|
244
309
|
@callback_jobs_migrated = nil
|
|
245
310
|
@warned_callback_jobs_unmigrated = nil
|
|
311
|
+
@warned_on_finish = nil
|
|
246
312
|
end
|
|
247
313
|
end
|
|
248
314
|
|
|
@@ -521,7 +587,7 @@ module Pgbus
|
|
|
521
587
|
@batch_id = record.batch_id
|
|
522
588
|
@description = record.description
|
|
523
589
|
@properties = parse_properties(record.properties)
|
|
524
|
-
@
|
|
590
|
+
@on_complete = nil
|
|
525
591
|
@on_success = nil
|
|
526
592
|
@on_failure = nil
|
|
527
593
|
@uniqueness_key = nil
|
|
@@ -572,20 +638,37 @@ module Pgbus
|
|
|
572
638
|
attrs = {
|
|
573
639
|
batch_id: batch_id,
|
|
574
640
|
description: description,
|
|
575
|
-
on_finish_class: callback_class_name(on_finish),
|
|
576
|
-
on_success_class: callback_class_name(on_success),
|
|
577
641
|
properties: JSON.generate(properties),
|
|
578
642
|
status: "pending"
|
|
579
643
|
}
|
|
580
|
-
|
|
581
|
-
attrs[:on_failure_class] = callback_class_name(on_failure)
|
|
582
|
-
else
|
|
583
|
-
attrs[:on_discard_class] = callback_class_name(on_failure)
|
|
584
|
-
end
|
|
585
|
-
attrs.merge!(callback_job_attributes) if self.class.callback_jobs_migrated?
|
|
644
|
+
CALLBACK_COLUMNS.each_key { |slot| attrs.merge!(callback_attributes(slot, public_send(slot))) }
|
|
586
645
|
@record = BatchEntry.create!(attrs)
|
|
587
646
|
end
|
|
588
647
|
|
|
648
|
+
# Replace one callback on a batch that has not finished yet (the
|
|
649
|
+
# builder's setters, inside the Pgbus.batch block, while the row is
|
|
650
|
+
# still pending and so cannot fire its callbacks).
|
|
651
|
+
def write_callback(slot, callback)
|
|
652
|
+
updated = BatchEntry.where(batch_id: batch_id, status: %w[pending processing])
|
|
653
|
+
.update_all(callback_attributes(slot, callback))
|
|
654
|
+
raise AlreadyFinished, "Can't change the callbacks of an already finished batch" if updated.zero?
|
|
655
|
+
|
|
656
|
+
instance_variable_set(:"@#{slot}", callback)
|
|
657
|
+
reload
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# The pgbus_batches columns for one callback slot. Before the
|
|
661
|
+
# add_batch_executions migration the failure class lives in
|
|
662
|
+
# on_discard_class; before add_batch_callback_jobs there is no *_job
|
|
663
|
+
# column, and a configured instance degrades to its class.
|
|
664
|
+
def callback_attributes(slot, callback)
|
|
665
|
+
prefix = CALLBACK_COLUMNS.fetch(slot)
|
|
666
|
+
class_column = slot == :on_failure && !self.class.executions_migrated? ? "on_discard" : prefix
|
|
667
|
+
attrs = { "#{class_column}_class": callback_class_name(callback) }
|
|
668
|
+
attrs[:"#{prefix}_job"] = serialize_callback(callback) if self.class.callback_jobs_migrated?
|
|
669
|
+
attrs
|
|
670
|
+
end
|
|
671
|
+
|
|
589
672
|
# A callback given as a bare class keeps the legacy *_class column; a
|
|
590
673
|
# configured ActiveJob instance is serialized now (so .set options resolve
|
|
591
674
|
# at creation, matching solid_queue) into the *_job jsonb column. Before
|
|
@@ -600,14 +683,6 @@ module Pgbus
|
|
|
600
683
|
callback.class.name
|
|
601
684
|
end
|
|
602
685
|
|
|
603
|
-
def callback_job_attributes
|
|
604
|
-
{
|
|
605
|
-
on_finish_job: serialize_callback(on_finish),
|
|
606
|
-
on_success_job: serialize_callback(on_success),
|
|
607
|
-
on_failure_job: serialize_callback(on_failure)
|
|
608
|
-
}
|
|
609
|
-
end
|
|
610
|
-
|
|
611
686
|
def serialize_callback(callback)
|
|
612
687
|
return nil if callback.nil? || callback.is_a?(Class)
|
|
613
688
|
|
|
@@ -263,7 +263,7 @@ module Pgbus
|
|
|
263
263
|
[:add_batch_executions]
|
|
264
264
|
end
|
|
265
265
|
|
|
266
|
-
# Configured callback instances (
|
|
266
|
+
# Configured callback instances (on_complete: MyJob.new.set(...)) need
|
|
267
267
|
# somewhere to store the serialized job — issue #415.
|
|
268
268
|
def batch_callback_jobs_migrations
|
|
269
269
|
return [] unless table_exists?("pgbus_batches")
|
|
@@ -21,6 +21,17 @@ module Pgbus
|
|
|
21
21
|
# payload: queue, job_class, msg_id, vt, extensions
|
|
22
22
|
# pgbus.event_processed — event handler succeeded
|
|
23
23
|
# pgbus.event_failed — event handler raised; carries :exception_object
|
|
24
|
+
# pgbus.event_dead_lettered — a consumer moved an event that exceeded max_retries
|
|
25
|
+
# to its DLQ; fires once, after the move
|
|
26
|
+
# payload: queue_name, routing_key, handler, event_id,
|
|
27
|
+
# read_ct, msg_id, error (the class of the last
|
|
28
|
+
# recorded failure: a handler's error, or the
|
|
29
|
+
# consumer's own parse error for a body it could
|
|
30
|
+
# not route; nil when none was recorded). Any of
|
|
31
|
+
# routing_key/handler/event_id can be nil: all
|
|
32
|
+
# three when the body is not a JSON object, a
|
|
33
|
+
# field the envelope lacks, and handler when no
|
|
34
|
+
# single subscriber in this process owns the queue
|
|
24
35
|
# pgbus.event_unrouted — a consumer read an event from a queue no
|
|
25
36
|
# subscriber in this process owns (stale topic
|
|
26
37
|
# binding); the message is archived
|
|
@@ -60,6 +60,7 @@ module Pgbus
|
|
|
60
60
|
subscribe("pgbus.job_dead_lettered") { |event| on_job_dead_lettered(event) },
|
|
61
61
|
subscribe("pgbus.event_processed") { |event| on_event_processed(event) },
|
|
62
62
|
subscribe("pgbus.event_failed") { |event| on_event_failed(event) },
|
|
63
|
+
subscribe("pgbus.event_dead_lettered") { |event| on_event_dead_lettered(event) },
|
|
63
64
|
subscribe("pgbus.client.send_message") { |event| on_send_message(event) },
|
|
64
65
|
subscribe("pgbus.client.send_batch") { |event| on_send_batch(event) },
|
|
65
66
|
subscribe("pgbus.client.read_batch") { |event| on_read_batch(event) },
|
|
@@ -175,6 +176,20 @@ module Pgbus
|
|
|
175
176
|
::Appsignal.set_error(err) if err && ::Appsignal.respond_to?(:set_error)
|
|
176
177
|
end
|
|
177
178
|
|
|
179
|
+
# 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
|
|
182
|
+
# that is not JSON leaves handler and routing_key nil; compact drops
|
|
183
|
+
# those tags, as Metrics::Subscriber does.
|
|
184
|
+
def on_event_dead_lettered(event)
|
|
185
|
+
payload = event.payload
|
|
186
|
+
::Appsignal.increment_counter(
|
|
187
|
+
"#{METRIC_PREFIX}event_count",
|
|
188
|
+
1,
|
|
189
|
+
{ handler: payload[:handler], routing_key: payload[:routing_key], status: "dead_lettered" }.compact
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
178
193
|
# ── Client (PGMQ wrapper) ───────────────────────────────────────
|
|
179
194
|
|
|
180
195
|
def on_send_message(event)
|
|
@@ -63,6 +63,7 @@ module Pgbus
|
|
|
63
63
|
subscribe("pgbus.job_visibility_extended") { |event| on_job_visibility_extended(event) },
|
|
64
64
|
subscribe("pgbus.event_processed") { |event| on_event_processed(event) },
|
|
65
65
|
subscribe("pgbus.event_failed") { |event| on_event_failed(event) },
|
|
66
|
+
subscribe("pgbus.event_dead_lettered") { |event| on_event_dead_lettered(event) },
|
|
66
67
|
subscribe("pgbus.event_skipped") { |event| on_event_skipped(event) },
|
|
67
68
|
subscribe("pgbus.client.send_message") { |event| on_send_message(event) },
|
|
68
69
|
subscribe("pgbus.client.send_batch") { |event| on_send_batch(event) },
|
|
@@ -154,6 +155,17 @@ module Pgbus
|
|
|
154
155
|
)
|
|
155
156
|
end
|
|
156
157
|
|
|
158
|
+
# The consumer moved an event that exhausted max_retries to its DLQ
|
|
159
|
+
# (issue #543). A body that is not JSON leaves handler and routing_key
|
|
160
|
+
# nil; compact drops those tags.
|
|
161
|
+
def on_event_dead_lettered(event)
|
|
162
|
+
payload = event.payload
|
|
163
|
+
backend.increment(
|
|
164
|
+
"#{METRIC_PREFIX}event_count", 1,
|
|
165
|
+
compact(handler: payload[:handler], routing_key: payload[:routing_key], status: "dead_lettered")
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
157
169
|
# An idempotent handler that did not run this delivery. `reason` is what
|
|
158
170
|
# makes the skip readable: :completed/:cached is deduplication working,
|
|
159
171
|
# :owned means a second delivery arrived while the holder was still
|
|
@@ -289,9 +289,9 @@ module Pgbus
|
|
|
289
289
|
@jobs_processed.increment
|
|
290
290
|
end
|
|
291
291
|
|
|
292
|
-
# Order: read the last error, move with the dead-letter block,
|
|
293
|
-
#
|
|
294
|
-
# next attempt still carries the reason.
|
|
292
|
+
# Order: read the last error, move with the dead-letter block, clear the
|
|
293
|
+
# failed-event row, then announce it. A move that raises keeps the row,
|
|
294
|
+
# so the next attempt still carries the reason, and announces nothing.
|
|
295
295
|
def dead_letter(message, queue_name)
|
|
296
296
|
last_error = FailedEventRecorder.last_error(queue_name: queue_name, msg_id: message.msg_id.to_i)
|
|
297
297
|
headers = DeadLetterHeader.build(
|
|
@@ -301,6 +301,59 @@ module Pgbus
|
|
|
301
301
|
)
|
|
302
302
|
Pgbus.client.move_to_dead_letter(queue_name, message, headers: headers)
|
|
303
303
|
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: message.msg_id.to_i)
|
|
304
|
+
instrument_dead_lettered(message, queue_name, last_error)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# pgbus.event_dead_lettered, the event twin of the executor's
|
|
308
|
+
# pgbus.job_dead_lettered (issue #543). The dead-letter branch runs
|
|
309
|
+
# before handle_message parses the envelope, so it is parsed here: once
|
|
310
|
+
# per dead-lettered message, never on the success path. The move has
|
|
311
|
+
# already happened, so nothing here may turn the dead-letter into a
|
|
312
|
+
# failure (a failed stat, a circuit-breaker trip): the guard is the
|
|
313
|
+
# executor's instrument rescue, widened on purpose to cover building the
|
|
314
|
+
# payload as well as the subscribers, and logs at the same debug level.
|
|
315
|
+
def instrument_dead_lettered(message, queue_name, last_error)
|
|
316
|
+
envelope = dead_letter_envelope(message)
|
|
317
|
+
routing_key = envelope_routing_key(envelope)
|
|
318
|
+
Pgbus::Instrumentation.instrument(
|
|
319
|
+
"pgbus.event_dead_lettered",
|
|
320
|
+
queue_name: queue_name, routing_key: routing_key,
|
|
321
|
+
handler: owning_handler_name(routing_key, queue_name), event_id: envelope["event_id"],
|
|
322
|
+
read_ct: message.read_ct.to_i, msg_id: message.msg_id.to_i, error: last_error&.dig(:error_class)
|
|
323
|
+
)
|
|
324
|
+
rescue StandardError => e
|
|
325
|
+
Pgbus.logger.debug { "[Pgbus] Notification failure pgbus.event_dead_lettered: #{e.class}: #{e.message}" }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# The body that killed a message may not be a JSON object at all: PGMQ
|
|
329
|
+
# stores jsonb, so a producer writing straight to the queue can leave a
|
|
330
|
+
# scalar or an array, and the success path's dig raises on those every
|
|
331
|
+
# read. Any of them reads as an empty envelope, so the notification
|
|
332
|
+
# still fires.
|
|
333
|
+
def dead_letter_envelope(message)
|
|
334
|
+
raw = JSON.parse(message.message)
|
|
335
|
+
raw.is_a?(Hash) ? raw : {}
|
|
336
|
+
rescue JSON::ParserError, TypeError
|
|
337
|
+
{}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# The success path's lookup (headers first, then the top level),
|
|
341
|
+
# tolerating a headers value that is not an object.
|
|
342
|
+
def envelope_routing_key(envelope)
|
|
343
|
+
headers = envelope["headers"]
|
|
344
|
+
(headers["routing_key"] if headers.is_a?(Hash)) || envelope["routing_key"]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The handler of the one subscriber that owns +queue_name+ in this
|
|
348
|
+
# process, or nil when there is none, no usable routing key, or more than
|
|
349
|
+
# one: #469 makes one owner per queue the convention, but Registry does
|
|
350
|
+
# not enforce it, and dispatch runs every owner, so naming one of several
|
|
351
|
+
# would misattribute the dead letter.
|
|
352
|
+
def owning_handler_name(routing_key, queue_name)
|
|
353
|
+
return unless routing_key.is_a?(String)
|
|
354
|
+
|
|
355
|
+
owners = @registry.handlers_for(routing_key, queue_name: queue_name)
|
|
356
|
+
owners.first.handler_class.name if owners.one?
|
|
304
357
|
end
|
|
305
358
|
|
|
306
359
|
# The executor's handle_failure, for events (issue #494): one upsert per
|
data/lib/pgbus/version.rb
CHANGED
data/lib/pgbus.rb
CHANGED
|
@@ -298,6 +298,17 @@ module Pgbus
|
|
|
298
298
|
Streams::Key.queue_name_budget
|
|
299
299
|
end
|
|
300
300
|
|
|
301
|
+
# Open a batch, yield a Pgbus::Batch::Builder, and start the batch when
|
|
302
|
+
# the block returns — the top-level shortcut for Pgbus::Batch.start.
|
|
303
|
+
#
|
|
304
|
+
# Pgbus.batch(description: "Import") do |b|
|
|
305
|
+
# b.on_complete ImportDoneJob
|
|
306
|
+
# b.add(*rows.map { |row| ImportRowJob.new(row.id) })
|
|
307
|
+
# end
|
|
308
|
+
def batch(**, &)
|
|
309
|
+
Batch.start(**, &)
|
|
310
|
+
end
|
|
311
|
+
|
|
301
312
|
# Publish an event to the bus — the top-level shortcut for
|
|
302
313
|
# `Pgbus::EventBus::Publisher.publish`, symmetric with `Pgbus.stream`.
|
|
303
314
|
#
|
data/sig/pgbus.rbs
CHANGED
|
@@ -49,6 +49,13 @@ module Pgbus
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Partial: only the types Pgbus.batch names. Batch's own methods are not
|
|
53
|
+
# covered by spec/pgbus/rbs_spec.rb yet.
|
|
54
|
+
class Batch
|
|
55
|
+
class Builder
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
def self.configuration: () -> Configuration
|
|
53
60
|
def self.configure: () { (Configuration) -> void } -> Configuration
|
|
54
61
|
def self.finalize!: () -> Configuration
|
|
@@ -57,6 +64,8 @@ module Pgbus
|
|
|
57
64
|
def self.stream_key: (*untyped parts, ?digest_bits: Integer) -> String
|
|
58
65
|
def self.stream_key!: (String key) -> String
|
|
59
66
|
def self.stream_name_budget: () -> Integer
|
|
67
|
+
# Keywords go to Pgbus::Batch.new (description:, properties:, on_complete:, uniqueness_key:, …).
|
|
68
|
+
def self.batch: (**untyped options) { (Batch::Builder) -> void } -> Batch
|
|
60
69
|
# delay: seconds, as a Numeric or an ActiveSupport::Duration (1.hour).
|
|
61
70
|
def self.publish: (String routing_key, untyped payload, ?headers: Hash[String, untyped]?, ?delay: Numeric | _ToI) -> untyped
|
|
62
71
|
def self.publish_later: (String routing_key, untyped payload, delay: Numeric | _ToI, ?headers: Hash[String, untyped]?) -> untyped
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -307,6 +307,7 @@ files:
|
|
|
307
307
|
- lib/pgbus/active_job/executor.rb
|
|
308
308
|
- lib/pgbus/autovacuum_tuning.rb
|
|
309
309
|
- lib/pgbus/batch.rb
|
|
310
|
+
- lib/pgbus/batch/builder.rb
|
|
310
311
|
- lib/pgbus/batch/sweep.rb
|
|
311
312
|
- lib/pgbus/bus_record.rb
|
|
312
313
|
- lib/pgbus/circuit_breaker.rb
|