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 CHANGED
@@ -1,6 +1,8 @@
1
1
  ![Dynflow](doc/images/logo.png)
2
2
  =======
3
3
 
4
+ ![Build](https://travis-ci.org/Dynflow/dynflow.svg?branch=master)
5
+
4
6
  Dynflow [DYN(amic work)FLOW] is a workflow engine
5
7
  written in Ruby that allows to:
6
8
 
@@ -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/iNecas/dynflow"
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"
@@ -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
@@ -12,7 +12,7 @@ module Dynflow
12
12
  @world.event execution_plan_id, step_id, event, future
13
13
  end
14
14
 
15
- def <<(event)
15
+ def <<(event = nil)
16
16
  event event
17
17
  end
18
18
 
@@ -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
@@ -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 'uuidtools'
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 = UUIDTools::UUID.random_create.to_s,
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 = Algebrick.type { fields! work: Work }
40
- WorkerDone = Algebrick.type { fields! work: Work, worker: Worker }
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
- end),
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
- end)
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
- try_to_terminate
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
- logger.warn format('dropping event %s - no manager for %s:%s',
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 try_to_terminate
143
- if terminating? && @execution_plan_managers.empty?
144
- @pool.ask(Terminate).wait
145
- logger.info '... Core terminated.'
146
- terminate!
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 >-> work do
79
+ (on ~Work do |work|
80
80
  @jobs.add work
81
81
  distribute_jobs
82
- end,
83
- WorkerDone.(~any, ~any) >-> step, worker do
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
- end
87
+ end),
88
+ (on Errors::PersistenceError do
89
+ @core << message
90
+ end)
88
91
  end
89
92
 
90
93
  def termination
91
- raise unless @free_workers.size == @pool_size
92
- @free_workers.map { |worker| worker.ask(Terminate) }.each(&:wait)
93
- super
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
@@ -29,6 +29,12 @@ module Dynflow
29
29
  !present?(key)
30
30
  end
31
31
 
32
+ def clear
33
+ ret = @stash.dup
34
+ @stash.clear
35
+ ret
36
+ end
37
+
32
38
  def size(key)
33
39
  return 0 if empty?(key)
34
40
  @stash[key].size
@@ -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
- end)
25
- @pool << WorkerDone[work: message, worker: self]
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
@@ -9,6 +9,7 @@ module Dynflow
9
9
  def initialize(world, persistence_adapter)
10
10
  @world = world
11
11
  @adapter = persistence_adapter
12
+ @adapter.register_world(world)
12
13
  end
13
14
 
14
15
  def load_action(step)
@@ -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(db_path)
31
- @db = initialize_db db_path
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
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.7.5'
2
+ VERSION = '0.7.6'
3
3
  end
@@ -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: Action.all_children,
29
- logger_adapter: LoggerAdapters::Simple.new,
30
- executor: -> world { Executors::Parallel.new(world, options[:pool_size]) },
31
- auto_rescue: true }
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)
@@ -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.5
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: 2014-09-10 00:00:00.000000000 Z
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/iNecas/dynflow
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: '0'
344
+ version: 1.9.3
360
345
  required_rubygems_version: !ruby/object:Gem::Requirement
361
346
  none: false
362
347
  requirements: