ductwork 0.25.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 +72 -0
- data/CHANGELOG.md +148 -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 +46 -1
- data/lib/ductwork/dsl/definition_builder.rb +101 -2
- 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 +152 -255
- 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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
# NOTE: these are SQL fragments that resolve against the database server's
|
|
5
|
+
# clock instead of the calling Ruby process's clock. use them in `WHERE`
|
|
6
|
+
# clauses that compare between a stored timestamp and "now" so that NTP
|
|
7
|
+
# drift between hosts cannot make healthy work look stale (or vice versa)
|
|
8
|
+
class DatabaseClock
|
|
9
|
+
def self.ago_sql(column, interval)
|
|
10
|
+
new(column).ago_sql(interval)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.now_sql(column)
|
|
14
|
+
new(column).now_sql
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.now
|
|
18
|
+
adapter = Ductwork::Record.connection.adapter_name.downcase
|
|
19
|
+
sql =
|
|
20
|
+
case adapter
|
|
21
|
+
when /postgresql|cockroach/i
|
|
22
|
+
"SELECT clock_timestamp()"
|
|
23
|
+
when /mysql|trilogy/i
|
|
24
|
+
"SELECT CURRENT_TIMESTAMP(6)"
|
|
25
|
+
when /sqlite/i
|
|
26
|
+
"SELECT strftime('%Y-%m-%d %H:%M:%f', 'now')"
|
|
27
|
+
when /oracle/i
|
|
28
|
+
"SELECT CURRENT_TIMESTAMP FROM dual"
|
|
29
|
+
else
|
|
30
|
+
raise NotImplementedError, "Database clock does not support adapter #{adapter}"
|
|
31
|
+
end
|
|
32
|
+
raw = Ductwork::Record.connection.select_value(sql)
|
|
33
|
+
|
|
34
|
+
case raw
|
|
35
|
+
when ::Time, ::DateTime
|
|
36
|
+
raw.in_time_zone
|
|
37
|
+
else
|
|
38
|
+
# NOTE: SQLite returns an ISO-8601 string already expressed in UTC
|
|
39
|
+
::Time.find_zone!("UTC").parse(raw.to_s)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(column)
|
|
44
|
+
@adapter = Ductwork::Record.connection.adapter_name.downcase
|
|
45
|
+
@column = column
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ago_sql(interval)
|
|
49
|
+
seconds = interval.to_i
|
|
50
|
+
|
|
51
|
+
case adapter
|
|
52
|
+
when /postgresql|cockroach/i
|
|
53
|
+
"#{column} <= clock_timestamp() - INTERVAL '#{seconds} seconds'"
|
|
54
|
+
when /mysql|trilogy/i
|
|
55
|
+
"#{column} <= CURRENT_TIMESTAMP(6) - INTERVAL #{seconds} SECOND"
|
|
56
|
+
when /sqlite/i
|
|
57
|
+
"julianday(#{column}) <= julianday('now', '-#{seconds} seconds')"
|
|
58
|
+
when /oracle/i
|
|
59
|
+
"#{column} <= CURRENT_TIMESTAMP - NUMTODSINTERVAL(#{seconds}, 'SECOND')"
|
|
60
|
+
else
|
|
61
|
+
raise NotImplementedError, "Database clock does not support adapter #{adapter}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def now_sql
|
|
66
|
+
case adapter
|
|
67
|
+
when /sqlite/i
|
|
68
|
+
"julianday(#{column}) <= julianday('now')"
|
|
69
|
+
when /mysql|trilogy/i
|
|
70
|
+
"#{column} <= CURRENT_TIMESTAMP(6)"
|
|
71
|
+
when /postgresql|cockroach/i
|
|
72
|
+
"#{column} <= clock_timestamp()"
|
|
73
|
+
when /oracle/i
|
|
74
|
+
"#{column} <= CURRENT_TIMESTAMP"
|
|
75
|
+
else
|
|
76
|
+
raise NotImplementedError, "Database clock does not support adapter #{adapter}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
attr_reader :adapter, :column
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -11,6 +11,7 @@ module Ductwork
|
|
|
11
11
|
@last_node = last_node
|
|
12
12
|
@definition = definition
|
|
13
13
|
@expansions = 0
|
|
14
|
+
@expand_sources = []
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def chain(next_klass)
|
|
@@ -43,6 +44,31 @@ module Ductwork
|
|
|
43
44
|
self
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
def divert(to:)
|
|
48
|
+
to_map = {}
|
|
49
|
+
next_nodes = []
|
|
50
|
+
to.each do |key, klass|
|
|
51
|
+
node = node_name(klass)
|
|
52
|
+
definition[:edges][node] ||= { klass: klass.name }
|
|
53
|
+
to_map[key.to_s] = node
|
|
54
|
+
next_nodes << node
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
definition[:edges][last_node][:to] = to_map
|
|
58
|
+
definition[:edges][last_node][:type] = :divert
|
|
59
|
+
definition[:nodes].push(*next_nodes)
|
|
60
|
+
|
|
61
|
+
if block_given?
|
|
62
|
+
sub_branches = next_nodes.map do |last_node|
|
|
63
|
+
Ductwork::DSL::BranchBuilder.new(last_node:, definition:)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
yield sub_branches
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
46
72
|
def combine(*branch_builders, into:)
|
|
47
73
|
next_klass_name = node_name(into)
|
|
48
74
|
definition[:edges][last_node][:to] = [next_klass_name]
|
|
@@ -58,8 +84,26 @@ module Ductwork
|
|
|
58
84
|
self
|
|
59
85
|
end
|
|
60
86
|
|
|
87
|
+
def converge(*branch_builders, into:)
|
|
88
|
+
next_klass_name = node_name(into)
|
|
89
|
+
definition[:edges][last_node][:to] = [next_klass_name]
|
|
90
|
+
definition[:edges][last_node][:type] = :converge
|
|
91
|
+
|
|
92
|
+
branch_builders.each do |branch|
|
|
93
|
+
definition[:edges][branch.last_node][:to] = [next_klass_name]
|
|
94
|
+
definition[:edges][branch.last_node][:type] = :converge
|
|
95
|
+
end
|
|
96
|
+
definition[:nodes].push(next_klass_name)
|
|
97
|
+
definition[:edges][next_klass_name] ||= { klass: into.name }
|
|
98
|
+
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
61
102
|
def expand(to:)
|
|
62
103
|
next_klass_name = node_name(to)
|
|
104
|
+
# NOTE: record the expanding node so the matching `collapse` can stamp it
|
|
105
|
+
# as `barrier_node` (see `Ductwork::DSL::DefinitionBuilder#expand`).
|
|
106
|
+
expand_sources.push(last_node)
|
|
63
107
|
definition[:edges][last_node][:to] = [next_klass_name]
|
|
64
108
|
definition[:edges][last_node][:type] = :expand
|
|
65
109
|
definition[:nodes].push(next_klass_name)
|
|
@@ -79,6 +123,7 @@ module Ductwork
|
|
|
79
123
|
next_klass_name = node_name(into)
|
|
80
124
|
definition[:edges][last_node][:to] = [next_klass_name]
|
|
81
125
|
definition[:edges][last_node][:type] = :collapse
|
|
126
|
+
definition[:edges][last_node][:barrier_node] = expand_sources.pop
|
|
82
127
|
|
|
83
128
|
definition[:nodes].push(next_klass_name)
|
|
84
129
|
definition[:edges][next_klass_name] ||= { klass: into.name }
|
|
@@ -90,7 +135,7 @@ module Ductwork
|
|
|
90
135
|
|
|
91
136
|
private
|
|
92
137
|
|
|
93
|
-
attr_reader :definition, :expansions
|
|
138
|
+
attr_reader :definition, :expansions, :expand_sources
|
|
94
139
|
|
|
95
140
|
def node_name(klass)
|
|
96
141
|
"#{klass.name}.#{SecureRandom.hex(4)}"
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
module DSL
|
|
5
|
-
class DefinitionBuilder
|
|
5
|
+
class DefinitionBuilder # rubocop:todo Metrics/ClassLength
|
|
6
6
|
class StartError < StandardError; end
|
|
7
7
|
class CollapseError < StandardError; end
|
|
8
8
|
class CombineError < StandardError; end
|
|
9
|
+
class DivertError < StandardError; end
|
|
10
|
+
class ConvergeError < StandardError; end
|
|
9
11
|
|
|
10
12
|
def initialize
|
|
11
13
|
@definition = {
|
|
@@ -15,6 +17,7 @@ module Ductwork
|
|
|
15
17
|
}
|
|
16
18
|
@divergences = []
|
|
17
19
|
@last_nodes = []
|
|
20
|
+
@expand_sources = []
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def start(klass)
|
|
@@ -96,6 +99,13 @@ module Ductwork
|
|
|
96
99
|
def expand(to:)
|
|
97
100
|
validate_classes!(to)
|
|
98
101
|
validate_definition_started!(action: "expanding chain")
|
|
102
|
+
|
|
103
|
+
# NOTE: record the node(s) carrying this `expand` edge before
|
|
104
|
+
# `add_edge_to_last_nodes` reassigns `@last_nodes`. The matching
|
|
105
|
+
# `collapse` stamps this on its edge as `barrier_node` so the runtime
|
|
106
|
+
# fan-in can find the expanding branch (which carries the barrier) no
|
|
107
|
+
# matter how many transitions sit between the `expand` and `collapse`.
|
|
108
|
+
expand_sources.push(last_nodes.dup)
|
|
99
109
|
add_edge_to_last_nodes(to, type: :expand)
|
|
100
110
|
divergences.push(:expand)
|
|
101
111
|
|
|
@@ -106,8 +116,73 @@ module Ductwork
|
|
|
106
116
|
validate_classes!(into)
|
|
107
117
|
validate_definition_started!(action: "collapsing steps")
|
|
108
118
|
validate_can_collapse!
|
|
119
|
+
collapse_sources = last_nodes.dup
|
|
109
120
|
add_edge_to_last_nodes(into, type: :collapse)
|
|
110
121
|
divergences.pop
|
|
122
|
+
barrier_node = pop_barrier_node
|
|
123
|
+
collapse_sources.each do |source|
|
|
124
|
+
definition[:edges][source][:barrier_node] = barrier_node
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def divert(to:) # rubocop:todo Metrics/AbcSize
|
|
131
|
+
validate_classes!(to.values)
|
|
132
|
+
validate_definition_started!(action: "diverting chain")
|
|
133
|
+
validate_fallback_step!(to.keys)
|
|
134
|
+
|
|
135
|
+
to_map = {}
|
|
136
|
+
to_nodes = []
|
|
137
|
+
to.each do |key, klass|
|
|
138
|
+
node = node_name(klass)
|
|
139
|
+
definition[:edges][node] ||= { klass: klass.name }
|
|
140
|
+
to_map[key.to_s] = node
|
|
141
|
+
to_nodes << node
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
last_nodes.each do |last_node|
|
|
145
|
+
definition[:edges][last_node][:to] = to_map
|
|
146
|
+
definition[:edges][last_node][:type] = :divert
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
@last_nodes = to_nodes
|
|
150
|
+
|
|
151
|
+
definition[:nodes].push(*to_nodes)
|
|
152
|
+
divergences.push(:divert)
|
|
153
|
+
|
|
154
|
+
if block_given?
|
|
155
|
+
branches = last_nodes.map do |last_node|
|
|
156
|
+
Ductwork::DSL::BranchBuilder
|
|
157
|
+
.new(last_node:, definition:)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
yield branches
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def converge(into:) # rubocop:todo Metrics/AbcSize
|
|
167
|
+
validate_classes!(into)
|
|
168
|
+
validate_definition_started!(action: "converging steps")
|
|
169
|
+
validate_can_converge!
|
|
170
|
+
|
|
171
|
+
divergences.pop
|
|
172
|
+
|
|
173
|
+
into_node = node_name(into)
|
|
174
|
+
definition[:edges][into_node] ||= { klass: into.name }
|
|
175
|
+
last_nodes = definition[:nodes].reverse.select do |node|
|
|
176
|
+
definition.dig(:edges, node, :to).blank?
|
|
177
|
+
end
|
|
178
|
+
last_nodes.each do |last_node|
|
|
179
|
+
definition[:edges][last_node][:to] = [into_node]
|
|
180
|
+
definition[:edges][last_node][:type] = :converge
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@last_nodes = Array(into_node)
|
|
184
|
+
|
|
185
|
+
definition[:nodes].push(into_node)
|
|
111
186
|
|
|
112
187
|
self
|
|
113
188
|
end
|
|
@@ -128,7 +203,17 @@ module Ductwork
|
|
|
128
203
|
|
|
129
204
|
private
|
|
130
205
|
|
|
131
|
-
attr_reader :definition, :last_nodes, :divergences
|
|
206
|
+
attr_reader :definition, :last_nodes, :divergences, :expand_sources
|
|
207
|
+
|
|
208
|
+
def pop_barrier_node
|
|
209
|
+
sources = expand_sources.pop || []
|
|
210
|
+
|
|
211
|
+
if sources.length == 1
|
|
212
|
+
sources.first
|
|
213
|
+
else
|
|
214
|
+
sources
|
|
215
|
+
end
|
|
216
|
+
end
|
|
132
217
|
|
|
133
218
|
def validate_classes!(klasses)
|
|
134
219
|
valid = Array(klasses).all? do |klass|
|
|
@@ -176,6 +261,20 @@ module Ductwork
|
|
|
176
261
|
end
|
|
177
262
|
end
|
|
178
263
|
|
|
264
|
+
def validate_can_converge!
|
|
265
|
+
if divergences.empty?
|
|
266
|
+
raise ConvergeError, "Must divert pipeline definition before converging steps"
|
|
267
|
+
elsif divergences[-1] != :divert
|
|
268
|
+
raise ConvergeError, "Ambiguous converge on most recently divided/expanded definition"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def validate_fallback_step!(keys)
|
|
273
|
+
if keys.exclude?(:otherwise)
|
|
274
|
+
raise DivertError, "Must specify an `otherwise` branch"
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
179
278
|
def add_edge_to_last_nodes(*klasses, type:)
|
|
180
279
|
to_nodes = klasses.map do |klass|
|
|
181
280
|
node = node_name(klass)
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ductwork
|
|
4
|
-
class
|
|
5
|
-
def initialize(klass)
|
|
4
|
+
class ExecutionClaim
|
|
5
|
+
def initialize(klass, owner_process_id)
|
|
6
6
|
@klass = klass
|
|
7
|
+
@owner_process_id = owner_process_id
|
|
7
8
|
@adapter = Ductwork::Record.connection.adapter_name.downcase
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def latest
|
|
11
12
|
claim = if supports_row_locking?
|
|
12
|
-
|
|
13
|
+
RowLockingExecutionClaim
|
|
13
14
|
else
|
|
14
|
-
|
|
15
|
+
OptimisticLockingExecutionClaim
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
claim.new(klass).latest
|
|
18
|
+
claim.new(klass, owner_process_id).latest
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
private
|
|
21
22
|
|
|
22
|
-
attr_reader :klass, :adapter
|
|
23
|
+
attr_reader :klass, :owner_process_id, :adapter
|
|
23
24
|
|
|
24
25
|
def supports_row_locking?
|
|
25
26
|
adapter.match?(/postgresql/i) ||
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
module FaultInjection
|
|
5
|
+
ENV_KEY = "DUCTWORK_FAULT"
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def checkpoint(key)
|
|
9
|
+
return if !ENV.key?(ENV_KEY)
|
|
10
|
+
|
|
11
|
+
spec = ENV.fetch(ENV_KEY, "").split(":")
|
|
12
|
+
|
|
13
|
+
return if spec[0] != key.to_s
|
|
14
|
+
|
|
15
|
+
warn "FAULT FIRING: #{spec[0]} -> #{spec[1]} (pid=#{::Process.pid})"
|
|
16
|
+
|
|
17
|
+
case spec[1]
|
|
18
|
+
when "kill" then ::Process.kill(:KILL, ::Process.pid)
|
|
19
|
+
when "raise" then raise "FaultInjection: #{key}"
|
|
20
|
+
when "sleep" then sleep(1)
|
|
21
|
+
when "exit" then exit!(1)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def with(key, action)
|
|
26
|
+
previous_value = ENV.fetch(ENV_KEY, nil)
|
|
27
|
+
ENV[ENV_KEY] = "#{key}:#{action}"
|
|
28
|
+
|
|
29
|
+
yield
|
|
30
|
+
ensure
|
|
31
|
+
ENV[ENV_KEY] = previous_value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Advancement < Ductwork::Record
|
|
5
|
+
belongs_to :process, class_name: "Ductwork::Process", optional: true
|
|
6
|
+
belongs_to :transition, class_name: "Ductwork::Transition"
|
|
7
|
+
|
|
8
|
+
validates :started_at, presence: true
|
|
9
|
+
|
|
10
|
+
CRASH_ERROR_KLASSES = %w[Ductwork::ProcessCrash Ductwork::ThreadCrash].freeze
|
|
11
|
+
|
|
12
|
+
def crash?
|
|
13
|
+
CRASH_ERROR_KLASSES.include?(error_klass)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process_crashed!
|
|
17
|
+
Ductwork::Record.transaction do
|
|
18
|
+
rows_updated = self.class
|
|
19
|
+
.where(id: id, completed_at: nil)
|
|
20
|
+
.update_all(
|
|
21
|
+
completed_at: Time.current,
|
|
22
|
+
error_klass: "Ductwork::ProcessCrash",
|
|
23
|
+
error_message: "Reaped from orphaned process"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return if rows_updated.zero?
|
|
27
|
+
|
|
28
|
+
transition.branch.release!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def thread_crashed!(expected_token)
|
|
33
|
+
Ductwork::Record.transaction do
|
|
34
|
+
rows_updated = self.class
|
|
35
|
+
.where(id: id, completed_at: nil)
|
|
36
|
+
.update_all(
|
|
37
|
+
completed_at: Time.current,
|
|
38
|
+
error_klass: "Ductwork::ThreadCrash",
|
|
39
|
+
error_message: "Advancement abandoned from a thread crash"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return if rows_updated.zero?
|
|
43
|
+
|
|
44
|
+
transition.branch.release!(expected_token)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
class Availability < Ductwork::Record
|
|
5
5
|
belongs_to :execution, class_name: "Ductwork::Execution"
|
|
6
|
+
belongs_to :process, class_name: "Ductwork::Process", optional: true
|
|
6
7
|
|
|
7
8
|
validates :started_at, presence: true
|
|
9
|
+
validates :pipeline_klass, presence: true
|
|
8
10
|
end
|
|
9
11
|
end
|