dynflow 0.1.0 → 0.2.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.
- data/.gitignore +6 -0
- data/.travis.yml +9 -0
- data/Gemfile +0 -10
- data/MIT-LICENSE +1 -1
- data/README.md +99 -37
- data/Rakefile +2 -6
- data/doc/images/logo.png +0 -0
- data/dynflow.gemspec +10 -1
- data/examples/generate_work_for_daemon.rb +24 -0
- data/examples/orchestrate.rb +121 -0
- data/examples/run_daemon.rb +17 -0
- data/examples/web_console.rb +29 -0
- data/lib/dynflow.rb +27 -6
- data/lib/dynflow/action.rb +185 -77
- data/lib/dynflow/action/cancellable_polling.rb +18 -0
- data/lib/dynflow/action/finalize_phase.rb +18 -0
- data/lib/dynflow/action/flow_phase.rb +44 -0
- data/lib/dynflow/action/format.rb +46 -0
- data/lib/dynflow/action/missing.rb +26 -0
- data/lib/dynflow/action/plan_phase.rb +85 -0
- data/lib/dynflow/action/polling.rb +49 -0
- data/lib/dynflow/action/presenter.rb +51 -0
- data/lib/dynflow/action/progress.rb +62 -0
- data/lib/dynflow/action/run_phase.rb +43 -0
- data/lib/dynflow/action/suspended.rb +21 -0
- data/lib/dynflow/clock.rb +133 -0
- data/lib/dynflow/daemon.rb +29 -0
- data/lib/dynflow/execution_plan.rb +285 -33
- data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
- data/lib/dynflow/execution_plan/output_reference.rb +52 -0
- data/lib/dynflow/execution_plan/steps.rb +12 -0
- data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
- data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
- data/lib/dynflow/execution_plan/steps/error.rb +33 -0
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
- data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
- data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
- data/lib/dynflow/executors.rb +9 -0
- data/lib/dynflow/executors/abstract.rb +32 -0
- data/lib/dynflow/executors/parallel.rb +88 -0
- data/lib/dynflow/executors/parallel/core.rb +119 -0
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
- data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
- data/lib/dynflow/executors/parallel/pool.rb +102 -0
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
- data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
- data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
- data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
- data/lib/dynflow/executors/parallel/worker.rb +30 -0
- data/lib/dynflow/executors/remote_via_socket.rb +38 -0
- data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
- data/lib/dynflow/flows.rb +13 -0
- data/lib/dynflow/flows/abstract.rb +36 -0
- data/lib/dynflow/flows/abstract_composed.rb +104 -0
- data/lib/dynflow/flows/atom.rb +36 -0
- data/lib/dynflow/flows/concurrence.rb +28 -0
- data/lib/dynflow/flows/sequence.rb +13 -0
- data/lib/dynflow/future.rb +173 -0
- data/lib/dynflow/listeners.rb +7 -0
- data/lib/dynflow/listeners/abstract.rb +13 -0
- data/lib/dynflow/listeners/serialization.rb +41 -0
- data/lib/dynflow/listeners/socket.rb +88 -0
- data/lib/dynflow/logger_adapters.rb +8 -0
- data/lib/dynflow/logger_adapters/abstract.rb +30 -0
- data/lib/dynflow/logger_adapters/delegator.rb +13 -0
- data/lib/dynflow/logger_adapters/formatters.rb +8 -0
- data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
- data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
- data/lib/dynflow/logger_adapters/simple.rb +59 -0
- data/lib/dynflow/micro_actor.rb +102 -0
- data/lib/dynflow/persistence.rb +53 -0
- data/lib/dynflow/persistence_adapters.rb +6 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
- data/lib/dynflow/serializable.rb +66 -0
- data/lib/dynflow/simple_world.rb +18 -0
- data/lib/dynflow/stateful.rb +40 -0
- data/lib/dynflow/testing.rb +32 -0
- data/lib/dynflow/testing/assertions.rb +64 -0
- data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
- data/lib/dynflow/testing/dummy_executor.rb +29 -0
- data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
- data/lib/dynflow/testing/dummy_step.rb +19 -0
- data/lib/dynflow/testing/dummy_world.rb +33 -0
- data/lib/dynflow/testing/factories.rb +83 -0
- data/lib/dynflow/testing/managed_clock.rb +23 -0
- data/lib/dynflow/testing/mimic.rb +38 -0
- data/lib/dynflow/transaction_adapters.rb +9 -0
- data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
- data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
- data/lib/dynflow/transaction_adapters/none.rb +12 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +277 -0
- data/lib/dynflow/world.rb +168 -0
- data/test/action_test.rb +89 -11
- data/test/clock_test.rb +59 -0
- data/test/code_workflow_example.rb +382 -0
- data/test/execution_plan_test.rb +195 -64
- data/test/executor_test.rb +692 -0
- data/test/persistance_adapters_test.rb +173 -0
- data/test/test_helper.rb +316 -1
- data/test/testing_test.rb +148 -0
- data/test/web_console_test.rb +38 -0
- data/web/assets/javascripts/application.js +25 -0
- data/web/assets/stylesheets/application.css +101 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
- data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
- data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
- data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
- data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
- data/web/assets/vendor/jquery/jquery.js +9807 -0
- data/web/views/flow.erb +19 -0
- data/web/views/flow_step.erb +31 -0
- data/web/views/index.erb +39 -0
- data/web/views/layout.erb +20 -0
- data/web/views/plan_step.erb +11 -0
- data/web/views/show.erb +54 -0
- metadata +250 -11
- data/examples/events.rb +0 -71
- data/examples/workflow.rb +0 -140
- data/lib/dynflow/bus.rb +0 -168
- data/lib/dynflow/dispatcher.rb +0 -36
- data/lib/dynflow/logger.rb +0 -34
- data/lib/dynflow/step.rb +0 -234
- data/test/bus_test.rb +0 -150
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
class World
|
|
3
|
+
include Algebrick::TypeCheck
|
|
4
|
+
|
|
5
|
+
attr_reader :executor, :persistence, :transaction_adapter, :action_classes, :subscription_index,
|
|
6
|
+
:logger_adapter, :options
|
|
7
|
+
|
|
8
|
+
def initialize(options_hash = {})
|
|
9
|
+
@options = default_options.merge options_hash
|
|
10
|
+
@logger_adapter = Type! option_val(:logger_adapter), LoggerAdapters::Abstract
|
|
11
|
+
@transaction_adapter = Type! option_val(:transaction_adapter), TransactionAdapters::Abstract
|
|
12
|
+
persistence_adapter = Type! option_val(:persistence_adapter), PersistenceAdapters::Abstract
|
|
13
|
+
@persistence = Persistence.new(self, persistence_adapter)
|
|
14
|
+
@executor = Type! option_val(:executor), Executors::Abstract
|
|
15
|
+
@action_classes = option_val(:action_classes)
|
|
16
|
+
calculate_subscription_index
|
|
17
|
+
|
|
18
|
+
executor.initialized.wait
|
|
19
|
+
@termination_barrier = Mutex.new
|
|
20
|
+
|
|
21
|
+
transaction_adapter.check self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_options
|
|
25
|
+
@default_options ||=
|
|
26
|
+
{ action_classes: Action.all_children,
|
|
27
|
+
logger_adapter: LoggerAdapters::Simple.new,
|
|
28
|
+
executor: -> world { Executors::Parallel.new(world, options[:pool_size]) } }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clock
|
|
32
|
+
@clock ||= Clock.new(logger)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def logger
|
|
36
|
+
logger_adapter.dynflow_logger
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def action_logger
|
|
40
|
+
logger_adapter.action_logger
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def subscribed_actions(action_class)
|
|
44
|
+
@subscription_index.has_key?(action_class) ? @subscription_index[action_class] : []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# reload actions classes, intended only for devel
|
|
48
|
+
def reload!
|
|
49
|
+
@action_classes.map! { |klass| klass.to_s.constantize }
|
|
50
|
+
calculate_subscription_index
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class TriggerResult
|
|
54
|
+
include Algebrick::TypeCheck
|
|
55
|
+
|
|
56
|
+
attr_reader :execution_plan_id, :planned, :finished
|
|
57
|
+
alias_method :id, :execution_plan_id
|
|
58
|
+
alias_method :planned?, :planned
|
|
59
|
+
|
|
60
|
+
def initialize(execution_plan_id, planned, finished)
|
|
61
|
+
@execution_plan_id = Type! execution_plan_id, String
|
|
62
|
+
@planned = Type! planned, TrueClass, FalseClass
|
|
63
|
+
@finished = Type! finished, Future
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_a
|
|
67
|
+
[execution_plan_id, planned, finished]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [TriggerResult]
|
|
72
|
+
# blocks until action_class is planned
|
|
73
|
+
def trigger(action_class, *args)
|
|
74
|
+
execution_plan = plan(action_class, *args)
|
|
75
|
+
planned = execution_plan.state == :planned
|
|
76
|
+
finished = if planned
|
|
77
|
+
execute(execution_plan.id)
|
|
78
|
+
else
|
|
79
|
+
Future.new.resolve(execution_plan)
|
|
80
|
+
end
|
|
81
|
+
return TriggerResult.new(execution_plan.id, planned, finished)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def event(execution_plan_id, step_id, event, future = Future.new)
|
|
85
|
+
executor.event execution_plan_id, step_id, event, future
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def plan(action_class, *args)
|
|
89
|
+
ExecutionPlan.new(self).tap do |execution_plan|
|
|
90
|
+
execution_plan.prepare(action_class)
|
|
91
|
+
execution_plan.plan(*args)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Future] containing execution_plan when finished
|
|
96
|
+
# raises when ExecutionPlan is not accepted for execution
|
|
97
|
+
def execute(execution_plan_id, finished = Future.new)
|
|
98
|
+
executor.execute execution_plan_id, finished
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def terminate(future = Future.new)
|
|
102
|
+
@termination_barrier.synchronize do
|
|
103
|
+
if @executor_terminated.nil?
|
|
104
|
+
@executor_terminated = Future.new
|
|
105
|
+
@clock_terminated = Future.new
|
|
106
|
+
executor.terminate(@executor_terminated).
|
|
107
|
+
do_then { clock.ask(MicroActor::Terminate, @clock_terminated) }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
Future.join([@executor_terminated, @clock_terminated], future)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Detects execution plans that are marked as running but no executor
|
|
114
|
+
# handles them (probably result of non-standard executor termination)
|
|
115
|
+
#
|
|
116
|
+
# The current implementation expects no execution_plan being actually run
|
|
117
|
+
# by the executor.
|
|
118
|
+
#
|
|
119
|
+
# TODO: persist the running executors in the system, so that we can detect
|
|
120
|
+
# the orphaned execution plans. The register should be managable by the
|
|
121
|
+
# console, so that the administrator can unregister dead executors when needed.
|
|
122
|
+
# After the executor is unregistered, the consistency check should be performed
|
|
123
|
+
# to fix the orphaned plans as well.
|
|
124
|
+
def consistency_check
|
|
125
|
+
abnormal_execution_plans = self.persistence.find_execution_plans filters: { 'state' => %w(running planning) }
|
|
126
|
+
if abnormal_execution_plans.empty?
|
|
127
|
+
logger.info 'Clean start.'
|
|
128
|
+
else
|
|
129
|
+
format_str = '%36s %10s %10s'
|
|
130
|
+
message = ['Abnormal execution plans, process was probably killed.',
|
|
131
|
+
'Following ExecutionPlans will be set to paused, admin has to fix them manually.',
|
|
132
|
+
(format format_str, 'ExecutionPlan', 'state', 'result'),
|
|
133
|
+
*(abnormal_execution_plans.map { |ep| format format_str, ep.id, ep.state, ep.result })]
|
|
134
|
+
|
|
135
|
+
logger.error message.join("\n")
|
|
136
|
+
|
|
137
|
+
abnormal_execution_plans.each do |ep|
|
|
138
|
+
ep.update_state case ep.state
|
|
139
|
+
when :planning
|
|
140
|
+
:stopped
|
|
141
|
+
when :running
|
|
142
|
+
:paused
|
|
143
|
+
else
|
|
144
|
+
raise
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def calculate_subscription_index
|
|
153
|
+
@subscription_index = action_classes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |klass, index|
|
|
154
|
+
next unless klass.subscribe
|
|
155
|
+
Array(klass.subscribe).each { |subscribed_class| index[subscribed_class.to_s.constantize] << klass }
|
|
156
|
+
end.tap { |o| o.freeze }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def option_val(key)
|
|
160
|
+
val = options.fetch(key)
|
|
161
|
+
if val.is_a? Proc
|
|
162
|
+
options[key] = val.call(self)
|
|
163
|
+
else
|
|
164
|
+
val
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
data/test/action_test.rb
CHANGED
|
@@ -1,25 +1,103 @@
|
|
|
1
|
-
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
require_relative 'code_workflow_example'
|
|
2
3
|
|
|
3
4
|
module Dynflow
|
|
4
|
-
class ActionTest < Action
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
describe Action::Missing do
|
|
8
|
+
include WorldInstance
|
|
9
|
+
|
|
10
|
+
let :action_data do
|
|
11
|
+
{ class: 'RenamedAction',
|
|
12
|
+
id: 123,
|
|
13
|
+
input: {},
|
|
14
|
+
execution_plan_id: 123 }
|
|
8
15
|
end
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
subject do
|
|
18
|
+
state_holder = ExecutionPlan::Steps::Abstract.allocate
|
|
19
|
+
state_holder.set_state :success, true
|
|
20
|
+
Action.from_hash(action_data, :run_phase, state_holder, world)
|
|
12
21
|
end
|
|
13
22
|
|
|
23
|
+
specify { subject.action_class.name.must_equal 'RenamedAction' }
|
|
24
|
+
specify { assert subject.is_a? Action }
|
|
14
25
|
end
|
|
15
26
|
|
|
16
|
-
describe
|
|
27
|
+
describe "extending action phase" do
|
|
28
|
+
|
|
29
|
+
module TestExtending
|
|
30
|
+
|
|
31
|
+
module Extension
|
|
32
|
+
def new_method
|
|
33
|
+
end
|
|
34
|
+
end
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
class ExtendedAction < Dynflow::Action
|
|
37
|
+
def self.phase_modules
|
|
38
|
+
super.merge(run_phase: [Extension]) { |key, old, new| old + new }.freeze
|
|
39
|
+
end
|
|
40
|
+
end
|
|
22
41
|
end
|
|
23
42
|
|
|
43
|
+
it "is possible to extend the action just for some phase" do
|
|
44
|
+
refute TestExtending::ExtendedAction.plan_phase.instance_methods.include?(:new_method)
|
|
45
|
+
refute Dynflow::Action.run_phase.instance_methods.include?(:new_method)
|
|
46
|
+
assert TestExtending::ExtendedAction.run_phase.instance_methods.include?(:new_method)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
describe 'children' do
|
|
52
|
+
include WorldInstance
|
|
53
|
+
|
|
54
|
+
smart_action_class = Class.new(Dynflow::Action)
|
|
55
|
+
smarter_action_class = Class.new(smart_action_class)
|
|
56
|
+
|
|
57
|
+
specify { refute smart_action_class.phase? }
|
|
58
|
+
specify { refute smarter_action_class.phase? }
|
|
59
|
+
specify { assert smarter_action_class.plan_phase.phase? }
|
|
60
|
+
|
|
61
|
+
specify { smart_action_class.all_children.must_include smarter_action_class }
|
|
62
|
+
specify { smart_action_class.all_children.size.must_equal 1 }
|
|
63
|
+
specify { smart_action_class.all_children.wont_include smarter_action_class.plan_phase }
|
|
64
|
+
specify { smart_action_class.all_children.wont_include smarter_action_class.run_phase }
|
|
65
|
+
specify { smart_action_class.all_children.wont_include smarter_action_class.finalize_phase }
|
|
66
|
+
|
|
67
|
+
describe 'World#subscribed_actions' do
|
|
68
|
+
event_action_class = CodeWorkflowExample::Triage
|
|
69
|
+
subscribed_action_class = CodeWorkflowExample::NotifyAssignee
|
|
70
|
+
|
|
71
|
+
specify { subscribed_action_class.subscribe.must_equal event_action_class }
|
|
72
|
+
specify { world.subscribed_actions(event_action_class).must_include subscribed_action_class }
|
|
73
|
+
specify { world.subscribed_actions(event_action_class).size.must_equal 1 }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe Action::Presenter do
|
|
78
|
+
include WorldInstance
|
|
79
|
+
|
|
80
|
+
let :execution_plan do
|
|
81
|
+
id, planned, finished = *world.trigger(CodeWorkflowExample::IncomingIssues, issues_data)
|
|
82
|
+
raise unless planned
|
|
83
|
+
finished.value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
let :issues_data do
|
|
87
|
+
[{ 'author' => 'Peter Smith', 'text' => 'Failing test' },
|
|
88
|
+
{ 'author' => 'John Doe', 'text' => 'Internal server error' }]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
let :presenter do
|
|
92
|
+
execution_plan.actions.find do |action|
|
|
93
|
+
action.is_a? CodeWorkflowExample::IncomingIssues
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
specify { presenter.action_class.must_equal CodeWorkflowExample::IncomingIssues }
|
|
98
|
+
|
|
99
|
+
it 'allows aggregating data from other actions' do
|
|
100
|
+
presenter.summary.must_equal(assignees: ["John Doe"])
|
|
101
|
+
end
|
|
24
102
|
end
|
|
25
103
|
end
|
data/test/clock_test.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
require 'logger'
|
|
3
|
+
|
|
4
|
+
clock_class = Dynflow::Clock
|
|
5
|
+
|
|
6
|
+
describe clock_class do
|
|
7
|
+
|
|
8
|
+
let(:clock) { clock_class.new Logger.new($stderr) }
|
|
9
|
+
|
|
10
|
+
it 'refuses who without #<< method' do
|
|
11
|
+
clock.initialized.wait
|
|
12
|
+
-> { clock.ping Object.new, 0.1, :pong }.must_raise TypeError
|
|
13
|
+
clock.ping [], 0.1, :pong
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
it 'pongs' do
|
|
18
|
+
q = Queue.new
|
|
19
|
+
clock.initialized.wait
|
|
20
|
+
start = Time.now
|
|
21
|
+
|
|
22
|
+
clock.ping q, 0.1, o = Object.new
|
|
23
|
+
assert_equal o, q.pop
|
|
24
|
+
finish = Time.now
|
|
25
|
+
assert_in_delta 0.1, finish - start, 0.02
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'pongs on expected times' do
|
|
29
|
+
q = Queue.new
|
|
30
|
+
clock.initialized.wait
|
|
31
|
+
start = Time.now
|
|
32
|
+
|
|
33
|
+
clock.ping q, 0.3, :a
|
|
34
|
+
clock.ping q, 0.1, :b
|
|
35
|
+
clock.ping q, 0.2, :c
|
|
36
|
+
|
|
37
|
+
assert_equal :b, q.pop
|
|
38
|
+
assert_in_delta 0.1, Time.now - start, 0.02
|
|
39
|
+
assert_equal :c, q.pop
|
|
40
|
+
assert_in_delta 0.2, Time.now - start, 0.02
|
|
41
|
+
assert_equal :a, q.pop
|
|
42
|
+
assert_in_delta 0.3, Time.now - start, 0.02
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'works under stress' do
|
|
46
|
+
clock.initialized.wait
|
|
47
|
+
threads = Array.new(4) do
|
|
48
|
+
Thread.new do
|
|
49
|
+
q = Queue.new
|
|
50
|
+
times = 20
|
|
51
|
+
times.times { |i| clock.ping q, rand, i }
|
|
52
|
+
assert_equal (0...times).to_a, Array.new(times) { q.pop }.sort
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
threads.each &:join
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
end
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module Dynflow
|
|
4
|
+
module CodeWorkflowExample
|
|
5
|
+
|
|
6
|
+
class IncomingIssues < Action
|
|
7
|
+
|
|
8
|
+
def plan(issues)
|
|
9
|
+
issues.each do |issue|
|
|
10
|
+
plan_action(IncomingIssue, issue)
|
|
11
|
+
end
|
|
12
|
+
plan_self('issues' => issues)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
input_format do
|
|
16
|
+
param :issues, Array do
|
|
17
|
+
param :author, String
|
|
18
|
+
param :text, String
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def finalize
|
|
23
|
+
TestExecutionLog.finalize << self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def summary
|
|
27
|
+
triages = all_actions.find_all do |action|
|
|
28
|
+
action.is_a? Dynflow::CodeWorkflowExample::Triage
|
|
29
|
+
end
|
|
30
|
+
assignees = triages.map do |triage|
|
|
31
|
+
triage.output[:classification] &&
|
|
32
|
+
triage.output[:classification][:assignee]
|
|
33
|
+
end.compact.uniq
|
|
34
|
+
{ assignees: assignees }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Slow < Action
|
|
40
|
+
def plan(seconds)
|
|
41
|
+
plan_self interval: seconds
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run
|
|
45
|
+
sleep input[:interval]
|
|
46
|
+
action_logger.debug 'done with sleeping'
|
|
47
|
+
$slow_actions_done ||= 0
|
|
48
|
+
$slow_actions_done +=1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class IncomingIssue < Action
|
|
53
|
+
|
|
54
|
+
def plan(issue)
|
|
55
|
+
plan_self(issue)
|
|
56
|
+
plan_action(Triage, issue)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
input_format do
|
|
60
|
+
param :author, String
|
|
61
|
+
param :text, String
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class Triage < Action
|
|
67
|
+
|
|
68
|
+
def plan(issue)
|
|
69
|
+
triage = plan_self(issue)
|
|
70
|
+
plan_action(UpdateIssue,
|
|
71
|
+
author: triage.input[:author],
|
|
72
|
+
text: triage.input[:text],
|
|
73
|
+
assignee: triage.output[:classification][:assignee],
|
|
74
|
+
severity: triage.output[:classification][:severity])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
input_format do
|
|
78
|
+
param :author, String
|
|
79
|
+
param :text, String
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
output_format do
|
|
83
|
+
param :classification, Hash do
|
|
84
|
+
param :assignee, String
|
|
85
|
+
param :severity, %w[low medium high]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run
|
|
90
|
+
TestExecutionLog.run << self
|
|
91
|
+
TestPause.pause if input[:text].include? 'get a break'
|
|
92
|
+
error! 'Trolling detected' if input[:text] == "trolling"
|
|
93
|
+
self.output[:classification] = { assignee: 'John Doe', severity: 'medium' }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def finalize
|
|
97
|
+
error! 'Trolling detected' if input[:text] == "trolling in finalize"
|
|
98
|
+
TestExecutionLog.finalize << self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class UpdateIssue < Action
|
|
104
|
+
|
|
105
|
+
input_format do
|
|
106
|
+
param :author, String
|
|
107
|
+
param :text, String
|
|
108
|
+
param :assignee, String
|
|
109
|
+
param :severity, %w[low medium high]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def run
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class NotifyAssignee < Action
|
|
117
|
+
|
|
118
|
+
def self.subscribe
|
|
119
|
+
Triage
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
input_format do
|
|
123
|
+
param :triage, Triage.output_format
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def plan(*args)
|
|
127
|
+
plan_self(:triage => trigger.output)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def run
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def finalize
|
|
134
|
+
TestExecutionLog.finalize << self
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class Commit < Action
|
|
139
|
+
input_format do
|
|
140
|
+
param :sha, String
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def plan(commit, reviews = { 'Morfeus' => true, 'Neo' => true })
|
|
144
|
+
sequence do
|
|
145
|
+
ci, review_actions = concurrence do
|
|
146
|
+
[plan_action(Ci, :commit => commit),
|
|
147
|
+
reviews.map do |name, result|
|
|
148
|
+
plan_action(Review, commit, name, result)
|
|
149
|
+
end]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
plan_action(Merge,
|
|
153
|
+
commit: commit,
|
|
154
|
+
ci_result: ci.output[:passed],
|
|
155
|
+
review_results: review_actions.map { |ra| ra.output[:passed] })
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class FastCommit < Action
|
|
161
|
+
|
|
162
|
+
def plan(commit)
|
|
163
|
+
sequence do
|
|
164
|
+
ci, review = concurrence do
|
|
165
|
+
[plan_action(Ci, commit: commit),
|
|
166
|
+
plan_action(Review, commit, 'Morfeus', true)]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
plan_action(Merge,
|
|
170
|
+
commit: commit,
|
|
171
|
+
ci_result: ci.output[:passed],
|
|
172
|
+
review_results: [review.output[:passed]])
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
input_format do
|
|
177
|
+
param :sha, String
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class Ci < Action
|
|
183
|
+
|
|
184
|
+
input_format do
|
|
185
|
+
param :commit, Commit.input_format
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
output_format do
|
|
189
|
+
param :passed, :boolean
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def run
|
|
193
|
+
output.update passed: true
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
class Review < Action
|
|
198
|
+
|
|
199
|
+
input_format do
|
|
200
|
+
param :reviewer, String
|
|
201
|
+
param :commit, Commit.input_format
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
output_format do
|
|
205
|
+
param :passed, :boolean
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def plan(commit, reviewer, result = true)
|
|
209
|
+
plan_self commit: commit, reviewer: reviewer, result: result
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def run
|
|
213
|
+
output.update passed: input[:result]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
class Merge < Action
|
|
218
|
+
|
|
219
|
+
input_format do
|
|
220
|
+
param :commit, Commit.input_format
|
|
221
|
+
param :ci_result, Ci.output_format
|
|
222
|
+
param :review_results, array_of(Review.output_format)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def run
|
|
226
|
+
output.update passed: (input.fetch(:ci_result) && input.fetch(:review_results).all?)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
class Dummy < Action
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
class DummyWithFinalize < Action
|
|
234
|
+
def finalize
|
|
235
|
+
TestExecutionLog.finalize << self
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
class DummyTrigger < Action
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
class DummyAnotherTrigger < Action
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
class DummySubscribe < Action
|
|
246
|
+
|
|
247
|
+
def self.subscribe
|
|
248
|
+
DummyTrigger
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def run
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
class DummyMultiSubscribe < Action
|
|
257
|
+
|
|
258
|
+
def self.subscribe
|
|
259
|
+
[DummyTrigger, DummyAnotherTrigger]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def run
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class CancelableSuspended < Action
|
|
268
|
+
include Dynflow::Action::CancellablePolling
|
|
269
|
+
|
|
270
|
+
Cancel = Dynflow::Action::CancellablePolling::Cancel
|
|
271
|
+
|
|
272
|
+
def invoke_external_task
|
|
273
|
+
{ progress: 0 }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def poll_external_task
|
|
277
|
+
progress = external_task[:progress] + 10
|
|
278
|
+
if progress > 25 && input[:text] =~ /cancel/
|
|
279
|
+
world.event execution_plan_id, run_step_id, Cancel
|
|
280
|
+
end
|
|
281
|
+
{ progress: progress }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def cancel_external_task
|
|
285
|
+
if input[:text] !~ /cancel fail/
|
|
286
|
+
{ cancelled: true }
|
|
287
|
+
else
|
|
288
|
+
error! 'action cancelled'
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def external_task=(external_task_data)
|
|
293
|
+
self.output.update external_task_data
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def external_task
|
|
297
|
+
output
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def done?
|
|
301
|
+
external_task[:progress] >= 100
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def poll_interval
|
|
305
|
+
0.1
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def run_progress
|
|
309
|
+
output[:progress].to_f / 100
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
class DummySuspended < Action
|
|
314
|
+
include Action::Polling
|
|
315
|
+
|
|
316
|
+
def invoke_external_task
|
|
317
|
+
error! 'Trolling detected' if input[:text] == 'troll setup'
|
|
318
|
+
{ progress: 0, done: false }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def external_task=(external_task_data)
|
|
322
|
+
self.output.update external_task_data
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def external_task
|
|
326
|
+
output
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def poll_external_task
|
|
330
|
+
if input[:text] == 'troll progress' && !output[:trolled]
|
|
331
|
+
output[:trolled] = true
|
|
332
|
+
error! 'Trolling detected'
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
if input[:text] =~ /pause in progress (\d+)/
|
|
336
|
+
TestPause.pause if output[:progress] == $1.to_i
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
progress = output[:progress] + 10
|
|
340
|
+
{ progress: progress, done: progress >= 100 }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def done?
|
|
344
|
+
external_task[:progress] >= 100
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def poll_interval
|
|
348
|
+
0.001
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def run_progress
|
|
352
|
+
output[:progress].to_f / 100
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
class DummyHeavyProgress < Action
|
|
357
|
+
|
|
358
|
+
def plan(input)
|
|
359
|
+
sequence do
|
|
360
|
+
plan_self(input)
|
|
361
|
+
plan_action(DummySuspended, input)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def run
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def finalize
|
|
369
|
+
$dummy_heavy_progress = 'dummy_heavy_progress'
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def run_progress_weight
|
|
373
|
+
4
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def finalize_progress_weight
|
|
377
|
+
5
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
end
|
|
382
|
+
end
|