ductwork 0.26.0 → 1.0.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/.claude/skills/audit-clock-drift/SKILL.md +11 -0
- data/.claude/skills/audit-database-indexes/SKILL.md +11 -0
- data/.claude/skills/audit-database-support/SKILL.md +18 -0
- data/.claude/skills/audit-durability/SKILL.md +17 -0
- data/.saturnci/database.yml +1 -0
- data/CHANGELOG-PRO.md +60 -0
- data/CHANGELOG.md +144 -1
- data/CLAUDE.md +34 -0
- data/README.md +24 -14
- data/app/controllers/ductwork/application_controller.rb +3 -3
- data/app/controllers/ductwork/dashboards_controller.rb +1 -1
- data/app/controllers/ductwork/pipelines_controller.rb +4 -3
- data/app/helpers/ductwork/application_helper.rb +2 -2
- data/app/views/ductwork/dashboards/show.html.erb +12 -12
- data/app/views/ductwork/pipelines/index.html.erb +12 -12
- data/app/views/ductwork/pipelines/show.html.erb +13 -13
- data/app/views/ductwork/step_errors/index.html.erb +2 -2
- data/lib/ductwork/branch_claim.rb +185 -0
- data/lib/ductwork/cli.rb +73 -24
- data/lib/ductwork/configuration.rb +73 -3
- data/lib/ductwork/context.rb +17 -19
- data/lib/ductwork/database_clock.rb +84 -0
- data/lib/ductwork/dsl/branch_builder.rb +6 -1
- data/lib/ductwork/dsl/definition_builder.rb +24 -1
- data/lib/ductwork/{job_claim.rb → execution_claim.rb} +7 -6
- data/lib/ductwork/fault_injection.rb +35 -0
- data/lib/ductwork/migration_helper.rb +1 -1
- data/lib/ductwork/models/advancement.rb +48 -0
- data/lib/ductwork/models/attempt.rb +9 -0
- data/lib/ductwork/models/availability.rb +2 -0
- data/lib/ductwork/models/branch.rb +800 -0
- data/lib/ductwork/models/branch_link.rb +10 -0
- data/lib/ductwork/models/execution.rb +199 -2
- data/lib/ductwork/models/job.rb +8 -121
- data/lib/ductwork/models/pipeline.rb +150 -272
- data/lib/ductwork/models/process.rb +177 -5
- data/lib/ductwork/models/result.rb +2 -1
- data/lib/ductwork/models/run.rb +119 -1
- data/lib/ductwork/models/step.rb +26 -6
- data/lib/ductwork/models/transition.rb +15 -0
- data/lib/ductwork/models/tuple.rb +1 -4
- data/lib/ductwork/models/workflow.rb +3 -0
- data/lib/ductwork/optimistic_locking_execution_claim.rb +97 -0
- data/lib/ductwork/processes/health_check.rb +87 -0
- data/lib/ductwork/processes/job_worker.rb +86 -20
- data/lib/ductwork/processes/job_worker_runner.rb +25 -37
- data/lib/ductwork/processes/pipeline_advancer.rb +48 -76
- data/lib/ductwork/processes/pipeline_advancer_runner.rb +34 -44
- data/lib/ductwork/processes/process_supervisor.rb +87 -10
- data/lib/ductwork/processes/thread_supervisor.rb +55 -21
- data/lib/ductwork/processes/thread_supervisor_runner.rb +8 -5
- data/lib/ductwork/processes/worker_health_check.rb +55 -0
- data/lib/ductwork/row_locking_execution_claim.rb +77 -0
- data/lib/ductwork/testing/rspec.rb +28 -11
- data/lib/ductwork/version.rb +1 -1
- data/lib/ductwork.rb +5 -0
- data/lib/generators/ductwork/install/install_generator.rb +14 -4
- data/lib/generators/ductwork/install/templates/config/ductwork.yml +4 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_advancements.rb +29 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_attempts.rb +20 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_availabilities.rb +18 -5
- data/lib/generators/ductwork/install/templates/db/create_ductwork_branch_links.rb +27 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_branches.rb +37 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_executions.rb +8 -1
- data/lib/generators/ductwork/install/templates/db/create_ductwork_pipelines.rb +0 -8
- data/lib/generators/ductwork/install/templates/db/create_ductwork_processes.rb +2 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_results.rb +1 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_runs.rb +45 -6
- data/lib/generators/ductwork/install/templates/db/create_ductwork_steps.rb +20 -5
- data/lib/generators/ductwork/install/templates/db/create_ductwork_transitions.rb +43 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_tuples.rb +3 -3
- data/lib/generators/ductwork/update/templates/db/add_crash_count_to_ductwork_advancements.rb +7 -0
- data/lib/generators/ductwork/update/templates/db/add_crash_count_to_ductwork_executions.rb +14 -0
- data/lib/generators/ductwork/update/templates/db/add_indexes_to_ductwork_results.rb +7 -0
- data/lib/generators/ductwork/update/templates/db/add_indexes_to_ductwork_runs.rb +9 -0
- data/lib/generators/ductwork/update/templates/db/add_indexes_to_ductwork_transitions.rb +16 -0
- data/lib/generators/ductwork/update/templates/db/add_pipeline_started_index_to_ductwork_runs.rb +7 -0
- data/lib/generators/ductwork/update/templates/db/add_process_id_to_ductwork_executions.rb +35 -0
- data/lib/generators/ductwork/update/templates/db/add_role_to_ductwork_processes.rb +20 -0
- data/lib/generators/ductwork/update/templates/db/associate_branches_to_runs.rb +74 -0
- data/lib/generators/ductwork/update/templates/db/associate_steps_to_branches.rb +24 -0
- data/lib/generators/ductwork/update/templates/db/associate_steps_to_runs.rb +72 -0
- data/lib/generators/ductwork/update/templates/db/associate_tuples_to_runs.rb +73 -0
- data/lib/generators/ductwork/update/templates/db/backfill_branch_ids_on_steps.rb +21 -0
- data/lib/generators/ductwork/update/templates/db/create_ductwork_advancements.rb +28 -0
- data/lib/generators/ductwork/update/templates/db/create_ductwork_branch_links.rb +27 -0
- data/lib/generators/ductwork/update/templates/db/create_ductwork_branches.rb +37 -0
- data/lib/generators/ductwork/update/templates/db/create_ductwork_runs.rb +55 -0
- data/lib/generators/ductwork/update/templates/db/create_ductwork_transitions.rb +32 -0
- data/lib/generators/ductwork/update/templates/db/denormalize_pipeline_klass_on_availabilities.rb +11 -4
- data/lib/generators/ductwork/update/templates/db/migrate_tables_to_uuid_primary_key.rb +5 -0
- data/lib/generators/ductwork/update/templates/db/rename_runs_to_attempts.rb +7 -0
- data/lib/generators/ductwork/update/templates/db/update_process_associations.rb +29 -0
- data/lib/generators/ductwork/update/update_generator.rb +99 -0
- metadata +47 -6
- data/lib/ductwork/optimistic_locking_job_claim.rb +0 -88
- data/lib/ductwork/row_locking_job_claim.rb +0 -75
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
module Processes
|
|
5
5
|
class JobWorker
|
|
6
|
-
attr_reader :thread, :last_heartbeat_at, :
|
|
6
|
+
attr_reader :thread, :last_heartbeat_at, :execution, :pipeline
|
|
7
7
|
|
|
8
8
|
def initialize(pipeline, id)
|
|
9
9
|
@pipeline = pipeline
|
|
@@ -18,12 +18,19 @@ module Ductwork
|
|
|
18
18
|
@thread.name = name
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
def restart
|
|
22
|
+
cleanup_dead_thread!
|
|
23
|
+
start
|
|
24
|
+
end
|
|
22
25
|
|
|
23
26
|
def alive?
|
|
24
27
|
thread&.alive? || false
|
|
25
28
|
end
|
|
26
29
|
|
|
30
|
+
def stuck?
|
|
31
|
+
execution.nil? && last_heartbeat_too_long_ago?
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def stop
|
|
28
35
|
running_context.shutdown!
|
|
29
36
|
end
|
|
@@ -45,7 +52,7 @@ module Ductwork
|
|
|
45
52
|
|
|
46
53
|
attr_reader :id, :running_context
|
|
47
54
|
|
|
48
|
-
def work_loop
|
|
55
|
+
def work_loop # rubocop:todo Metrics
|
|
49
56
|
run_hooks_for(:start)
|
|
50
57
|
|
|
51
58
|
Ductwork.logger.debug(
|
|
@@ -55,29 +62,74 @@ module Ductwork
|
|
|
55
62
|
)
|
|
56
63
|
|
|
57
64
|
while running_context.running?
|
|
58
|
-
|
|
59
|
-
msg: "Attempting to claim job",
|
|
60
|
-
role: :job_worker,
|
|
61
|
-
pipeline: pipeline
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
@job = Ductwork.wrap_with_app_executor do
|
|
65
|
-
Job.claim_latest(pipeline)
|
|
66
|
-
end
|
|
65
|
+
owner_process_id = nil
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
Ductwork.
|
|
70
|
-
job
|
|
67
|
+
begin
|
|
68
|
+
Ductwork.logger.debug(
|
|
69
|
+
msg: "Attempting to claim job",
|
|
70
|
+
role: :job_worker,
|
|
71
|
+
pipeline: pipeline
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@execution = Ductwork.wrap_with_app_executor do
|
|
75
|
+
owner_process_id = Ductwork::Process.current.id
|
|
76
|
+
Ductwork::ExecutionClaim.new(pipeline, owner_process_id).latest
|
|
71
77
|
end
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
if execution.present?
|
|
80
|
+
Ductwork::FaultInjection.checkpoint(:after_job_claim)
|
|
81
|
+
|
|
82
|
+
Ductwork.wrap_with_app_executor do
|
|
83
|
+
execution.call(pipeline, owner_process_id)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@execution = nil
|
|
87
|
+
else
|
|
88
|
+
Ductwork.logger.debug(
|
|
89
|
+
msg: "No job to claim, looping",
|
|
90
|
+
role: :job_worker,
|
|
91
|
+
pipeline: pipeline
|
|
92
|
+
)
|
|
93
|
+
sleep(polling_timeout)
|
|
94
|
+
end
|
|
95
|
+
rescue Ductwork::Execution::CommitFailed => e
|
|
96
|
+
Ductwork.logger.error(
|
|
97
|
+
msg: "Execution commit failed",
|
|
98
|
+
error_klass: e.class.name,
|
|
99
|
+
error_message: e.message,
|
|
100
|
+
job_id: execution&.job_id,
|
|
101
|
+
job_klass: execution&.job&.klass,
|
|
77
102
|
role: :job_worker,
|
|
78
103
|
pipeline: pipeline
|
|
79
104
|
)
|
|
80
|
-
|
|
105
|
+
|
|
106
|
+
@execution = nil
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
if execution.present?
|
|
109
|
+
Ductwork.wrap_with_app_executor do
|
|
110
|
+
execution.crashed!
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
Ductwork.logger.error(
|
|
115
|
+
msg: "Unexpected error in work loop",
|
|
116
|
+
error_klass: e.class.name,
|
|
117
|
+
error_message: e.message,
|
|
118
|
+
job_id: execution&.job_id,
|
|
119
|
+
job_klass: execution&.job&.klass,
|
|
120
|
+
role: :job_worker,
|
|
121
|
+
pipeline: pipeline
|
|
122
|
+
)
|
|
123
|
+
ensure
|
|
124
|
+
if execution.present? && execution.reload.completed_at.nil?
|
|
125
|
+
Ductwork.wrap_with_app_executor do
|
|
126
|
+
execution.crashed!
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@execution = nil
|
|
81
133
|
end
|
|
82
134
|
|
|
83
135
|
@last_heartbeat_at = Time.current
|
|
@@ -92,6 +144,20 @@ module Ductwork
|
|
|
92
144
|
run_hooks_for(:stop)
|
|
93
145
|
end
|
|
94
146
|
|
|
147
|
+
def cleanup_dead_thread!
|
|
148
|
+
if execution.present? && execution.reload.completed_at.nil?
|
|
149
|
+
execution.crashed!
|
|
150
|
+
end
|
|
151
|
+
ensure
|
|
152
|
+
@execution = nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def last_heartbeat_too_long_ago?
|
|
156
|
+
threshold = Ductwork.configuration.supervisor_reaper_timeout
|
|
157
|
+
|
|
158
|
+
(Time.current - last_heartbeat_at) > threshold
|
|
159
|
+
end
|
|
160
|
+
|
|
95
161
|
def run_hooks_for(event)
|
|
96
162
|
Ductwork.hooks[:worker].fetch(event, []).each do |block|
|
|
97
163
|
Ductwork.wrap_with_app_executor do
|
|
@@ -24,7 +24,7 @@ module Ductwork
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def run
|
|
27
|
-
|
|
27
|
+
adopt_or_create_process!
|
|
28
28
|
start_job_workers
|
|
29
29
|
|
|
30
30
|
Ductwork.logger.debug(
|
|
@@ -36,7 +36,7 @@ module Ductwork
|
|
|
36
36
|
while running?
|
|
37
37
|
# TODO: Increase or make configurable
|
|
38
38
|
sleep(5)
|
|
39
|
-
|
|
39
|
+
check_workers_health
|
|
40
40
|
report_heartbeat!
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -47,13 +47,9 @@ module Ductwork
|
|
|
47
47
|
|
|
48
48
|
attr_reader :pipelines, :running_context, :job_workers
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def adopt_or_create_process!
|
|
51
51
|
Ductwork.wrap_with_app_executor do
|
|
52
|
-
Ductwork::Process.
|
|
53
|
-
pid: ::Process.pid,
|
|
54
|
-
machine_identifier: Ductwork::MachineIdentifier.fetch,
|
|
55
|
-
last_heartbeat_at: Time.current
|
|
56
|
-
)
|
|
52
|
+
Ductwork::Process.adopt_or_create_current!(:job_worker)
|
|
57
53
|
end
|
|
58
54
|
end
|
|
59
55
|
|
|
@@ -78,37 +74,32 @@ module Ductwork
|
|
|
78
74
|
running_context.running?
|
|
79
75
|
end
|
|
80
76
|
|
|
81
|
-
def
|
|
82
|
-
Ductwork
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Ductwork.logger.warn(
|
|
92
|
-
msg: "Restarted job worker",
|
|
93
|
-
role: :job_worker_runner,
|
|
94
|
-
pipeline: job_worker.pipeline,
|
|
95
|
-
thread: job_worker.name
|
|
96
|
-
)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
Ductwork.logger.debug(
|
|
100
|
-
msg: "Checked thread health",
|
|
101
|
-
role: :job_worker_runner,
|
|
102
|
-
pipelines: pipelines
|
|
77
|
+
def check_workers_health
|
|
78
|
+
Ductwork::Processes::WorkerHealthCheck
|
|
79
|
+
.new(job_workers, :job_worker_runner)
|
|
80
|
+
.check
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
Ductwork.logger.warn(
|
|
83
|
+
msg: "Checking worker health failed",
|
|
84
|
+
error_klass: e.class.to_s,
|
|
85
|
+
error_message: e.message,
|
|
86
|
+
role: :job_worker_runner
|
|
103
87
|
)
|
|
104
88
|
end
|
|
105
89
|
|
|
106
90
|
def report_heartbeat!
|
|
107
91
|
Ductwork.logger.debug(msg: "Reporting heartbeat", role: :job_worker_runner)
|
|
108
92
|
Ductwork.wrap_with_app_executor do
|
|
109
|
-
Ductwork::Process.report_heartbeat!
|
|
93
|
+
Ductwork::Process.report_heartbeat!(:job_worker)
|
|
110
94
|
end
|
|
111
95
|
Ductwork.logger.debug(msg: "Reported heartbeat", role: :job_worker_runner)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
Ductwork.logger.warn(
|
|
98
|
+
msg: "Reporting heartbeat failed",
|
|
99
|
+
error_klass: e.class.to_s,
|
|
100
|
+
error_message: e.message,
|
|
101
|
+
role: :job_worker_runner
|
|
102
|
+
)
|
|
112
103
|
end
|
|
113
104
|
|
|
114
105
|
def shutdown!
|
|
@@ -116,7 +107,7 @@ module Ductwork
|
|
|
116
107
|
job_workers.each(&:stop)
|
|
117
108
|
await_threads_graceful_shutdown
|
|
118
109
|
kill_remaining_job_workers
|
|
119
|
-
|
|
110
|
+
reap_process_record!
|
|
120
111
|
end
|
|
121
112
|
|
|
122
113
|
def await_threads_graceful_shutdown
|
|
@@ -152,12 +143,9 @@ module Ductwork
|
|
|
152
143
|
end
|
|
153
144
|
end
|
|
154
145
|
|
|
155
|
-
def
|
|
146
|
+
def reap_process_record!
|
|
156
147
|
Ductwork.wrap_with_app_executor do
|
|
157
|
-
Ductwork::Process.
|
|
158
|
-
pid: ::Process.pid,
|
|
159
|
-
machine_identifier: Ductwork::MachineIdentifier.fetch
|
|
160
|
-
).delete
|
|
148
|
+
Ductwork::Process.current&.reap!(:job_worker_runner, force: true)
|
|
161
149
|
end
|
|
162
150
|
end
|
|
163
151
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
module Processes
|
|
5
5
|
class PipelineAdvancer
|
|
6
|
-
attr_reader :thread, :last_heartbeat_at, :
|
|
6
|
+
attr_reader :thread, :last_heartbeat_at, :branch
|
|
7
7
|
|
|
8
8
|
def initialize(klass, index = nil)
|
|
9
9
|
@klass = klass
|
|
@@ -18,12 +18,19 @@ module Ductwork
|
|
|
18
18
|
@thread.name = name
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
def restart
|
|
22
|
+
cleanup_dead_thread!
|
|
23
|
+
start
|
|
24
|
+
end
|
|
22
25
|
|
|
23
26
|
def alive?
|
|
24
27
|
thread&.alive? || false
|
|
25
28
|
end
|
|
26
29
|
|
|
30
|
+
def stuck?
|
|
31
|
+
branch.nil? && last_heartbeat_too_long_ago?
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def stop
|
|
28
35
|
running_context.shutdown!
|
|
29
36
|
end
|
|
@@ -43,9 +50,9 @@ module Ductwork
|
|
|
43
50
|
|
|
44
51
|
private
|
|
45
52
|
|
|
46
|
-
attr_reader :klass, :index, :running_context
|
|
53
|
+
attr_reader :klass, :index, :running_context, :original_claim_token
|
|
47
54
|
|
|
48
|
-
def work_loop
|
|
55
|
+
def work_loop
|
|
49
56
|
run_hooks_for(:start)
|
|
50
57
|
|
|
51
58
|
Ductwork.logger.debug(
|
|
@@ -55,85 +62,27 @@ module Ductwork
|
|
|
55
62
|
)
|
|
56
63
|
|
|
57
64
|
while running_context.running?
|
|
58
|
-
|
|
59
|
-
Ductwork::Pipeline
|
|
60
|
-
.in_progress
|
|
61
|
-
.where(klass: klass, claimed_for_advancing_at: nil)
|
|
62
|
-
.where(steps: Ductwork::Step.where(status: :advancing))
|
|
63
|
-
.where.not(steps: Ductwork::Step.where.not(status: %w[advancing completed]))
|
|
64
|
-
.order(:last_advanced_at)
|
|
65
|
-
.limit(1)
|
|
66
|
-
.pluck(:id)
|
|
67
|
-
.first
|
|
68
|
-
end
|
|
65
|
+
claimed = false
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
status: "advancing"
|
|
77
|
-
)
|
|
78
|
-
end
|
|
67
|
+
Ductwork.wrap_with_app_executor do
|
|
68
|
+
claimed = Branch.with_latest_claimed(klass) do |branch, transition, advancement|
|
|
69
|
+
@branch = branch
|
|
70
|
+
@original_claim_token = branch.claim_token
|
|
71
|
+
|
|
72
|
+
branch.advance!(transition, advancement)
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
pipeline: klass,
|
|
85
|
-
role: :pipeline_advancer
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
@pipeline = Ductwork.wrap_with_app_executor do
|
|
89
|
-
@pipeline = Ductwork::Pipeline.find(id)
|
|
90
|
-
pipeline.advance!
|
|
91
|
-
|
|
92
|
-
Ductwork.logger.debug(
|
|
93
|
-
msg: "Pipeline advanced",
|
|
94
|
-
pipeline_id: id,
|
|
95
|
-
pipeline: klass,
|
|
96
|
-
role: :pipeline_advancer
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# rubocop:todo Metrics/BlockNesting
|
|
100
|
-
status = if pipeline.completed?
|
|
101
|
-
"completed"
|
|
102
|
-
elsif pipeline.dampened?
|
|
103
|
-
"dampened"
|
|
104
|
-
else
|
|
105
|
-
"in_progress"
|
|
106
|
-
end
|
|
107
|
-
# rubocop:enable Metrics/BlockNesting
|
|
108
|
-
ensure
|
|
109
|
-
# release the pipeline and set last advanced at so it doesn't
|
|
110
|
-
# block. we're not using a queue so we have to use a db
|
|
111
|
-
# timestamp
|
|
112
|
-
pipeline.update!(
|
|
113
|
-
claimed_for_advancing_at: nil,
|
|
114
|
-
last_advanced_at: Time.current,
|
|
115
|
-
status: status || "in_progress"
|
|
116
|
-
)
|
|
117
|
-
end
|
|
118
|
-
else
|
|
119
|
-
Ductwork.logger.debug(
|
|
120
|
-
msg: "Did not claim pipeline, avoided race condition",
|
|
121
|
-
pipeline_id: id,
|
|
122
|
-
pipeline: klass,
|
|
123
|
-
role: :pipeline_advancer
|
|
124
|
-
)
|
|
74
|
+
Ductwork::FaultInjection.checkpoint(:after_branch_advancement)
|
|
75
|
+
ensure
|
|
76
|
+
@branch = nil
|
|
77
|
+
@original_claim_token = nil
|
|
125
78
|
end
|
|
126
|
-
else
|
|
127
|
-
Ductwork.logger.debug(
|
|
128
|
-
msg: "No pipeline needs advancing",
|
|
129
|
-
pipeline: klass,
|
|
130
|
-
role: :pipeline_advancer
|
|
131
|
-
)
|
|
132
79
|
end
|
|
133
80
|
|
|
134
81
|
@last_heartbeat_at = Time.current
|
|
135
82
|
|
|
136
|
-
|
|
83
|
+
if !claimed
|
|
84
|
+
sleep(polling_timeout)
|
|
85
|
+
end
|
|
137
86
|
end
|
|
138
87
|
|
|
139
88
|
Ductwork.logger.debug(
|
|
@@ -145,6 +94,29 @@ module Ductwork
|
|
|
145
94
|
run_hooks_for(:stop)
|
|
146
95
|
end
|
|
147
96
|
|
|
97
|
+
def cleanup_dead_thread!
|
|
98
|
+
if branch.present?
|
|
99
|
+
advancement = branch
|
|
100
|
+
.transitions
|
|
101
|
+
.joins(:advancements)
|
|
102
|
+
.where(ductwork_advancements: { completed_at: nil })
|
|
103
|
+
.first
|
|
104
|
+
&.advancements
|
|
105
|
+
&.find_by(completed_at: nil)
|
|
106
|
+
|
|
107
|
+
advancement&.thread_crashed!(original_claim_token)
|
|
108
|
+
end
|
|
109
|
+
ensure
|
|
110
|
+
@branch = nil
|
|
111
|
+
@original_claim_token = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def last_heartbeat_too_long_ago?
|
|
115
|
+
threshold = Ductwork.configuration.supervisor_reaper_timeout
|
|
116
|
+
|
|
117
|
+
(Time.current - last_heartbeat_at) > threshold
|
|
118
|
+
end
|
|
119
|
+
|
|
148
120
|
def run_hooks_for(event)
|
|
149
121
|
Ductwork.hooks[:advancer].fetch(event, []).each do |block|
|
|
150
122
|
Ductwork.wrap_with_app_executor do
|
|
@@ -24,7 +24,7 @@ module Ductwork
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def run
|
|
27
|
-
|
|
27
|
+
adopt_or_create_process!
|
|
28
28
|
start_pipeline_advancers
|
|
29
29
|
|
|
30
30
|
Ductwork.logger.debug(
|
|
@@ -36,7 +36,7 @@ module Ductwork
|
|
|
36
36
|
while running_context.running?
|
|
37
37
|
# TODO: Increase or make configurable
|
|
38
38
|
sleep(5)
|
|
39
|
-
|
|
39
|
+
check_workers_health
|
|
40
40
|
report_heartbeat!
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -49,60 +49,53 @@ module Ductwork
|
|
|
49
49
|
|
|
50
50
|
def start_pipeline_advancers
|
|
51
51
|
klasses.each do |klass|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Ductwork.logger.debug(
|
|
57
|
-
msg: "Created new pipeline advancer",
|
|
58
|
-
role: :pipeline_advancer_runner,
|
|
59
|
-
pipeline: klass,
|
|
60
|
-
thread: advancer.name
|
|
61
|
-
)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
52
|
+
Ductwork.configuration.pipeline_advancer_count(klass).times do |index|
|
|
53
|
+
advancer = Ductwork::Processes::PipelineAdvancer.new(klass, index)
|
|
54
|
+
advancers.push(advancer)
|
|
55
|
+
advancer.start
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
msg: "Checking threads health",
|
|
68
|
-
role: :pipeline_advancer_runner,
|
|
69
|
-
pipelines: klasses
|
|
70
|
-
)
|
|
71
|
-
advancers.each do |advancer|
|
|
72
|
-
if !advancer.alive?
|
|
73
|
-
advancer.restart
|
|
74
|
-
|
|
75
|
-
Ductwork.logger.warn(
|
|
76
|
-
msg: "Restarted pipeline advancer",
|
|
57
|
+
Ductwork.logger.debug(
|
|
58
|
+
msg: "Created new pipeline advancer",
|
|
77
59
|
role: :pipeline_advancer_runner,
|
|
78
|
-
pipeline:
|
|
60
|
+
pipeline: klass,
|
|
79
61
|
thread: advancer.name
|
|
80
62
|
)
|
|
81
63
|
end
|
|
82
64
|
end
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check_workers_health
|
|
68
|
+
Ductwork::Processes::WorkerHealthCheck
|
|
69
|
+
.new(advancers, :pipeline_advancer_runner)
|
|
70
|
+
.check
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
Ductwork.logger.warn(
|
|
73
|
+
msg: "Checking worker health failed",
|
|
74
|
+
error_klass: e.class.to_s,
|
|
75
|
+
error_message: e.message,
|
|
76
|
+
role: :pipeline_advancer_runner
|
|
87
77
|
)
|
|
88
78
|
end
|
|
89
79
|
|
|
90
|
-
def
|
|
80
|
+
def adopt_or_create_process!
|
|
91
81
|
Ductwork.wrap_with_app_executor do
|
|
92
|
-
Ductwork::Process.
|
|
93
|
-
pid: ::Process.pid,
|
|
94
|
-
machine_identifier: Ductwork::MachineIdentifier.fetch,
|
|
95
|
-
last_heartbeat_at: Time.current
|
|
96
|
-
)
|
|
82
|
+
Ductwork::Process.adopt_or_create_current!(:pipeline_advancer)
|
|
97
83
|
end
|
|
98
84
|
end
|
|
99
85
|
|
|
100
86
|
def report_heartbeat!
|
|
101
87
|
Ductwork.logger.debug(msg: "Reporting heartbeat", role: :pipeline_advancer_runner)
|
|
102
88
|
Ductwork.wrap_with_app_executor do
|
|
103
|
-
Ductwork::Process.report_heartbeat!
|
|
89
|
+
Ductwork::Process.report_heartbeat!(:pipeline_advancer)
|
|
104
90
|
end
|
|
105
91
|
Ductwork.logger.debug(msg: "Reported heartbeat", role: :pipeline_advancer_runner)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
Ductwork.logger.warn(
|
|
94
|
+
msg: "Reporting heartbeat failed",
|
|
95
|
+
error_klass: e.class.to_s,
|
|
96
|
+
error_message: e.message,
|
|
97
|
+
role: :pipeline_advancer_runner
|
|
98
|
+
)
|
|
106
99
|
end
|
|
107
100
|
|
|
108
101
|
def shutdown!
|
|
@@ -111,7 +104,7 @@ module Ductwork
|
|
|
111
104
|
advancers.each(&:stop)
|
|
112
105
|
await_threads_graceful_shutdown
|
|
113
106
|
kill_remaining_threads
|
|
114
|
-
|
|
107
|
+
reap_process_record!
|
|
115
108
|
end
|
|
116
109
|
|
|
117
110
|
def log_shutting_down
|
|
@@ -154,12 +147,9 @@ module Ductwork
|
|
|
154
147
|
end
|
|
155
148
|
end
|
|
156
149
|
|
|
157
|
-
def
|
|
150
|
+
def reap_process_record!
|
|
158
151
|
Ductwork.wrap_with_app_executor do
|
|
159
|
-
Ductwork::Process.
|
|
160
|
-
pid: ::Process.pid,
|
|
161
|
-
machine_identifier: Ductwork::MachineIdentifier.fetch
|
|
162
|
-
).delete
|
|
152
|
+
Ductwork::Process.current&.reap!(:pipeline_advancer_runner, force: true)
|
|
163
153
|
end
|
|
164
154
|
end
|
|
165
155
|
end
|