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,46 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
|
|
3
|
+
# Input/output format validation logic calling
|
|
4
|
+
# input_format/output_format with block acts as a setter for
|
|
5
|
+
# specifying the format. Without a block it acts as a getter
|
|
6
|
+
module Action::Format
|
|
7
|
+
|
|
8
|
+
# we don't evaluate tbe block immediatelly, but postpone it till all the
|
|
9
|
+
# action classes are loaded, because we can use them to reference output format
|
|
10
|
+
def input_format(&block)
|
|
11
|
+
case
|
|
12
|
+
when block && !@input_format_block
|
|
13
|
+
@input_format_block = block
|
|
14
|
+
when !block && @input_format_block
|
|
15
|
+
return @input_format ||= Apipie::Params::Description.define(&@input_format_block)
|
|
16
|
+
when block && @input_format_block
|
|
17
|
+
raise "The input_format has already been defined in #{self.action_class}"
|
|
18
|
+
when !block && !@input_format_block
|
|
19
|
+
if superclass.respond_to? :input_format
|
|
20
|
+
superclass.input_format
|
|
21
|
+
else
|
|
22
|
+
raise "The input_format has not been defined yet in #{self.action_class}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def output_format(&block)
|
|
28
|
+
case
|
|
29
|
+
when block && !@output_format_block
|
|
30
|
+
@output_format_block = block
|
|
31
|
+
when !block && @output_format_block
|
|
32
|
+
return @output_format ||= Apipie::Params::Description.define(&@output_format_block)
|
|
33
|
+
when block && @output_format_block
|
|
34
|
+
raise "The output_format has already been defined in #{self.action_class}"
|
|
35
|
+
when !block && !@output_format_block
|
|
36
|
+
if superclass.respond_to? :output_format
|
|
37
|
+
superclass.output_format
|
|
38
|
+
else
|
|
39
|
+
raise "The output_format has not been defined yet in #{self.action_class}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
# for cases the serialized action was renamed and it's not available
|
|
3
|
+
# in the code base anymore.
|
|
4
|
+
class Action::Missing < Dynflow::Action
|
|
5
|
+
|
|
6
|
+
def self.generate(action_name)
|
|
7
|
+
Class.new(self).tap do |klass|
|
|
8
|
+
klass.singleton_class.send(:define_method, :name) do
|
|
9
|
+
action_name
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def plan(*args)
|
|
15
|
+
raise StandardError, "This action is not meant to be planned"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
raise StandardError, "This action is not meant to be run"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def finalize
|
|
23
|
+
raise StandardError, "This action is not meant to be finalized"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
module Action::PlanPhase
|
|
3
|
+
attr_reader :execution_plan, :trigger
|
|
4
|
+
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.attr_indifferent_access_hash :input
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(attributes, execution_plan, trigger)
|
|
10
|
+
super attributes, execution_plan.world
|
|
11
|
+
plan_step_id || raise(ArgumentError, 'missing plan_step_id')
|
|
12
|
+
|
|
13
|
+
self.input = attributes[:input] || {}
|
|
14
|
+
@execution_plan = Type! execution_plan, ExecutionPlan
|
|
15
|
+
@plan_step_id = plan_step_id
|
|
16
|
+
@trigger = Type! trigger, Action, NilClass
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute(*args)
|
|
20
|
+
self.state = :running
|
|
21
|
+
save_state
|
|
22
|
+
with_error_handling do
|
|
23
|
+
execution_plan.switch_flow(Flows::Concurrence.new([])) do
|
|
24
|
+
plan(*args)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
subscribed_actions = world.subscribed_actions(self.action_class)
|
|
28
|
+
if subscribed_actions.any?
|
|
29
|
+
# we encapsulate the flow for this action into a concurrence and
|
|
30
|
+
# add the subscribed flows to it as well.
|
|
31
|
+
trigger_flow = execution_plan.current_run_flow.sub_flows.pop
|
|
32
|
+
execution_plan.switch_flow(Flows::Concurrence.new([trigger_flow].compact)) do
|
|
33
|
+
subscribed_actions.each do |action_class|
|
|
34
|
+
new_plan_step = execution_plan.add_plan_step(action_class, self)
|
|
35
|
+
new_plan_step.execute(execution_plan, self, *args)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_hash
|
|
43
|
+
super.merge recursive_to_hash(input: input)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# DSL for plan method
|
|
47
|
+
|
|
48
|
+
def concurrence(&block)
|
|
49
|
+
execution_plan.switch_flow(Flows::Concurrence.new([]), &block)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sequence(&block)
|
|
53
|
+
execution_plan.switch_flow(Flows::Sequence.new([]), &block)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def plan_self(input)
|
|
57
|
+
@input = input.with_indifferent_access
|
|
58
|
+
if self.respond_to?(:run)
|
|
59
|
+
run_step = execution_plan.add_run_step(self)
|
|
60
|
+
@run_step_id = run_step.id
|
|
61
|
+
@output_reference = ExecutionPlan::OutputReference.new(run_step.id, id)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if self.respond_to?(:finalize)
|
|
65
|
+
finalize_step = execution_plan.add_finalize_step(self)
|
|
66
|
+
@finalize_step_id = finalize_step.id
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return self # to stay consistent with plan_action
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def plan_action(action_class, *args)
|
|
73
|
+
execution_plan.add_plan_step(action_class, self).execute(execution_plan, nil, *args)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def output
|
|
77
|
+
unless @output_reference
|
|
78
|
+
raise 'plan_self has to be invoked before being able to reference the output'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return @output_reference
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
module Action::Polling
|
|
3
|
+
|
|
4
|
+
Poll = Algebrick.atom
|
|
5
|
+
|
|
6
|
+
def run(event = nil)
|
|
7
|
+
case event
|
|
8
|
+
when nil
|
|
9
|
+
self.external_task = invoke_external_task
|
|
10
|
+
suspend_and_ping
|
|
11
|
+
when Poll
|
|
12
|
+
self.external_task = poll_external_task
|
|
13
|
+
suspend_and_ping unless done?
|
|
14
|
+
else
|
|
15
|
+
raise "unrecognized event #{event}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def external_task
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def done?
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def invoke_external_task
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def external_task=(external_task_data)
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def poll_external_task
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def suspend_and_ping
|
|
42
|
+
suspend { |suspended_action| world.clock.ping suspended_action, poll_interval, Poll }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def poll_interval
|
|
46
|
+
0.5
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
|
|
3
|
+
# This module is used for providing access to the results of the
|
|
4
|
+
# action. It's used in ExecutionPlan#actions to provide access to
|
|
5
|
+
# data of the actions in the execution plan.
|
|
6
|
+
#
|
|
7
|
+
# It also defines helper methods to extract usable data from the action itself,
|
|
8
|
+
# as well as other actions involved in the execution plan. One action (usually the
|
|
9
|
+
# main trigger, can use them to collect data across the whole execution_plan)
|
|
10
|
+
module Action::Presenter
|
|
11
|
+
|
|
12
|
+
def self.included(base)
|
|
13
|
+
base.send(:attr_reader, :input)
|
|
14
|
+
base.send(:attr_reader, :output)
|
|
15
|
+
base.send(:attr_reader, :all_actions)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.load(execution_plan, action_id, involved_steps, all_actions)
|
|
19
|
+
persistence_adapter = execution_plan.world.persistence.adapter
|
|
20
|
+
attributes = persistence_adapter.load_action(execution_plan.id,
|
|
21
|
+
action_id)
|
|
22
|
+
raise ArgumentError, 'missing :class' unless attributes[:class]
|
|
23
|
+
Action.constantize(attributes[:class]).presenter.new(attributes,
|
|
24
|
+
involved_steps,
|
|
25
|
+
all_actions)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_hash
|
|
29
|
+
recursive_to_hash(action: action_class,
|
|
30
|
+
input: input,
|
|
31
|
+
output: output)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param [Hash] attributes - the action attributes, usually loaded form persistence layer
|
|
35
|
+
# @param [Array<ExecutionPlan::Steps::AbstractStep> - steps that operate on top of the action
|
|
36
|
+
# @param [Array<Action::Presenter>] - array of all the actions involved in the execution plan
|
|
37
|
+
# with this action. Allows to access data from other actions
|
|
38
|
+
def initialize(attributes, involved_steps, all_actions)
|
|
39
|
+
@execution_plan_id = attributes[:execution_plan_id] || raise(ArgumentError, 'missing execution_plan_id')
|
|
40
|
+
@id = attributes[:id] || raise(ArgumentError, 'missing id')
|
|
41
|
+
|
|
42
|
+
# TODO: use the involved_steps to provide summary state and error for the action
|
|
43
|
+
@involved_steps = involved_steps
|
|
44
|
+
@all_actions = all_actions
|
|
45
|
+
|
|
46
|
+
indifferent_access_hash_variable_set :input, attributes[:input]
|
|
47
|
+
indifferent_access_hash_variable_set :output, attributes[:output] || {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
|
|
3
|
+
# Methods for specifying the progress of the action
|
|
4
|
+
# the +*_progress+ methods should return number in 0..1.
|
|
5
|
+
# The weight is there to increase/decrease the portion of this task
|
|
6
|
+
# in the context of other tasks in execution plan. Normal action has
|
|
7
|
+
# weight 1.
|
|
8
|
+
#
|
|
9
|
+
# The +*_progress+ is run only when the action is in running/suspend state. Otherwise
|
|
10
|
+
# the progress is 1 for success/skipped actions and 0 for errorneous ones.
|
|
11
|
+
module Action::Progress
|
|
12
|
+
|
|
13
|
+
def run_progress
|
|
14
|
+
0.5
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run_progress_weight
|
|
18
|
+
1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def finalize_progress
|
|
22
|
+
0.5
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def finalize_progress_weight
|
|
26
|
+
1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# this method is not intended to be overriden. Use +{run, finalize}_progress+
|
|
30
|
+
# variants instead
|
|
31
|
+
def progress_done
|
|
32
|
+
case self.state
|
|
33
|
+
when :success, :skipped
|
|
34
|
+
1
|
|
35
|
+
when :running, :suspended
|
|
36
|
+
case self
|
|
37
|
+
when Action::RunPhase
|
|
38
|
+
run_progress
|
|
39
|
+
when Action::FinalizePhase
|
|
40
|
+
finalize_progress
|
|
41
|
+
else
|
|
42
|
+
raise "Calculating progress for this phase is not supported"
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def progress_weight
|
|
50
|
+
case self
|
|
51
|
+
when Action::RunPhase
|
|
52
|
+
run_progress_weight
|
|
53
|
+
when Action::FinalizePhase
|
|
54
|
+
finalize_progress_weight
|
|
55
|
+
else
|
|
56
|
+
raise "Calculating progress for this phase is not supported"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
module Action::RunPhase
|
|
3
|
+
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.send(:include, Action::FlowPhase)
|
|
6
|
+
base.attr_indifferent_access_hash :output
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
SUSPEND = Object.new
|
|
10
|
+
|
|
11
|
+
def execute(event)
|
|
12
|
+
@world.logger.debug "step #{execution_plan_id}:#{@step.id} got event #{event}" if event
|
|
13
|
+
case
|
|
14
|
+
when state == :running
|
|
15
|
+
raise NotImplementedError, 'recovery after restart is not implemented'
|
|
16
|
+
|
|
17
|
+
when [:pending, :error, :suspended].include?(state)
|
|
18
|
+
if [:pending, :error].include?(state) && event
|
|
19
|
+
raise 'event can be processed only when in suspended state'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
self.state = :running
|
|
23
|
+
save_state
|
|
24
|
+
with_error_handling do
|
|
25
|
+
result = catch(SUSPEND) { event ? run(event) : run }
|
|
26
|
+
if result == SUSPEND
|
|
27
|
+
self.state = :suspended
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
else
|
|
32
|
+
raise "wrong state #{state} when event:#{event}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# DSL for run
|
|
37
|
+
|
|
38
|
+
def suspend(&block)
|
|
39
|
+
block.call Action::Suspended.new self if block
|
|
40
|
+
throw SUSPEND, SUSPEND
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
class Action::Suspended
|
|
3
|
+
attr_reader :execution_plan_id, :step_id
|
|
4
|
+
|
|
5
|
+
def initialize(action)
|
|
6
|
+
@world = action.world
|
|
7
|
+
@execution_plan_id = action.execution_plan_id
|
|
8
|
+
@step_id = action.run_step_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def event(event, future = Future.new)
|
|
12
|
+
@world.event execution_plan_id, step_id, event, future
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def <<(event)
|
|
16
|
+
event event
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
alias_method :ask, :event
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
require 'set'
|
|
3
|
+
|
|
4
|
+
class Clock < MicroActor
|
|
5
|
+
|
|
6
|
+
include Algebrick::Types
|
|
7
|
+
|
|
8
|
+
Tick = Algebrick.atom
|
|
9
|
+
Timer = Algebrick.type do
|
|
10
|
+
fields! who: Object, # to ping back
|
|
11
|
+
when: Time, # to deliver
|
|
12
|
+
what: Maybe[Object], # to send
|
|
13
|
+
where: Symbol # it should be delivered, which method
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Timer
|
|
17
|
+
def self.[](*fields)
|
|
18
|
+
super(*fields).tap { |v| Match! v.who, -> who { who.respond_to? v.where } }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
include Comparable
|
|
22
|
+
|
|
23
|
+
def <=>(other)
|
|
24
|
+
Type! other, self.class
|
|
25
|
+
self.when <=> other.when
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def apply
|
|
29
|
+
if Algebrick::Some[Object] === what
|
|
30
|
+
who.send where, what.value
|
|
31
|
+
else
|
|
32
|
+
who.send where
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Pills = Algebrick.type do
|
|
38
|
+
variants None = atom,
|
|
39
|
+
Took = atom,
|
|
40
|
+
Pill = type { fields Float }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ping(who, time, with_what = nil, where = :<<)
|
|
44
|
+
Type! time, Time, Numeric
|
|
45
|
+
time = Time.now + time if time.is_a? Numeric
|
|
46
|
+
timer = Timer[who, time, with_what.nil? ? None : Some[Object][with_what], where]
|
|
47
|
+
if terminated?
|
|
48
|
+
Thread.new do
|
|
49
|
+
sleep [timer.when - Time.now, 0].max
|
|
50
|
+
timer.apply
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
self << timer
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def delayed_initialize
|
|
60
|
+
@timers = SortedSet.new
|
|
61
|
+
@sleeping_pill = None
|
|
62
|
+
@sleep_barrier = Mutex.new
|
|
63
|
+
@sleeper = Thread.new { sleeping }
|
|
64
|
+
Thread.pass until @sleep_barrier.locked? || @sleeper.status == 'sleep'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def termination
|
|
68
|
+
@sleeper.kill
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_message(message)
|
|
73
|
+
match message,
|
|
74
|
+
Tick >-> do
|
|
75
|
+
run_ready_timers
|
|
76
|
+
sleep_to first_timer
|
|
77
|
+
end,
|
|
78
|
+
~Timer >-> timer do
|
|
79
|
+
@timers.add timer
|
|
80
|
+
if @timers.size == 1
|
|
81
|
+
sleep_to timer
|
|
82
|
+
else
|
|
83
|
+
wakeup if timer == first_timer
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def run_ready_timers
|
|
89
|
+
while first_timer && first_timer.when <= Time.now
|
|
90
|
+
first_timer.apply
|
|
91
|
+
@timers.delete(first_timer)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def first_timer
|
|
96
|
+
@timers.first
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def wakeup
|
|
100
|
+
while @sleep_barrier.synchronize { Pill === @sleeping_pill }
|
|
101
|
+
Thread.pass
|
|
102
|
+
end
|
|
103
|
+
@sleep_barrier.synchronize do
|
|
104
|
+
@sleeper.wakeup if Took === @sleeping_pill
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def sleep_to(timer)
|
|
109
|
+
return unless timer
|
|
110
|
+
sec = [timer.when - Time.now, 0.0].max
|
|
111
|
+
@sleep_barrier.synchronize do
|
|
112
|
+
@sleeping_pill = Pill[sec]
|
|
113
|
+
@sleeper.wakeup
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def sleeping
|
|
118
|
+
@sleep_barrier.synchronize do
|
|
119
|
+
loop do
|
|
120
|
+
@sleeping_pill = None
|
|
121
|
+
@sleep_barrier.sleep
|
|
122
|
+
pill = @sleeping_pill
|
|
123
|
+
@sleeping_pill = Took
|
|
124
|
+
@sleep_barrier.sleep pill.value
|
|
125
|
+
self << Tick
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
|