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 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: