dynflow 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|