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
data/Gemfile
ADDED
data/LICENSE.TXT
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# @markup markdown
|
2
|
+
# @title AWS Flow Framework for Ruby License
|
3
|
+
|
4
|
+
Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
5
|
+
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License"). You
|
7
|
+
may not use this file except in compliance with the License. A copy of
|
8
|
+
the License is located at:
|
9
|
+
|
10
|
+
* <http://aws.amazon.com/apache2.0/>
|
11
|
+
|
12
|
+
or in the "LICENSE" file accompanying this file. This file is
|
13
|
+
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
14
|
+
ANY KIND, either express or implied. See the License for the specific
|
15
|
+
language governing permissions and limitations under the License.
|
data/NOTICE.TXT
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
AWS Flow for Ruby
|
2
|
+
Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3
|
+
|
4
|
+
This product includes software developed by
|
5
|
+
Amazon Technologies, Inc (http://www.amazon.com/).
|
6
|
+
|
7
|
+
**********************
|
8
|
+
THIRD PARTY COMPONENTS
|
9
|
+
**********************
|
10
|
+
This software includes third party software subject to the following copyrights:
|
11
|
+
- XML parsing and utility functions from JetS3t - Copyright 2006-2009 James Murty.
|
12
|
+
- JSON parsing and utility functions from JSON.org - Copyright 2002 JSON.org.
|
13
|
+
|
14
|
+
The licenses for these third party components are included in LICENSE.txt
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
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
|
+
require 'spec/rake/spectask'
|
17
|
+
|
18
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
19
|
+
t.libs << 'lib'
|
20
|
+
t.spec_opts = ['--color', '--format nested']
|
21
|
+
t.spec_files = FileList['test/**/*.rb']
|
22
|
+
t.spec_files.delete_if {|x| x =~ /.*factories.rb/ || x =~ /.*spec_helper.rb/}
|
23
|
+
t.spec_files.unshift("test/aws/factories.rb")
|
24
|
+
t.spec_files.unshift("test/aws/spec_helper.rb")
|
25
|
+
end
|
26
|
+
|
27
|
+
task :test => :spec
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'aws-flow-core'
|
3
|
+
s.version = '1.0.0'
|
4
|
+
s.date = Time.now
|
5
|
+
s.summary = "AWS Flow Core"
|
6
|
+
s.description = "Library to provide all the base asynchronous constructs that aws-flow uses"
|
7
|
+
s.authors = "Michael Steger"
|
8
|
+
s.email = ""
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
11
|
+
s.require_paths << "lib/aws/"
|
12
|
+
end
|
data/lib/aws/flow.rb
ADDED
@@ -0,0 +1,26 @@
|
|
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
|
+
def require_all(path)
|
17
|
+
glob = File.join(File.dirname(__FILE__), path, "*.rb")
|
18
|
+
Dir[glob].each { |f| require f}
|
19
|
+
Dir[glob].map { |f| File.basename(f) }
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Everything depends on fiber, so we have to require that before anything else
|
24
|
+
require 'aws/flow/fiber'
|
25
|
+
|
26
|
+
$RUBY_FLOW_FILES = require_all 'flow/'
|
@@ -0,0 +1,134 @@
|
|
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 AsyncBacktrace, which takes care of decorating and properly filtering backtraces
|
17
|
+
|
18
|
+
module AWS
|
19
|
+
module Flow
|
20
|
+
module Core
|
21
|
+
# @!visibility private
|
22
|
+
class AsyncBacktrace
|
23
|
+
|
24
|
+
# @!visibility private
|
25
|
+
def initialize(parent, backtrace)
|
26
|
+
@backtrace = AsyncBacktrace.filter(backtrace)
|
27
|
+
@parent = parent
|
28
|
+
end
|
29
|
+
|
30
|
+
# @!visibility private
|
31
|
+
def backtrace
|
32
|
+
if @parent
|
33
|
+
AsyncBacktrace.merge(@backtrace, @parent.backtrace)
|
34
|
+
else
|
35
|
+
@backtrace
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @!visibility private
|
40
|
+
class << self
|
41
|
+
|
42
|
+
# @!visibility private
|
43
|
+
def create(parent, frames_to_skip)
|
44
|
+
|
45
|
+
unless @disable_async_backtrace
|
46
|
+
b = AsyncBacktrace.caller(frames_to_skip)
|
47
|
+
AsyncBacktrace.new(parent, b)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @!visibility private
|
52
|
+
def create_from_exception(parent, exception)
|
53
|
+
unless @disable_async_backtrace
|
54
|
+
AsyncBacktrace.new(parent, exception.backtrace);
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Remove all framework related frames after application frames. Keep framework frames before application
|
59
|
+
# frames.
|
60
|
+
#
|
61
|
+
# @todo
|
62
|
+
# The correct implementation should not have framework frames before application frames as it is expected to
|
63
|
+
# call Kernel.caller with the correct number. But in cases when due to changes this number is not correct
|
64
|
+
# the frames are kept to not create confusion.
|
65
|
+
#
|
66
|
+
def filter(backtrace)
|
67
|
+
if @disable_filtering
|
68
|
+
backtrace
|
69
|
+
else
|
70
|
+
do_filter(backtrace)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!visibility private
|
75
|
+
def merge(*backtraces)
|
76
|
+
result = []
|
77
|
+
backtraces.each do | b |
|
78
|
+
if b
|
79
|
+
result << "------ continuation ------" if result.size > 0
|
80
|
+
result += b
|
81
|
+
end
|
82
|
+
end
|
83
|
+
result
|
84
|
+
end
|
85
|
+
|
86
|
+
# @!visibility private
|
87
|
+
def disable_filtering
|
88
|
+
@disable_filtering = true
|
89
|
+
end
|
90
|
+
|
91
|
+
# @!visibility private
|
92
|
+
def enable_filtering
|
93
|
+
@disable_filtering = false
|
94
|
+
end
|
95
|
+
|
96
|
+
# @!visibility private
|
97
|
+
def disable
|
98
|
+
@disable_async_backtrace = true
|
99
|
+
end
|
100
|
+
|
101
|
+
# @!visibility private
|
102
|
+
def enable
|
103
|
+
@disable_async_backtrace = false
|
104
|
+
end
|
105
|
+
|
106
|
+
# @!visibility private
|
107
|
+
def caller(skip)
|
108
|
+
random_var = Kernel.caller 0
|
109
|
+
this_stuff = 1.upto(6).map { |x| Kernel.caller(x) }
|
110
|
+
other_var = Kernel.caller skip
|
111
|
+
Kernel.caller(@disable_filtering ? 0 : skip)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# @!visibility private
|
117
|
+
def do_filter(backtrace)
|
118
|
+
return nil unless backtrace
|
119
|
+
# keep asynchrony frames at the top of the backtrace only
|
120
|
+
# then cut all frames starting from asynchrony frame
|
121
|
+
skip_asynchrony_frames = false
|
122
|
+
@backtrace = backtrace.take_while do |frame|
|
123
|
+
if ! $RUBY_FLOW_FILES.select {|file| Regexp.new(file) =~ frame}.empty?
|
124
|
+
!skip_asynchrony_frames
|
125
|
+
else
|
126
|
+
skip_asynchrony_frames = true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,195 @@
|
|
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 module contains the Root of the heirarchy for calls into flow, the AsyncScope
|
17
|
+
|
18
|
+
module AWS
|
19
|
+
module Flow
|
20
|
+
module Core
|
21
|
+
|
22
|
+
def gate_by_version(version, method, &block)
|
23
|
+
if RUBY_VERSION.send(method, version)
|
24
|
+
block.call
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @!visibility private
|
29
|
+
class AsyncScope
|
30
|
+
attr_accessor :stackTrace, :root, :failure, :root_context
|
31
|
+
|
32
|
+
def is_complete?
|
33
|
+
@root_context.complete
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_closest_containing_scope
|
37
|
+
@root_error_handler
|
38
|
+
end
|
39
|
+
|
40
|
+
def cancel(error); @root_error_handler.cancel(error); end
|
41
|
+
|
42
|
+
def initialize(&block)
|
43
|
+
@root_context = RootAsyncScope.new
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
# 1 for the function that skips frames
|
48
|
+
# 1 for the create function
|
49
|
+
# 1 for the the initialize of the backtrace
|
50
|
+
|
51
|
+
# "./lib/aws/rubyflow/asyncBacktrace.rb:75:in `caller'"
|
52
|
+
# "./lib/aws/rubyflow/asyncBacktrace.rb:21:in `create'"
|
53
|
+
# "./lib/aws/rubyflow/asyncScope.rb:18:in `initialize'"
|
54
|
+
|
55
|
+
@root_context.backtrace = AsyncBacktrace.create(nil, 3)
|
56
|
+
@root_error_handler = BeginRescueEnsure.new(:parent => @root_context)
|
57
|
+
begin
|
58
|
+
@root_error_handler.begin lambda { block.call if ! block.nil? }
|
59
|
+
@root_error_handler.rescue(Exception, lambda { |e| raise e })
|
60
|
+
end
|
61
|
+
@root_context << @root_error_handler
|
62
|
+
end
|
63
|
+
|
64
|
+
# Collects all the heirs of a task for use in async_stack_dump
|
65
|
+
def get_heirs
|
66
|
+
@root_error_handler.get_heirs
|
67
|
+
end
|
68
|
+
|
69
|
+
# Execute all queued tasks. If execution of those tasks results in addition of new tasks to the queue, execute
|
70
|
+
# them as well.
|
71
|
+
#
|
72
|
+
# Unless there are external dependencies or bugs in the tasks to be executed, a single call to this method
|
73
|
+
# performs the complete asynchronous execution.
|
74
|
+
#
|
75
|
+
# @note In the presence of external dependencies, it is expected that {AsyncScope#eventLoop} is called every
|
76
|
+
# time after a change in the state in a dependency can unblock asynchronous execution.
|
77
|
+
#
|
78
|
+
def eventLoop
|
79
|
+
#TODO Figure out when to raise Done raise "Done" if ! @root_task.alive?
|
80
|
+
raise IllegalStateException, "Already complete" if is_complete?
|
81
|
+
@root_context.eventLoop
|
82
|
+
# TODO Does this need to be taken care of? It's supposed to protect
|
83
|
+
# against people having errors that are classes, so like, passing
|
84
|
+
# Exception into cancel. We might want to just catch that at the
|
85
|
+
# entry point
|
86
|
+
if @root_context.failure
|
87
|
+
if @root_context.failure.respond_to? :message
|
88
|
+
failure_message = @root_context.failure.message + "\n" +
|
89
|
+
@root_context.failure.backtrace.join("\n")
|
90
|
+
raise @root_context.failure, failure_message
|
91
|
+
else
|
92
|
+
raise @root_context.failure
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
return is_complete?
|
97
|
+
end
|
98
|
+
|
99
|
+
def <<(task)
|
100
|
+
@root_context << task
|
101
|
+
task.parent = @root_context
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# @!visibility private
|
106
|
+
class RootAsyncScope < FlowFiber
|
107
|
+
|
108
|
+
attr_accessor :backtrace, :failure, :executor, :complete
|
109
|
+
|
110
|
+
def initialize(options = {}, &block)
|
111
|
+
@parent = options[:parent_context]
|
112
|
+
@daemon = options[:daemon]
|
113
|
+
@context = @parent
|
114
|
+
@executor = AsyncEventLoop.new
|
115
|
+
@task_queue = []
|
116
|
+
@complete = false
|
117
|
+
@task_queue << Task.new(context, &block) if block
|
118
|
+
end
|
119
|
+
|
120
|
+
# The only thing that should be removed from the RootAsyncScope is the
|
121
|
+
# root BeginRescueEnsure, so upon removal we are complete
|
122
|
+
def remove(task)
|
123
|
+
@complete = true
|
124
|
+
end
|
125
|
+
|
126
|
+
# As with remove, the only thing that is under RootAsyncScope should be
|
127
|
+
# the root BeginRescueEnsure, so upon failure we will be complete. Also
|
128
|
+
# sets failure variable for later raising.
|
129
|
+
def fail(task, error)
|
130
|
+
@failure = error
|
131
|
+
@complete = true
|
132
|
+
end
|
133
|
+
|
134
|
+
def <<(this_task)
|
135
|
+
@executor << this_task
|
136
|
+
end
|
137
|
+
|
138
|
+
# Reture self, a RootAsyncScope is the closest containing scope
|
139
|
+
def get_closest_containing_scope
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
# Call out to the AsyncEventLoop
|
144
|
+
def eventLoop
|
145
|
+
@executor.executeQueuedTasks
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
private
|
150
|
+
DELEGATED_METHODS = [:push, :<<, :enq, :empty?, :length, :size, :delete, :shift]
|
151
|
+
|
152
|
+
def method_missing(method_name, *args)
|
153
|
+
if DELEGATED_METHODS.include? method_name
|
154
|
+
@executor.send(method_name, *args)
|
155
|
+
else
|
156
|
+
super
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# @!visibility private
|
162
|
+
class AsyncEventLoop
|
163
|
+
|
164
|
+
def initialize
|
165
|
+
@tasks = []
|
166
|
+
end
|
167
|
+
|
168
|
+
def remove(task)
|
169
|
+
@tasks.delete(task)
|
170
|
+
end
|
171
|
+
# TODO Make sure that it's okay to fail from the AsyncEventLoop, and that
|
172
|
+
# this is the correct behavior
|
173
|
+
def fail(task, error)
|
174
|
+
raise error
|
175
|
+
end
|
176
|
+
def <<(task)
|
177
|
+
@tasks << task
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# TODO should this be synchronized somehow?
|
183
|
+
|
184
|
+
# Actually executes the eventLoop
|
185
|
+
def executeQueuedTasks
|
186
|
+
until @tasks.empty?
|
187
|
+
task = @tasks.shift
|
188
|
+
task.resume if task.alive?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,386 @@
|
|
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
|
+
require 'aws/flow/simple_dfa'
|
17
|
+
require 'set'
|
18
|
+
|
19
|
+
module AWS
|
20
|
+
module Flow
|
21
|
+
module Core
|
22
|
+
|
23
|
+
# This class allows asynchronous error handling within the AWS Flow Framework for Ruby. Calling
|
24
|
+
# {#begin}/{#rescue}/{#ensure} is similar to Ruby's native `begin`/`rescue`/`end` semantics.
|
25
|
+
class BeginRescueEnsure < FlowFiber
|
26
|
+
|
27
|
+
extend SimpleDFA
|
28
|
+
attr_accessor :parent, :begin_task, :ensure_task, :rescue_tasks,
|
29
|
+
:rescue_exceptions, :failure, :cancelled, :heirs, :nonDaemonHeirsCount, :executor, :result
|
30
|
+
attr_reader :backtrace, :__context__
|
31
|
+
|
32
|
+
# Create a new BeginRescueEnsure object, with the provided options.
|
33
|
+
#
|
34
|
+
# @param options
|
35
|
+
# Options to set for the class.
|
36
|
+
#
|
37
|
+
# @option options [Object] :parent
|
38
|
+
# The parent object.
|
39
|
+
#
|
40
|
+
def initialize(options = {})
|
41
|
+
# We have two different arrays, rather than a hash,
|
42
|
+
# because we want to ensure that we process the rescues in the order
|
43
|
+
# they are written, and because prior to Ruby 1.9, hashes will not
|
44
|
+
# return their elements in the order they were inserted.
|
45
|
+
@rescue_exceptions = []
|
46
|
+
@rescue_tasks = []
|
47
|
+
@parent = options[:parent] || Fiber.current.__context__
|
48
|
+
@current = @parent
|
49
|
+
@executor = @parent.executor
|
50
|
+
@__context__ = self
|
51
|
+
@nonDaemonHeirsCount = 0
|
52
|
+
@current_state ||= self.class.get_start_state
|
53
|
+
@heirs = Set.new
|
54
|
+
@backtrace = make_backtrace(@parent.backtrace)
|
55
|
+
@result = Future.new
|
56
|
+
super() { consume(:run) }
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# @!visibility private
|
61
|
+
def is_daemon?
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# @!visibility private
|
67
|
+
def <<(async_task)
|
68
|
+
# Not going to include the promise to wait for, as it would appear that
|
69
|
+
# Fibers can wait on futures from their point of origin as part of their
|
70
|
+
# implementation, as opposed to adding the callback here.
|
71
|
+
check_closed
|
72
|
+
if ! @heirs.member? async_task
|
73
|
+
@heirs << async_task
|
74
|
+
if ! async_task.is_daemon?
|
75
|
+
@nonDaemonHeirsCount += 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
@executor << async_task
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# @!visibility private
|
83
|
+
def get_closest_containing_scope
|
84
|
+
# BRE's are special in that they act as a containing scope, so that things
|
85
|
+
# created in BRE's treat it as the parent, so that it can track the heirs
|
86
|
+
# correctly and close only when nonDaemonHeirsCount is 0
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
# @!visibility private
|
91
|
+
def check_closed
|
92
|
+
raise IllegalStateException, @failure if @current_state == :closed
|
93
|
+
end
|
94
|
+
|
95
|
+
# Fails the task, cancels all of its heirs, and then updates the state.
|
96
|
+
#
|
97
|
+
# @param this_task
|
98
|
+
# The task to fail.
|
99
|
+
#
|
100
|
+
# @param error
|
101
|
+
# The error associated with the failure.
|
102
|
+
#
|
103
|
+
def fail(this_task, error)
|
104
|
+
check_closed
|
105
|
+
if ( ! (error.class <= CancellationException) || @failure == nil && !@daemondCausedCancellation)
|
106
|
+
backtrace = AsyncBacktrace.create_from_exception(@backtrace, error)
|
107
|
+
error.set_backtrace(backtrace.backtrace) if backtrace
|
108
|
+
@failure = error
|
109
|
+
end
|
110
|
+
task_out = @heirs.delete?(this_task)
|
111
|
+
raise "There was a task attempted to be removed from a BRE, when the BRE did not have that task as an heir" unless task_out
|
112
|
+
@nonDaemonHeirsCount -= 1 if ! this_task.is_daemon?
|
113
|
+
cancelHeirs
|
114
|
+
update_state
|
115
|
+
end
|
116
|
+
|
117
|
+
# Removes the task and updates the state
|
118
|
+
#
|
119
|
+
# @param this_task
|
120
|
+
# The task to remove.
|
121
|
+
#
|
122
|
+
def remove(this_task)
|
123
|
+
check_closed
|
124
|
+
|
125
|
+
task_out = @heirs.delete?(this_task)
|
126
|
+
raise "There was a task attempted to be removed from a BRE, when the BRE did not have that task as an heir" unless task_out
|
127
|
+
@nonDaemonHeirsCount -= 1 if ! this_task.is_daemon?
|
128
|
+
update_state
|
129
|
+
end
|
130
|
+
|
131
|
+
# @!visibility private
|
132
|
+
def cancelHeirs
|
133
|
+
toCancel = @heirs.dup
|
134
|
+
toCancel.each { |heir| heir.cancel(@failure) }
|
135
|
+
end
|
136
|
+
|
137
|
+
# @!visibility private
|
138
|
+
def merge_stacktraces(failure, this_backtrace, error)
|
139
|
+
backtrace = AsyncBacktrace.create_from_exception(this_backtrace, error)
|
140
|
+
failure.set_backtrace(backtrace.backtrace) if backtrace
|
141
|
+
end
|
142
|
+
|
143
|
+
# @!visibility private
|
144
|
+
def cancel(error)
|
145
|
+
if @current_state == :created
|
146
|
+
@current_state = :closed
|
147
|
+
@parent.remove(self)
|
148
|
+
return
|
149
|
+
end
|
150
|
+
if @failure == nil
|
151
|
+
@cancelled = true
|
152
|
+
details = (error.respond_to? :details) ? error.details : nil
|
153
|
+
reason = (error.respond_to? :reason) ? error.reason : nil
|
154
|
+
@failure = CancellationException.new(reason, details)
|
155
|
+
@failure.set_backtrace(@backtrace.backtrace) if @backtrace
|
156
|
+
if @current_state == :begin
|
157
|
+
cancelHeirs
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Actually runs the BRE, by going through the DFA with the symbol :run.
|
163
|
+
# @!visibility private
|
164
|
+
def run
|
165
|
+
this_failure = @failure
|
166
|
+
begin
|
167
|
+
consume(:run)
|
168
|
+
rescue Exception => error
|
169
|
+
if this_failure != error
|
170
|
+
backtrace = AsyncBacktrace.create_from_exception(@backtrace, error)
|
171
|
+
error.set_backtrace(backtrace.backtrace) if backtrace
|
172
|
+
end
|
173
|
+
@failure = error
|
174
|
+
cancelHeirs
|
175
|
+
ensure
|
176
|
+
update_state
|
177
|
+
raise @failure if (!!@failure && @current_state == :closed)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# @!visibility private
|
182
|
+
def alive?
|
183
|
+
@current_state != :closed
|
184
|
+
end
|
185
|
+
|
186
|
+
# Updates the state based on the most recent transitions in the DFA
|
187
|
+
# @!visibility private
|
188
|
+
def update_state
|
189
|
+
#TODO ? Add the ! @executed part
|
190
|
+
#return if @current_state == :closed || ! @executed
|
191
|
+
return if @current_state == :closed
|
192
|
+
if @nonDaemonHeirsCount == 0
|
193
|
+
if @heirs.empty?
|
194
|
+
consume(:update_state)
|
195
|
+
else
|
196
|
+
@daemondCausedCancellation = true if @failure == nil
|
197
|
+
cancelHeirs
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# @!visibility private
|
203
|
+
def get_heirs
|
204
|
+
# TODO fix this so it returns string instead of printing to stdout
|
205
|
+
str = "I am a BeginRescueEnsure with #{heirs.length} heirs
|
206
|
+
my begin block looks like #{@begin_task}" +
|
207
|
+
@heirs.map(&:get_heirs).to_s
|
208
|
+
|
209
|
+
# (@heirs.each(&:get_heirs) + [self]).flatten
|
210
|
+
end
|
211
|
+
|
212
|
+
# @!visibility private
|
213
|
+
init(:created)
|
214
|
+
{
|
215
|
+
[:created, :run] => lambda { |bre| bre.current_state = :begin; bre.run },
|
216
|
+
[:begin, :run] => lambda { |bre| bre << bre.begin_task },
|
217
|
+
[:begin, :update_state] => lambda do |bre|
|
218
|
+
if bre.failure == nil
|
219
|
+
bre.current_state = :ensure
|
220
|
+
else
|
221
|
+
bre.current_state = :rescue;
|
222
|
+
end
|
223
|
+
bre.run
|
224
|
+
end,
|
225
|
+
[:rescue, :run] => lambda do |bre|
|
226
|
+
# Emulates the behavior of the actual Ruby rescue, see
|
227
|
+
# http://Ruby-doc.org/docs/ProgrammingRuby/html/tut_exceptions.html
|
228
|
+
# for more details
|
229
|
+
bre.rescue_exceptions.each_index do |index|
|
230
|
+
this_failure = bre.failure
|
231
|
+
failure_class = bre.failure.is_a?(Exception) ? bre.failure.class : bre.failure
|
232
|
+
if failure_class <= bre.rescue_exceptions[index]
|
233
|
+
bre.result.unset
|
234
|
+
bre.failure = nil
|
235
|
+
task = bre.rescue_tasks[index]
|
236
|
+
bre << Task.new(bre) { bre.result.set(task.call(this_failure)) }
|
237
|
+
# bre.rescue_tasks[index].call(this_failure)
|
238
|
+
break
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end,
|
242
|
+
[:rescue, :update_state] => lambda { |bre| bre.current_state = :ensure; bre.run},
|
243
|
+
[:ensure, :run] => lambda do |bre|
|
244
|
+
bre << bre.ensure_task if bre.ensure_task
|
245
|
+
end,
|
246
|
+
[:ensure, :update_state] => lambda do |bre|
|
247
|
+
bre.current_state = :closed
|
248
|
+
if bre.failure == nil
|
249
|
+
bre.parent.remove(bre)
|
250
|
+
else
|
251
|
+
bre.parent.fail(bre, bre.failure)
|
252
|
+
end
|
253
|
+
end,
|
254
|
+
}.each_pair do |key, func|
|
255
|
+
add_transition(key.first, key.last) { |t| func.call(t) }
|
256
|
+
end
|
257
|
+
# That is, any transition from closed leads back to itself
|
258
|
+
define_general(:closed) { |t| t.current_state = :closed }
|
259
|
+
|
260
|
+
# Binds the block to the a lambda to be called when we get to the begin part of the DFA
|
261
|
+
#
|
262
|
+
# @param block
|
263
|
+
# The code block to be called when asynchronous *begin* starts.
|
264
|
+
#
|
265
|
+
def begin(block)
|
266
|
+
raise "Duplicated begin" if @begin_task
|
267
|
+
# @begin_task = lambda { block.call }
|
268
|
+
@begin_task = Task.new(self) { @result.set(block.call) }
|
269
|
+
end
|
270
|
+
|
271
|
+
# Binds the block to the a lambda to be called when we get to the rescue part of the DFA
|
272
|
+
#
|
273
|
+
# @param error_type
|
274
|
+
# The error type.
|
275
|
+
#
|
276
|
+
# @param block
|
277
|
+
# The code block to be called when asynchronous *rescue* starts.
|
278
|
+
#
|
279
|
+
def rescue(error_type, block)
|
280
|
+
this_task = lambda { |failure| block.call(failure) }
|
281
|
+
if @rescue_exceptions.include? error_type
|
282
|
+
raise "You have already registered #{error_type}!"
|
283
|
+
end
|
284
|
+
@rescue_exceptions << error_type
|
285
|
+
@rescue_tasks << this_task
|
286
|
+
end
|
287
|
+
|
288
|
+
# Binds the block to the a lambda to be called when we get to the ensure part of the DFA
|
289
|
+
#
|
290
|
+
# @param block
|
291
|
+
# The code block to be called when asynchronous *ensure* starts.
|
292
|
+
#
|
293
|
+
def ensure(block)
|
294
|
+
raise "Duplicated ensure" if @ensure_task
|
295
|
+
@ensure_task = Task.new(self) { block.call }
|
296
|
+
end
|
297
|
+
|
298
|
+
def schedule
|
299
|
+
@parent << self
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Class to ensure that all the inner guts of BRE aren't exposed. This function is passed in when error_handler is
|
304
|
+
# called, like this:
|
305
|
+
#
|
306
|
+
# error_handler do |t|
|
307
|
+
# t.begin { "This is the begin" }
|
308
|
+
# t.rescue(Exception) { "This is the rescue" }
|
309
|
+
# t.ensure { trace << t.begin_task }
|
310
|
+
# end
|
311
|
+
#
|
312
|
+
# The *t* that is passed in is actually a {BeginRescueEnsureWrapper}, which will only pass begin/rescue/ensure
|
313
|
+
# onto the {BeginRescueEnsure} class itself.
|
314
|
+
#
|
315
|
+
class BeginRescueEnsureWrapper < FlowFiber
|
316
|
+
# Also has a few methods to ensure Fiber-ness, such as get_heirs and cancel.
|
317
|
+
attr_reader :__context__
|
318
|
+
|
319
|
+
# Creates a new BeginRescueEnsureWrapper instance.
|
320
|
+
#
|
321
|
+
# @param block
|
322
|
+
# A code block to be called.
|
323
|
+
#
|
324
|
+
# @param begin_rescue_ensure
|
325
|
+
# The {BeginRescueEnsure} instance to wrap.
|
326
|
+
#
|
327
|
+
def initialize(block, begin_rescue_ensure)
|
328
|
+
@beginRescueEnsure = begin_rescue_ensure
|
329
|
+
@__context__ = @beginRescueEnsure
|
330
|
+
super() do
|
331
|
+
begin
|
332
|
+
block.call(self)
|
333
|
+
ensure
|
334
|
+
@__context__.parent.remove(self)
|
335
|
+
end
|
336
|
+
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# @!visibility private
|
341
|
+
def get_heirs
|
342
|
+
p "I am a BREWrapper"
|
343
|
+
return
|
344
|
+
end
|
345
|
+
|
346
|
+
def cancel(error_type)
|
347
|
+
@beginRescueEnsure.parent.cancel(self)
|
348
|
+
end
|
349
|
+
|
350
|
+
# @!visibility private
|
351
|
+
#
|
352
|
+
# @return [false]
|
353
|
+
# Always returns `false`.
|
354
|
+
#
|
355
|
+
def is_daemon?
|
356
|
+
false
|
357
|
+
end
|
358
|
+
|
359
|
+
# Gets the parent of the {BeginRescueEnsure} instance held by this class.
|
360
|
+
def get_closest_containing_scope
|
361
|
+
@beginRescueEnsure.parent
|
362
|
+
end
|
363
|
+
|
364
|
+
# (see BeginRescueEnsure#begin)
|
365
|
+
def begin(&block) @beginRescueEnsure.begin(block) end
|
366
|
+
|
367
|
+
# (see BeginRescueEnsure#ensure)
|
368
|
+
def ensure(&block) @beginRescueEnsure.ensure(block) end
|
369
|
+
|
370
|
+
# (see BeginRescueEnsure#rescue)
|
371
|
+
def rescue(error_type, &block)
|
372
|
+
@beginRescueEnsure.rescue(error_type, block)
|
373
|
+
end
|
374
|
+
|
375
|
+
private
|
376
|
+
attr_accessor :beginRescueEnsure
|
377
|
+
end
|
378
|
+
|
379
|
+
class DaemonBeginRescueEnsure < BeginRescueEnsure
|
380
|
+
def is_daemon?
|
381
|
+
true
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|