aws-flow-core 1.0.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/Gemfile +9 -0
- data/LICENSE.TXT +15 -0
- data/NOTICE.TXT +14 -0
- data/Rakefile +27 -0
- data/aws-flow-core.gemspec +12 -0
- data/lib/aws/flow.rb +26 -0
- data/lib/aws/flow/async_backtrace.rb +134 -0
- data/lib/aws/flow/async_scope.rb +195 -0
- data/lib/aws/flow/begin_rescue_ensure.rb +386 -0
- data/lib/aws/flow/fiber.rb +77 -0
- data/lib/aws/flow/flow_utils.rb +50 -0
- data/lib/aws/flow/future.rb +109 -0
- data/lib/aws/flow/implementation.rb +151 -0
- data/lib/aws/flow/simple_dfa.rb +85 -0
- data/lib/aws/flow/tasks.rb +405 -0
- data/test/aws/async_backtrace_spec.rb +41 -0
- data/test/aws/async_scope_spec.rb +118 -0
- data/test/aws/begin_rescue_ensure_spec.rb +665 -0
- data/test/aws/external_task_spec.rb +197 -0
- data/test/aws/factories.rb +52 -0
- data/test/aws/fiber_condition_variable_spec.rb +163 -0
- data/test/aws/fiber_spec.rb +78 -0
- data/test/aws/flow_spec.rb +255 -0
- data/test/aws/future_spec.rb +210 -0
- data/test/aws/rubyflow.rb +22 -0
- data/test/aws/simple_dfa_spec.rb +63 -0
- data/test/aws/spec_helper.rb +36 -0
- metadata +85 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
# This file contains our implementation of fibers for 1.8
|
17
|
+
module AWS
|
18
|
+
module Flow
|
19
|
+
module Core
|
20
|
+
require 'fiber'
|
21
|
+
class FlowFiber < Fiber
|
22
|
+
def initialize(*args)
|
23
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(self.object_id))
|
24
|
+
super(args)
|
25
|
+
end
|
26
|
+
class << self
|
27
|
+
attr_accessor :local_variables
|
28
|
+
end
|
29
|
+
@local_variables = Hash.new {|hash, key| hash[key] = {}}
|
30
|
+
def self.finalize(obj_id)
|
31
|
+
proc { FlowFiber.local_variables.delete(obj_id) }
|
32
|
+
end
|
33
|
+
def self.[](index)
|
34
|
+
self.local_variables[index]
|
35
|
+
end
|
36
|
+
def self.[]=(key, value)
|
37
|
+
self.local_variables[key] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
# Will unset all the values for ancestors of this fiber, assuming that
|
41
|
+
# they have the same value for key. That is, they will unset upwards until
|
42
|
+
# the first time the value stored at key is changed
|
43
|
+
def self.unset(current_fiber, key)
|
44
|
+
current_value = FlowFiber[current_fiber.object_id][key]
|
45
|
+
parent = FlowFiber[current_fiber.object_id][:parent]
|
46
|
+
ancestor_fibers = []
|
47
|
+
while parent != nil
|
48
|
+
ancestor_fibers << parent
|
49
|
+
parent = FlowFiber[parent.object_id][:parent]
|
50
|
+
end
|
51
|
+
ancestor_fibers.each do |fiber|
|
52
|
+
FlowFiber[fiber.object_id].delete(key) if FlowFiber[fiber.object_id][key] == current_value
|
53
|
+
end
|
54
|
+
FlowFiber[current_fiber.object_id].delete(key)
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
# Child fibers should inherit their parents FiberLocals
|
59
|
+
FlowFiber[Fiber.current.object_id].each_pair do |key, val|
|
60
|
+
FlowFiber[self.object_id][key] = val
|
61
|
+
end
|
62
|
+
FlowFiber[self.object_id][:parent] = Fiber.current
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
def [](key)
|
67
|
+
FlowFiber[self.object_id][key]
|
68
|
+
end
|
69
|
+
def []=(key, value)
|
70
|
+
FlowFiber[self.object_id][key] = value
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
# This file simply contains definitions and functions useful to the overall running of flow
|
17
|
+
module AWS
|
18
|
+
module Flow
|
19
|
+
module Core
|
20
|
+
class IllegalStateException < Exception; end
|
21
|
+
class CancellationException < Exception
|
22
|
+
attr_accessor :reason, :details
|
23
|
+
def initialize(reason = nil, details = nil)
|
24
|
+
@reason = reason
|
25
|
+
@details = details
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def make_backtrace(parent_backtrace)
|
30
|
+
# 1 frame for the function that actually removes the stack traces
|
31
|
+
# 1 frame for the function that calls into the function that removes
|
32
|
+
# frames in AsyncBacktrace
|
33
|
+
# 1 frame for the call into this function
|
34
|
+
# 1 frame for the initialize call of the BRE or External Task
|
35
|
+
# 1 frame for the new call into the BRE or ET
|
36
|
+
# 1 frame for the AsyncScope initialize that the BRE/ET has to be in
|
37
|
+
|
38
|
+
# "./lib/aws/rubyflow/asyncBacktrace.rb:75:in `caller'"
|
39
|
+
# "./lib/aws/rubyflow/asyncBacktrace.rb:21:in `create'"
|
40
|
+
# "./lib/aws/rubyflow/flow.rb:16:in `make_backtrace'"
|
41
|
+
# "./lib/aws/rubyflow/flow.rb:103:in `initialize'"
|
42
|
+
# "./lib/aws/rubyflow/asyncScope.rb:17:in `new'"
|
43
|
+
# "./lib/aws/rubyflow/asyncScope.rb:17:in `initialize'"
|
44
|
+
|
45
|
+
frames_to_skip = 7
|
46
|
+
backtrace = AsyncBacktrace.create(parent_backtrace, frames_to_skip)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
# This file contains our Future implementation, which allows us to have asynchronous, blocking promises
|
17
|
+
|
18
|
+
module AWS
|
19
|
+
module Flow
|
20
|
+
module Core
|
21
|
+
class AlreadySetException < Exception; end
|
22
|
+
|
23
|
+
# A Future represents the result of an asynchronous computation. Methods are
|
24
|
+
# provided to check if the computation is complete(Future#set), to wait for
|
25
|
+
# its completion(Future#wait), and to retrieve the result of the
|
26
|
+
# computation(Future#get). The result can only be retrieved using method get
|
27
|
+
# when the computation has completed, blocking if necessary until it is
|
28
|
+
# ready. This is okay, however, because it will block that Fiber, and
|
29
|
+
# another Fiber will start executing
|
30
|
+
class Future
|
31
|
+
|
32
|
+
# Sets the value of the future, and notifies all of the Fibers that tried
|
33
|
+
# to get when this future wasn't ready.
|
34
|
+
def set(result=nil)
|
35
|
+
raise AlreadySetException if @set
|
36
|
+
@set = true
|
37
|
+
@result = result
|
38
|
+
@conditional.broadcast if @conditional
|
39
|
+
@listeners.each { |b| b.call(self) } if @listeners
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# Blocks if Future is not set
|
44
|
+
# raises CancellationError when task is cancelled
|
45
|
+
def get
|
46
|
+
until @set
|
47
|
+
@conditional ||= FiberConditionVariable.new
|
48
|
+
@conditional.wait
|
49
|
+
end
|
50
|
+
@result
|
51
|
+
end
|
52
|
+
|
53
|
+
def set?
|
54
|
+
@set
|
55
|
+
end
|
56
|
+
|
57
|
+
def unset
|
58
|
+
@set = nil
|
59
|
+
@result = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# Add a callback, block, which will fire when the future is set
|
63
|
+
def on_set(&block)
|
64
|
+
@listeners ||= []
|
65
|
+
# TODO probably want to use lambda here
|
66
|
+
@listeners << block
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Based on the ruby core source:
|
71
|
+
# https://github.com/ruby/ruby/blob/trunk/lib/thread.rb
|
72
|
+
class FiberConditionVariable
|
73
|
+
#
|
74
|
+
# Creates a new ConditionVariable
|
75
|
+
#
|
76
|
+
def initialize
|
77
|
+
@waiters = []
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# Have the current fiber wait on this condition variable, and wake up when
|
82
|
+
# the FiberConditionVariable is signalled/broadcaster
|
83
|
+
def wait
|
84
|
+
fiber = ::Fiber.current
|
85
|
+
@waiters << fiber
|
86
|
+
Fiber.yield
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Wakes up the first fiber in line waiting for this lock.
|
92
|
+
#
|
93
|
+
def signal
|
94
|
+
t = @waiters.shift
|
95
|
+
t.schedule if t && t.alive?
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Wakes up all fibers waiting for this lock.
|
101
|
+
#
|
102
|
+
def broadcast
|
103
|
+
signal until @waiters.empty?
|
104
|
+
self
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
#++
|
15
|
+
|
16
|
+
# This file contains the externally visible parts of flow that are expected to be used by customers of flow
|
17
|
+
module AWS
|
18
|
+
module Flow
|
19
|
+
module Core
|
20
|
+
class NoContextException < Exception; end
|
21
|
+
|
22
|
+
# @param block
|
23
|
+
# The block of code to be executed when the task is run.
|
24
|
+
#
|
25
|
+
# @raise [NoContextException]
|
26
|
+
# If the current fiber does not respond to {#__context__}.
|
27
|
+
#
|
28
|
+
# @return [Future]
|
29
|
+
# The tasks result, which is a {Future}.
|
30
|
+
#
|
31
|
+
def task(future = nil, &block)
|
32
|
+
fiber = ::Fiber.current
|
33
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
34
|
+
context = fiber.__context__
|
35
|
+
t = Task.new(nil, &block)
|
36
|
+
task_context = TaskContext.new(:parent => context.get_closest_containing_scope, :task => t)
|
37
|
+
context << t
|
38
|
+
t.result
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# @param block
|
43
|
+
# The block of code to be executed when the daemon task is run.
|
44
|
+
#
|
45
|
+
# @return [Future]
|
46
|
+
# The tasks result, which is a {Future}.
|
47
|
+
#
|
48
|
+
# @raise [NoContextException]
|
49
|
+
# If the current fiber does not respond to {#__context__}.
|
50
|
+
#
|
51
|
+
def daemon_task(&block)
|
52
|
+
fiber = ::Fiber.current
|
53
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
54
|
+
context = fiber.__context__
|
55
|
+
t = DaemonTask.new(nil, &block)
|
56
|
+
task_context = TaskContext.new(:parent => context.get_closest_containing_scope, :task => t)
|
57
|
+
context << t
|
58
|
+
t.result
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# @param block
|
63
|
+
# The block of code to be executed when the external task is run.
|
64
|
+
#
|
65
|
+
# @return [nil]
|
66
|
+
#
|
67
|
+
# @raise [NoContextException]
|
68
|
+
# If the current fiber does not respond to {#__context__}.
|
69
|
+
#
|
70
|
+
def external_task(&block)
|
71
|
+
fiber = ::Fiber.current
|
72
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
73
|
+
context = fiber.__context__
|
74
|
+
t = ExternalTask.new(:parent => context.get_closest_containing_scope, &block)
|
75
|
+
task_context = TaskContext.new(:parent => context.get_closest_containing_scope, :task => t)
|
76
|
+
context << t
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
#
|
82
|
+
#
|
83
|
+
# * *Args* :
|
84
|
+
# - block -> a block, which is passed in a BeginRescueEnsureWrapper, and which will define the BeginRescueEnsure#begin, BeginRescueEnsure#rescue, and BeginRescueEnsure#ensure methods
|
85
|
+
# * *Returns* :
|
86
|
+
# - The result of the begin statement, if there is no error, otherwise the value of the return statement
|
87
|
+
# * *Raises* :
|
88
|
+
# - +NoContextException+ -> If the current fiber does not respond to #__context__
|
89
|
+
#
|
90
|
+
def error_handler(&block)
|
91
|
+
fiber = ::Fiber.current
|
92
|
+
raise NoContextException unless fiber.respond_to? :__context__
|
93
|
+
context = fiber.__context__
|
94
|
+
begin_rescue_ensure = BeginRescueEnsure.new(:parent => context.get_closest_containing_scope)
|
95
|
+
bge = BeginRescueEnsureWrapper.new(block, begin_rescue_ensure)
|
96
|
+
context << bge
|
97
|
+
context << begin_rescue_ensure
|
98
|
+
begin_rescue_ensure
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param block
|
102
|
+
# A code block, which is passed within a {BeginRescueEnsureWrapper}, and which must define the
|
103
|
+
# {BeginRescueEnsure#begin}, {BeginRescueEnsure#rescue}, and {BeginRescueEnsure#ensure} methods.
|
104
|
+
# @!visibility private
|
105
|
+
def _error_handler(&block)
|
106
|
+
error_handler(&block).result
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
def wait_for_function(function, *futures)
|
111
|
+
conditional = FiberConditionVariable.new
|
112
|
+
futures.flatten!
|
113
|
+
return nil if futures.empty?
|
114
|
+
result = futures.select(&:set?)
|
115
|
+
return futures.find(&:set?)if function.call(result, futures)
|
116
|
+
futures.each do |f|
|
117
|
+
f.on_set do |set_one|
|
118
|
+
result << set_one
|
119
|
+
conditional.broadcast if function.call(result, futures)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
conditional.wait
|
123
|
+
result
|
124
|
+
end
|
125
|
+
|
126
|
+
# Blocks until *any* of the arguments are set.
|
127
|
+
#
|
128
|
+
# @param [Array] futures
|
129
|
+
# A list of futures to wait for. The function will return when at least one of these is set.
|
130
|
+
#
|
131
|
+
# @return [Array]
|
132
|
+
# A list of the set futures, in the order of being set.
|
133
|
+
#
|
134
|
+
def wait_for_any(*futures)
|
135
|
+
wait_for_function(lambda {|result, future_list| result.length >= 1 }, futures)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Blocks until *all* of the arguments are set.
|
139
|
+
#
|
140
|
+
# @param [Array<Future>] futures
|
141
|
+
# A list of futures to wait for. The function will return only when all of them are set.
|
142
|
+
#
|
143
|
+
# @return [Array]
|
144
|
+
# A list of the set futures, in the order of being set.
|
145
|
+
#
|
146
|
+
def wait_for_all(*futures)
|
147
|
+
wait_for_function(lambda {|result, future_list| result.size == future_list.size}, futures)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
##
|
2
|
+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
5
|
+
# You may not use this file except in compliance with the License.
|
6
|
+
# A copy of the License is located at
|
7
|
+
#
|
8
|
+
# http://aws.amazon.com/apache2.0
|
9
|
+
#
|
10
|
+
# or in the "license" file accompanying this file. This file is distributed
|
11
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
12
|
+
# express or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
##
|
15
|
+
|
16
|
+
module AWS
|
17
|
+
module Flow
|
18
|
+
module Core
|
19
|
+
|
20
|
+
# Contains a Data Flow Analysis (DFA)-like framework, where transition functions can perform arbitrary computation
|
21
|
+
# before moving to the next state
|
22
|
+
module SimpleDFA
|
23
|
+
attr_accessor :transitions, :symbols, :states, :start_state
|
24
|
+
|
25
|
+
# Creates a new SimpleDFA instance.
|
26
|
+
#
|
27
|
+
# @param start_state
|
28
|
+
# The state with which to start the framework.
|
29
|
+
#
|
30
|
+
def init(start_state)
|
31
|
+
include InstanceMethods
|
32
|
+
@start_state = start_state
|
33
|
+
@symbols = []
|
34
|
+
@states = []
|
35
|
+
@transitions = {}
|
36
|
+
@states << start_state
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return the start state
|
40
|
+
# The start state that was provided when this instance was created.
|
41
|
+
#
|
42
|
+
def get_start_state
|
43
|
+
@start_state
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Array]
|
47
|
+
# The list of all transitions that were added with {#add_transition}.
|
48
|
+
#
|
49
|
+
def get_transitions
|
50
|
+
@transitions
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_general(state, &block)
|
54
|
+
@symbols.each do |symbol|
|
55
|
+
if @transitions[[state, symbol]].nil?
|
56
|
+
@transitions[[state, symbol]] = block
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_transition(state, symbol, &block)
|
62
|
+
@symbols << symbol unless @symbols.include? symbol
|
63
|
+
@states << state unless @states.include? state
|
64
|
+
@transitions[[state, symbol]] = block
|
65
|
+
end
|
66
|
+
|
67
|
+
def uncovered_transitions
|
68
|
+
@states.product(@symbols) - @transitions.keys
|
69
|
+
end
|
70
|
+
|
71
|
+
module InstanceMethods
|
72
|
+
attr_accessor :current_state
|
73
|
+
|
74
|
+
def consume(symbol)
|
75
|
+
@current_state ||= self.class.get_start_state
|
76
|
+
func_to_call = self.class.get_transitions[[@current_state, symbol]]
|
77
|
+
raise "This is not a legal transition" unless func_to_call
|
78
|
+
func_to_call.call(self)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|