dynflow 1.2.3 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 403ea79d6ea134104cc54ccaca3edbdce414306b944ccd957f37421ef7070d65
4
- data.tar.gz: 7361cc2b7e099d7b537738880dd4197034a7323b9e386c6e937518518931cadc
3
+ metadata.gz: ef3a326c234f3687c367017a5a64befabfb11075944eed324b5babc8b7adec95
4
+ data.tar.gz: 615bad06c6a5223613b974e1d7d3ae5c6c9e87d53aaf4d038678ec64168cc498
5
5
  SHA512:
6
- metadata.gz: 0b8f9367989b27fc3a55294659441c3d0360ccc87d752cebd1f723d5a68400083ec0a0330a29811b868b87b459bf651a1c8fc3bce5c8399556cc26671c0c3d54
7
- data.tar.gz: 95248248daebcf5d83b9be6bf3593a1ed55b41d5ef048180d30464091810d36361bdf3bdbc688f58625cdd6f5fdbb2a558fd1d8f56063b21cf65c8f03a8f8458
6
+ metadata.gz: 1307e77ffb5874d7d7ee4bfc010d85c69de267420e6273790971a54921144114afd9d6c02885213feb8b67fca0de28a8835b149f628330ed7976139842764568
7
+ data.tar.gz: ceda3668f29ff0aac123d9c767c327dda98822dfdf4d8aca9d6e03df7f28bb7c41a60300f4ee25c4348e2789c1d34cf651d692d196dfe2a0fde4be6c888825be
@@ -2,13 +2,11 @@ sudo: false
2
2
  language:
3
3
  - ruby
4
4
 
5
- before_install: >-
6
- if ruby -v | grep 'ruby 2.2'; then
7
- gem install bundler -v '~> 1.17'
8
- fi
5
+ services:
6
+ - mysql
7
+ - postgresql
9
8
 
10
9
  rvm:
11
- - "2.2.2"
12
10
  - "2.3.1"
13
11
  - "2.4.0"
14
12
  - "2.5.0"
data/Gemfile CHANGED
@@ -12,26 +12,13 @@ group :pry do
12
12
  end
13
13
 
14
14
  group :postgresql do
15
- if RUBY_VERSION <= '2'
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
@@ -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.0.0'
19
+ s.required_ruby_version = '>= 2.3.0'
20
20
 
21
21
  s.add_dependency "multi_json"
22
22
  s.add_dependency "apipie-params"
@@ -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
- [Concurrent::Actor::Behaviour::SetResults, :just_log],
121
+ [SetResultsWithOriginLogging, :just_log],
50
122
  Concurrent::Actor::Behaviour::Awaits,
51
123
  PoliteTermination,
52
124
  Concurrent::Actor::Behaviour::ExecutesContext,
@@ -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
- "#{message.message} (#{message.class})\n#{message.backtrace.join("\n")}"
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
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '1.2.3'.freeze
2
+ VERSION = '1.3.0'.freeze
3
3
  end
@@ -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
- execution_plan.prepare(action_class, caller_action: caller_action)
204
- execution_plan.plan(*args)
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 do |step|
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
 
@@ -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
@@ -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
@@ -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.2.3
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-04-11 00:00:00.000000000 Z
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.0.0
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
- rubyforge_project:
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