pgbus 0.14.0 → 0.14.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 +30 -0
- data/app/models/pgbus/batch_entry.rb +78 -6
- data/app/models/pgbus/batch_execution.rb +28 -0
- data/app/models/pgbus/blocked_execution.rb +2 -2
- data/app/models/pgbus/uniqueness_key.rb +50 -4
- data/app/views/pgbus/batches/_batches_table.html.erb +3 -3
- data/app/views/pgbus/batches/show.html.erb +7 -7
- data/config/locales/da.yml +2 -2
- data/config/locales/de.yml +2 -2
- data/config/locales/en.yml +2 -2
- data/config/locales/es.yml +2 -2
- data/config/locales/fi.yml +2 -2
- data/config/locales/fr.yml +2 -2
- data/config/locales/it.yml +2 -2
- data/config/locales/ja.yml +2 -2
- data/config/locales/nb.yml +2 -2
- data/config/locales/nl.yml +2 -2
- data/config/locales/pt.yml +2 -2
- data/config/locales/sv.yml +2 -2
- data/lib/generators/pgbus/add_batch_callback_jobs_generator.rb +47 -0
- data/lib/generators/pgbus/add_batch_executions_generator.rb +44 -0
- data/lib/generators/pgbus/templates/add_batch_callback_jobs.rb.erb +13 -0
- data/lib/generators/pgbus/templates/add_batch_executions.rb.erb +50 -0
- data/lib/generators/pgbus/templates/initializer.rb.erb +2 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +23 -2
- data/lib/pgbus/active_job/adapter.rb +142 -28
- data/lib/pgbus/active_job/batch_id.rb +48 -0
- data/lib/pgbus/active_job/executor.rb +45 -9
- data/lib/pgbus/batch/sweep.rb +163 -0
- data/lib/pgbus/batch.rb +448 -63
- data/lib/pgbus/bus_record.rb +15 -1
- data/lib/pgbus/client.rb +191 -15
- data/lib/pgbus/concurrency/blocked_execution.rb +14 -1
- data/lib/pgbus/configuration.rb +24 -1
- data/lib/pgbus/engine.rb +1 -0
- data/lib/pgbus/generators/migration_detector.rb +30 -0
- data/lib/pgbus/instrumentation.rb +5 -0
- data/lib/pgbus/outbox/poller.rb +6 -7
- data/lib/pgbus/process/dispatcher.rb +41 -17
- data/lib/pgbus/recurring/schedule.rb +12 -1
- data/lib/pgbus/uniqueness.rb +35 -7
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +47 -9
- metadata +8 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class AddPgbusBatchCallbackJobs < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
add_column :pgbus_batches, :on_finish_job, :jsonb unless column_exists?(:pgbus_batches, :on_finish_job)
|
|
4
|
+
add_column :pgbus_batches, :on_success_job, :jsonb unless column_exists?(:pgbus_batches, :on_success_job)
|
|
5
|
+
add_column :pgbus_batches, :on_failure_job, :jsonb unless column_exists?(:pgbus_batches, :on_failure_job)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def down
|
|
9
|
+
remove_column :pgbus_batches, :on_finish_job if column_exists?(:pgbus_batches, :on_finish_job)
|
|
10
|
+
remove_column :pgbus_batches, :on_success_job if column_exists?(:pgbus_batches, :on_success_job)
|
|
11
|
+
remove_column :pgbus_batches, :on_failure_job if column_exists?(:pgbus_batches, :on_failure_job)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class AddPgbusBatchExecutions < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
create_table :pgbus_batch_executions do |t|
|
|
4
|
+
t.string :batch_id, null: false
|
|
5
|
+
t.string :job_id, null: false
|
|
6
|
+
t.bigint :msg_id
|
|
7
|
+
t.string :queue_name
|
|
8
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_index :pgbus_batch_executions, :job_id,
|
|
12
|
+
unique: true, name: "idx_pgbus_batch_executions_job_id"
|
|
13
|
+
add_index :pgbus_batch_executions, :batch_id,
|
|
14
|
+
name: "idx_pgbus_batch_executions_batch_id"
|
|
15
|
+
add_index :pgbus_batch_executions, :created_at,
|
|
16
|
+
where: "msg_id IS NULL",
|
|
17
|
+
name: "idx_pgbus_batch_executions_orphans"
|
|
18
|
+
add_foreign_key :pgbus_batch_executions, :pgbus_batches,
|
|
19
|
+
column: :batch_id, primary_key: :batch_id, on_delete: :cascade
|
|
20
|
+
|
|
21
|
+
if column_exists?(:pgbus_batches, :on_discard_class) && !column_exists?(:pgbus_batches, :on_failure_class)
|
|
22
|
+
rename_column :pgbus_batches, :on_discard_class, :on_failure_class
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Drain/restart pgbus workers after this migration: a process that still
|
|
26
|
+
# writes discarded_jobs will error once the column is gone.
|
|
27
|
+
if column_exists?(:pgbus_batches, :discarded_jobs)
|
|
28
|
+
if column_exists?(:pgbus_batches, :failed_jobs)
|
|
29
|
+
execute "UPDATE pgbus_batches SET failed_jobs = failed_jobs + discarded_jobs"
|
|
30
|
+
remove_column :pgbus_batches, :discarded_jobs
|
|
31
|
+
else
|
|
32
|
+
rename_column :pgbus_batches, :discarded_jobs, :failed_jobs
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def down
|
|
38
|
+
unless column_exists?(:pgbus_batches, :discarded_jobs)
|
|
39
|
+
add_column :pgbus_batches, :discarded_jobs, :integer, null: false, default: 0
|
|
40
|
+
if column_exists?(:pgbus_batches, :failed_jobs)
|
|
41
|
+
execute "UPDATE pgbus_batches SET discarded_jobs = failed_jobs"
|
|
42
|
+
execute "UPDATE pgbus_batches SET failed_jobs = 0"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
if column_exists?(:pgbus_batches, :on_failure_class) && !column_exists?(:pgbus_batches, :on_discard_class)
|
|
46
|
+
rename_column :pgbus_batches, :on_failure_class, :on_discard_class
|
|
47
|
+
end
|
|
48
|
+
drop_table :pgbus_batch_executions if table_exists?(:pgbus_batch_executions)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -44,6 +44,8 @@ Pgbus.configure do |c|
|
|
|
44
44
|
|
|
45
45
|
# --- Dispatcher (maintenance tasks) ------------------------------------
|
|
46
46
|
# c.dispatch_interval = 1.0
|
|
47
|
+
# c.batch_retention = 7.days # finished-batch cleanup; nil disables
|
|
48
|
+
# c.batch_sweep_interval = 5.minutes # stalled-batch repair sweep
|
|
47
49
|
|
|
48
50
|
# --- Realtime broadcast isolation (turbo-rails) ------------------------
|
|
49
51
|
# Route turbo-rails' async render+broadcast jobs to a dedicated queue so a
|
|
@@ -104,12 +104,14 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
104
104
|
t.string :description
|
|
105
105
|
t.string :on_finish_class
|
|
106
106
|
t.string :on_success_class
|
|
107
|
-
t.string :
|
|
107
|
+
t.string :on_failure_class
|
|
108
|
+
t.jsonb :on_finish_job
|
|
109
|
+
t.jsonb :on_success_job
|
|
110
|
+
t.jsonb :on_failure_job
|
|
108
111
|
t.jsonb :properties, default: {}
|
|
109
112
|
t.integer :total_jobs, null: false, default: 0
|
|
110
113
|
t.integer :completed_jobs, null: false, default: 0
|
|
111
114
|
t.integer :failed_jobs, null: false, default: 0
|
|
112
|
-
t.integer :discarded_jobs, null: false, default: 0
|
|
113
115
|
t.string :status, null: false, default: "pending"
|
|
114
116
|
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
115
117
|
t.datetime :finished_at
|
|
@@ -120,6 +122,24 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
120
122
|
add_index :pgbus_batches, :status,
|
|
121
123
|
name: "idx_pgbus_batches_status"
|
|
122
124
|
|
|
125
|
+
create_table :pgbus_batch_executions do |t|
|
|
126
|
+
t.string :batch_id, null: false
|
|
127
|
+
t.string :job_id, null: false
|
|
128
|
+
t.bigint :msg_id
|
|
129
|
+
t.string :queue_name
|
|
130
|
+
t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
add_index :pgbus_batch_executions, :job_id,
|
|
134
|
+
unique: true, name: "idx_pgbus_batch_executions_job_id"
|
|
135
|
+
add_index :pgbus_batch_executions, :batch_id,
|
|
136
|
+
name: "idx_pgbus_batch_executions_batch_id"
|
|
137
|
+
add_index :pgbus_batch_executions, :created_at,
|
|
138
|
+
where: "msg_id IS NULL",
|
|
139
|
+
name: "idx_pgbus_batch_executions_orphans"
|
|
140
|
+
add_foreign_key :pgbus_batch_executions, :pgbus_batches,
|
|
141
|
+
column: :batch_id, primary_key: :batch_id, on_delete: :cascade
|
|
142
|
+
|
|
123
143
|
# Recurring task definitions (synced from config/recurring.yml)
|
|
124
144
|
create_table :pgbus_recurring_tasks do |t|
|
|
125
145
|
t.string :key, null: false
|
|
@@ -186,6 +206,7 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
186
206
|
drop_table :pgbus_stream_queues
|
|
187
207
|
drop_table :pgbus_recurring_executions
|
|
188
208
|
drop_table :pgbus_recurring_tasks
|
|
209
|
+
drop_table :pgbus_batch_executions
|
|
189
210
|
drop_table :pgbus_batches
|
|
190
211
|
drop_table :pgbus_blocked_executions
|
|
191
212
|
drop_table :pgbus_semaphores
|
|
@@ -10,9 +10,12 @@ module Pgbus
|
|
|
10
10
|
payload_hash = Serializer.serialize_job_hash(active_job)
|
|
11
11
|
payload_hash = Concurrency.inject_metadata(active_job, payload_hash)
|
|
12
12
|
payload_hash = Uniqueness.inject_metadata(active_job, payload_hash)
|
|
13
|
-
payload_hash = inject_batch_metadata(payload_hash)
|
|
13
|
+
payload_hash = inject_batch_metadata(payload_hash, active_job: active_job)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
if uniqueness_rejected?(active_job, payload_hash, queue: queue)
|
|
16
|
+
uncount_batch_job(payload_hash)
|
|
17
|
+
return active_job
|
|
18
|
+
end
|
|
16
19
|
|
|
17
20
|
enqueue_with_concurrency(active_job, queue, payload_hash)
|
|
18
21
|
end
|
|
@@ -22,18 +25,22 @@ module Pgbus
|
|
|
22
25
|
payload_hash = Serializer.serialize_job_hash(active_job)
|
|
23
26
|
payload_hash = Concurrency.inject_metadata(active_job, payload_hash)
|
|
24
27
|
payload_hash = Uniqueness.inject_metadata(active_job, payload_hash)
|
|
25
|
-
payload_hash = inject_batch_metadata(payload_hash)
|
|
28
|
+
payload_hash = inject_batch_metadata(payload_hash, active_job: active_job)
|
|
26
29
|
delay = [(timestamp - Time.current.to_f).ceil, 0].max
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
if uniqueness_rejected?(active_job, payload_hash, queue: queue)
|
|
32
|
+
uncount_batch_job(payload_hash)
|
|
33
|
+
return active_job
|
|
34
|
+
end
|
|
29
35
|
|
|
30
36
|
enqueue_with_concurrency(active_job, queue, payload_hash, delay: delay)
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
def enqueue_all(active_jobs)
|
|
34
|
-
# Jobs with uniqueness must go through individual enqueue
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
# Jobs with uniqueness or concurrency must go through individual enqueue
|
|
41
|
+
# to acquire locks/semaphores — the bulk path cannot (issue #413)
|
|
42
|
+
individual, bulk = active_jobs.partition { |j| Uniqueness.uniqueness_config(j) || concurrency_config(j) }
|
|
43
|
+
individual.each do |j|
|
|
37
44
|
if scheduled_in_future?(j)
|
|
38
45
|
enqueue_at(j, j.scheduled_at.to_f)
|
|
39
46
|
else
|
|
@@ -41,9 +48,12 @@ module Pgbus
|
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
# Group by priority too: send_batch routes through the queue strategy,
|
|
52
|
+
# so a mixed-priority bulk send needs one produce_batch per level.
|
|
53
|
+
bulk.group_by { |j| [j.queue_name || Pgbus.configuration.default_queue, j.try(:priority)] }
|
|
54
|
+
.each do |(queue, priority), jobs|
|
|
45
55
|
immediate, scheduled = jobs.partition { |j| !scheduled_in_future?(j) }
|
|
46
|
-
enqueue_immediate(queue, immediate)
|
|
56
|
+
enqueue_immediate(queue, immediate, priority: priority)
|
|
47
57
|
scheduled.each { |j| enqueue_at(j, j.scheduled_at.to_f) }
|
|
48
58
|
end
|
|
49
59
|
|
|
@@ -56,6 +66,8 @@ module Pgbus
|
|
|
56
66
|
key = Concurrency.extract_key(payload_hash)
|
|
57
67
|
concurrency = concurrency_config(active_job)
|
|
58
68
|
priority = active_job.try(:priority)
|
|
69
|
+
msg_id = nil
|
|
70
|
+
blocked = false
|
|
59
71
|
|
|
60
72
|
if key && concurrency
|
|
61
73
|
result = Concurrency::Semaphore.acquire(key, concurrency[:limit], concurrency[:duration])
|
|
@@ -64,34 +76,48 @@ module Pgbus
|
|
|
64
76
|
msg_id = Pgbus.client.send_message(queue, payload_hash, delay: delay, priority: priority)
|
|
65
77
|
active_job.provider_job_id = msg_id
|
|
66
78
|
else
|
|
67
|
-
handle_conflict(concurrency, active_job, key, queue, payload_hash, priority: priority)
|
|
79
|
+
blocked = handle_conflict(concurrency, active_job, key, queue, payload_hash, priority: priority)
|
|
68
80
|
end
|
|
69
81
|
else
|
|
70
82
|
msg_id = Pgbus.client.send_message(queue, payload_hash, delay: delay, priority: priority)
|
|
71
83
|
active_job.provider_job_id = msg_id
|
|
72
84
|
end
|
|
73
85
|
|
|
86
|
+
# Bind before backfill so a live message is never left with an unbound
|
|
87
|
+
# uniqueness row if execution-row bookkeeping raises.
|
|
88
|
+
bind_acquired_uniqueness_lock(queue, msg_id) if msg_id
|
|
89
|
+
Batch.backfill_execution(payload_hash, msg_id, physical_queue(queue, priority)) if msg_id
|
|
90
|
+
# A retry re-enqueue that is now live (sent, or parked as a blocked
|
|
91
|
+
# execution) must stop the original attempt from signalling completion.
|
|
92
|
+
Batch.note_retry_reenqueued(payload_hash["job_id"]) if (msg_id || blocked) && retry_retagged?(payload_hash)
|
|
93
|
+
uniqueness_key = Thread.current[:pgbus_acquired_uniqueness_key]
|
|
94
|
+
UniquenessKey.clear_bind_stamp!(uniqueness_key) if uniqueness_key
|
|
74
95
|
Thread.current[:pgbus_acquired_uniqueness_key] = nil
|
|
75
96
|
active_job
|
|
76
97
|
rescue StandardError => e
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
end
|
|
98
|
+
if msg_id.nil?
|
|
99
|
+
rollback_acquired_uniqueness_lock
|
|
100
|
+
uncount_batch_job(payload_hash)
|
|
101
|
+
else
|
|
102
|
+
# Message is live: drop the thread-local so a later discard on this
|
|
103
|
+
# thread cannot release that job's uniqueness lock, but do not
|
|
104
|
+
# DELETE the pgbus_uniqueness_keys row.
|
|
85
105
|
Thread.current[:pgbus_acquired_uniqueness_key] = nil
|
|
86
106
|
end
|
|
87
107
|
raise e
|
|
88
108
|
end
|
|
89
109
|
|
|
110
|
+
def physical_queue(queue, priority)
|
|
111
|
+
Pgbus.client.target_queue(queue, priority)
|
|
112
|
+
end
|
|
113
|
+
|
|
90
114
|
def concurrency_config(active_job)
|
|
91
115
|
active_job.class.respond_to?(:pgbus_concurrency) && active_job.class.pgbus_concurrency
|
|
92
116
|
end
|
|
93
117
|
|
|
94
|
-
|
|
118
|
+
# Returns true when the job was parked as a blocked execution (it will
|
|
119
|
+
# run later), false when it was dropped.
|
|
120
|
+
def handle_conflict(concurrency, active_job, key, queue, payload_hash, priority: nil) # rubocop:disable Naming/PredicateMethod
|
|
95
121
|
case concurrency[:on_conflict]
|
|
96
122
|
when :block
|
|
97
123
|
Concurrency::BlockedExecution.insert(
|
|
@@ -101,14 +127,37 @@ module Pgbus
|
|
|
101
127
|
priority: priority || Pgbus.configuration.default_priority,
|
|
102
128
|
duration: concurrency[:duration]
|
|
103
129
|
)
|
|
130
|
+
return true
|
|
104
131
|
when :discard
|
|
105
132
|
Pgbus.logger.info { "[Pgbus] Discarding job #{active_job.class.name}: concurrency limit for #{key}" }
|
|
133
|
+
# The job will never run: roll back an :until_executed uniqueness lock
|
|
134
|
+
# acquired earlier in this enqueue (no executor will release it), and
|
|
135
|
+
# uncount it from its batch so completion is not waited on forever.
|
|
136
|
+
rollback_acquired_uniqueness_lock
|
|
137
|
+
uncount_batch_job(payload_hash)
|
|
106
138
|
when :raise
|
|
107
139
|
raise ConcurrencyLimitExceeded, "Concurrency limit reached for key: #{key}"
|
|
108
140
|
end
|
|
141
|
+
false
|
|
109
142
|
end
|
|
110
143
|
|
|
111
|
-
|
|
144
|
+
# Releases an :until_executed lock this enqueue acquired, if any.
|
|
145
|
+
# Used when the job is dropped before a message is sent (concurrency
|
|
146
|
+
# :discard, or send_message raising) — otherwise the lock is orphaned
|
|
147
|
+
# because no executor will ever release it.
|
|
148
|
+
def rollback_acquired_uniqueness_lock
|
|
149
|
+
rollback_key = Thread.current[:pgbus_acquired_uniqueness_key]
|
|
150
|
+
return unless rollback_key
|
|
151
|
+
|
|
152
|
+
begin
|
|
153
|
+
Uniqueness.release_lock(rollback_key)
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
Pgbus.logger.warn { "[Pgbus] Lock rollback failed: #{e.message}" }
|
|
156
|
+
end
|
|
157
|
+
Thread.current[:pgbus_acquired_uniqueness_key] = nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def uniqueness_rejected?(active_job, payload_hash, queue:)
|
|
112
161
|
uniqueness_key = Uniqueness.extract_key(payload_hash)
|
|
113
162
|
return false unless uniqueness_key
|
|
114
163
|
|
|
@@ -123,7 +172,7 @@ module Pgbus
|
|
|
123
172
|
# See issue #333.
|
|
124
173
|
return false if active_job.executions.to_i.positive?
|
|
125
174
|
|
|
126
|
-
result = Uniqueness.acquire_enqueue_lock(uniqueness_key, active_job)
|
|
175
|
+
result = Uniqueness.acquire_enqueue_lock(uniqueness_key, active_job, queue_name: queue)
|
|
127
176
|
|
|
128
177
|
# :no_lock means no enqueue-time lock needed (e.g. :while_executing strategy)
|
|
129
178
|
return false if result == :no_lock
|
|
@@ -147,25 +196,90 @@ module Pgbus
|
|
|
147
196
|
end
|
|
148
197
|
end
|
|
149
198
|
|
|
150
|
-
def
|
|
199
|
+
def bind_acquired_uniqueness_lock(queue, msg_id)
|
|
200
|
+
key = Thread.current[:pgbus_acquired_uniqueness_key]
|
|
201
|
+
return unless key
|
|
202
|
+
|
|
203
|
+
Uniqueness.bind_lock(key, queue_name: queue, msg_id: msg_id)
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
Pgbus.logger.warn { "[Pgbus] Uniqueness bind failed: #{e.message}" }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Reverses inject_batch_metadata for a job discarded at enqueue time
|
|
209
|
+
# (uniqueness duplicate or concurrency :discard conflict): the message is
|
|
210
|
+
# never sent, so it can never signal completion. Only applies while the
|
|
211
|
+
# tagging batch's block is still active on this thread.
|
|
212
|
+
def uncount_batch_job(payload_hash)
|
|
213
|
+
batch_id = payload_hash[Batch::METADATA_KEY]
|
|
214
|
+
return unless batch_id
|
|
215
|
+
|
|
216
|
+
if batch_id == Thread.current[:pgbus_batch_id]
|
|
217
|
+
Batch.untrack_enqueue(payload_hash)
|
|
218
|
+
elsif retry_retagged?(payload_hash)
|
|
219
|
+
# The retry never became live; the original attempt's row and its
|
|
220
|
+
# normal completion signal stand.
|
|
221
|
+
Batch.forget_retry_reenqueued(payload_hash["job_id"])
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Tagged for a batch while no Batch#enqueue block is active on this
|
|
226
|
+
# thread — only a retry_on re-enqueue gets there (issue #424).
|
|
227
|
+
def retry_retagged?(payload_hash)
|
|
228
|
+
payload_hash[Batch::METADATA_KEY] && Thread.current[:pgbus_batch_id].nil?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# A retry_on re-enqueue of a job that is already a batch member: same
|
|
232
|
+
# job_id (executions > 0), batch_id carried by the BatchId mixin. It
|
|
233
|
+
# rejoins its batch without being counted again. A first-attempt job
|
|
234
|
+
# that merely has a batch_id outside a block is NOT tagged — membership
|
|
235
|
+
# stays explicit; only the callback_batch_id never re-tags.
|
|
236
|
+
def retry_batch_id_for(active_job)
|
|
237
|
+
return nil unless active_job.respond_to?(:batch_id)
|
|
238
|
+
return nil unless active_job.executions.to_i.positive?
|
|
239
|
+
|
|
240
|
+
active_job.batch_id
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Tag the payload with the active batch and count it in (guarded
|
|
244
|
+
# increment + execution row, before the send — Batch.track_enqueue). Pass
|
|
245
|
+
# track: false to only tag, when the caller counts a bulk once.
|
|
246
|
+
def inject_batch_metadata(payload_hash, active_job: nil, track: true)
|
|
151
247
|
batch_id = Thread.current[:pgbus_batch_id]
|
|
152
|
-
|
|
248
|
+
if batch_id
|
|
249
|
+
tagged = payload_hash.merge(Batch::METADATA_KEY => batch_id)
|
|
250
|
+
Batch.track_enqueue(tagged) if track
|
|
251
|
+
return tagged
|
|
252
|
+
end
|
|
153
253
|
|
|
154
|
-
|
|
155
|
-
payload_hash
|
|
254
|
+
retry_batch_id = active_job && retry_batch_id_for(active_job)
|
|
255
|
+
return payload_hash unless retry_batch_id
|
|
256
|
+
|
|
257
|
+
tagged = payload_hash.merge(Batch::METADATA_KEY => retry_batch_id)
|
|
258
|
+
Batch.track_retry(tagged)
|
|
259
|
+
tagged
|
|
156
260
|
end
|
|
157
261
|
|
|
158
|
-
def enqueue_immediate(queue, jobs)
|
|
262
|
+
def enqueue_immediate(queue, jobs, priority: nil)
|
|
159
263
|
return if jobs.empty?
|
|
160
264
|
|
|
161
|
-
payloads = jobs.map { |j| Serializer.serialize_job_hash(j) }
|
|
162
|
-
|
|
265
|
+
payloads = jobs.map { |j| inject_batch_metadata(Serializer.serialize_job_hash(j), track: false) }
|
|
266
|
+
# One guarded increment for the whole bulk, not one per job.
|
|
267
|
+
Batch.track_enqueue(payloads) if Thread.current[:pgbus_batch_id]
|
|
268
|
+
physical = physical_queue(queue, priority)
|
|
269
|
+
msg_ids = nil
|
|
270
|
+
msg_ids = Pgbus.client.send_batch(queue, payloads, priority: priority)
|
|
163
271
|
|
|
164
272
|
unless msg_ids.is_a?(Array) && msg_ids.size == jobs.size
|
|
165
273
|
raise Pgbus::EnqueueError, "Pgbus batch enqueue failed: expected #{jobs.size} ids, got #{msg_ids&.size || 0}"
|
|
166
274
|
end
|
|
167
275
|
|
|
168
276
|
jobs.zip(msg_ids).each { |job, id| job.provider_job_id = id }
|
|
277
|
+
payloads.zip(msg_ids).each { |payload, id| Batch.backfill_execution(payload, id, physical) }
|
|
278
|
+
rescue Pgbus::EnqueueError
|
|
279
|
+
Array(payloads).each_with_index do |payload, index|
|
|
280
|
+
Batch.untrack_enqueue(payload) if msg_ids.nil? || msg_ids[index].nil?
|
|
281
|
+
end
|
|
282
|
+
raise
|
|
169
283
|
rescue Pgbus::SchemaNotReady => e
|
|
170
284
|
Pgbus.logger.error { "[Pgbus] #{e.message}" }
|
|
171
285
|
raise
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module ActiveJob
|
|
5
|
+
# Gives every ActiveJob a handle on the batch it belongs to.
|
|
6
|
+
#
|
|
7
|
+
# Two distinct ids, mirroring solid_queue's ActiveJob::BatchId:
|
|
8
|
+
#
|
|
9
|
+
# * +batch_id+ — the batch this job is a *member* of. Assigned by the
|
|
10
|
+
# executor from the payload's +pgbus_batch_id+ before +perform+, so a
|
|
11
|
+
# running job can call +batch.enqueue+ to add siblings.
|
|
12
|
+
# * +callback_batch_id+ — the batch this job *reports on*. Set on
|
|
13
|
+
# +on_finish+/+on_success+/+on_failure+ jobs at fire time. A callback is
|
|
14
|
+
# never a member of the batch it reports on, so its +batch_id+ is nil.
|
|
15
|
+
#
|
|
16
|
+
# Both round-trip through +serialize+/+deserialize+, and both are omitted
|
|
17
|
+
# from the serialized hash when unset — an unbatched job's payload is
|
|
18
|
+
# byte-for-byte what it was before this mixin existed.
|
|
19
|
+
module BatchId
|
|
20
|
+
extend ActiveSupport::Concern
|
|
21
|
+
|
|
22
|
+
included do
|
|
23
|
+
attr_accessor :batch_id, :callback_batch_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def serialize
|
|
27
|
+
data = super
|
|
28
|
+
data["batch_id"] = batch_id if batch_id
|
|
29
|
+
data["callback_batch_id"] = callback_batch_id if callback_batch_id
|
|
30
|
+
data
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def deserialize(job_data)
|
|
34
|
+
super
|
|
35
|
+
self.batch_id = job_data["batch_id"]
|
|
36
|
+
self.callback_batch_id = job_data["callback_batch_id"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The batch this job reports on, or is a member of. nil when neither.
|
|
40
|
+
def batch
|
|
41
|
+
return @batch if defined?(@batch)
|
|
42
|
+
|
|
43
|
+
id = callback_batch_id || batch_id
|
|
44
|
+
@batch = id && Pgbus::Batch.find(id)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -56,9 +56,14 @@ module Pgbus
|
|
|
56
56
|
# released on completion or DLQ.
|
|
57
57
|
nil
|
|
58
58
|
when :while_executing
|
|
59
|
-
# Acquire the lock now. If another worker is
|
|
60
|
-
# this job, skip it — VT will expire and it'll be
|
|
61
|
-
|
|
59
|
+
# Acquire the lock now, bound to this message. If another worker is
|
|
60
|
+
# already executing this job, skip it — VT will expire and it'll be
|
|
61
|
+
# retried. A row left by a crashed attempt of THIS message is
|
|
62
|
+
# re-acquired, not treated as a duplicate.
|
|
63
|
+
acquired = Uniqueness.acquire_execution_lock(
|
|
64
|
+
uniqueness_key, payload, msg_id: message.msg_id.to_i, queue_name: queue_name
|
|
65
|
+
)
|
|
66
|
+
unless acquired
|
|
62
67
|
Pgbus.logger.info { "[Pgbus] Skipping duplicate execution for #{job_class}" }
|
|
63
68
|
return :skipped
|
|
64
69
|
end
|
|
@@ -67,6 +72,7 @@ module Pgbus
|
|
|
67
72
|
|
|
68
73
|
Pgbus.logger.debug { "[Pgbus::Executor] deserialized #{tag} job_class=#{job_class}" }
|
|
69
74
|
job_succeeded = false
|
|
75
|
+
retried = false
|
|
70
76
|
|
|
71
77
|
msg_id = message.msg_id.to_i
|
|
72
78
|
instrument_payload = {
|
|
@@ -85,13 +91,22 @@ module Pgbus
|
|
|
85
91
|
# (issue #368). Pass this executor's config so an injected allowlist
|
|
86
92
|
# is not silently ignored in favour of Pgbus.configuration.
|
|
87
93
|
job = Serializer.deserialize_job_data(payload, configuration: config)
|
|
94
|
+
# Batch membership rides the pgbus metadata key, not the serialized
|
|
95
|
+
# job data, so hand it to the job before perform — that is what makes
|
|
96
|
+
# `batch` (and `batch.enqueue` for open batches) work inside a job.
|
|
97
|
+
assign_batch_id(job, payload)
|
|
88
98
|
Pgbus.logger.debug { "[Pgbus::Executor] running #{tag} job_class=#{job_class}" }
|
|
89
99
|
execute_job(job)
|
|
100
|
+
# retry_on re-enqueues from inside perform_now and returns normally:
|
|
101
|
+
# this attempt is done (archive it) but the job is not — the retry
|
|
102
|
+
# message carries the batch tag and signals on its own outcome.
|
|
103
|
+
retried = Batch.retry_reenqueued?(payload["job_id"])
|
|
90
104
|
Pgbus.logger.debug { "[Pgbus::Executor] perform_returned #{tag} job_class=#{job_class}" }
|
|
91
105
|
archive_from(queue_name, msg_id, source_queue: source_queue)
|
|
92
106
|
Pgbus.logger.debug { "[Pgbus::Executor] archived #{tag} job_class=#{job_class}" }
|
|
93
|
-
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: msg_id)
|
|
94
107
|
job_succeeded = true
|
|
108
|
+
release_uniqueness_lock(uniqueness_key)
|
|
109
|
+
FailedEventRecorder.clear!(queue_name: queue_name, msg_id: msg_id)
|
|
95
110
|
end
|
|
96
111
|
|
|
97
112
|
instrument("pgbus.job_completed", queue: queue_name, job_class: job_class)
|
|
@@ -108,6 +123,10 @@ module Pgbus
|
|
|
108
123
|
# silently lost control flow — no failed event row, no job_failed
|
|
109
124
|
# notification, uniqueness lock held until VT expired. See issue #126.
|
|
110
125
|
handle_failure(message, queue_name, e, payload: payload)
|
|
126
|
+
# A failed :while_executing attempt is no longer executing — release
|
|
127
|
+
# so the retry (same message, after VT) can acquire. :until_executed
|
|
128
|
+
# keeps its lock until success/DLQ by design (#126, #333).
|
|
129
|
+
release_uniqueness_lock(uniqueness_key) if uniqueness_strategy == :while_executing
|
|
111
130
|
instrument(
|
|
112
131
|
"pgbus.job_failed",
|
|
113
132
|
queue: queue_name,
|
|
@@ -129,15 +148,32 @@ module Pgbus
|
|
|
129
148
|
# job_succeeded is set AFTER archive_message, so if archive fails the
|
|
130
149
|
# semaphore slot stays held until VT expires and the job is retried.
|
|
131
150
|
if job_succeeded
|
|
151
|
+
# Uniqueness is released once, immediately after archive. A second
|
|
152
|
+
# key-only DELETE here can drop a successor that acquired the same
|
|
153
|
+
# key if the first DELETE committed but the client raised afterward.
|
|
132
154
|
signal_concurrency(payload)
|
|
133
|
-
signal_batch_completed(payload)
|
|
134
|
-
# Release uniqueness lock on successful completion (both strategies)
|
|
135
|
-
Uniqueness.release_lock(uniqueness_key) if uniqueness_key
|
|
155
|
+
signal_batch_completed(payload) unless retried
|
|
136
156
|
end
|
|
157
|
+
Batch.clear_retry_reenqueued
|
|
137
158
|
end
|
|
138
159
|
|
|
139
160
|
private
|
|
140
161
|
|
|
162
|
+
def assign_batch_id(job, payload)
|
|
163
|
+
batch_id = payload[Batch::METADATA_KEY]
|
|
164
|
+
return unless batch_id && job.respond_to?(:batch_id=)
|
|
165
|
+
|
|
166
|
+
job.batch_id = batch_id
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def release_uniqueness_lock(key)
|
|
170
|
+
return unless key
|
|
171
|
+
|
|
172
|
+
Uniqueness.release_lock(key)
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
Pgbus.logger.warn { "[Pgbus] Uniqueness release failed: #{e.message}" }
|
|
175
|
+
end
|
|
176
|
+
|
|
141
177
|
def execute_job(job)
|
|
142
178
|
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
143
179
|
wrapper = reloading? ? Rails.application.reloader : Rails.application.executor
|
|
@@ -286,7 +322,7 @@ module Pgbus
|
|
|
286
322
|
batch_id = payload[Batch::METADATA_KEY]
|
|
287
323
|
return unless batch_id
|
|
288
324
|
|
|
289
|
-
Batch.job_completed(batch_id)
|
|
325
|
+
Batch.job_completed(batch_id, job_id: payload["job_id"])
|
|
290
326
|
rescue StandardError => e
|
|
291
327
|
Pgbus.logger.warn { "[Pgbus] Batch completion signal failed: #{e.message}" }
|
|
292
328
|
end
|
|
@@ -295,7 +331,7 @@ module Pgbus
|
|
|
295
331
|
batch_id = payload[Batch::METADATA_KEY]
|
|
296
332
|
return unless batch_id
|
|
297
333
|
|
|
298
|
-
Batch.job_discarded(batch_id)
|
|
334
|
+
Batch.job_discarded(batch_id, job_id: payload["job_id"])
|
|
299
335
|
rescue StandardError => e
|
|
300
336
|
Pgbus.logger.warn { "[Pgbus] Batch discard signal failed: #{e.message}" }
|
|
301
337
|
end
|