dynflow 0.7.5 → 0.7.6
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.
- 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:
|