dynflow 1.2.3 → 1.3.0

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