dynflow 0.7.5 → 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +2 -0
- data/dynflow.gemspec +4 -2
- data/examples/example_helper.rb +10 -1
- data/lib/dynflow/action/suspended.rb +1 -1
- data/lib/dynflow/clock.rb +1 -1
- data/lib/dynflow/errors.rb +8 -0
- data/lib/dynflow/execution_plan.rb +2 -2
- data/lib/dynflow/executors/parallel.rb +3 -2
- data/lib/dynflow/executors/parallel/core.rb +37 -13
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +7 -0
- data/lib/dynflow/executors/parallel/pool.rb +17 -7
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -0
- data/lib/dynflow/executors/parallel/work_queue.rb +6 -0
- data/lib/dynflow/executors/parallel/worker.rb +4 -2
- data/lib/dynflow/persistence.rb +1 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +13 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +28 -7
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +11 -5
- data/test/remote_via_socket_test.rb +2 -0
- data/test/test_helper.rb +2 -0
- metadata +6 -21
data/README.md
CHANGED
data/dynflow.gemspec
CHANGED
@@ -7,20 +7,22 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = Dynflow::VERSION
|
8
8
|
s.authors = ["Ivan Necas"]
|
9
9
|
s.email = ["inecas@redhat.com"]
|
10
|
-
s.homepage = "http://github.com/
|
10
|
+
s.homepage = "http://github.com/Dynflow/dynflow"
|
11
11
|
s.summary = "DYNamic workFLOW engine"
|
12
12
|
s.description = "Generate and executed workflows dynamically based "+
|
13
13
|
"on input data and leave it open for others to jump into it as well"
|
14
|
+
s.license = "MIT"
|
14
15
|
|
15
16
|
s.files = `git ls-files`.split("\n")
|
16
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
18
|
s.require_paths = ["lib"]
|
18
19
|
|
20
|
+
s.required_ruby_version = '>= 1.9.3'
|
21
|
+
|
19
22
|
s.add_dependency "activesupport"
|
20
23
|
s.add_dependency "multi_json"
|
21
24
|
s.add_dependency "apipie-params"
|
22
25
|
s.add_dependency "algebrick", '~> 0.4.0'
|
23
|
-
s.add_dependency "uuidtools"
|
24
26
|
|
25
27
|
s.add_development_dependency "rack-test"
|
26
28
|
s.add_development_dependency "minitest"
|
data/examples/example_helper.rb
CHANGED
@@ -13,8 +13,17 @@ class ExampleHelper
|
|
13
13
|
Dynflow::SimpleWorld.new(options)
|
14
14
|
end
|
15
15
|
|
16
|
+
def persistence_conn_string
|
17
|
+
ENV['DB_CONN_STRING'] || 'sqlite:/'
|
18
|
+
end
|
19
|
+
|
20
|
+
def persistence_adapter
|
21
|
+
Dynflow::PersistenceAdapters::Sequel.new persistence_conn_string
|
22
|
+
end
|
23
|
+
|
16
24
|
def default_world_options
|
17
|
-
{ logger_adapter: logger_adapter
|
25
|
+
{ logger_adapter: logger_adapter,
|
26
|
+
persistence_adapter: persistence_adapter }
|
18
27
|
end
|
19
28
|
|
20
29
|
def logger_adapter
|
data/lib/dynflow/clock.rb
CHANGED
@@ -43,7 +43,7 @@ module Dynflow
|
|
43
43
|
def ping(who, time, with_what = nil, where = :<<)
|
44
44
|
Type! time, Time, Numeric
|
45
45
|
time = Time.now + time if time.is_a? Numeric
|
46
|
-
timer = Timer[who, time, with_what.nil? ? None : Some[Object][with_what], where]
|
46
|
+
timer = Timer[who, time, with_what.nil? ? Algebrick::Types::None : Some[Object][with_what], where]
|
47
47
|
if terminated?
|
48
48
|
Thread.new do
|
49
49
|
sleep [timer.when - Time.now, 0].max
|
data/lib/dynflow/errors.rb
CHANGED
@@ -24,5 +24,13 @@ module Dynflow
|
|
24
24
|
"#{self.class.inspect}: #{message}"
|
25
25
|
end
|
26
26
|
end
|
27
|
+
|
28
|
+
class PersistenceError < StandardError
|
29
|
+
def self.delegate(original_exception)
|
30
|
+
self.new("caused by #{original_exception.class}: #{original_exception.message}").tap do |e|
|
31
|
+
e.set_backtrace original_exception.backtrace
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
27
35
|
end
|
28
36
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'securerandom'
|
2
2
|
|
3
3
|
module Dynflow
|
4
4
|
|
@@ -30,7 +30,7 @@ module Dynflow
|
|
30
30
|
|
31
31
|
# all params with default values are part of *private* api
|
32
32
|
def initialize(world,
|
33
|
-
id =
|
33
|
+
id = SecureRandom.uuid,
|
34
34
|
state = :pending,
|
35
35
|
root_plan_step = nil,
|
36
36
|
run_flow = Flows::Concurrence.new([]),
|
@@ -36,8 +36,9 @@ module Dynflow
|
|
36
36
|
variants Work::Step, Work::Event, Work::Finalize
|
37
37
|
end
|
38
38
|
|
39
|
-
PoolDone
|
40
|
-
|
39
|
+
PoolDone = Algebrick.type { fields! work: Work }
|
40
|
+
PoolTerminated = Algebrick.atom
|
41
|
+
WorkerDone = Algebrick.type { fields! work: Work, worker: Worker }
|
41
42
|
|
42
43
|
def initialize(world, pool_size = 10)
|
43
44
|
super(world)
|
@@ -25,15 +25,25 @@ module Dynflow
|
|
25
25
|
end),
|
26
26
|
(on ~Parallel::Event do |event|
|
27
27
|
event(event)
|
28
|
-
|
28
|
+
end),
|
29
|
+
(on Parallel::PoolTerminated do
|
30
|
+
finish_termination
|
31
|
+
end),
|
29
32
|
(on PoolDone.(~any) do |step|
|
30
33
|
update_manager(step)
|
31
|
-
|
34
|
+
end),
|
35
|
+
(on ~Errors::PersistenceError.to_m do |error|
|
36
|
+
logger.fatal "PersistenceError in executor: terminating"
|
37
|
+
logger.fatal error
|
38
|
+
@world.terminate
|
39
|
+
end)
|
40
|
+
rescue Errors::PersistenceError => e
|
41
|
+
self << e
|
32
42
|
end
|
33
43
|
|
34
44
|
def termination
|
35
45
|
logger.info 'shutting down Core ...'
|
36
|
-
|
46
|
+
@pool << MicroActor::Terminate
|
37
47
|
end
|
38
48
|
|
39
49
|
# @return false on problem
|
@@ -85,6 +95,7 @@ module Dynflow
|
|
85
95
|
end
|
86
96
|
|
87
97
|
def rescue?(manager)
|
98
|
+
return false if terminating?
|
88
99
|
@world.auto_rescue && manager.execution_plan.state == :paused &&
|
89
100
|
!@plan_ids_in_rescue.include?(manager.execution_plan.id)
|
90
101
|
end
|
@@ -121,30 +132,43 @@ module Dynflow
|
|
121
132
|
def set_future(manager)
|
122
133
|
@plan_ids_in_rescue.delete(manager.execution_plan.id)
|
123
134
|
manager.future.resolve manager.execution_plan
|
124
|
-
try_to_terminate
|
125
135
|
end
|
126
136
|
|
127
137
|
|
128
138
|
def event(event)
|
129
139
|
Type! event, Parallel::Event
|
140
|
+
if terminating?
|
141
|
+
raise Dynflow::Error,
|
142
|
+
"cannot accept event: #{event} core is terminating"
|
143
|
+
end
|
130
144
|
execution_plan_manager = @execution_plan_managers[event.execution_plan_id]
|
131
145
|
if execution_plan_manager
|
132
146
|
feed_pool execution_plan_manager.event(event)
|
133
147
|
true
|
134
148
|
else
|
135
|
-
|
136
|
-
event, event.execution_plan_id, event.step_id)
|
137
|
-
event.result.fail UnprocessableEvent.new(
|
138
|
-
"no manager for #{event.execution_plan_id}:#{event.step_id}")
|
149
|
+
raise Dynflow::Error, "no manager for #{event.execution_plan_id}:#{event.step_id}"
|
139
150
|
end
|
151
|
+
rescue Dynflow::Error => e
|
152
|
+
event.result.fail e.message
|
153
|
+
raise e
|
140
154
|
end
|
141
155
|
|
142
|
-
def
|
143
|
-
|
144
|
-
@
|
145
|
-
|
146
|
-
|
156
|
+
def finish_termination
|
157
|
+
unless @execution_plan_managers.empty?
|
158
|
+
logger.error "... cleaning #{@execution_plan_managers.size} execution plans ..."
|
159
|
+
begin
|
160
|
+
@execution_plan_managers.values.each do |manager|
|
161
|
+
manager.terminate
|
162
|
+
end
|
163
|
+
rescue Errors::PersistenceError
|
164
|
+
logger.error "could not to clean the data properly"
|
165
|
+
end
|
166
|
+
@execution_plan_managers.values.each do |manager|
|
167
|
+
finish_plan(manager.execution_plan.id)
|
168
|
+
end
|
147
169
|
end
|
170
|
+
logger.error '... core terminated.'
|
171
|
+
terminate!
|
148
172
|
end
|
149
173
|
end
|
150
174
|
end
|
@@ -71,6 +71,13 @@ module Dynflow
|
|
71
71
|
(!@run_manager || @run_manager.done?) && (!@finalize_manager || @finalize_manager.done?)
|
72
72
|
end
|
73
73
|
|
74
|
+
def terminate
|
75
|
+
@running_steps_manager.terminate
|
76
|
+
unless @execution_plan.state == :paused
|
77
|
+
@execution_plan.update_state(:paused)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
74
81
|
private
|
75
82
|
|
76
83
|
def no_work
|
@@ -76,24 +76,34 @@ module Dynflow
|
|
76
76
|
|
77
77
|
def on_message(message)
|
78
78
|
match message,
|
79
|
-
~Work
|
79
|
+
(on ~Work do |work|
|
80
80
|
@jobs.add work
|
81
81
|
distribute_jobs
|
82
|
-
|
83
|
-
WorkerDone.(~any, ~any)
|
82
|
+
end),
|
83
|
+
(on WorkerDone.(~any, ~any) do |step, worker|
|
84
84
|
@core << PoolDone[step]
|
85
85
|
@free_workers << worker
|
86
86
|
distribute_jobs
|
87
|
-
|
87
|
+
end),
|
88
|
+
(on Errors::PersistenceError do
|
89
|
+
@core << message
|
90
|
+
end)
|
88
91
|
end
|
89
92
|
|
90
93
|
def termination
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
+
try_to_terminate
|
95
|
+
end
|
96
|
+
|
97
|
+
def try_to_terminate
|
98
|
+
if terminating? && @free_workers.size == @pool_size
|
99
|
+
@free_workers.map { |worker| worker.ask(Terminate) }.each(&:wait)
|
100
|
+
@core << PoolTerminated
|
101
|
+
terminate!
|
102
|
+
end
|
94
103
|
end
|
95
104
|
|
96
105
|
def distribute_jobs
|
106
|
+
try_to_terminate
|
97
107
|
@free_workers.pop << @jobs.pop until @free_workers.empty? || @jobs.empty?
|
98
108
|
end
|
99
109
|
end
|
@@ -13,6 +13,15 @@ module Dynflow
|
|
13
13
|
@events = WorkQueue.new(Integer, Work)
|
14
14
|
end
|
15
15
|
|
16
|
+
def terminate
|
17
|
+
pending_work = @events.clear.values.flatten
|
18
|
+
pending_work.each do |w|
|
19
|
+
if Work::Event === w
|
20
|
+
w.event.result.fail UnprocessableEvent.new("dropping due to termination")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
16
25
|
def add(step, work)
|
17
26
|
Type! step, ExecutionPlan::Steps::RunStep
|
18
27
|
@running_steps[step.id] = step
|
@@ -21,9 +21,11 @@ module Dynflow
|
|
21
21
|
end),
|
22
22
|
(on Work::Finalize.(~any, any) do |sequential_manager|
|
23
23
|
sequential_manager.finalize
|
24
|
-
|
25
|
-
|
24
|
+
end)
|
25
|
+
rescue Errors::PersistenceError => e
|
26
|
+
@pool << e
|
26
27
|
ensure
|
28
|
+
@pool << WorkerDone[work: message, worker: self]
|
27
29
|
@transaction_adapter.cleanup
|
28
30
|
end
|
29
31
|
end
|
data/lib/dynflow/persistence.rb
CHANGED
@@ -1,6 +1,19 @@
|
|
1
1
|
module Dynflow
|
2
2
|
module PersistenceAdapters
|
3
3
|
class Abstract
|
4
|
+
|
5
|
+
# The logger is set by the world when used inside it
|
6
|
+
attr_accessor :logger
|
7
|
+
|
8
|
+
def register_world(world)
|
9
|
+
@worlds ||= Set.new
|
10
|
+
@worlds << world
|
11
|
+
end
|
12
|
+
|
13
|
+
def log(level, message)
|
14
|
+
(@worlds.first && @worlds.first.logger).send(level, message)
|
15
|
+
end
|
16
|
+
|
4
17
|
def pagination?
|
5
18
|
false
|
6
19
|
end
|
@@ -9,6 +9,9 @@ module Dynflow
|
|
9
9
|
class Sequel < Abstract
|
10
10
|
include Algebrick::TypeCheck
|
11
11
|
|
12
|
+
MAX_RETRIES = 10
|
13
|
+
RETRY_DELAY = 1
|
14
|
+
|
12
15
|
attr_reader :db
|
13
16
|
|
14
17
|
def pagination?
|
@@ -27,8 +30,8 @@ module Dynflow
|
|
27
30
|
action: [],
|
28
31
|
step: %w(state started_at ended_at real_time execution_time action_id progress_done progress_weight) }
|
29
32
|
|
30
|
-
def initialize(
|
31
|
-
@db = initialize_db
|
33
|
+
def initialize(config)
|
34
|
+
@db = initialize_db config
|
32
35
|
migrate_db
|
33
36
|
end
|
34
37
|
|
@@ -94,7 +97,7 @@ module Dynflow
|
|
94
97
|
|
95
98
|
def save(what, condition, value)
|
96
99
|
table = table(what)
|
97
|
-
existing_record = table.first condition
|
100
|
+
existing_record = with_retry { table.first condition }
|
98
101
|
|
99
102
|
if value
|
100
103
|
value = value.with_indifferent_access
|
@@ -105,20 +108,20 @@ module Dynflow
|
|
105
108
|
record.each { |k, v| record[k] = v.to_s if v.is_a? Symbol }
|
106
109
|
|
107
110
|
if existing_record
|
108
|
-
table.where(condition).update(record)
|
111
|
+
with_retry { table.where(condition).update(record) }
|
109
112
|
else
|
110
|
-
table.insert record
|
113
|
+
with_retry { table.insert record }
|
111
114
|
end
|
112
115
|
|
113
116
|
else
|
114
|
-
existing_record and table.where(condition).delete
|
117
|
+
existing_record and with_retry { table.where(condition).delete }
|
115
118
|
end
|
116
119
|
value
|
117
120
|
end
|
118
121
|
|
119
122
|
def load(what, condition)
|
120
123
|
table = table(what)
|
121
|
-
if (record = table.first(condition.symbolize_keys))
|
124
|
+
if (record = with_retry { table.first(condition.symbolize_keys) } )
|
122
125
|
HashWithIndifferentAccess.new MultiJson.load(record[:data])
|
123
126
|
else
|
124
127
|
raise KeyError, "searching: #{what} by: #{condition.inspect}"
|
@@ -155,6 +158,24 @@ module Dynflow
|
|
155
158
|
|
156
159
|
data_set.where filters.symbolize_keys
|
157
160
|
end
|
161
|
+
|
162
|
+
def with_retry
|
163
|
+
attempts = 0
|
164
|
+
begin
|
165
|
+
yield
|
166
|
+
rescue Exception => e
|
167
|
+
attempts += 1
|
168
|
+
log(:error, e)
|
169
|
+
if attempts > MAX_RETRIES
|
170
|
+
log(:error, "The number of MAX_RETRIES exceeded")
|
171
|
+
raise Errors::PersistenceError.delegate(e)
|
172
|
+
else
|
173
|
+
log(:error, "Persistence retry no. #{attempts}")
|
174
|
+
sleep RETRY_DELAY
|
175
|
+
retry
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
158
179
|
end
|
159
180
|
end
|
160
181
|
end
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/world.rb
CHANGED
@@ -14,25 +14,28 @@ module Dynflow
|
|
14
14
|
@executor = Type! option_val(:executor), Executors::Abstract
|
15
15
|
@action_classes = option_val(:action_classes)
|
16
16
|
@auto_rescue = option_val(:auto_rescue)
|
17
|
+
@exit_on_terminate = option_val(:exit_on_terminate)
|
17
18
|
@middleware = Middleware::World.new
|
18
19
|
calculate_subscription_index
|
19
20
|
|
20
21
|
executor.initialized.wait
|
21
22
|
@termination_barrier = Mutex.new
|
23
|
+
@clock_barrier = Mutex.new
|
22
24
|
|
23
25
|
transaction_adapter.check self
|
24
26
|
end
|
25
27
|
|
26
28
|
def default_options
|
27
29
|
@default_options ||=
|
28
|
-
{ action_classes:
|
29
|
-
logger_adapter:
|
30
|
-
executor:
|
31
|
-
|
30
|
+
{ action_classes: Action.all_children,
|
31
|
+
logger_adapter: LoggerAdapters::Simple.new,
|
32
|
+
executor: -> world { Executors::Parallel.new(world, options[:pool_size]) },
|
33
|
+
exit_on_terminate: true,
|
34
|
+
auto_rescue: true }
|
32
35
|
end
|
33
36
|
|
34
37
|
def clock
|
35
|
-
@clock ||= Clock.new(logger)
|
38
|
+
@clock_barrier.synchronize { @clock ||= Clock.new(logger) }
|
36
39
|
end
|
37
40
|
|
38
41
|
def logger
|
@@ -127,6 +130,9 @@ module Dynflow
|
|
127
130
|
@clock_terminated = Future.new
|
128
131
|
executor.terminate(@executor_terminated).
|
129
132
|
do_then { clock.ask(MicroActor::Terminate, @clock_terminated) }
|
133
|
+
if @exit_on_terminate
|
134
|
+
future.do_then { Kernel.exit }
|
135
|
+
end
|
130
136
|
end
|
131
137
|
end
|
132
138
|
Future.join([@executor_terminated, @clock_terminated], future)
|
@@ -16,6 +16,7 @@ describe 'remote communication' do
|
|
16
16
|
def create_world
|
17
17
|
Dynflow::SimpleWorld.new logger_adapter: logger_adapter,
|
18
18
|
auto_terminate: false,
|
19
|
+
exit_on_terminate: false,
|
19
20
|
persistence_adapter: persistence_adapter
|
20
21
|
end
|
21
22
|
|
@@ -24,6 +25,7 @@ describe 'remote communication' do
|
|
24
25
|
logger_adapter: logger_adapter,
|
25
26
|
auto_terminate: false,
|
26
27
|
persistence_adapter: persistence_adapter,
|
28
|
+
exit_on_terminate: false,
|
27
29
|
executor: -> remote_world do
|
28
30
|
Dynflow::Executors::RemoteViaSocket.new(remote_world, socket_path)
|
29
31
|
end)
|
data/test/test_helper.rb
CHANGED
@@ -116,6 +116,7 @@ module WorldInstance
|
|
116
116
|
options = { pool_size: 5,
|
117
117
|
persistence_adapter: Dynflow::PersistenceAdapters::Sequel.new('sqlite:/'),
|
118
118
|
transaction_adapter: Dynflow::TransactionAdapters::None.new,
|
119
|
+
exit_on_terminate: false,
|
119
120
|
logger_adapter: logger_adapter,
|
120
121
|
auto_rescue: false }.merge(options)
|
121
122
|
Dynflow::World.new(options)
|
@@ -128,6 +129,7 @@ module WorldInstance
|
|
128
129
|
world = Dynflow::World.new(
|
129
130
|
logger_adapter: logger_adapter,
|
130
131
|
auto_terminate: false,
|
132
|
+
exit_on_terminate: false,
|
131
133
|
persistence_adapter: -> remote_world { world.persistence.adapter },
|
132
134
|
transaction_adapter: Dynflow::TransactionAdapters::None.new,
|
133
135
|
executor: -> remote_world do
|
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: 0.7.
|
4
|
+
version: 0.7.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2015-01-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -75,22 +75,6 @@ dependencies:
|
|
75
75
|
- - ~>
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: 0.4.0
|
78
|
-
- !ruby/object:Gem::Dependency
|
79
|
-
name: uuidtools
|
80
|
-
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
|
-
requirements:
|
83
|
-
- - ! '>='
|
84
|
-
- !ruby/object:Gem::Version
|
85
|
-
version: '0'
|
86
|
-
type: :runtime
|
87
|
-
prerelease: false
|
88
|
-
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
|
-
requirements:
|
91
|
-
- - ! '>='
|
92
|
-
- !ruby/object:Gem::Version
|
93
|
-
version: '0'
|
94
78
|
- !ruby/object:Gem::Dependency
|
95
79
|
name: rack-test
|
96
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -345,8 +329,9 @@ files:
|
|
345
329
|
- web/views/layout.erb
|
346
330
|
- web/views/plan_step.erb
|
347
331
|
- web/views/show.erb
|
348
|
-
homepage: http://github.com/
|
349
|
-
licenses:
|
332
|
+
homepage: http://github.com/Dynflow/dynflow
|
333
|
+
licenses:
|
334
|
+
- MIT
|
350
335
|
post_install_message:
|
351
336
|
rdoc_options: []
|
352
337
|
require_paths:
|
@@ -356,7 +341,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
356
341
|
requirements:
|
357
342
|
- - ! '>='
|
358
343
|
- !ruby/object:Gem::Version
|
359
|
-
version:
|
344
|
+
version: 1.9.3
|
360
345
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
361
346
|
none: false
|
362
347
|
requirements:
|