dynflow 1.2.3 → 1.3.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/.travis.yml +3 -5
- data/Gemfile +1 -14
- data/dynflow.gemspec +1 -1
- data/lib/dynflow/actor.rb +73 -1
- data/lib/dynflow/coordinator.rb +16 -0
- data/lib/dynflow/logger_adapters/formatters/exception.rb +2 -1
- data/lib/dynflow/rails/daemon.rb +3 -1
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +4 -2
- data/lib/dynflow/world/invalidation.rb +27 -8
- data/test/abnormal_states_recovery_test.rb +46 -0
- data/test/daemon_test.rb +6 -2
- data/test/executor_test.rb +2 -0
- data/web/views/flow_step.erb +1 -0
- metadata +4 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef3a326c234f3687c367017a5a64befabfb11075944eed324b5babc8b7adec95
|
4
|
+
data.tar.gz: 615bad06c6a5223613b974e1d7d3ae5c6c9e87d53aaf4d038678ec64168cc498
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1307e77ffb5874d7d7ee4bfc010d85c69de267420e6273790971a54921144114afd9d6c02885213feb8b67fca0de28a8835b149f628330ed7976139842764568
|
7
|
+
data.tar.gz: ceda3668f29ff0aac123d9c767c327dda98822dfdf4d8aca9d6e03df7f28bb7c41a60300f4ee25c4348e2789c1d34cf651d692d196dfe2a0fde4be6c888825be
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -12,26 +12,13 @@ group :pry do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
group :postgresql do
|
15
|
-
|
16
|
-
gem 'pg', '< 0.19'
|
17
|
-
else
|
18
|
-
gem "pg"
|
19
|
-
end
|
15
|
+
gem "pg"
|
20
16
|
end
|
21
17
|
|
22
18
|
group :mysql do
|
23
19
|
gem "mysql2"
|
24
20
|
end
|
25
21
|
|
26
|
-
if RUBY_VERSION < "2.2.2"
|
27
|
-
gem 'activesupport', '~> 4.2'
|
28
|
-
gem 'sinatra', '~> 1.4.8'
|
29
|
-
end
|
30
|
-
|
31
|
-
if RUBY_VERSION < '2.3.0'
|
32
|
-
gem 'i18n', '<= 1.5.1'
|
33
|
-
end
|
34
|
-
|
35
22
|
group :lint do
|
36
23
|
gem 'rubocop', '0.39.0'
|
37
24
|
end
|
data/dynflow.gemspec
CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
17
|
s.require_paths = ["lib"]
|
18
18
|
|
19
|
-
s.required_ruby_version = '>= 2.
|
19
|
+
s.required_ruby_version = '>= 2.3.0'
|
20
20
|
|
21
21
|
s.add_dependency "multi_json"
|
22
22
|
s.add_dependency "apipie-params"
|
data/lib/dynflow/actor.rb
CHANGED
@@ -7,10 +7,82 @@ module Dynflow
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
+
# Extend the Concurrent::Actor::Envelope to include information about the origin of the message
|
11
|
+
module EnvelopeBacktraceExtension
|
12
|
+
def initialize(*args)
|
13
|
+
super
|
14
|
+
@origin_backtrace = caller + Actor::BacktraceCollector.current_actor_backtrace
|
15
|
+
end
|
16
|
+
|
17
|
+
def origin_backtrace
|
18
|
+
@origin_backtrace
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#<#{self.class.name}:#{object_id}> @message=#{@message.inspect}, @sender=#{@sender.inspect}, @address=#{@address.inspect}>"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
Concurrent::Actor::Envelope.prepend(EnvelopeBacktraceExtension)
|
26
|
+
|
10
27
|
# Common parent for all the Dynflow actors defining some defaults
|
11
28
|
# that we preffer here.
|
12
29
|
class Actor < Concurrent::Actor::Context
|
30
|
+
module LogWithFullBacktrace
|
31
|
+
def log(level, message = nil, &block)
|
32
|
+
if message.is_a? Exception
|
33
|
+
error = message
|
34
|
+
backtrace = Actor::BacktraceCollector.full_backtrace(error.backtrace)
|
35
|
+
log(level, format("%s (%s)\n%s", error.message, error.class, backtrace.join("\n")))
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class SetResultsWithOriginLogging < Concurrent::Actor::Behaviour::SetResults
|
43
|
+
include LogWithFullBacktrace
|
44
|
+
|
45
|
+
def on_envelope(envelope)
|
46
|
+
Actor::BacktraceCollector.with_backtrace(envelope.origin_backtrace) { super }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class BacktraceCollector
|
51
|
+
CONCURRENT_RUBY_LINE = '[ concurrent-ruby ]'.freeze
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def with_backtrace(backtrace)
|
55
|
+
previous_actor_backtrace = Thread.current[:_dynflow_actor_backtrace]
|
56
|
+
Thread.current[:_dynflow_actor_backtrace] = backtrace
|
57
|
+
yield
|
58
|
+
ensure
|
59
|
+
Thread.current[:_dynflow_actor_backtrace] = previous_actor_backtrace
|
60
|
+
end
|
61
|
+
|
62
|
+
def current_actor_backtrace
|
63
|
+
Thread.current[:_dynflow_actor_backtrace] || []
|
64
|
+
end
|
65
|
+
|
66
|
+
def full_backtrace(backtrace)
|
67
|
+
filter_backtrace((backtrace || []) + current_actor_backtrace)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def filter_line?(line)
|
73
|
+
%w[concurrent-ruby gems/logging actor.rb].any? { |pattern| line.include?(pattern) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# takes an array of backtrace lines and replaces each chunk
|
77
|
+
def filter_backtrace(backtrace)
|
78
|
+
backtrace.map { |line| filter_line?(line) ? CONCURRENT_RUBY_LINE : line }
|
79
|
+
.chunk_while { |l1, l2| l1.equal?(CONCURRENT_RUBY_LINE) && l2.equal?(CONCURRENT_RUBY_LINE) }
|
80
|
+
.map(&:first)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
13
84
|
|
85
|
+
include LogWithFullBacktrace
|
14
86
|
include MethodicActor
|
15
87
|
|
16
88
|
# Behaviour that watches for polite asking for termination
|
@@ -46,7 +118,7 @@ module Dynflow
|
|
46
118
|
def behaviour_definition
|
47
119
|
[*Concurrent::Actor::Behaviour.base(:just_log),
|
48
120
|
Concurrent::Actor::Behaviour::Buffer,
|
49
|
-
[
|
121
|
+
[SetResultsWithOriginLogging, :just_log],
|
50
122
|
Concurrent::Actor::Behaviour::Awaits,
|
51
123
|
PoliteTermination,
|
52
124
|
Concurrent::Actor::Behaviour::ExecutesContext,
|
data/lib/dynflow/coordinator.rb
CHANGED
@@ -294,6 +294,22 @@ module Dynflow
|
|
294
294
|
end
|
295
295
|
end
|
296
296
|
|
297
|
+
class PlanningLock < LockByWorld
|
298
|
+
def initialize(world, execution_plan_id)
|
299
|
+
super(world)
|
300
|
+
@data.merge!(id: self.class.lock_id(execution_plan_id),
|
301
|
+
execution_plan_id: execution_plan_id)
|
302
|
+
end
|
303
|
+
|
304
|
+
def self.lock_id(execution_plan_id)
|
305
|
+
'execution-plan:' + execution_plan_id
|
306
|
+
end
|
307
|
+
|
308
|
+
def execution_plan_id
|
309
|
+
@data[:execution_plan_id]
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
297
313
|
attr_reader :adapter
|
298
314
|
|
299
315
|
def initialize(coordinator_adapter)
|
@@ -4,7 +4,8 @@ module Dynflow
|
|
4
4
|
class Exception < Abstract
|
5
5
|
def format(message)
|
6
6
|
if ::Exception === message
|
7
|
-
|
7
|
+
backtrace = Actor::BacktraceCollector.full_backtrace(message.backtrace)
|
8
|
+
"#{message.message} (#{message.class})\n#{backtrace.join("\n")}"
|
8
9
|
else
|
9
10
|
message
|
10
11
|
end
|
data/lib/dynflow/rails/daemon.rb
CHANGED
@@ -104,6 +104,7 @@ module Dynflow
|
|
104
104
|
:log_output => true,
|
105
105
|
:log_output_syslog => true,
|
106
106
|
:monitor_interval => [options[:memory_polling_interval] / 2, 30].min,
|
107
|
+
:force_kill_waittime => options[:force_kill_waittime].try(:to_i),
|
107
108
|
:ARGV => [command]
|
108
109
|
}
|
109
110
|
end
|
@@ -123,7 +124,8 @@ module Dynflow
|
|
123
124
|
ENV['EXECUTOR_MEMORY_LIMIT'].to_i
|
124
125
|
end,
|
125
126
|
memory_init_delay: (ENV['EXECUTOR_MEMORY_MONITOR_DELAY'] || 7200).to_i, # 2 hours
|
126
|
-
memory_polling_interval: (ENV['EXECUTOR_MEMORY_MONITOR_INTERVAL'] || 60).to_i
|
127
|
+
memory_polling_interval: (ENV['EXECUTOR_MEMORY_MONITOR_INTERVAL'] || 60).to_i,
|
128
|
+
force_kill_waittime: (ENV['EXECUTOR_FORCE_KILL_WAITTIME'] || 60).to_i
|
127
129
|
}
|
128
130
|
end
|
129
131
|
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/world.rb
CHANGED
@@ -200,8 +200,10 @@ module Dynflow
|
|
200
200
|
|
201
201
|
def plan_with_options(action_class:, args:, id: nil, caller_action: nil)
|
202
202
|
ExecutionPlan.new(self, id).tap do |execution_plan|
|
203
|
-
|
204
|
-
|
203
|
+
coordinator.acquire(Coordinator::PlanningLock.new(self, execution_plan.id)) do
|
204
|
+
execution_plan.prepare(action_class, caller_action: caller_action)
|
205
|
+
execution_plan.plan(*args)
|
206
|
+
end
|
205
207
|
end
|
206
208
|
end
|
207
209
|
|
@@ -11,6 +11,11 @@ module Dynflow
|
|
11
11
|
Type! world, Coordinator::ClientWorld, Coordinator::ExecutorWorld
|
12
12
|
|
13
13
|
coordinator.acquire(Coordinator::WorldInvalidationLock.new(self, world)) do
|
14
|
+
coordinator.find_locks(class: Coordinator::PlanningLock.name,
|
15
|
+
owner_id: 'world:' + world.id).each do |lock|
|
16
|
+
invalidate_planning_lock lock
|
17
|
+
end
|
18
|
+
|
14
19
|
if world.is_a? Coordinator::ExecutorWorld
|
15
20
|
old_execution_locks = coordinator.find_locks(class: Coordinator::ExecutionLock.name,
|
16
21
|
owner_id: "world:#{world.id}")
|
@@ -26,6 +31,17 @@ module Dynflow
|
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
34
|
+
def invalidate_planning_lock(planning_lock)
|
35
|
+
with_valid_execution_plan_for_lock(planning_lock) do |plan|
|
36
|
+
plan.steps.values.each { |step| invalidate_step step }
|
37
|
+
|
38
|
+
state = plan.plan_steps.all? { |step| step.state == :success } ? :planned : :stopped
|
39
|
+
plan.update_state(state)
|
40
|
+
coordinator.release(planning_lock)
|
41
|
+
execute(plan.id) if plan.state == :planned
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
29
45
|
# Invalidate an execution lock, left behind by a executor that
|
30
46
|
# was executing an execution plan when it was terminated.
|
31
47
|
#
|
@@ -33,14 +49,7 @@ module Dynflow
|
|
33
49
|
# @return [void]
|
34
50
|
def invalidate_execution_lock(execution_lock)
|
35
51
|
with_valid_execution_plan_for_lock(execution_lock) do |plan|
|
36
|
-
plan.steps.values.each
|
37
|
-
if step.state == :running
|
38
|
-
step.error = ExecutionPlan::Steps::Error.new("Abnormal termination (previous state: #{step.state})")
|
39
|
-
step.state = :error
|
40
|
-
step.save
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
52
|
+
plan.steps.values.each { |step| invalidate_step step }
|
44
53
|
plan.execution_history.add('terminate execution', execution_lock.world_id)
|
45
54
|
plan.update_state(:paused, history_notice: false) if plan.state == :running
|
46
55
|
plan.save
|
@@ -155,6 +164,16 @@ module Dynflow
|
|
155
164
|
|
156
165
|
return orphaned_locks
|
157
166
|
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def invalidate_step(step)
|
171
|
+
if step.state == :running
|
172
|
+
step.error = ExecutionPlan::Steps::Error.new("Abnormal termination (previous state: #{step.state})")
|
173
|
+
step.state = :error
|
174
|
+
step.save
|
175
|
+
end
|
176
|
+
end
|
158
177
|
end
|
159
178
|
end
|
160
179
|
end
|
@@ -136,6 +136,52 @@ module Dynflow
|
|
136
136
|
"unlock world-invalidation:#{executor_world.id}"]
|
137
137
|
client_world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
138
138
|
end
|
139
|
+
|
140
|
+
describe 'planning locks' do
|
141
|
+
it 'releases orphaned planning locks and executes associated execution plans' do
|
142
|
+
plan = client_world.plan(Support::DummyExample::Dummy)
|
143
|
+
plan.set_state(:planning, true)
|
144
|
+
plan.save
|
145
|
+
client_world.coordinator.acquire Coordinator::PlanningLock.new(client_world, plan.id)
|
146
|
+
executor_world.invalidate(client_world.registered_world)
|
147
|
+
expected_locks = ["lock world-invalidation:#{client_world.id}",
|
148
|
+
"unlock execution-plan:#{plan.id}", # planning lock
|
149
|
+
"lock execution-plan:#{plan.id}", # execution lock
|
150
|
+
"unlock world-invalidation:#{client_world.id}"]
|
151
|
+
executor_world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
152
|
+
wait_for do
|
153
|
+
plan = client_world_2.persistence.load_execution_plan(plan.id)
|
154
|
+
plan.state == :stopped
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'releases orphaned planning locks and stops associated execution plans which did not finish planning' do
|
159
|
+
plan = client_world.plan(Support::DummyExample::Dummy)
|
160
|
+
plan.set_state(:planning, true)
|
161
|
+
plan.save
|
162
|
+
step = plan.plan_steps.first
|
163
|
+
step.set_state(:pending, true)
|
164
|
+
step.save
|
165
|
+
client_world.coordinator.acquire Coordinator::PlanningLock.new(client_world, plan.id)
|
166
|
+
client_world_2.invalidate(client_world.registered_world)
|
167
|
+
expected_locks = ["lock world-invalidation:#{client_world.id}",
|
168
|
+
"unlock execution-plan:#{plan.id}",
|
169
|
+
"unlock world-invalidation:#{client_world.id}"]
|
170
|
+
client_world_2.coordinator.adapter.lock_log.must_equal(expected_locks)
|
171
|
+
plan = client_world_2.persistence.load_execution_plan(plan.id)
|
172
|
+
plan.state.must_equal :stopped
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'releases orphaned planning locks without execution plans' do
|
176
|
+
uuid = SecureRandom.uuid
|
177
|
+
client_world.coordinator.acquire Coordinator::PlanningLock.new(client_world, uuid)
|
178
|
+
client_world_2.invalidate(client_world.registered_world)
|
179
|
+
expected_locks = ["lock world-invalidation:#{client_world.id}",
|
180
|
+
"unlock execution-plan:#{uuid}",
|
181
|
+
"unlock world-invalidation:#{client_world.id}"]
|
182
|
+
client_world_2.coordinator.adapter.lock_log.must_equal(expected_locks)
|
183
|
+
end
|
184
|
+
end
|
139
185
|
end
|
140
186
|
end
|
141
187
|
|
data/test/daemon_test.rb
CHANGED
@@ -85,7 +85,8 @@ class DaemonTest < ActiveSupport::TestCase
|
|
85
85
|
@daemon.expects(:run).twice.with do |_folder, options|
|
86
86
|
options[:memory_limit] == 1000 &&
|
87
87
|
options[:memory_init_delay] == 100 &&
|
88
|
-
options[:memory_polling_interval] == 200
|
88
|
+
options[:memory_polling_interval] == 200 &&
|
89
|
+
options[:force_kill_waittime] == 40
|
89
90
|
end
|
90
91
|
@daemons.expects(:run_proc).twice.yields
|
91
92
|
|
@@ -94,7 +95,8 @@ class DaemonTest < ActiveSupport::TestCase
|
|
94
95
|
executors_count: 2,
|
95
96
|
memory_limit: 1000,
|
96
97
|
memory_init_delay: 100,
|
97
|
-
memory_polling_interval: 200
|
98
|
+
memory_polling_interval: 200,
|
99
|
+
force_kill_waittime: 40
|
98
100
|
)
|
99
101
|
end
|
100
102
|
|
@@ -103,6 +105,7 @@ class DaemonTest < ActiveSupport::TestCase
|
|
103
105
|
ENV['EXECUTOR_MEMORY_LIMIT'] = '1gb'
|
104
106
|
ENV['EXECUTOR_MEMORY_MONITOR_DELAY'] = '3'
|
105
107
|
ENV['EXECUTOR_MEMORY_MONITOR_INTERVAL'] = '4'
|
108
|
+
ENV['EXECUTOR_FORCE_KILL_WAITTIME'] = '40'
|
106
109
|
|
107
110
|
actual = @daemon.send(:default_options)
|
108
111
|
|
@@ -110,5 +113,6 @@ class DaemonTest < ActiveSupport::TestCase
|
|
110
113
|
assert_equal 1.gigabytes, actual[:memory_limit]
|
111
114
|
assert_equal 3, actual[:memory_init_delay]
|
112
115
|
assert_equal 4, actual[:memory_polling_interval]
|
116
|
+
assert_equal 40, actual[:force_kill_waittime]
|
113
117
|
end
|
114
118
|
end
|
data/test/executor_test.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
require_relative 'test_helper'
|
3
|
+
require 'mocha/minitest'
|
3
4
|
|
4
5
|
module Dynflow
|
5
6
|
module ExecutorTest
|
@@ -669,6 +670,7 @@ module Dynflow
|
|
669
670
|
|
670
671
|
it 'does not accept new work' do
|
671
672
|
assert world.terminate.wait
|
673
|
+
::Dynflow::Coordinator::PlanningLock.any_instance.stubs(:validate!)
|
672
674
|
result = world.trigger(Support::DummyExample::Slow, 0.02)
|
673
675
|
result.must_be :planned?
|
674
676
|
result.finished.wait
|
data/web/views/flow_step.erb
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
|
24
24
|
<div class="action">
|
25
25
|
<% unless @plan.state == :pending %>
|
26
|
+
<p><b>Queue:</b> <%= step.queue %></p>
|
26
27
|
<p><b>Started at:</b> <%= h(step.started_at) %></p>
|
27
28
|
<p><b>Ended at:</b> <%= h(step.ended_at) %></p>
|
28
29
|
<p><b>Real time:</b> <%= duration_to_s(step.real_time) %></p>
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Necas
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-
|
12
|
+
date: 2019-08-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: multi_json
|
@@ -608,15 +608,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
608
608
|
requirements:
|
609
609
|
- - ">="
|
610
610
|
- !ruby/object:Gem::Version
|
611
|
-
version: 2.
|
611
|
+
version: 2.3.0
|
612
612
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
613
613
|
requirements:
|
614
614
|
- - ">="
|
615
615
|
- !ruby/object:Gem::Version
|
616
616
|
version: '0'
|
617
617
|
requirements: []
|
618
|
-
|
619
|
-
rubygems_version: 2.7.6
|
618
|
+
rubygems_version: 3.0.3
|
620
619
|
signing_key:
|
621
620
|
specification_version: 4
|
622
621
|
summary: DYNamic workFLOW engine
|