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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f64abfa6d358cd7b225baae1b801894032936bef4de86ff0843e3163552aa530
4
- data.tar.gz: aedd9daa71379091503375926ea4863a7834193a1701ee265a9b0b99a7fbfe50
3
+ metadata.gz: 9edf0e9d07be90e39d099d4f2f9a65677b4b37144c8fa5c7d3d0a88ab81ed1a3
4
+ data.tar.gz: 91b2177670ee8333062ed6182512f4c5b2cd3ecfc3db9c36a77fcdf54397f65a
5
5
  SHA512:
6
- metadata.gz: 85598e36b5edb64f33217d19cb2f2af66e6991c68f74c0a8ede9c445d88884bd804da2b381b2dd9cdeaa6a6936a340628af25c45ff259632f36d1c4429665bcb
7
- data.tar.gz: 4f59ad232e7a00c8e34403dfb8412af7e7351f0d921b3fd7fc6c27ee10524a1bd37e57bd18aa020c9e7daf1c3374127927e54474302364f3ee6962d157b9e468
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 Tyler Ewing
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
- def initialize(pipeline, running_context)
6
+ attr_reader :thread, :last_hearthbeat_at
7
+
8
+ def initialize(pipeline, id)
7
9
  @pipeline = pipeline
8
- @running_context = running_context
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
- def run
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
- attr_reader :pipeline, :running_context
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
- @threads = create_threads
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
- create_process!
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
- attempt_synchronize_threads
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, :threads
48
+ attr_reader :pipeline, :running_context, :job_workers
47
49
 
48
- def worker_count
49
- Ductwork.configuration.job_worker_count(pipeline)
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 create_threads
53
- worker_count.times.map do |i|
54
- job_worker = Ductwork::Processes::JobWorker.new(
55
- pipeline,
56
- running_context
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 thread",
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 attempt_synchronize_threads
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
- threads.each { |thread| thread.join(0.1) }
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
- kill_remaining_threads
118
- delete_process
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(msg: "Attempting graceful shutdown", role: :job_worker_runner)
126
- while Time.current < deadline && threads.any?(&:alive?)
127
- threads.each do |thread|
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 kill_remaining_threads
138
- threads.each do |thread|
139
- if thread.alive?
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 delete_process
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::Record.uncached do
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::Pipeline
28
- .where(id: id, claimed_for_advancing_at: nil)
29
- .update_all(
30
- claimed_for_advancing_at: Time.current,
31
- status: "advancing"
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::Pipeline.find(id)
43
- pipeline.advance!
45
+ pipeline = Ductwork.wrap_with_app_executor do
46
+ pipeline = Ductwork::Pipeline.find(id)
47
+ pipeline.advance!
44
48
 
45
- Ductwork.logger.debug(
46
- msg: "Pipeline advanced",
47
- pipeline_id: id,
48
- pipeline: klass,
49
- role: :pipeline_advancer
50
- )
49
+ Ductwork.logger.debug(
50
+ msg: "Pipeline advanced",
51
+ pipeline_id: id,
52
+ pipeline: klass,
53
+ role: :pipeline_advancer
54
+ )
51
55
 
52
- # release the pipeline and set last advanced at so it doesnt block.
53
- # we're not using a queue so we have to use a db timestamp
54
- pipeline.update!(
55
- claimed_for_advancing_at: nil,
56
- last_advanced_at: Time.current,
57
- status: "in_progress"
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.wrap_with_app_executor do
61
- pipeline_advancer.run
62
- end
55
+ Ductwork::Processes::PipelineAdvancer
56
+ .new(running_context, klass)
57
+ .run
63
58
  end
64
59
  thread.name = "ductwork.pipeline_advancer.#{klass}"
65
60
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ductwork
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ductwork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Ewing