ductwork 0.8.0 → 0.9.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 +11 -0
- data/LICENSE.txt +1 -1
- data/lib/ductwork/processes/job_worker.rb +32 -11
- data/lib/ductwork/processes/job_worker_runner.rb +42 -47
- data/lib/ductwork/processes/pipeline_advancer.rb +33 -22
- data/lib/ductwork/processes/pipeline_advancer_runner.rb +3 -8
- data/lib/ductwork/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9edf0e9d07be90e39d099d4f2f9a65677b4b37144c8fa5c7d3d0a88ab81ed1a3
|
|
4
|
+
data.tar.gz: 91b2177670ee8333062ed6182512f4c5b2cd3ecfc3db9c36a77fcdf54397f65a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7583ac23a38875a63ac31b333348ddc9ec4e35b431ec90b335eb2580a241c508acbfcfdc462957a2ce39f5c7cb6094d146f60b8b4e49b753bfc0ead13967bcc4
|
|
7
|
+
data.tar.gz: 466d021eb4635d0d7ba284569100905069598f5c58edbdc3480e9db3d2de2ba09485b72935280598ac8223374cefebfa16b5d3c60a17709df9d2455a8c1273c5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Ductwork Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0]
|
|
4
|
+
|
|
5
|
+
- feat: add health check to job worker runner process - this is a basic check if a thread is healthy via `Thread#alive?` and restarts the thread if it is dead
|
|
6
|
+
|
|
7
|
+
## [0.8.1]
|
|
8
|
+
|
|
9
|
+
- fix: properly wrap "units of work" in rails application executor in pipeline advancer
|
|
10
|
+
- fix: remove wrapping thread creation with the rails application executor - these threads are long-running so they should not be wrapped; later commits will wrap each individual "unit of work" with the executor as recommended
|
|
11
|
+
- fix: move pipeline advancer creation into thread initialization block - this effectively doesn't change anything but is useful in case we need to do something on the advancer thread in the initializer
|
|
12
|
+
- fix: move job worker creation into thread initialization block - this effectively doesn't change anything but is useful in case we need to do something on the worker thread in the initializer
|
|
13
|
+
|
|
3
14
|
## [0.8.0]
|
|
4
15
|
|
|
5
16
|
- chore: re-organize `Ductwork::CLI` class
|
data/LICENSE.txt
CHANGED
|
@@ -4,7 +4,7 @@ the LGPLv3.0 license. Please see below for license text:
|
|
|
4
4
|
GNU LESSER GENERAL PUBLIC LICENSE
|
|
5
5
|
Version 3, 29 June 2007
|
|
6
6
|
|
|
7
|
-
Copyright (c) 2024-2025
|
|
7
|
+
Copyright (c) 2024-2025 Pen and Paper Solutions LLC
|
|
8
8
|
Everyone is permitted to copy and distribute verbatim copies
|
|
9
9
|
of this license document, but changing it is not allowed.
|
|
10
10
|
|
|
@@ -3,18 +3,44 @@
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
module Processes
|
|
5
5
|
class JobWorker
|
|
6
|
-
|
|
6
|
+
attr_reader :thread, :last_hearthbeat_at
|
|
7
|
+
|
|
8
|
+
def initialize(pipeline, id)
|
|
7
9
|
@pipeline = pipeline
|
|
8
|
-
@
|
|
10
|
+
@id = id
|
|
11
|
+
@running_context = Ductwork::RunningContext.new
|
|
12
|
+
@thread = nil
|
|
13
|
+
@last_hearthbeat_at = Time.current
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start
|
|
17
|
+
@thread = Thread.new { work_loop }
|
|
18
|
+
@thread.name = "ductwork.job_worker.#{id}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
alias restart start
|
|
22
|
+
|
|
23
|
+
def alive?
|
|
24
|
+
thread&.alive? || false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stop
|
|
28
|
+
running_context.shutdown!
|
|
9
29
|
end
|
|
10
30
|
|
|
11
|
-
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :pipeline, :id, :running_context
|
|
34
|
+
|
|
35
|
+
def work_loop
|
|
12
36
|
run_hooks_for(:start)
|
|
37
|
+
|
|
13
38
|
Ductwork.logger.debug(
|
|
14
39
|
msg: "Entering main work loop",
|
|
15
40
|
role: :job_worker,
|
|
16
41
|
pipeline: pipeline
|
|
17
42
|
)
|
|
43
|
+
|
|
18
44
|
while running_context.running?
|
|
19
45
|
Ductwork.logger.debug(
|
|
20
46
|
msg: "Attempting to claim job",
|
|
@@ -37,21 +63,16 @@ module Ductwork
|
|
|
37
63
|
)
|
|
38
64
|
sleep(polling_timeout)
|
|
39
65
|
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
shutdown
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
66
|
|
|
47
|
-
|
|
67
|
+
@last_hearthbeat_at = Time.current
|
|
68
|
+
end
|
|
48
69
|
|
|
49
|
-
def shutdown
|
|
50
70
|
Ductwork.logger.debug(
|
|
51
71
|
msg: "Shutting down",
|
|
52
72
|
role: :job_worker,
|
|
53
73
|
pipeline: pipeline
|
|
54
74
|
)
|
|
75
|
+
|
|
55
76
|
run_hooks_for(:stop)
|
|
56
77
|
end
|
|
57
78
|
|
|
@@ -6,7 +6,7 @@ module Ductwork
|
|
|
6
6
|
def initialize(pipeline)
|
|
7
7
|
@pipeline = pipeline
|
|
8
8
|
@running_context = Ductwork::RunningContext.new
|
|
9
|
-
@
|
|
9
|
+
@job_workers = []
|
|
10
10
|
|
|
11
11
|
Signal.trap(:INT) { running_context.shutdown! }
|
|
12
12
|
Signal.trap(:TERM) { running_context.shutdown! }
|
|
@@ -24,7 +24,9 @@ module Ductwork
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def run
|
|
27
|
-
|
|
27
|
+
create_process_record!
|
|
28
|
+
start_job_workers
|
|
29
|
+
|
|
28
30
|
Ductwork.logger.debug(
|
|
29
31
|
msg: "Entering main work loop",
|
|
30
32
|
role: :job_worker_runner,
|
|
@@ -34,7 +36,7 @@ module Ductwork
|
|
|
34
36
|
while running?
|
|
35
37
|
# TODO: Increase or make configurable
|
|
36
38
|
sleep(5)
|
|
37
|
-
|
|
39
|
+
check_thread_health
|
|
38
40
|
report_heartbeat!
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -43,45 +45,29 @@ module Ductwork
|
|
|
43
45
|
|
|
44
46
|
private
|
|
45
47
|
|
|
46
|
-
attr_reader :pipeline, :running_context, :
|
|
48
|
+
attr_reader :pipeline, :running_context, :job_workers
|
|
47
49
|
|
|
48
|
-
def
|
|
49
|
-
Ductwork.
|
|
50
|
+
def create_process_record!
|
|
51
|
+
Ductwork.wrap_with_app_executor do
|
|
52
|
+
Ductwork::Process.create!(
|
|
53
|
+
pid: ::Process.pid,
|
|
54
|
+
machine_identifier: Ductwork::MachineIdentifier.fetch,
|
|
55
|
+
last_heartbeat_at: Time.current
|
|
56
|
+
)
|
|
57
|
+
end
|
|
50
58
|
end
|
|
51
59
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
job_worker = Ductwork::Processes::JobWorker.new(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
Ductwork.logger.debug(
|
|
59
|
-
msg: "Creating new thread",
|
|
60
|
-
role: :job_worker_runner,
|
|
61
|
-
pipeline: pipeline
|
|
62
|
-
)
|
|
63
|
-
thread = Thread.new do
|
|
64
|
-
job_worker.run
|
|
65
|
-
end
|
|
66
|
-
thread.name = "ductwork.job_worker.#{i}"
|
|
60
|
+
def start_job_workers
|
|
61
|
+
Ductwork.configuration.job_worker_count(pipeline).times do |i|
|
|
62
|
+
job_worker = Ductwork::Processes::JobWorker.new(pipeline, i)
|
|
63
|
+
job_workers.push(job_worker)
|
|
64
|
+
job_worker.start
|
|
67
65
|
|
|
68
66
|
Ductwork.logger.debug(
|
|
69
|
-
msg: "Created new
|
|
67
|
+
msg: "Created new job worker",
|
|
70
68
|
role: :job_worker_runner,
|
|
71
69
|
pipeline: pipeline
|
|
72
70
|
)
|
|
73
|
-
|
|
74
|
-
thread
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def create_process!
|
|
79
|
-
Ductwork.wrap_with_app_executor do
|
|
80
|
-
Ductwork::Process.create!(
|
|
81
|
-
pid: ::Process.pid,
|
|
82
|
-
machine_identifier: Ductwork::MachineIdentifier.fetch,
|
|
83
|
-
last_heartbeat_at: Time.current
|
|
84
|
-
)
|
|
85
71
|
end
|
|
86
72
|
end
|
|
87
73
|
|
|
@@ -89,13 +75,17 @@ module Ductwork
|
|
|
89
75
|
running_context.running?
|
|
90
76
|
end
|
|
91
77
|
|
|
92
|
-
def
|
|
78
|
+
def check_thread_health
|
|
93
79
|
Ductwork.logger.debug(
|
|
94
80
|
msg: "Attempting to synchronize threads",
|
|
95
81
|
role: :job_worker_runner,
|
|
96
82
|
pipeline: pipeline
|
|
97
83
|
)
|
|
98
|
-
|
|
84
|
+
job_workers.each do |job_worker|
|
|
85
|
+
if !job_worker.alive?
|
|
86
|
+
job_worker.restart
|
|
87
|
+
end
|
|
88
|
+
end
|
|
99
89
|
Ductwork.logger.debug(
|
|
100
90
|
msg: "Synchronizing threads timed out",
|
|
101
91
|
role: :job_worker_runner,
|
|
@@ -113,31 +103,36 @@ module Ductwork
|
|
|
113
103
|
|
|
114
104
|
def shutdown!
|
|
115
105
|
running_context.shutdown!
|
|
106
|
+
job_workers.each(&:stop)
|
|
116
107
|
await_threads_graceful_shutdown
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
kill_remaining_job_workers
|
|
109
|
+
delete_process_record!
|
|
119
110
|
end
|
|
120
111
|
|
|
121
112
|
def await_threads_graceful_shutdown
|
|
122
113
|
timeout = Ductwork.configuration.job_worker_shutdown_timeout
|
|
123
114
|
deadline = Time.current + timeout
|
|
124
115
|
|
|
125
|
-
Ductwork.logger.debug(
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
Ductwork.logger.debug(
|
|
117
|
+
msg: "Attempting graceful shutdown",
|
|
118
|
+
role: :job_worker_runner
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
while Time.current < deadline && job_workers.any?(&:alive?)
|
|
122
|
+
job_workers.each do |job_worker|
|
|
128
123
|
break if Time.current < deadline
|
|
129
124
|
|
|
130
125
|
# TODO: Maybe make this configurable. If there's a ton of workers
|
|
131
126
|
# it may not even get to the "later" ones depending on the timeout
|
|
132
|
-
thread.join(1)
|
|
127
|
+
job_worker.thread.join(1)
|
|
133
128
|
end
|
|
134
129
|
end
|
|
135
130
|
end
|
|
136
131
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
if
|
|
140
|
-
thread.kill
|
|
132
|
+
def kill_remaining_job_workers
|
|
133
|
+
job_workers.each do |job_worker|
|
|
134
|
+
if job_worker.alive?
|
|
135
|
+
job_worker.thread.kill
|
|
141
136
|
Ductwork.logger.debug(
|
|
142
137
|
msg: "Killed thread",
|
|
143
138
|
role: :job_worker_runner,
|
|
@@ -147,7 +142,7 @@ module Ductwork
|
|
|
147
142
|
end
|
|
148
143
|
end
|
|
149
144
|
|
|
150
|
-
def
|
|
145
|
+
def delete_process_record!
|
|
151
146
|
Ductwork.wrap_with_app_executor do
|
|
152
147
|
Ductwork::Process.find_by!(
|
|
153
148
|
pid: ::Process.pid,
|
|
@@ -10,8 +10,9 @@ module Ductwork
|
|
|
10
10
|
|
|
11
11
|
def run # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
|
|
12
12
|
run_hooks_for(:start)
|
|
13
|
+
|
|
13
14
|
while running_context.running?
|
|
14
|
-
id = Ductwork
|
|
15
|
+
id = Ductwork.wrap_with_app_executor do
|
|
15
16
|
Ductwork::Pipeline
|
|
16
17
|
.in_progress
|
|
17
18
|
.where(klass: klass, claimed_for_advancing_at: nil)
|
|
@@ -24,12 +25,14 @@ module Ductwork
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
if id.present?
|
|
27
|
-
rows_updated = Ductwork
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
rows_updated = Ductwork.wrap_with_app_executor do
|
|
29
|
+
Ductwork::Pipeline
|
|
30
|
+
.where(id: id, claimed_for_advancing_at: nil)
|
|
31
|
+
.update_all(
|
|
32
|
+
claimed_for_advancing_at: Time.current,
|
|
33
|
+
status: "advancing"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
33
36
|
|
|
34
37
|
if rows_updated == 1
|
|
35
38
|
Ductwork.logger.debug(
|
|
@@ -39,23 +42,31 @@ module Ductwork
|
|
|
39
42
|
role: :pipeline_advancer
|
|
40
43
|
)
|
|
41
44
|
|
|
42
|
-
pipeline = Ductwork
|
|
43
|
-
|
|
45
|
+
pipeline = Ductwork.wrap_with_app_executor do
|
|
46
|
+
pipeline = Ductwork::Pipeline.find(id)
|
|
47
|
+
pipeline.advance!
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
Ductwork.logger.debug(
|
|
50
|
+
msg: "Pipeline advanced",
|
|
51
|
+
pipeline_id: id,
|
|
52
|
+
pipeline: klass,
|
|
53
|
+
role: :pipeline_advancer
|
|
54
|
+
)
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
# rubocop:todo Metrics/BlockNesting
|
|
57
|
+
status = pipeline.completed? ? "completed" : "in_progress"
|
|
58
|
+
# rubocop:enable Metrics/BlockNesting
|
|
59
|
+
|
|
60
|
+
# release the pipeline and set last advanced at so it doesn't
|
|
61
|
+
# block. we're not using a queue so we have to use a db
|
|
62
|
+
# timestamp
|
|
63
|
+
ensure
|
|
64
|
+
pipeline.update!(
|
|
65
|
+
claimed_for_advancing_at: nil,
|
|
66
|
+
last_advanced_at: Time.current,
|
|
67
|
+
status: status
|
|
68
|
+
)
|
|
69
|
+
end
|
|
59
70
|
else
|
|
60
71
|
Ductwork.logger.debug(
|
|
61
72
|
msg: "Did not claim pipeline, avoided race condition",
|
|
@@ -46,20 +46,15 @@ module Ductwork
|
|
|
46
46
|
|
|
47
47
|
def create_threads
|
|
48
48
|
klasses.map do |klass|
|
|
49
|
-
pipeline_advancer = Ductwork::Processes::PipelineAdvancer.new(
|
|
50
|
-
running_context,
|
|
51
|
-
klass
|
|
52
|
-
)
|
|
53
|
-
|
|
54
49
|
Ductwork.logger.debug(
|
|
55
50
|
msg: "Creating new thread",
|
|
56
51
|
role: :pipeline_advancer_runner,
|
|
57
52
|
pipeline: klass
|
|
58
53
|
)
|
|
59
54
|
thread = Thread.new do
|
|
60
|
-
Ductwork
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
Ductwork::Processes::PipelineAdvancer
|
|
56
|
+
.new(running_context, klass)
|
|
57
|
+
.run
|
|
63
58
|
end
|
|
64
59
|
thread.name = "ductwork.pipeline_advancer.#{klass}"
|
|
65
60
|
|
data/lib/ductwork/version.rb
CHANGED