pgbus 0.19.3 → 0.20.0
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 +8 -0
- data/README.md +104 -9
- data/Rakefile +6 -1
- data/app/controllers/pgbus/dashboard_controller.rb +2 -0
- data/app/controllers/pgbus/workflow_definitions_controller.rb +28 -0
- data/app/controllers/pgbus/workflows_controller.rb +197 -0
- data/app/frontend/pgbus/modules/charts.js +130 -4
- data/app/frontend/pgbus/modules/charts.min.js +1 -1
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -1
- data/app/frontend/pgbus/style.css +1 -1
- data/app/helpers/pgbus/button_helper.rb +4 -2
- data/app/helpers/pgbus/workflow_insights_helper.rb +129 -0
- data/app/helpers/pgbus/workflows_helper.rb +165 -0
- data/app/models/pgbus/workflow_entry.rb +172 -0
- data/app/models/pgbus/workflow_event.rb +43 -0
- data/app/models/pgbus/workflow_signal.rb +77 -0
- data/app/models/pgbus/workflow_step.rb +142 -0
- data/app/views/layouts/pgbus/application.html.erb +2 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +17 -0
- data/app/views/pgbus/workflows/_actions.html.erb +59 -0
- data/app/views/pgbus/workflows/_events_table.html.erb +24 -0
- data/app/views/pgbus/workflows/_filters.html.erb +29 -0
- data/app/views/pgbus/workflows/_signal_form.html.erb +31 -0
- data/app/views/pgbus/workflows/_signals_table.html.erb +40 -0
- data/app/views/pgbus/workflows/_status_filter.html.erb +12 -0
- data/app/views/pgbus/workflows/_steps_table.html.erb +79 -0
- data/app/views/pgbus/workflows/_timeline.html.erb +39 -0
- data/app/views/pgbus/workflows/_workflows_table.html.erb +59 -0
- data/app/views/pgbus/workflows/attention.html.erb +88 -0
- data/app/views/pgbus/workflows/definitions/_by_revision.html.erb +52 -0
- data/app/views/pgbus/workflows/definitions/_duration_table.html.erb +18 -0
- data/app/views/pgbus/workflows/definitions/_funnel.html.erb +49 -0
- data/app/views/pgbus/workflows/definitions/_pipeline.html.erb +16 -0
- data/app/views/pgbus/workflows/definitions/_step_card.html.erb +31 -0
- data/app/views/pgbus/workflows/definitions/_throughput_table.html.erb +32 -0
- data/app/views/pgbus/workflows/definitions/_window.html.erb +17 -0
- data/app/views/pgbus/workflows/definitions/index.html.erb +71 -0
- data/app/views/pgbus/workflows/definitions/not_found.html.erb +5 -0
- data/app/views/pgbus/workflows/definitions/show.html.erb +80 -0
- data/app/views/pgbus/workflows/index.html.erb +15 -0
- data/app/views/pgbus/workflows/not_found.html.erb +5 -0
- data/app/views/pgbus/workflows/show.html.erb +93 -0
- data/config/i18n-tasks.yml +6 -0
- data/config/locales/da.yml +442 -0
- data/config/locales/de.yml +442 -0
- data/config/locales/en.yml +442 -0
- data/config/locales/es.yml +442 -0
- data/config/locales/fi.yml +442 -0
- data/config/locales/fr.yml +442 -0
- data/config/locales/it.yml +442 -0
- data/config/locales/ja.yml +442 -0
- data/config/locales/nb.yml +442 -0
- data/config/locales/nl.yml +442 -0
- data/config/locales/pt.yml +442 -0
- data/config/locales/sv.yml +442 -0
- data/config/routes.rb +26 -0
- data/lib/generators/pgbus/add_workflow_signals_generator.rb +56 -0
- data/lib/generators/pgbus/add_workflows_generator.rb +54 -0
- data/lib/generators/pgbus/templates/add_workflow_signals.rb.erb +51 -0
- data/lib/generators/pgbus/templates/add_workflows.rb.erb +150 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +127 -0
- data/lib/pgbus/active_job/adapter.rb +67 -26
- data/lib/pgbus/active_job/batch_id.rb +23 -1
- data/lib/pgbus/active_job/delivery.rb +49 -0
- data/lib/pgbus/active_job/executor.rb +3 -1
- data/lib/pgbus/batch/builder.rb +1 -0
- data/lib/pgbus/batch.rb +35 -3
- data/lib/pgbus/configuration/workflow_settings.rb +99 -0
- data/lib/pgbus/configuration.rb +21 -3
- data/lib/pgbus/generators/migration_detector.rb +40 -0
- data/lib/pgbus/instrumentation.rb +32 -0
- data/lib/pgbus/integrations/appsignal/dashboard.json +128 -1
- 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 +50 -100
- data/lib/pgbus/mcp/rack_app.rb +7 -2
- data/lib/pgbus/mcp/runner.rb +2 -1
- data/lib/pgbus/mcp/serialization.rb +25 -0
- data/lib/pgbus/mcp/server.rb +53 -10
- data/lib/pgbus/mcp/tools/deploy_tasks_tool.rb +1 -10
- data/lib/pgbus/mcp/tools/workflow_cancel_tool.rb +23 -0
- data/lib/pgbus/mcp/tools/workflow_definitions_tool.rb +36 -0
- data/lib/pgbus/mcp/tools/workflow_detail_tool.rb +38 -0
- data/lib/pgbus/mcp/tools/workflow_resume_at_tool.rb +32 -0
- data/lib/pgbus/mcp/tools/workflow_resume_tool.rb +24 -0
- data/lib/pgbus/mcp/tools/workflow_run_now_tool.rb +23 -0
- data/lib/pgbus/mcp/tools/workflow_signal_tool.rb +77 -0
- data/lib/pgbus/mcp/tools/workflow_skip_step_tool.rb +23 -0
- data/lib/pgbus/mcp/tools/workflow_stats_tool.rb +42 -0
- data/lib/pgbus/mcp/tools/workflows_attention_tool.rb +42 -0
- data/lib/pgbus/mcp/tools/workflows_tool.rb +60 -0
- data/lib/pgbus/mcp/workflow_insights_payload.rb +15 -0
- data/lib/pgbus/mcp/workflow_payloads.rb +53 -0
- data/lib/pgbus/mcp/write_tool.rb +134 -0
- data/lib/pgbus/mcp.rb +15 -0
- data/lib/pgbus/metrics/backends/statsd.rb +6 -0
- data/lib/pgbus/metrics/subscriber.rb +14 -1
- data/lib/pgbus/metrics/workflow_metrics.rb +65 -0
- data/lib/pgbus/process/dispatcher.rb +26 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/workflow_actions.rb +109 -0
- data/lib/pgbus/web/data_source/workflow_insights.rb +99 -0
- data/lib/pgbus/web/data_source/workflow_list.rb +248 -0
- data/lib/pgbus/web/data_source.rb +3 -0
- data/lib/pgbus/web/workflow_action_plan.rb +62 -0
- data/lib/pgbus/web/workflow_insights_queries.rb +250 -0
- data/lib/pgbus/web/workflow_insights_report.rb +171 -0
- data/lib/pgbus/web/workflow_payload.rb +36 -0
- data/lib/pgbus/web/workflow_query.rb +82 -0
- data/lib/pgbus/web/workflow_state.rb +76 -0
- data/lib/pgbus/web/workflow_timeline.rb +113 -0
- data/lib/pgbus/web/workflow_window.rb +51 -0
- data/lib/pgbus/workflow/batch_finished_job.rb +21 -0
- data/lib/pgbus/workflow/dsl.rb +105 -0
- data/lib/pgbus/workflow/error_capture.rb +63 -0
- data/lib/pgbus/workflow/exception_policy.rb +170 -0
- data/lib/pgbus/workflow/fan_out.rb +157 -0
- data/lib/pgbus/workflow/fan_out_definition.rb +41 -0
- data/lib/pgbus/workflow/flow_control.rb +94 -0
- data/lib/pgbus/workflow/instance.rb +183 -0
- data/lib/pgbus/workflow/outside_verbs.rb +138 -0
- data/lib/pgbus/workflow/payload.rb +53 -0
- data/lib/pgbus/workflow/registry.rb +53 -0
- data/lib/pgbus/workflow/removed_step.rb +14 -0
- data/lib/pgbus/workflow/runner.rb +218 -0
- data/lib/pgbus/workflow/signal_matcher.rb +53 -0
- data/lib/pgbus/workflow/signals.rb +132 -0
- data/lib/pgbus/workflow/states.rb +119 -0
- data/lib/pgbus/workflow/step_context.rb +58 -0
- data/lib/pgbus/workflow/step_definition.rb +131 -0
- data/lib/pgbus/workflow/step_execution.rb +242 -0
- data/lib/pgbus/workflow/step_job.rb +28 -0
- data/lib/pgbus/workflow/step_outcome.rb +151 -0
- data/lib/pgbus/workflow/steps.rb +103 -0
- data/lib/pgbus/workflow/sweep.rb +156 -0
- data/lib/pgbus/workflow/test_helpers.rb +152 -0
- data/lib/pgbus/workflow/wait_timeout.rb +73 -0
- data/lib/pgbus/workflow/waiting.rb +82 -0
- data/lib/pgbus/workflow.rb +183 -0
- data/sig/pgbus/configuration.rbs +22 -0
- metadata +95 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
class AddPgbusWorkflows < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
# if_not_exists throughout: pgbus:update queues this migration when any of
|
|
3
|
+
# the three core tables is missing, so a half-present schema must not fail
|
|
4
|
+
# it. The waiting-step columns and pgbus_workflow_signals (issue #541) are
|
|
5
|
+
# here too, so a fresh install needs no pgbus:add_workflow_signals.
|
|
6
|
+
# up/down rather than change: Rails 7.1 cannot reverse an add_foreign_key
|
|
7
|
+
# that names its primary_key, and dropping the tables drops the keys.
|
|
8
|
+
def up
|
|
9
|
+
# One row per workflow run (Pgbus::WorkflowEntry).
|
|
10
|
+
create_table :pgbus_workflows, if_not_exists: true do |t|
|
|
11
|
+
t.string :workflow_id, null: false
|
|
12
|
+
t.string :workflow_class, null: false
|
|
13
|
+
t.string :subject_type
|
|
14
|
+
t.string :subject_id
|
|
15
|
+
t.string :subject_gid
|
|
16
|
+
t.jsonb :arguments, null: false, default: {}
|
|
17
|
+
t.string :status, null: false, default: "ready"
|
|
18
|
+
t.string :current_step
|
|
19
|
+
t.jsonb :step_job_options, null: false, default: {}
|
|
20
|
+
t.string :error_class
|
|
21
|
+
t.text :error_message
|
|
22
|
+
t.text :error_backtrace
|
|
23
|
+
t.datetime :paused_at
|
|
24
|
+
t.datetime :finished_at
|
|
25
|
+
t.datetime :cancelled_at
|
|
26
|
+
t.string :idempotency_key
|
|
27
|
+
t.string :definition_digest, null: false
|
|
28
|
+
t.string :rerun_of_workflow_id
|
|
29
|
+
t.datetime :created_at, null: false
|
|
30
|
+
t.datetime :updated_at, null: false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
add_index :pgbus_workflows, :workflow_id,
|
|
34
|
+
unique: true, name: "idx_pgbus_workflows_workflow_id", if_not_exists: true
|
|
35
|
+
add_index :pgbus_workflows, [:workflow_class, :subject_type, :subject_id],
|
|
36
|
+
unique: true, where: "status IN ('ready','performing','waiting','paused')",
|
|
37
|
+
name: "idx_pgbus_workflows_one_ongoing", if_not_exists: true
|
|
38
|
+
add_index :pgbus_workflows, [:workflow_class, :idempotency_key],
|
|
39
|
+
unique: true, where: "idempotency_key IS NOT NULL",
|
|
40
|
+
name: "idx_pgbus_workflows_idempotency", if_not_exists: true
|
|
41
|
+
add_index :pgbus_workflows, [:status, :updated_at],
|
|
42
|
+
name: "idx_pgbus_workflows_status", if_not_exists: true
|
|
43
|
+
add_index :pgbus_workflows, [:workflow_class, :status, :created_at],
|
|
44
|
+
name: "idx_pgbus_workflows_class_status", if_not_exists: true
|
|
45
|
+
add_index :pgbus_workflows, [:subject_type, :subject_id],
|
|
46
|
+
name: "idx_pgbus_workflows_subject", if_not_exists: true
|
|
47
|
+
|
|
48
|
+
# One row per step attempt (Pgbus::WorkflowStep).
|
|
49
|
+
create_table :pgbus_workflow_steps, if_not_exists: true do |t|
|
|
50
|
+
t.string :workflow_id, null: false
|
|
51
|
+
t.string :workflow_class, null: false
|
|
52
|
+
t.string :step_name, null: false
|
|
53
|
+
t.integer :attempt, null: false, default: 1
|
|
54
|
+
t.string :status, null: false, default: "scheduled"
|
|
55
|
+
t.string :job_id
|
|
56
|
+
t.bigint :msg_id
|
|
57
|
+
t.datetime :scheduled_at, null: false
|
|
58
|
+
t.datetime :enqueued_at
|
|
59
|
+
t.datetime :started_at
|
|
60
|
+
t.datetime :finished_at
|
|
61
|
+
t.datetime :stall_at
|
|
62
|
+
t.string :outcome
|
|
63
|
+
t.jsonb :output
|
|
64
|
+
t.string :error_class
|
|
65
|
+
t.text :error_message
|
|
66
|
+
t.text :error_backtrace
|
|
67
|
+
t.jsonb :error_causes
|
|
68
|
+
t.string :worker
|
|
69
|
+
t.string :app_revision
|
|
70
|
+
# Waiting steps (fan_out, wait_for:): also added by pgbus:add_workflow_signals.
|
|
71
|
+
t.string :batch_id
|
|
72
|
+
t.string :wait_for
|
|
73
|
+
t.bigint :signal_id
|
|
74
|
+
t.datetime :waiting_since
|
|
75
|
+
t.datetime :timeout_at
|
|
76
|
+
t.datetime :created_at, null: false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
add_index :pgbus_workflow_steps, [:workflow_id, :step_name, :attempt],
|
|
80
|
+
unique: true, name: "idx_pgbus_workflow_steps_attempt", if_not_exists: true
|
|
81
|
+
add_index :pgbus_workflow_steps, [:status, :scheduled_at],
|
|
82
|
+
name: "idx_pgbus_workflow_steps_scheduled", if_not_exists: true
|
|
83
|
+
add_index :pgbus_workflow_steps, [:status, :stall_at],
|
|
84
|
+
name: "idx_pgbus_workflow_steps_stall", if_not_exists: true
|
|
85
|
+
add_index :pgbus_workflow_steps, [:workflow_class, :step_name, :finished_at],
|
|
86
|
+
name: "idx_pgbus_workflow_steps_insights", if_not_exists: true
|
|
87
|
+
# A steps table that predates waiting steps lacks these columns (the
|
|
88
|
+
# queued pgbus:add_workflow_signals adds them and their indexes).
|
|
89
|
+
if column_exists?(:pgbus_workflow_steps, :batch_id)
|
|
90
|
+
add_index :pgbus_workflow_steps, :batch_id,
|
|
91
|
+
name: "idx_pgbus_workflow_steps_batch", if_not_exists: true
|
|
92
|
+
end
|
|
93
|
+
if column_exists?(:pgbus_workflow_steps, :timeout_at)
|
|
94
|
+
add_index :pgbus_workflow_steps, [:status, :timeout_at],
|
|
95
|
+
name: "idx_pgbus_workflow_steps_timeout", if_not_exists: true
|
|
96
|
+
end
|
|
97
|
+
add_foreign_key :pgbus_workflow_steps, :pgbus_workflows,
|
|
98
|
+
column: :workflow_id, primary_key: :workflow_id, on_delete: :cascade, if_not_exists: true
|
|
99
|
+
|
|
100
|
+
# Append-only audit trail: one row per transition or operator action
|
|
101
|
+
# (Pgbus::WorkflowEvent).
|
|
102
|
+
create_table :pgbus_workflow_events, if_not_exists: true do |t|
|
|
103
|
+
t.string :workflow_id, null: false
|
|
104
|
+
t.bigint :step_id
|
|
105
|
+
t.string :step_name
|
|
106
|
+
t.integer :attempt
|
|
107
|
+
t.string :event, null: false
|
|
108
|
+
t.string :actor, null: false
|
|
109
|
+
t.text :reason
|
|
110
|
+
t.jsonb :details, null: false, default: {}
|
|
111
|
+
t.datetime :created_at, null: false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
add_index :pgbus_workflow_events, [:workflow_id, :id],
|
|
115
|
+
name: "idx_pgbus_workflow_events_workflow", if_not_exists: true
|
|
116
|
+
add_foreign_key :pgbus_workflow_events, :pgbus_workflows,
|
|
117
|
+
column: :workflow_id, primary_key: :workflow_id, on_delete: :cascade, if_not_exists: true
|
|
118
|
+
|
|
119
|
+
# One row per signal delivered to a run (Pgbus::WorkflowSignal):
|
|
120
|
+
# pending → claimed → consumed.
|
|
121
|
+
create_table :pgbus_workflow_signals, if_not_exists: true do |t|
|
|
122
|
+
t.string :workflow_id, null: false
|
|
123
|
+
t.string :name, null: false
|
|
124
|
+
t.jsonb :payload, null: false, default: {}
|
|
125
|
+
t.string :idempotency_key
|
|
126
|
+
t.string :status, null: false, default: "pending"
|
|
127
|
+
t.bigint :claimed_by_step_id
|
|
128
|
+
t.datetime :created_at, null: false
|
|
129
|
+
t.datetime :claimed_at
|
|
130
|
+
t.datetime :consumed_at
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
add_index :pgbus_workflow_signals, [:workflow_id, :idempotency_key],
|
|
134
|
+
unique: true, where: "idempotency_key IS NOT NULL",
|
|
135
|
+
name: "idx_pgbus_workflow_signals_idempotency", if_not_exists: true
|
|
136
|
+
add_index :pgbus_workflow_signals, [:workflow_id, :name, :status],
|
|
137
|
+
name: "idx_pgbus_workflow_signals_pending", if_not_exists: true
|
|
138
|
+
add_index :pgbus_workflow_signals, [:workflow_id, :status, :claimed_by_step_id],
|
|
139
|
+
name: "idx_pgbus_workflow_signals_claimed", if_not_exists: true
|
|
140
|
+
add_foreign_key :pgbus_workflow_signals, :pgbus_workflows,
|
|
141
|
+
column: :workflow_id, primary_key: :workflow_id, on_delete: :cascade, if_not_exists: true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def down
|
|
145
|
+
drop_table :pgbus_workflow_signals, if_exists: true
|
|
146
|
+
drop_table :pgbus_workflow_events, if_exists: true
|
|
147
|
+
drop_table :pgbus_workflow_steps, if_exists: true
|
|
148
|
+
drop_table :pgbus_workflows, if_exists: true
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -211,6 +211,129 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
211
211
|
add_index :pgbus_deploy_task_runs, :run_at,
|
|
212
212
|
name: "idx_pgbus_deploy_task_runs_cleanup"
|
|
213
213
|
|
|
214
|
+
# Workflows (Pgbus::Workflow): one row per run, one per step attempt, and
|
|
215
|
+
# the append-only audit trail. At most one ongoing run per (class, subject).
|
|
216
|
+
create_table :pgbus_workflows do |t|
|
|
217
|
+
t.string :workflow_id, null: false
|
|
218
|
+
t.string :workflow_class, null: false
|
|
219
|
+
t.string :subject_type
|
|
220
|
+
t.string :subject_id
|
|
221
|
+
t.string :subject_gid
|
|
222
|
+
t.jsonb :arguments, null: false, default: {}
|
|
223
|
+
t.string :status, null: false, default: "ready"
|
|
224
|
+
t.string :current_step
|
|
225
|
+
t.jsonb :step_job_options, null: false, default: {}
|
|
226
|
+
t.string :error_class
|
|
227
|
+
t.text :error_message
|
|
228
|
+
t.text :error_backtrace
|
|
229
|
+
t.datetime :paused_at
|
|
230
|
+
t.datetime :finished_at
|
|
231
|
+
t.datetime :cancelled_at
|
|
232
|
+
t.string :idempotency_key
|
|
233
|
+
t.string :definition_digest, null: false
|
|
234
|
+
t.string :rerun_of_workflow_id
|
|
235
|
+
t.datetime :created_at, null: false
|
|
236
|
+
t.datetime :updated_at, null: false
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
add_index :pgbus_workflows, :workflow_id,
|
|
240
|
+
unique: true, name: "idx_pgbus_workflows_workflow_id"
|
|
241
|
+
add_index :pgbus_workflows, [:workflow_class, :subject_type, :subject_id],
|
|
242
|
+
unique: true, where: "status IN ('ready','performing','waiting','paused')",
|
|
243
|
+
name: "idx_pgbus_workflows_one_ongoing"
|
|
244
|
+
add_index :pgbus_workflows, [:workflow_class, :idempotency_key],
|
|
245
|
+
unique: true, where: "idempotency_key IS NOT NULL",
|
|
246
|
+
name: "idx_pgbus_workflows_idempotency"
|
|
247
|
+
add_index :pgbus_workflows, [:status, :updated_at],
|
|
248
|
+
name: "idx_pgbus_workflows_status"
|
|
249
|
+
add_index :pgbus_workflows, [:workflow_class, :status, :created_at],
|
|
250
|
+
name: "idx_pgbus_workflows_class_status"
|
|
251
|
+
add_index :pgbus_workflows, [:subject_type, :subject_id],
|
|
252
|
+
name: "idx_pgbus_workflows_subject"
|
|
253
|
+
|
|
254
|
+
create_table :pgbus_workflow_steps do |t|
|
|
255
|
+
t.string :workflow_id, null: false
|
|
256
|
+
t.string :workflow_class, null: false
|
|
257
|
+
t.string :step_name, null: false
|
|
258
|
+
t.integer :attempt, null: false, default: 1
|
|
259
|
+
t.string :status, null: false, default: "scheduled"
|
|
260
|
+
t.string :job_id
|
|
261
|
+
t.bigint :msg_id
|
|
262
|
+
t.datetime :scheduled_at, null: false
|
|
263
|
+
t.datetime :enqueued_at
|
|
264
|
+
t.datetime :started_at
|
|
265
|
+
t.datetime :finished_at
|
|
266
|
+
t.datetime :stall_at
|
|
267
|
+
t.string :outcome
|
|
268
|
+
t.jsonb :output
|
|
269
|
+
t.string :error_class
|
|
270
|
+
t.text :error_message
|
|
271
|
+
t.text :error_backtrace
|
|
272
|
+
t.jsonb :error_causes
|
|
273
|
+
t.string :worker
|
|
274
|
+
t.string :app_revision
|
|
275
|
+
t.string :batch_id
|
|
276
|
+
t.string :wait_for
|
|
277
|
+
t.bigint :signal_id
|
|
278
|
+
t.datetime :waiting_since
|
|
279
|
+
t.datetime :timeout_at
|
|
280
|
+
t.datetime :created_at, null: false
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
add_index :pgbus_workflow_steps, [:workflow_id, :step_name, :attempt],
|
|
284
|
+
unique: true, name: "idx_pgbus_workflow_steps_attempt"
|
|
285
|
+
add_index :pgbus_workflow_steps, [:status, :scheduled_at],
|
|
286
|
+
name: "idx_pgbus_workflow_steps_scheduled"
|
|
287
|
+
add_index :pgbus_workflow_steps, [:status, :stall_at],
|
|
288
|
+
name: "idx_pgbus_workflow_steps_stall"
|
|
289
|
+
add_index :pgbus_workflow_steps, [:workflow_class, :step_name, :finished_at],
|
|
290
|
+
name: "idx_pgbus_workflow_steps_insights"
|
|
291
|
+
add_index :pgbus_workflow_steps, :batch_id,
|
|
292
|
+
name: "idx_pgbus_workflow_steps_batch"
|
|
293
|
+
add_index :pgbus_workflow_steps, [:status, :timeout_at],
|
|
294
|
+
name: "idx_pgbus_workflow_steps_timeout"
|
|
295
|
+
add_foreign_key :pgbus_workflow_steps, :pgbus_workflows,
|
|
296
|
+
column: :workflow_id, primary_key: :workflow_id, on_delete: :cascade
|
|
297
|
+
|
|
298
|
+
create_table :pgbus_workflow_events do |t|
|
|
299
|
+
t.string :workflow_id, null: false
|
|
300
|
+
t.bigint :step_id
|
|
301
|
+
t.string :step_name
|
|
302
|
+
t.integer :attempt
|
|
303
|
+
t.string :event, null: false
|
|
304
|
+
t.string :actor, null: false
|
|
305
|
+
t.text :reason
|
|
306
|
+
t.jsonb :details, null: false, default: {}
|
|
307
|
+
t.datetime :created_at, null: false
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
add_index :pgbus_workflow_events, [:workflow_id, :id],
|
|
311
|
+
name: "idx_pgbus_workflow_events_workflow"
|
|
312
|
+
add_foreign_key :pgbus_workflow_events, :pgbus_workflows,
|
|
313
|
+
column: :workflow_id, primary_key: :workflow_id, on_delete: :cascade
|
|
314
|
+
|
|
315
|
+
create_table :pgbus_workflow_signals do |t|
|
|
316
|
+
t.string :workflow_id, null: false
|
|
317
|
+
t.string :name, null: false
|
|
318
|
+
t.jsonb :payload, null: false, default: {}
|
|
319
|
+
t.string :idempotency_key
|
|
320
|
+
t.string :status, null: false, default: "pending"
|
|
321
|
+
t.bigint :claimed_by_step_id
|
|
322
|
+
t.datetime :created_at, null: false
|
|
323
|
+
t.datetime :claimed_at
|
|
324
|
+
t.datetime :consumed_at
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
add_index :pgbus_workflow_signals, [:workflow_id, :idempotency_key],
|
|
328
|
+
unique: true, where: "idempotency_key IS NOT NULL",
|
|
329
|
+
name: "idx_pgbus_workflow_signals_idempotency"
|
|
330
|
+
add_index :pgbus_workflow_signals, [:workflow_id, :name, :status],
|
|
331
|
+
name: "idx_pgbus_workflow_signals_pending"
|
|
332
|
+
add_index :pgbus_workflow_signals, [:workflow_id, :status, :claimed_by_step_id],
|
|
333
|
+
name: "idx_pgbus_workflow_signals_claimed"
|
|
334
|
+
add_foreign_key :pgbus_workflow_signals, :pgbus_workflows,
|
|
335
|
+
column: :workflow_id, primary_key: :workflow_id, on_delete: :cascade
|
|
336
|
+
|
|
214
337
|
# Stream queue registry — records which PGMQ queues back streams (vs jobs).
|
|
215
338
|
# Stream and job queues share the pgbus_ namespace, so maintenance
|
|
216
339
|
# (per-stream retention, orphan sweep) and wildcard workers rely on this
|
|
@@ -245,6 +368,10 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
245
368
|
execute "SELECT pgmq.drop_queue('pgbus_default')"
|
|
246
369
|
|
|
247
370
|
drop_table :pgbus_stream_queues
|
|
371
|
+
drop_table :pgbus_workflow_signals
|
|
372
|
+
drop_table :pgbus_workflow_events
|
|
373
|
+
drop_table :pgbus_workflow_steps
|
|
374
|
+
drop_table :pgbus_workflows
|
|
248
375
|
drop_table :pgbus_deploy_task_runs
|
|
249
376
|
drop_table :pgbus_deploy_tasks
|
|
250
377
|
drop_table :pgbus_recurring_executions
|
|
@@ -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
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module ActiveJob
|
|
5
|
+
# The PGMQ delivery of the job running in this fiber:
|
|
6
|
+
#
|
|
7
|
+
# Pgbus::ActiveJob::Delivery.current # => { msg_id: 42, read_ct: 2, queue_name: "default" }
|
|
8
|
+
#
|
|
9
|
+
# Set by Executor#execute around perform_now and cleared after, also when
|
|
10
|
+
# the job raises; nil outside the executor (perform_now in a test, a
|
|
11
|
+
# top-level inline-adapter job). A job run inline from inside an executing
|
|
12
|
+
# job sees the enclosing job's delivery; StepJob only acts on it when the
|
|
13
|
+
# step row's msg_id matches, so that is harmless. Fiber-local (Thread.current[]), so concurrent jobs under
|
|
14
|
+
# execution_mode: :async each see their own. Pgbus::Workflow::StepJob reads
|
|
15
|
+
# read_ct to tell a redelivery after a worker died from a first delivery.
|
|
16
|
+
#
|
|
17
|
+
# The three values sit in three fiber-locals and the Hash is built only
|
|
18
|
+
# when read: setting them allocates nothing, which keeps the executor's
|
|
19
|
+
# per-job allocation budget (spec/pgbus/active_job/executor_allocation_budget_spec.rb).
|
|
20
|
+
module Delivery
|
|
21
|
+
MSG_ID_KEY = :pgbus_delivery_msg_id
|
|
22
|
+
READ_CT_KEY = :pgbus_delivery_read_ct
|
|
23
|
+
QUEUE_NAME_KEY = :pgbus_delivery_queue_name
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def current
|
|
28
|
+
msg_id = Thread.current[MSG_ID_KEY]
|
|
29
|
+
return nil if msg_id.nil?
|
|
30
|
+
|
|
31
|
+
{ msg_id: msg_id, read_ct: Thread.current[READ_CT_KEY], queue_name: Thread.current[QUEUE_NAME_KEY] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with(msg_id:, read_ct:, queue_name:)
|
|
35
|
+
previous_msg_id = Thread.current[MSG_ID_KEY]
|
|
36
|
+
previous_read_ct = Thread.current[READ_CT_KEY]
|
|
37
|
+
previous_queue_name = Thread.current[QUEUE_NAME_KEY]
|
|
38
|
+
Thread.current[MSG_ID_KEY] = msg_id
|
|
39
|
+
Thread.current[READ_CT_KEY] = read_ct
|
|
40
|
+
Thread.current[QUEUE_NAME_KEY] = queue_name
|
|
41
|
+
yield
|
|
42
|
+
ensure
|
|
43
|
+
Thread.current[MSG_ID_KEY] = previous_msg_id
|
|
44
|
+
Thread.current[READ_CT_KEY] = previous_read_ct
|
|
45
|
+
Thread.current[QUEUE_NAME_KEY] = previous_queue_name
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -99,7 +99,9 @@ module Pgbus
|
|
|
99
99
|
# `batch` (and `batch.enqueue` for open batches) work inside a job.
|
|
100
100
|
assign_batch_id(job, payload)
|
|
101
101
|
Pgbus.logger.debug { "[Pgbus::Executor] running #{log_tag(message, queue_name)} job_class=#{job_class}" }
|
|
102
|
-
with_visibility_heartbeat(job, queue_name, msg_id, source_queue, payload)
|
|
102
|
+
with_visibility_heartbeat(job, queue_name, msg_id, source_queue, payload) do
|
|
103
|
+
Delivery.with(msg_id: msg_id, read_ct: read_count, queue_name: queue_name) { execute_job(job) }
|
|
104
|
+
end
|
|
103
105
|
# retry_on re-enqueues from inside perform_now and returns normally:
|
|
104
106
|
# this attempt is done (archive it) but the job is not — the retry
|
|
105
107
|
# message carries the batch tag and signals on its own outcome.
|
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
|
|