pgbus 0.19.0 → 0.19.1
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 +323 -147
- data/README.md +44 -0
- data/app/controllers/pgbus/deploy_tasks_controller.rb +38 -0
- data/app/helpers/pgbus/deploy_tasks_helper.rb +77 -0
- data/app/helpers/pgbus/processes_helper.rb +1 -1
- data/app/models/pgbus/deploy_task.rb +104 -0
- data/app/models/pgbus/deploy_task_run.rb +45 -0
- data/app/views/layouts/pgbus/application.html.erb +2 -0
- data/app/views/pgbus/deploy_tasks/_rollover_banner.html.erb +25 -0
- data/app/views/pgbus/deploy_tasks/_tasks_table.html.erb +71 -0
- data/app/views/pgbus/deploy_tasks/index.html.erb +6 -0
- data/app/views/pgbus/deploy_tasks/show.html.erb +134 -0
- data/config/i18n-tasks.yml +2 -0
- data/config/locales/da.yml +97 -0
- data/config/locales/de.yml +97 -0
- data/config/locales/en.yml +97 -0
- data/config/locales/es.yml +97 -0
- data/config/locales/fi.yml +97 -0
- data/config/locales/fr.yml +97 -0
- data/config/locales/it.yml +97 -0
- data/config/locales/ja.yml +97 -0
- data/config/locales/nb.yml +97 -0
- data/config/locales/nl.yml +97 -0
- data/config/locales/pt.yml +97 -0
- data/config/locales/sv.yml +97 -0
- data/config/routes.rb +7 -0
- data/lib/generators/pgbus/add_deploy_tasks_generator.rb +51 -0
- data/lib/generators/pgbus/templates/add_deploy_tasks_tables.rb.erb +43 -0
- data/lib/generators/pgbus/templates/deploy_tasks.yml.erb +42 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +43 -0
- data/lib/pgbus/configuration/deploy_settings.rb +46 -0
- data/lib/pgbus/configuration.rb +3 -0
- data/lib/pgbus/deploy/revision.rb +59 -0
- data/lib/pgbus/deploy/rollover.rb +65 -0
- data/lib/pgbus/deploy/runner.rb +259 -0
- data/lib/pgbus/deploy/task.rb +105 -0
- data/lib/pgbus/engine.rb +17 -0
- data/lib/pgbus/generators/migration_detector.rb +11 -0
- data/lib/pgbus/instrumentation.rb +3 -0
- data/lib/pgbus/mcp/server.rb +2 -1
- data/lib/pgbus/mcp/tools/deploy_tasks_tool.rb +86 -0
- data/lib/pgbus/mcp.rb +1 -0
- data/lib/pgbus/outbox/poller.rb +1 -0
- data/lib/pgbus/process/consumer.rb +1 -0
- data/lib/pgbus/process/dispatcher.rb +18 -1
- data/lib/pgbus/process/heartbeat.rb +7 -2
- data/lib/pgbus/process/supervisor.rb +35 -3
- data/lib/pgbus/process/worker.rb +1 -0
- data/lib/pgbus/recurring/scheduler.rb +54 -20
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/deploy_task_list.rb +114 -0
- data/lib/pgbus/web/data_source/list_counts.rb +2 -1
- data/lib/pgbus/web/data_source.rb +1 -0
- data/lib/pgbus/web/deploy_task_state.rb +51 -0
- data/sig/pgbus/configuration.rbs +10 -0
- metadata +20 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Deploy
|
|
5
|
+
# One declared deploy task (issue #536): an entry of
|
|
6
|
+
# config/deploy_tasks.yml. The sibling of Recurring::Task without a
|
|
7
|
+
# schedule: it runs once per (key, version) after a rollover, or after
|
|
8
|
+
# every rollover when every_deploy is set.
|
|
9
|
+
class Task
|
|
10
|
+
DEFAULT_VERSION = "1"
|
|
11
|
+
|
|
12
|
+
attr_reader :key, :class_name, :command, :version, :every_deploy, :queue_name,
|
|
13
|
+
:arguments, :priority, :description
|
|
14
|
+
|
|
15
|
+
def self.from_configuration(key, **options)
|
|
16
|
+
options = options.transform_keys(&:to_sym)
|
|
17
|
+
new(
|
|
18
|
+
key: key.to_s,
|
|
19
|
+
class_name: options[:class],
|
|
20
|
+
command: options[:command],
|
|
21
|
+
version: options.fetch(:version, DEFAULT_VERSION),
|
|
22
|
+
explicit_version: options.key?(:version),
|
|
23
|
+
every_deploy: options.fetch(:every_deploy, false),
|
|
24
|
+
queue_name: options[:queue],
|
|
25
|
+
arguments: options[:args].is_a?(Hash) ? [options[:args]] : Array(options[:args]),
|
|
26
|
+
priority: options.fetch(:priority, 0).to_i,
|
|
27
|
+
description: options[:description]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(key:, class_name: nil, command: nil, version: DEFAULT_VERSION, explicit_version: false,
|
|
32
|
+
every_deploy: false, queue_name: nil, arguments: [], priority: 0, description: nil)
|
|
33
|
+
@key = key
|
|
34
|
+
@class_name = class_name
|
|
35
|
+
@command = command
|
|
36
|
+
@version = version.to_s.strip
|
|
37
|
+
@explicit_version = explicit_version
|
|
38
|
+
@every_deploy = every_deploy
|
|
39
|
+
@queue_name = queue_name
|
|
40
|
+
@arguments = arguments || []
|
|
41
|
+
@priority = priority || 0
|
|
42
|
+
@description = description
|
|
43
|
+
@errors = []
|
|
44
|
+
@warnings = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def valid?
|
|
48
|
+
@errors = []
|
|
49
|
+
@warnings = []
|
|
50
|
+
validate!
|
|
51
|
+
@errors.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def errors
|
|
55
|
+
valid? unless @validated
|
|
56
|
+
@errors
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def warnings
|
|
60
|
+
valid? unless @validated
|
|
61
|
+
@warnings
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# What an arm for this task is stamped with: its version, or the
|
|
65
|
+
# revision it runs on for an every_deploy task.
|
|
66
|
+
def armed_for(revision)
|
|
67
|
+
every_deploy ? revision : version
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def job_class
|
|
71
|
+
class_name&.safe_constantize
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_h
|
|
75
|
+
{
|
|
76
|
+
key: key, class_name: class_name, command: command, version: version, every_deploy: every_deploy,
|
|
77
|
+
queue_name: queue_name, arguments: arguments, priority: priority, description: description
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def validate!
|
|
84
|
+
@validated = true
|
|
85
|
+
@errors << "Key must not be blank" if key.to_s.strip.empty?
|
|
86
|
+
validate_target!
|
|
87
|
+
@errors << "Version must not be blank" if version.empty?
|
|
88
|
+
@errors << "every_deploy must be true or false" unless [true, false].include?(every_deploy)
|
|
89
|
+
return unless every_deploy == true && @explicit_version
|
|
90
|
+
|
|
91
|
+
@warnings << "every_deploy is set, so version is ignored (it runs after every rollover)"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_target!
|
|
95
|
+
{ class: class_name, command: command }.each do |name, value|
|
|
96
|
+
@errors << "#{name} must be a String" unless value.nil? || value.is_a?(String)
|
|
97
|
+
end
|
|
98
|
+
has_class = !class_name.to_s.strip.empty?
|
|
99
|
+
has_command = !command.to_s.strip.empty?
|
|
100
|
+
@errors << "Either class or command is required" unless has_class || has_command
|
|
101
|
+
@errors << "Declare either class or command, not both" if has_class && has_command
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/pgbus/engine.rb
CHANGED
|
@@ -58,6 +58,23 @@ module Pgbus
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# config/deploy_tasks.yml (issue #536): same shape and loader as
|
|
62
|
+
# recurring.yml. Explicit deploy_tasks_files replace the default file, so
|
|
63
|
+
# this runs after the app's config/initializers set them.
|
|
64
|
+
initializer "pgbus.deploy_tasks", after: :load_config_initializers do |app|
|
|
65
|
+
config = Pgbus.configuration
|
|
66
|
+
next if config.deploy_tasks
|
|
67
|
+
|
|
68
|
+
files = config.deploy_tasks_files
|
|
69
|
+
default_path = app.root.join("config", "deploy_tasks.yml")
|
|
70
|
+
tasks = if files
|
|
71
|
+
Pgbus::Recurring::ConfigLoader.load_all(files)
|
|
72
|
+
elsif default_path.exist?
|
|
73
|
+
Pgbus::Recurring::ConfigLoader.load(default_path)
|
|
74
|
+
end
|
|
75
|
+
config.deploy_tasks = tasks if tasks.present?
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
initializer "pgbus.db" do
|
|
62
79
|
ActiveSupport.on_load(:active_record) do
|
|
63
80
|
Pgbus::BusRecord.connects_to(**Pgbus.configuration.connects_to) if Pgbus.configuration.connects_to
|
|
@@ -77,6 +77,7 @@ module Pgbus
|
|
|
77
77
|
add_queue_states: "pgbus:add_queue_states",
|
|
78
78
|
add_outbox: "pgbus:add_outbox",
|
|
79
79
|
add_recurring: "pgbus:add_recurring",
|
|
80
|
+
add_deploy_tasks: "pgbus:add_deploy_tasks",
|
|
80
81
|
add_failed_events_index: "pgbus:add_failed_events_index",
|
|
81
82
|
add_processed_event_completion: "pgbus:add_processed_event_completion",
|
|
82
83
|
add_batch_executions: "pgbus:add_batch_executions",
|
|
@@ -99,6 +100,7 @@ module Pgbus
|
|
|
99
100
|
add_queue_states: "queue states table (pause/resume)",
|
|
100
101
|
add_outbox: "outbox entries table (transactional outbox)",
|
|
101
102
|
add_recurring: "recurring tasks + executions tables",
|
|
103
|
+
add_deploy_tasks: "deploy tasks + runs tables (one-time jobs run after a rollover)",
|
|
102
104
|
add_failed_events_index: "unique index on pgbus_failed_events (queue_name, msg_id)",
|
|
103
105
|
add_processed_event_completion: "completed_at on pgbus_processed_events (two-phase idempotency claim)",
|
|
104
106
|
add_batch_executions: "batch execution-row tracking (self-healing completion, on_failure rename)",
|
|
@@ -127,6 +129,7 @@ module Pgbus
|
|
|
127
129
|
*queue_states_migrations,
|
|
128
130
|
*outbox_migrations,
|
|
129
131
|
*recurring_migrations,
|
|
132
|
+
*deploy_tasks_migrations,
|
|
130
133
|
*failed_events_index_migrations,
|
|
131
134
|
*processed_event_completion_migrations,
|
|
132
135
|
*batch_executions_migrations,
|
|
@@ -215,6 +218,14 @@ module Pgbus
|
|
|
215
218
|
[:add_recurring]
|
|
216
219
|
end
|
|
217
220
|
|
|
221
|
+
# Same shape as recurring_migrations: plain create_table in the
|
|
222
|
+
# template, so the generator is queued only while a table is absent.
|
|
223
|
+
def deploy_tasks_migrations
|
|
224
|
+
return [] if table_exists?("pgbus_deploy_tasks") && table_exists?("pgbus_deploy_task_runs")
|
|
225
|
+
|
|
226
|
+
[:add_deploy_tasks]
|
|
227
|
+
end
|
|
228
|
+
|
|
218
229
|
def failed_events_index_migrations
|
|
219
230
|
# pgbus_failed_events is created by pgbus:install, but the
|
|
220
231
|
# unique (queue_name, msg_id) index was added later via its own
|
|
@@ -28,6 +28,9 @@ module Pgbus
|
|
|
28
28
|
# pgbus.stream.broadcast — stream broadcast (sync or deferred)
|
|
29
29
|
# pgbus.outbox.publish — outbox row created
|
|
30
30
|
# pgbus.recurring.enqueue — scheduler enqueued a due recurring task
|
|
31
|
+
# pgbus.deploy_task.enqueue — a deploy task enqueue was attempted (after a rollover or by
|
|
32
|
+
# Run now); payload: task, version, revision, triggered_by, plus
|
|
33
|
+
# :exception/:exception_object when the enqueue raised
|
|
31
34
|
# pgbus.worker.recycle — worker hit a recycle threshold (reason/jobs_processed/memory_mb/lifetime_seconds)
|
|
32
35
|
# pgbus.consumer.recycle — consumer hit a recycle threshold (same payload shape as pgbus.worker.recycle)
|
|
33
36
|
# pgbus.serializer.serialize — job/event serialization
|
data/lib/pgbus/mcp/server.rb
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
# Deploy tasks with their state and the rollover the scheduler waits on.
|
|
7
|
+
# Maps to DataSource#deploy_tasks and #deploy_rollover. Job arguments are
|
|
8
|
+
# not part of the list payload (and are dropped here if a data source
|
|
9
|
+
# ever adds them), so the tool never carries them.
|
|
10
|
+
class DeployTasksTool < BaseTool
|
|
11
|
+
tool_name "pgbus_deploy_tasks"
|
|
12
|
+
title "Pgbus Deploy Tasks"
|
|
13
|
+
description <<~DESC
|
|
14
|
+
Deploy tasks: one-time jobs the scheduler enqueues once every live pgbus process runs the new app revision. Lists each task's state (disabled, waiting, due, ran, never) and the rollover status the scheduler waits on.
|
|
15
|
+
Returns every task unless page or per_page is given; then it is
|
|
16
|
+
paginated (per_page defaults to 100, capped at 100). The response
|
|
17
|
+
carries page, per_page, total (capped at 10000 when paginated; the
|
|
18
|
+
exact row count when not) and has_more; per_page is null when
|
|
19
|
+
unpaginated.
|
|
20
|
+
DESC
|
|
21
|
+
|
|
22
|
+
MAX_PER_PAGE = 100
|
|
23
|
+
MAX_PAGE = 1_000
|
|
24
|
+
|
|
25
|
+
input_schema(
|
|
26
|
+
properties: {
|
|
27
|
+
page: { type: "integer", description: "1-based page number (default 1, max 1000).", minimum: 1 },
|
|
28
|
+
per_page: { type: "integer", description: "Rows per page (default 100 once paginating, max 100).", minimum: 1 }
|
|
29
|
+
},
|
|
30
|
+
required: []
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def self.call(page: nil, per_page: nil, server_context: nil)
|
|
34
|
+
data_source = data_source_from(server_context)
|
|
35
|
+
return unpaged(data_source) if page.nil? && per_page.nil?
|
|
36
|
+
|
|
37
|
+
per_page = (per_page || MAX_PER_PAGE).to_i.clamp(1, MAX_PER_PAGE)
|
|
38
|
+
page = (page || 1).to_i.clamp(1, MAX_PAGE)
|
|
39
|
+
tasks = data_source.deploy_tasks(page: page, per_page: per_page)
|
|
40
|
+
count = data_source.list_count(:deploy_tasks)
|
|
41
|
+
|
|
42
|
+
json_response(
|
|
43
|
+
payload(data_source, tasks).merge(
|
|
44
|
+
page: page, per_page: per_page, total: count.total,
|
|
45
|
+
has_more: more_pages?(count, page: page, per_page: per_page, shown: tasks.size)
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.unpaged(data_source)
|
|
51
|
+
tasks = data_source.deploy_tasks
|
|
52
|
+
json_response(
|
|
53
|
+
payload(data_source, tasks).merge(page: 1, per_page: nil, total: tasks.size, has_more: false)
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
private_class_method :unpaged
|
|
57
|
+
|
|
58
|
+
def self.payload(data_source, tasks)
|
|
59
|
+
rollover = data_source.deploy_rollover
|
|
60
|
+
{ rollover: serialize(rollover), tasks: tasks.map { |row| task_hash(row, rollover) } }
|
|
61
|
+
end
|
|
62
|
+
private_class_method :payload
|
|
63
|
+
|
|
64
|
+
def self.task_hash(row, rollover)
|
|
65
|
+
state = Pgbus::Web::DeployTaskState.present(row, rollover)
|
|
66
|
+
serialize(row.except(:arguments)).merge(
|
|
67
|
+
"state" => state.state, "reason" => state.reason_key, "reason_args" => serialize(state.reason_args)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
private_class_method :task_hash
|
|
71
|
+
|
|
72
|
+
# Times as ISO 8601 UTC, symbols as strings, string keys throughout.
|
|
73
|
+
def self.serialize(value)
|
|
74
|
+
case value
|
|
75
|
+
when Hash then value.to_h { |key, val| [key.to_s, serialize(val)] }
|
|
76
|
+
when Array then value.map { |val| serialize(val) }
|
|
77
|
+
when Symbol then value.to_s
|
|
78
|
+
when Time, DateTime, ActiveSupport::TimeWithZone then value.utc.iso8601
|
|
79
|
+
else value
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
private_class_method :serialize
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/pgbus/mcp.rb
CHANGED
data/lib/pgbus/outbox/poller.rb
CHANGED
|
@@ -615,6 +615,7 @@ module Pgbus
|
|
|
615
615
|
def start_heartbeat
|
|
616
616
|
@heartbeat = Heartbeat.new(
|
|
617
617
|
kind: "consumer",
|
|
618
|
+
config: @config,
|
|
618
619
|
metadata: {
|
|
619
620
|
topics: topics, threads: threads, pid: ::Process.pid, execution_mode: @execution_mode,
|
|
620
621
|
started_at: @started_at.to_f, supervisor_pid: ::Process.ppid
|
|
@@ -13,6 +13,7 @@ module Pgbus
|
|
|
13
13
|
CONCURRENCY_INTERVAL = 300 # Run concurrency cleanup every 5 minutes
|
|
14
14
|
BATCH_CLEANUP_INTERVAL = 3600 # Run batch cleanup every hour
|
|
15
15
|
RECURRING_CLEANUP_INTERVAL = 3600 # Run recurring execution cleanup every hour
|
|
16
|
+
DEPLOY_RUN_CLEANUP_INTERVAL = 3600 # Run deploy task run cleanup every hour
|
|
16
17
|
ARCHIVE_COMPACTION_INTERVAL = 3600 # Run archive compaction every hour
|
|
17
18
|
OUTBOX_CLEANUP_INTERVAL = 3600 # Run outbox cleanup every hour
|
|
18
19
|
JOB_LOCK_CLEANUP_INTERVAL = 300 # Run job lock cleanup every 5 minutes
|
|
@@ -52,7 +53,8 @@ module Pgbus
|
|
|
52
53
|
MAINTENANCE_TIMESTAMPS = %i[
|
|
53
54
|
@last_cleanup_at @last_reap_at @last_concurrency_at @last_batch_cleanup_at
|
|
54
55
|
@last_batch_sweep_at
|
|
55
|
-
@last_recurring_cleanup_at @
|
|
56
|
+
@last_recurring_cleanup_at @last_deploy_run_cleanup_at @last_archive_compaction_at
|
|
57
|
+
@last_stream_archive_compaction_at
|
|
56
58
|
@last_outbox_cleanup_at @last_job_lock_cleanup_at @last_stats_cleanup_at
|
|
57
59
|
@last_orphan_stream_sweep_at @last_table_maintenance_at
|
|
58
60
|
].freeze
|
|
@@ -236,6 +238,7 @@ module Pgbus
|
|
|
236
238
|
results << run_if_due(now, :@last_batch_cleanup_at, BATCH_CLEANUP_INTERVAL) { cleanup_batches }
|
|
237
239
|
results << run_if_due(now, :@last_batch_sweep_at, config.batch_sweep_interval) { sweep_stalled_batches }
|
|
238
240
|
results << run_if_due(now, :@last_recurring_cleanup_at, RECURRING_CLEANUP_INTERVAL) { cleanup_recurring_executions }
|
|
241
|
+
results << run_if_due(now, :@last_deploy_run_cleanup_at, DEPLOY_RUN_CLEANUP_INTERVAL) { cleanup_deploy_task_runs }
|
|
239
242
|
results << run_if_due(now, :@last_archive_compaction_at, ARCHIVE_COMPACTION_INTERVAL) { compact_archives }
|
|
240
243
|
results << run_if_due(now, :@last_stream_archive_compaction_at, ARCHIVE_COMPACTION_INTERVAL) { prune_stream_archives }
|
|
241
244
|
results << run_if_due(now, :@last_outbox_cleanup_at, OUTBOX_CLEANUP_INTERVAL) { cleanup_outbox }
|
|
@@ -681,6 +684,19 @@ module Pgbus
|
|
|
681
684
|
e
|
|
682
685
|
end
|
|
683
686
|
|
|
687
|
+
# Guarded on the table: deploy tasks default on, and an install that has
|
|
688
|
+
# not run pgbus:add_deploy_tasks must not warn every hour.
|
|
689
|
+
def cleanup_deploy_task_runs
|
|
690
|
+
retention = config.deploy_task_run_retention
|
|
691
|
+
return unless retention&.positive? && DeployTaskRun.table_exists?
|
|
692
|
+
|
|
693
|
+
deleted = DeployTaskRun.prunable_before(Time.current - retention).delete_all
|
|
694
|
+
Pgbus.logger.debug { "[Pgbus] Cleaned up #{deleted} old deploy task runs" } if deleted.positive?
|
|
695
|
+
rescue StandardError => e
|
|
696
|
+
log_maintenance_failure("Deploy task run cleanup", e)
|
|
697
|
+
e
|
|
698
|
+
end
|
|
699
|
+
|
|
684
700
|
def monotonic_now
|
|
685
701
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
686
702
|
end
|
|
@@ -697,6 +713,7 @@ module Pgbus
|
|
|
697
713
|
def start_heartbeat
|
|
698
714
|
@heartbeat = Heartbeat.new(
|
|
699
715
|
kind: "dispatcher",
|
|
716
|
+
config: @config,
|
|
700
717
|
metadata: { pid: ::Process.pid, started_at: Time.now.to_f, supervisor_pid: ::Process.ppid },
|
|
701
718
|
loop_tick_supplier: -> { @loop_tick_at.get }
|
|
702
719
|
)
|
|
@@ -11,9 +11,14 @@ module Pgbus
|
|
|
11
11
|
|
|
12
12
|
attr_reader :process_entry
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# config: the owning process's configuration, for the app revision.
|
|
15
|
+
def initialize(kind:, metadata: {}, on_beat: nil, loop_tick_supplier: nil, metadata_supplier: nil,
|
|
16
|
+
config: Pgbus.configuration)
|
|
15
17
|
@kind = kind
|
|
16
|
-
|
|
18
|
+
# Every kind's row carries the app revision from its first
|
|
19
|
+
# registration on, nil included: the deploy-task rollover gate reads
|
|
20
|
+
# it (issue #536), and beat_metadata dups this hash so it survives.
|
|
21
|
+
@metadata = metadata.merge(revision: Deploy::Revision.current(config))
|
|
17
22
|
@on_beat = on_beat
|
|
18
23
|
@loop_tick_supplier = loop_tick_supplier
|
|
19
24
|
@metadata_supplier = metadata_supplier
|
|
@@ -425,9 +425,12 @@ module Pgbus
|
|
|
425
425
|
ErrorReporter.report(e, { action: "fork_dispatcher" })
|
|
426
426
|
end
|
|
427
427
|
|
|
428
|
+
# The scheduler runs recurring tasks and deploy tasks (issue #536);
|
|
429
|
+
# either one being enabled and configured forks it.
|
|
428
430
|
def boot_scheduler
|
|
429
|
-
|
|
430
|
-
|
|
431
|
+
recurring = config.recurring_enabled && recurring_tasks_configured?
|
|
432
|
+
deploy = config.deploy_tasks_enabled && deploy_tasks_configured?
|
|
433
|
+
return unless recurring || deploy
|
|
431
434
|
|
|
432
435
|
fork_scheduler
|
|
433
436
|
end
|
|
@@ -438,7 +441,8 @@ module Pgbus
|
|
|
438
441
|
restore_signals
|
|
439
442
|
setup_child_process
|
|
440
443
|
load_rails_app
|
|
441
|
-
load_recurring_config
|
|
444
|
+
load_recurring_config if config.recurring_enabled
|
|
445
|
+
load_deploy_tasks_config if config.deploy_tasks_enabled
|
|
442
446
|
bootstrap_queues!
|
|
443
447
|
scheduler = Recurring::Scheduler.new(config: config)
|
|
444
448
|
scheduler.run
|
|
@@ -488,6 +492,33 @@ module Pgbus
|
|
|
488
492
|
config.recurring_tasks = Recurring::ConfigLoader.load(path)
|
|
489
493
|
end
|
|
490
494
|
|
|
495
|
+
def deploy_tasks_configured?
|
|
496
|
+
return true if config.deploy_tasks&.any?
|
|
497
|
+
# Explicit files replace the default file, as in load_deploy_tasks_config.
|
|
498
|
+
return config.deploy_tasks_files.any? { |f| File.exist?(f.to_s) } if config.deploy_tasks_files
|
|
499
|
+
|
|
500
|
+
path = default_deploy_tasks_path
|
|
501
|
+
path ? File.exist?(path.to_s) : false
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# The engine initializer loads config/deploy_tasks.yml in a Rails boot;
|
|
505
|
+
# this covers a scheduler fork whose config was not loaded that way.
|
|
506
|
+
def load_deploy_tasks_config
|
|
507
|
+
return if config.deploy_tasks&.any?
|
|
508
|
+
|
|
509
|
+
files = config.deploy_tasks_files
|
|
510
|
+
tasks = if files
|
|
511
|
+
Recurring::ConfigLoader.load_all(files)
|
|
512
|
+
elsif (path = default_deploy_tasks_path) && File.exist?(path.to_s)
|
|
513
|
+
Recurring::ConfigLoader.load(path)
|
|
514
|
+
end
|
|
515
|
+
config.deploy_tasks = tasks if tasks&.any?
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def default_deploy_tasks_path
|
|
519
|
+
Rails.root.join("config", "deploy_tasks.yml") if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
520
|
+
end
|
|
521
|
+
|
|
491
522
|
def boot_consumers
|
|
492
523
|
return unless config.event_consumers
|
|
493
524
|
|
|
@@ -903,6 +934,7 @@ module Pgbus
|
|
|
903
934
|
def start_heartbeat
|
|
904
935
|
@heartbeat = Heartbeat.new(
|
|
905
936
|
kind: "supervisor",
|
|
937
|
+
config: @config,
|
|
906
938
|
metadata: { pid: ::Process.pid, hostname: Socket.gethostname, started_at: Time.now.to_f }
|
|
907
939
|
)
|
|
908
940
|
@heartbeat.start
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -7,24 +7,33 @@ module Pgbus
|
|
|
7
7
|
class Scheduler
|
|
8
8
|
include Process::SignalHandler
|
|
9
9
|
|
|
10
|
-
attr_reader :schedule, :config
|
|
10
|
+
attr_reader :schedule, :config, :deploy_runner
|
|
11
11
|
|
|
12
12
|
def initialize(config: Pgbus.configuration)
|
|
13
13
|
@config = config
|
|
14
14
|
@schedule = Schedule.new(config: config)
|
|
15
|
+
# Deploy tasks (issue #536) ride this loop: the scheduler is already
|
|
16
|
+
# the one-per-deployment process, dedup'd across the overlap window.
|
|
17
|
+
@deploy_runner = Deploy::Runner.new(config: config) if config.deploy_tasks_enabled && config.deploy_tasks&.any?
|
|
15
18
|
@shutting_down = false
|
|
16
19
|
@last_runs = {}
|
|
17
20
|
@loop_tick_at = Concurrent::AtomicReference.new(nil)
|
|
21
|
+
@deploy_error = nil
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# The order is load-bearing: the heartbeat registers this scheduler's row
|
|
25
|
+
# before it arms deploy tasks, so an old-revision scheduler in the
|
|
26
|
+
# overlap window sees it as lagging. A failed registration is logged and
|
|
27
|
+
# sync still runs; the new release's workers keep the old gate closed.
|
|
20
28
|
def run
|
|
21
29
|
setup_signals
|
|
22
30
|
start_heartbeat
|
|
23
31
|
sync_recurring_tasks
|
|
32
|
+
deploy_runner&.sync!
|
|
24
33
|
|
|
25
34
|
Pgbus.logger.info do
|
|
26
35
|
"[Pgbus] Scheduler started: #{schedule.tasks.size} recurring tasks, " \
|
|
27
|
-
"interval=#{config.recurring_schedule_interval}s"
|
|
36
|
+
"#{deploy_task_count} deploy tasks, interval=#{config.recurring_schedule_interval}s"
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
loop do
|
|
@@ -44,22 +53,12 @@ module Pgbus
|
|
|
44
53
|
end
|
|
45
54
|
|
|
46
55
|
def tick(now)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
run_at: run_at
|
|
54
|
-
) do
|
|
55
|
-
schedule.enqueue_task(task, run_at: run_at)
|
|
56
|
-
@last_runs[task.key] = now
|
|
57
|
-
end
|
|
58
|
-
rescue StandardError => e
|
|
59
|
-
Pgbus.logger.error do
|
|
60
|
-
"[Pgbus] Error scheduling recurring task #{task.key}: #{e.class}: #{e.message}"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
56
|
+
tick_recurring(now) if config.recurring_enabled
|
|
57
|
+
tick_deploy_tasks(now)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def deploy_task_count
|
|
61
|
+
deploy_runner&.task_count || 0
|
|
63
62
|
end
|
|
64
63
|
|
|
65
64
|
def last_run_at(key)
|
|
@@ -94,9 +93,43 @@ module Pgbus
|
|
|
94
93
|
|
|
95
94
|
private
|
|
96
95
|
|
|
96
|
+
def tick_recurring(now)
|
|
97
|
+
schedule.due_tasks(now).each do |task, run_at|
|
|
98
|
+
Pgbus::Instrumentation.instrument(
|
|
99
|
+
"pgbus.recurring.enqueue",
|
|
100
|
+
task: task.key,
|
|
101
|
+
class_name: task.class_name,
|
|
102
|
+
queue: task.queue_name,
|
|
103
|
+
run_at: run_at
|
|
104
|
+
) do
|
|
105
|
+
schedule.enqueue_task(task, run_at: run_at)
|
|
106
|
+
@last_runs[task.key] = now
|
|
107
|
+
end
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Pgbus.logger.error do
|
|
110
|
+
"[Pgbus] Error scheduling recurring task #{task.key}: #{e.class}: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A persistent failure (the tables missing, the database down) is logged
|
|
116
|
+
# and reported once per distinct error, then at debug, not every tick.
|
|
117
|
+
def tick_deploy_tasks(now)
|
|
118
|
+
deploy_runner&.tick(now)
|
|
119
|
+
@deploy_error = nil
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
signature = "#{e.class}: #{e.message}"
|
|
122
|
+
if signature == @deploy_error
|
|
123
|
+
Pgbus.logger.debug { "[Pgbus] Error running deploy tasks (repeated): #{signature}" }
|
|
124
|
+
else
|
|
125
|
+
@deploy_error = signature
|
|
126
|
+
ErrorReporter.report(e, { action: "deploy_tasks_tick" }) # logs at error itself
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
97
130
|
def sync_recurring_tasks
|
|
98
131
|
tasks = config.recurring_tasks
|
|
99
|
-
return if tasks.nil?
|
|
132
|
+
return if tasks.nil? || !config.recurring_enabled
|
|
100
133
|
|
|
101
134
|
RecurringTask.sync_from_config!(tasks)
|
|
102
135
|
Pgbus.logger.info { "[Pgbus] Synced #{tasks.size} recurring task(s) from configuration" }
|
|
@@ -116,8 +149,9 @@ module Pgbus
|
|
|
116
149
|
def start_heartbeat
|
|
117
150
|
@heartbeat = Process::Heartbeat.new(
|
|
118
151
|
kind: "scheduler",
|
|
152
|
+
config: @config,
|
|
119
153
|
metadata: { pid: ::Process.pid, tasks: schedule.tasks.size, started_at: Time.now.to_f,
|
|
120
|
-
supervisor_pid: ::Process.ppid },
|
|
154
|
+
supervisor_pid: ::Process.ppid }.merge(deploy_runner ? { deploy_tasks: deploy_task_count } : {}),
|
|
121
155
|
loop_tick_supplier: -> { @loop_tick_at.get }
|
|
122
156
|
)
|
|
123
157
|
@heartbeat.start
|
data/lib/pgbus/version.rb
CHANGED