pgbus 0.18.2 → 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 +345 -142
- data/README.md +49 -0
- data/app/controllers/pgbus/deploy_tasks_controller.rb +38 -0
- data/app/controllers/pgbus/frontends_controller.rb +10 -8
- data/app/helpers/pgbus/application_helper.rb +4 -3
- 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/models/pgbus/job_stat.rb +4 -0
- data/app/models/pgbus/processed_event.rb +3 -1
- data/app/models/pgbus/stream_queue.rb +34 -3
- data/app/models/pgbus/stream_stat.rb +3 -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/active_job/current_attributes.rb +3 -1
- data/lib/pgbus/batch.rb +17 -9
- data/lib/pgbus/cli.rb +3 -0
- data/lib/pgbus/client/read_after.rb +10 -1
- data/lib/pgbus/client.rb +54 -9
- data/lib/pgbus/concurrency.rb +3 -0
- data/lib/pgbus/configuration/deploy_settings.rb +46 -0
- data/lib/pgbus/configuration.rb +87 -9
- data/lib/pgbus/current_attributes.rb +3 -1
- data/lib/pgbus/dedup_cache.rb +15 -7
- 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 +28 -0
- data/lib/pgbus/event_bus/handler.rb +16 -4
- data/lib/pgbus/execution_pools/async_pool.rb +23 -2
- data/lib/pgbus/execution_pools/thread_pool.rb +22 -0
- data/lib/pgbus/execution_pools.rb +26 -0
- data/lib/pgbus/generators/migration_detector.rb +11 -0
- data/lib/pgbus/instrumentation.rb +3 -0
- data/lib/pgbus/integrations/appsignal/probe.rb +17 -9
- data/lib/pgbus/integrations/appsignal/subscriber.rb +30 -18
- data/lib/pgbus/integrations/appsignal.rb +26 -16
- data/lib/pgbus/mcp/rack_app.rb +1 -1
- 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/metrics/subscriber.rb +35 -21
- data/lib/pgbus/outbox/poller.rb +1 -0
- data/lib/pgbus/process/consumer.rb +2 -1
- data/lib/pgbus/process/consumer_priority.rb +1 -1
- data/lib/pgbus/process/dispatcher.rb +18 -1
- data/lib/pgbus/process/heartbeat.rb +7 -2
- data/lib/pgbus/process/lifecycle.rb +5 -5
- data/lib/pgbus/process/notify_listener.rb +32 -18
- data/lib/pgbus/process/supervisor.rb +35 -3
- data/lib/pgbus/process/wake_pipe.rb +4 -1
- data/lib/pgbus/process/worker.rb +5 -2
- data/lib/pgbus/recurring/scheduler.rb +54 -20
- data/lib/pgbus/retry_backoff.rb +3 -0
- data/lib/pgbus/shareable.rb +70 -0
- data/lib/pgbus/streams/broadcastable_override.rb +3 -0
- data/lib/pgbus/streams/presence.rb +6 -7
- data/lib/pgbus/streams.rb +38 -27
- data/lib/pgbus/testing.rb +19 -3
- data/lib/pgbus/uniqueness.rb +3 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/visibility_heartbeat.rb +51 -34
- data/lib/pgbus/web/authentication.rb +19 -2
- data/lib/pgbus/web/data_source/deploy_task_list.rb +114 -0
- data/lib/pgbus/web/data_source/list_counts.rb +10 -7
- data/lib/pgbus/web/data_source.rb +1 -0
- data/lib/pgbus/web/deploy_task_state.rb +51 -0
- data/lib/pgbus/web/health_server.rb +4 -1
- data/lib/pgbus/web/process_activity.rb +1 -1
- data/lib/pgbus/web/streamer/heartbeat.rb +4 -1
- data/lib/pgbus/web/streamer/hub_client.rb +4 -1
- data/lib/pgbus/web/streamer/listener.rb +10 -1
- data/lib/pgbus/web/streamer/master_hub.rb +11 -5
- data/lib/pgbus/web/streamer/master_hub_boot.rb +3 -1
- data/lib/pgbus/web/streamer/outbound_pump.rb +4 -1
- data/lib/pgbus/web/streamer/peer_close_watcher.rb +4 -1
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +13 -1
- data/lib/pgbus.rb +92 -30
- data/sig/pgbus/active_job/adapter.rbs +57 -0
- data/sig/pgbus/client.rbs +78 -0
- data/sig/pgbus/configuration.rbs +326 -0
- data/sig/pgbus/streams/stream.rbs +25 -0
- data/sig/pgbus/streams.rbs +22 -0
- data/sig/pgbus.rbs +73 -0
- metadata +27 -1
|
@@ -24,23 +24,31 @@ module Pgbus
|
|
|
24
24
|
METRIC_PREFIX = "pgbus_"
|
|
25
25
|
private_constant :METRIC_PREFIX
|
|
26
26
|
|
|
27
|
+
# Registered once at boot; @install_lock makes the check-then-register
|
|
28
|
+
# atomic so the probe is never registered twice (issue #527).
|
|
29
|
+
@install_lock = Mutex.new
|
|
30
|
+
@installed = false
|
|
31
|
+
|
|
27
32
|
class << self
|
|
28
|
-
def install!
|
|
29
|
-
|
|
33
|
+
def install!
|
|
34
|
+
@install_lock.synchronize do
|
|
35
|
+
next false if @installed
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
::Appsignal::Probes.register :pgbus, new_probe_instance
|
|
38
|
+
@installed = true
|
|
39
|
+
end
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
def installed?
|
|
37
|
-
@installed
|
|
43
|
+
@install_lock.synchronize { @installed }
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def reset!
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
@install_lock.synchronize do
|
|
48
|
+
::Appsignal::Probes.unregister(:pgbus) if defined?(::Appsignal::Probes) &&
|
|
49
|
+
::Appsignal::Probes.respond_to?(:unregister)
|
|
50
|
+
@installed = false
|
|
51
|
+
end
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
# Visible for testing — returns a fresh runnable probe.
|
|
@@ -21,14 +21,39 @@ module Pgbus
|
|
|
21
21
|
METRIC_PREFIX = "pgbus_"
|
|
22
22
|
private_constant :BACKGROUND_JOB, :METRIC_PREFIX
|
|
23
23
|
|
|
24
|
-
# Tracked so we can detach in reset! (used by specs).
|
|
25
|
-
@
|
|
24
|
+
# Tracked so we can detach in reset! (used by specs). Installed once
|
|
25
|
+
# at boot (the engine's after_initialize); @install_lock makes the
|
|
26
|
+
# check-then-install atomic so nothing subscribes twice (issue #527).
|
|
27
|
+
@install_lock = Mutex.new
|
|
28
|
+
@subscriptions = [].freeze
|
|
29
|
+
@installed = false
|
|
26
30
|
|
|
27
31
|
class << self
|
|
28
32
|
def install!
|
|
29
|
-
|
|
33
|
+
@install_lock.synchronize do
|
|
34
|
+
next false if @installed
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
@subscriptions = subscribe_all
|
|
37
|
+
@installed = true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def installed?
|
|
42
|
+
@install_lock.synchronize { @installed }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reset!
|
|
46
|
+
@install_lock.synchronize do
|
|
47
|
+
@subscriptions.each { |s| ActiveSupport::Notifications.unsubscribe(s) }
|
|
48
|
+
@subscriptions = [].freeze
|
|
49
|
+
@installed = false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def subscribe_all
|
|
56
|
+
[
|
|
32
57
|
subscribe("pgbus.executor.execute") { |event| on_executor_execute(event) },
|
|
33
58
|
subscribe("pgbus.job_completed") { |event| on_job_completed(event) },
|
|
34
59
|
subscribe("pgbus.job_failed") { |event| on_job_failed(event) },
|
|
@@ -48,22 +73,9 @@ module Pgbus
|
|
|
48
73
|
# pgbus_pool_available gauges for AppSignal (see probe.rb).
|
|
49
74
|
# Subscribing here too would double-report on every heartbeat
|
|
50
75
|
# and skew the gauges.
|
|
51
|
-
]
|
|
52
|
-
@installed = true
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def installed?
|
|
56
|
-
@installed == true
|
|
76
|
+
].freeze
|
|
57
77
|
end
|
|
58
78
|
|
|
59
|
-
def reset!
|
|
60
|
-
@subscriptions&.each { |s| ActiveSupport::Notifications.unsubscribe(s) }
|
|
61
|
-
@subscriptions = []
|
|
62
|
-
@installed = false
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
79
|
def subscribe(name, &block)
|
|
68
80
|
# silence rubocop unused
|
|
69
81
|
ActiveSupport::Notifications.subscribe(name) do |*args|
|
|
@@ -28,41 +28,51 @@ module Pgbus
|
|
|
28
28
|
DASHBOARD_PATH = File.expand_path("appsignal/dashboard.json", __dir__).freeze
|
|
29
29
|
DASHBOARDS_DIR = File.expand_path("appsignal/dashboards", __dir__).freeze
|
|
30
30
|
|
|
31
|
+
# Installed once at boot (the engine's after_initialize); @lock makes
|
|
32
|
+
# the check-then-install atomic and guards the parsed-dashboard memos
|
|
33
|
+
# (issue #527).
|
|
34
|
+
@lock = Mutex.new
|
|
35
|
+
@installed = false
|
|
36
|
+
|
|
31
37
|
module_function
|
|
32
38
|
|
|
33
|
-
def install!
|
|
39
|
+
def install!
|
|
34
40
|
return false unless defined?(::Appsignal)
|
|
35
|
-
return false if @installed
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
installed_now = @lock.synchronize do
|
|
43
|
+
next false if @installed
|
|
44
|
+
|
|
45
|
+
Subscriber.install!
|
|
46
|
+
Probe.install! if Pgbus.configuration.appsignal_probe_enabled
|
|
47
|
+
@installed = true
|
|
48
|
+
end
|
|
49
|
+
Pgbus.logger.info { "[Pgbus] AppSignal integration installed" } if installed_now
|
|
50
|
+
installed_now
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
def installed?
|
|
45
|
-
@installed
|
|
54
|
+
@lock.synchronize { @installed }
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
def dashboard_definition
|
|
49
|
-
@dashboard_definition ||= JSON.parse(File.read(DASHBOARD_PATH))
|
|
58
|
+
@lock.synchronize { @dashboard_definition ||= JSON.parse(File.read(DASHBOARD_PATH)) }
|
|
50
59
|
end
|
|
51
60
|
|
|
52
61
|
def dashboard_definitions
|
|
53
|
-
@
|
|
54
|
-
Dir[File.join(DASHBOARDS_DIR, "*.json")].map
|
|
55
|
-
|
|
56
|
-
end
|
|
62
|
+
@lock.synchronize do
|
|
63
|
+
@dashboard_definitions ||= Dir[File.join(DASHBOARDS_DIR, "*.json")].map { |path| JSON.parse(File.read(path)) }
|
|
64
|
+
end
|
|
57
65
|
end
|
|
58
66
|
|
|
59
67
|
# Test hook: tear everything down so a fresh install! can run.
|
|
60
68
|
def reset!
|
|
61
69
|
Subscriber.reset! if defined?(Subscriber)
|
|
62
70
|
Probe.reset! if defined?(Probe)
|
|
63
|
-
@
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
@lock.synchronize do
|
|
72
|
+
@installed = false
|
|
73
|
+
@dashboard_definition = nil
|
|
74
|
+
@dashboard_definitions = nil
|
|
75
|
+
end
|
|
66
76
|
end
|
|
67
77
|
end
|
|
68
78
|
end
|
data/lib/pgbus/mcp/rack_app.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Pgbus
|
|
|
37
37
|
# First mcp release with the transport's allowed_hosts / allowed_origins /
|
|
38
38
|
# dns_rebinding_protection options. Older gems would raise ArgumentError
|
|
39
39
|
# on the pass-through; fail with the fix spelled out instead.
|
|
40
|
-
MIN_MCP_VERSION = Gem::Version.new("0.23.0")
|
|
40
|
+
MIN_MCP_VERSION = Gem::Version.new("0.23.0").freeze
|
|
41
41
|
# Only the JSON body string is frozen and reused. The outer response triple
|
|
42
42
|
# and its headers hash MUST be built fresh per call (#unauthorized) so
|
|
43
43
|
# downstream Rack middleware can mutate them — Rack::TempfileReaper assigns
|
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
|
@@ -17,15 +17,45 @@ module Pgbus
|
|
|
17
17
|
METRIC_PREFIX = "pgbus_"
|
|
18
18
|
private_constant :METRIC_PREFIX
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Installed once at boot (the engine's after_initialize) and torn down by
|
|
21
|
+
# reset! in specs; @install_lock makes the check-then-install atomic so
|
|
22
|
+
# two callers can never subscribe twice (issue #527). Handlers read
|
|
23
|
+
# @backend through the private reader: a reference swapped whole.
|
|
24
|
+
@install_lock = Mutex.new
|
|
25
|
+
@subscriptions = [].freeze
|
|
21
26
|
@backend = nil
|
|
27
|
+
@installed = false
|
|
22
28
|
|
|
23
29
|
class << self
|
|
24
30
|
def install!(backend:)
|
|
25
|
-
|
|
31
|
+
@install_lock.synchronize do
|
|
32
|
+
next false if @installed
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
@backend = backend
|
|
35
|
+
@subscriptions = subscribe_all
|
|
36
|
+
@installed = true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def installed?
|
|
41
|
+
@install_lock.synchronize { @installed }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset!
|
|
45
|
+
@install_lock.synchronize do
|
|
46
|
+
@subscriptions.each { |s| ActiveSupport::Notifications.unsubscribe(s) }
|
|
47
|
+
@subscriptions = [].freeze
|
|
48
|
+
@backend = nil
|
|
49
|
+
@installed = false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
attr_reader :backend
|
|
56
|
+
|
|
57
|
+
def subscribe_all
|
|
58
|
+
[
|
|
29
59
|
subscribe("pgbus.executor.execute") { |event| on_executor_execute(event) },
|
|
30
60
|
subscribe("pgbus.job_completed") { |event| on_job_completed(event) },
|
|
31
61
|
subscribe("pgbus.job_failed") { |event| on_job_failed(event) },
|
|
@@ -43,25 +73,9 @@ module Pgbus
|
|
|
43
73
|
subscribe("pgbus.worker.recycle") { |event| on_worker_recycle(event) },
|
|
44
74
|
subscribe("pgbus.consumer.recycle") { |event| on_consumer_recycle(event) },
|
|
45
75
|
subscribe("pgbus.client.pool") { |event| on_client_pool(event) }
|
|
46
|
-
]
|
|
47
|
-
@installed = true
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def installed?
|
|
51
|
-
@installed == true
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def reset!
|
|
55
|
-
@subscriptions&.each { |s| ActiveSupport::Notifications.unsubscribe(s) }
|
|
56
|
-
@subscriptions = []
|
|
57
|
-
@backend = nil
|
|
58
|
-
@installed = false
|
|
76
|
+
].freeze
|
|
59
77
|
end
|
|
60
78
|
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
attr_reader :backend
|
|
64
|
-
|
|
65
79
|
def subscribe(name, &block)
|
|
66
80
|
ActiveSupport::Notifications.subscribe(name) do |*args|
|
|
67
81
|
event = ActiveSupport::Notifications::Event.new(*args)
|
data/lib/pgbus/outbox/poller.rb
CHANGED
|
@@ -170,7 +170,7 @@ module Pgbus
|
|
|
170
170
|
def immediate_shutdown
|
|
171
171
|
@shutting_down = true
|
|
172
172
|
@wake_signal.notify!
|
|
173
|
-
@pool.
|
|
173
|
+
ExecutionPools.terminate(@pool, grace: config.immediate_shutdown_grace)
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
private
|
|
@@ -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
|
|
@@ -17,8 +17,8 @@ module Pgbus
|
|
|
17
17
|
# 0.1s polling interval.
|
|
18
18
|
CACHE_TTL = 5
|
|
19
19
|
|
|
20
|
-
@cache = {}
|
|
21
20
|
@cache_mutex = Mutex.new
|
|
21
|
+
@cache = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable -- only read or written under @cache_mutex
|
|
22
22
|
|
|
23
23
|
# Check if this worker should yield to a higher-priority worker.
|
|
24
24
|
# Returns true if a higher-priority healthy worker exists for
|
|
@@ -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
|
|
@@ -22,11 +22,11 @@ module Pgbus
|
|
|
22
22
|
STATES = %i[starting running paused draining stopped].freeze
|
|
23
23
|
|
|
24
24
|
TRANSITIONS = {
|
|
25
|
-
starting: %i[running stopped],
|
|
26
|
-
running: %i[paused draining stopped],
|
|
27
|
-
paused: %i[running draining stopped],
|
|
28
|
-
draining: %i[stopped],
|
|
29
|
-
stopped: []
|
|
25
|
+
starting: %i[running stopped].freeze,
|
|
26
|
+
running: %i[paused draining stopped].freeze,
|
|
27
|
+
paused: %i[running draining stopped].freeze,
|
|
28
|
+
draining: %i[stopped].freeze,
|
|
29
|
+
stopped: [].freeze
|
|
30
30
|
}.freeze
|
|
31
31
|
|
|
32
32
|
attr_reader :state
|
|
@@ -64,6 +64,9 @@ module Pgbus
|
|
|
64
64
|
@health_check_ms = health_check_ms
|
|
65
65
|
@logger = logger
|
|
66
66
|
@state_mutex = Mutex.new
|
|
67
|
+
# Serializes #start and #stop whole (see #start). Never taken by the
|
|
68
|
+
# listener thread, so #stop can join it while holding this.
|
|
69
|
+
@lifecycle_mutex = Mutex.new
|
|
67
70
|
@listening_to = Set.new
|
|
68
71
|
@commands = Queue.new
|
|
69
72
|
@running = false
|
|
@@ -98,33 +101,44 @@ module Pgbus
|
|
|
98
101
|
@state_mutex.synchronize { @delivering }
|
|
99
102
|
end
|
|
100
103
|
|
|
104
|
+
# Under @lifecycle_mutex, like #stop: a stop that ran between the
|
|
105
|
+
# @running check and Thread.new found no thread to join, and the thread
|
|
106
|
+
# started after it was joined by nobody (and a restart could run a
|
|
107
|
+
# second run_loop beside it).
|
|
101
108
|
def start
|
|
102
|
-
@
|
|
103
|
-
|
|
109
|
+
@lifecycle_mutex.synchronize do
|
|
110
|
+
@state_mutex.synchronize do
|
|
111
|
+
return self if @running
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
@running = true
|
|
114
|
+
end
|
|
115
|
+
@physical_queues.each { |q| @commands << [:listen, q] }
|
|
116
|
+
# The listener's one thread, owned by this object: started once (the
|
|
117
|
+
# @running check above, with #start and #stop serialized), the sole
|
|
118
|
+
# owner of @conn, and joined by #stop after it clears @running.
|
|
119
|
+
@thread = Thread.new { run_loop } # rubocop:disable ThreadSafety/NewThread
|
|
106
120
|
end
|
|
107
|
-
@physical_queues.each { |q| @commands << [:listen, q] }
|
|
108
|
-
@thread = Thread.new { run_loop }
|
|
109
121
|
self
|
|
110
122
|
end
|
|
111
123
|
|
|
112
124
|
def stop
|
|
113
|
-
@
|
|
114
|
-
|
|
125
|
+
@lifecycle_mutex.synchronize do
|
|
126
|
+
@state_mutex.synchronize do
|
|
127
|
+
return self unless @running
|
|
115
128
|
|
|
116
|
-
|
|
129
|
+
@running = false
|
|
130
|
+
end
|
|
131
|
+
@commands << [:stop]
|
|
132
|
+
# Deliberately does NOT touch @conn. PG::Connection is not thread-safe
|
|
133
|
+
# and #close is PQfinish: it frees the PGconn and its OpenSSL objects
|
|
134
|
+
# out from under whatever libpq call the listener thread is making on
|
|
135
|
+
# the same connection. That is a use-after-free — a process-killing
|
|
136
|
+
# SEGV, not a rescuable PG::Error (issue #375). The listener thread is
|
|
137
|
+
# the sole owner of @conn for its entire life and closes it in
|
|
138
|
+
# run_loop's ensure; clearing @running above is the whole stop signal.
|
|
139
|
+
@thread&.join(stop_join_timeout)
|
|
140
|
+
@thread = nil
|
|
117
141
|
end
|
|
118
|
-
@commands << [:stop]
|
|
119
|
-
# Deliberately does NOT touch @conn. PG::Connection is not thread-safe
|
|
120
|
-
# and #close is PQfinish: it frees the PGconn and its OpenSSL objects
|
|
121
|
-
# out from under whatever libpq call the listener thread is making on
|
|
122
|
-
# the same connection. That is a use-after-free — a process-killing
|
|
123
|
-
# SEGV, not a rescuable PG::Error (issue #375). The listener thread is
|
|
124
|
-
# the sole owner of @conn for its entire life and closes it in
|
|
125
|
-
# run_loop's ensure; clearing @running above is the whole stop signal.
|
|
126
|
-
@thread&.join(stop_join_timeout)
|
|
127
|
-
@thread = nil
|
|
128
142
|
self
|
|
129
143
|
end
|
|
130
144
|
|
|
@@ -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
|
|
@@ -53,7 +53,10 @@ module Pgbus
|
|
|
53
53
|
|
|
54
54
|
@running = true
|
|
55
55
|
end
|
|
56
|
-
|
|
56
|
+
# The pipe watcher's one thread, owned by this object: started once
|
|
57
|
+
# (the @running check above, under @state_mutex) and stopped by #stop,
|
|
58
|
+
# which closes the pipe under it and joins it.
|
|
59
|
+
@thread = Thread.new { run_loop } # rubocop:disable ThreadSafety/NewThread
|
|
57
60
|
self
|
|
58
61
|
end
|
|
59
62
|
|