aws-flow-core 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,405 @@
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
+ # Contains all the task methods, which allow arbitrary blocks of code to be run asynchronously
17
+
18
+ module AWS
19
+ module Flow
20
+ module Core
21
+ class Task < FlowFiber
22
+ attr_reader :result, :block
23
+ attr_accessor :backtrace, :__context__, :parent
24
+
25
+ # Creates a new Task.
26
+ #
27
+ # @param __context__
28
+ # A Task needs a reference to the __context__ that created it so that when the "task" macro is called it can
29
+ # find the __context__ which the new Task should be added to.
30
+ #
31
+ # @param block
32
+ # A block of code that will be run by the task.
33
+ #
34
+ def initialize(__context__, &block)
35
+ @__context__ = __context__
36
+ @result = Future.new
37
+ @block = block
38
+
39
+ # Is the task alive?
40
+ def alive?
41
+ super && !@cancelled
42
+ #!!@alive# && !@cancelled
43
+ end
44
+
45
+ # @return
46
+ # The executor for this task.
47
+ def executor
48
+ @__context__.executor
49
+ end
50
+
51
+ super() do
52
+ begin
53
+ # Not return because 1.9 will freak about local jump problems if you
54
+ # try to return, as this is inside a block
55
+ next if @cancelled
56
+ @result.set(lambda(&block).call)
57
+ next if @cancelled
58
+ @__context__.remove(self)
59
+ rescue Exception => e
60
+ if @backtrace != e
61
+ backtrace = AsyncBacktrace.create_from_exception(@backtrace, e)
62
+ e.set_backtrace(backtrace.backtrace) if backtrace
63
+ end
64
+ @__context__.fail(self, e)
65
+ ensure
66
+ end
67
+ end
68
+ end
69
+
70
+ #
71
+ # Passes the get_heirs calls to the context, to ensure uniform handling of get_heirs
72
+ #
73
+ def get_heirs
74
+ @__context__.get_heirs
75
+ end
76
+
77
+ #
78
+ # Returns true/false, depending on if we are in a daemon task or not
79
+ #
80
+ def is_daemon?
81
+ return false
82
+ end
83
+
84
+ # Used by Future#signal to schedule the task for re-evaluation.
85
+ #
86
+ # This will simply add the task back to the list of things to be run in the parent's event loop.
87
+ #
88
+ def schedule
89
+ @__context__ << self
90
+ end
91
+
92
+ # Cancel will prevent the execution of this particular task, if possible
93
+ #
94
+ # @param error
95
+ # The error that is the cause of the cancellation
96
+ #
97
+ def cancel(error)
98
+ @cancelled = true
99
+ @__context__.remove(self)
100
+ end
101
+
102
+ # Fails the given task with the specified error.
103
+ #
104
+ # @param error
105
+ # The error that is the cause of the cancellation
106
+ #
107
+ # @!visibility private
108
+ def fail(this_task, error)
109
+ @__context__.fail(this_task, error)
110
+ end
111
+ # def fail(this_task, error)
112
+ # raise error
113
+ # end
114
+
115
+ # Adds a task to this task's context
116
+ #
117
+ # @param this_task
118
+ # The task to add.
119
+ #
120
+ def <<(this_task)
121
+ @__context__.parent << this_task
122
+ end
123
+
124
+ # Removes a task from this task's context
125
+ #
126
+ # @param this_task
127
+ # The task to remove.
128
+ #
129
+ def remove(this_task)
130
+ @__context__.remove(this_task)
131
+ end
132
+
133
+
134
+ end
135
+
136
+
137
+ # Similar to a regular Task in all functioning except that it is not assured a chance to execute. Whereas a
138
+ # begin/run/execute block cannot be closed out while there are still nonDaemonHeirs, it can happily entered the
139
+ # closed state with daemon heirs, making them essentially unrunnable.
140
+ class DaemonTask < Task
141
+
142
+ def is_daemon?
143
+ return true
144
+ end
145
+
146
+ end
147
+
148
+ # External Tasks are used to bridge asynchronous execution to external asynchronous APIs or events. It is passed a block, like so
149
+ #
150
+ # external_task do |t|
151
+ # t.cancellation_handler { |h, cause| h.fail(cause) }
152
+ # t.initiate_task { |h| trace << :task_started; h.complete; }
153
+ # end
154
+ #
155
+ # The {ExternalTask#initiate_task} method is expected to call an external API and return without blocking.
156
+ # Completion or failure of the external task is reported through ExternalTaskCompletionHandle, which is passed into
157
+ # the initiate_task and cancellation_handler blocks. The cancellation handler, defined in the same block as the
158
+ # initiate_task, is used to report the cancellation of the external task.
159
+ #
160
+ class ExternalTask < FlowFiber
161
+ attr_reader :block
162
+ attr_accessor :cancelled, :inCancellationHandler, :parent, :backtrace, :__context__
163
+
164
+
165
+ # Will always be false, provides a common api for BRE's to ensure they are maintaining their nonDaemonHeirsCount
166
+ # correctly
167
+ # @!visibility private
168
+ def is_daemon?
169
+ false
170
+ end
171
+
172
+ #
173
+ # Passes the get_heirs calls to the context, to ensure uniform handling of
174
+ # get_heirs
175
+ #
176
+ # @!visibility private
177
+ def get_heirs
178
+ @__context__.get_heirs
179
+ end
180
+
181
+ # @!visibility private
182
+ def initialize(options = {}, &block)
183
+ @inCancellationHandler = false
184
+ @block = block
185
+ # TODO: What should the default value be?
186
+ @parent = options[:parent]
187
+ @handle = ExternalTaskCompletionHandle.new(self)
188
+ block.call(self)
189
+ end
190
+
191
+ # This method is here because the way we create ExternalTasks is a little
192
+ # tricky - if the parent isn't passed in on construction(as is the case with
193
+ # the external_task function), then the parent will only be set after
194
+ # ExternalTask#initialize is called. We'd prefer to set it in the initiailze,
195
+ # however, the backtrace relies on the parent's backtrace, and so we cannot do
196
+ # that. Instead, we use this method to lazily create it, when it is
197
+ # called. The method itself simply sets the backtrace to the the
198
+ # make_backtrace of the parent's backtrace, if the backtrace is not already
199
+ # set, and will otherwise simply return the backtrace
200
+ # @!visibility private
201
+ def backtrace
202
+ @backtrace ||= make_backtrace(@parent.backtrace)
203
+ end
204
+
205
+ # Add a task which removes yourself, and pass it through the parents executor
206
+ # @!visibility private
207
+ def remove_from_parent
208
+ @__context__.executor << FlowFiber.new { @parent.remove(self) }
209
+ end
210
+
211
+ # Add a task which fails yourself with the suppiled error, and pass it through
212
+ # the parents executor
213
+ # @!visibility private
214
+ def fail_to_parent(error)
215
+ @__context__.executor << FlowFiber.new { @parent.fail(self, error) }
216
+ end
217
+
218
+
219
+ # Part of the interface provided by Fiber, has to overridden to properly
220
+ # reflect that an ExternalTasks alive-ness relies on it's
221
+ # ExternalTaskCompletionHandle
222
+ # @!visibility private
223
+ def alive?
224
+ ! @handle.completed
225
+ end
226
+
227
+ # @!visibility private
228
+ def cancel(cause)
229
+ return if @cancelled
230
+ return if @handle.failure != nil || @handle.completed
231
+ @cancelled = true
232
+ if @cancellation_task != nil
233
+ begin
234
+ @inCancellationHandler = true
235
+ @cancellation_task.call(cause)
236
+ rescue Exception => e
237
+ if ! self.backtrace.nil?
238
+ backtrace = AsyncBacktrace.create_from_exception(@backtrace, e)
239
+ e.set_backtrace(backtrace.backtrace) if backtrace
240
+ end
241
+ @handle.failure = e
242
+ ensure
243
+ @inCancellationHandler = false
244
+ if ! @handle.failure.nil?
245
+ fail_to_parent(@handle.failure)
246
+ elsif @handle.completed
247
+ remove_from_parent
248
+ end
249
+ end
250
+ else
251
+ remove_from_parent
252
+ end
253
+ end
254
+
255
+ # Store the cancellation handler block passed in for later reference
256
+ # @!visibility private
257
+ def cancellation_handler(&block)
258
+ @cancellation_task = lambda { |cause| block.call(@handle, cause) }
259
+ end
260
+
261
+ # Store the block passed in for later
262
+ # @!visibility private
263
+ def initiate_task(&block)
264
+ @initiation_task = lambda { block.call(@handle) }
265
+ end
266
+
267
+ # From the interface provided by Fiber, will execute the External Task
268
+ # @!visibility private
269
+ def resume
270
+ return if @cancelled
271
+ begin
272
+ @cancellation_handler = @initiation_task.call
273
+ rescue Exception => e
274
+ backtrace = AsyncBacktrace.create_from_exception(self.backtrace, e)
275
+ e.set_backtrace(backtrace.backtrace) if backtrace
276
+ @parent.fail(self, e)
277
+ end
278
+ end
279
+ end
280
+
281
+ # Used to complete or fail an external task initiated through
282
+ # ExternalTask#initiate_task, and thus handles the logic of what to do when the
283
+ # external task is failed.
284
+ # @!visibility private
285
+ class ExternalTaskCompletionHandle
286
+ attr_accessor :completed, :failure, :external_task
287
+
288
+ # @!visibility private
289
+ def initialize(external_task)
290
+ @external_task = external_task
291
+ end
292
+
293
+ # Will merge the backtrace, set the @failure, and then fail the task from the parent
294
+ # @!visibility private
295
+ #
296
+ # @param error
297
+ # The exception to fail on
298
+ #
299
+ # @raise IllegalStateException
300
+ # Raises if failure hasn't been set, or if the task is already completed
301
+ #
302
+ # @!visibility private
303
+ def fail(error)
304
+ if ! @failure.nil?
305
+ raise IllegalStateException, "Invalid ExternalTaskCompletionHandle"
306
+ end
307
+ if @completed
308
+ raise IllegalStateException, "Already completed"
309
+ end
310
+ #TODO Might want to flip the logic to only alert if variable is set
311
+ if @stacktrace.nil?
312
+ if ! @external_task.backtrace.nil?
313
+ backtrace = AsyncBacktrace.create_from_exception(@external_task.backtrace, error)
314
+ error.set_backtrace(backtrace.backtrace) if backtrace
315
+ end
316
+ end
317
+ @failure = error
318
+ if ! @external_task.inCancellationHandler
319
+ @external_task.fail_to_parent(error)
320
+ end
321
+ end
322
+
323
+ # Set's the task to complete, and removes it from it's parent
324
+ #
325
+ # @raise IllegalStateException
326
+ # If the failure hasn't been set, or if the task is already completed
327
+ #
328
+ # @!visibility private
329
+ def complete
330
+ if ! failure.nil?
331
+ raise IllegalStateException, ""
332
+ end
333
+
334
+ if @completed
335
+ raise IllegalStateException, "Already Completed"
336
+ end
337
+ @completed = true
338
+ @external_task.remove_from_parent if ! @external_task.inCancellationHandler
339
+ end
340
+ end
341
+
342
+ # TaskContext is the class that holds some meta-information for tasks, and which stores the parent link for tasks.
343
+ # It seperates some of the concerns between tasks and what they have to know to follow back up the chain.
344
+ #
345
+ # All the methods here will simply delegate calls, either up to the parent, or down to the task
346
+ # @!visibility private
347
+ class TaskContext
348
+
349
+ attr_accessor :daemon, :parent, :backtrace, :cancelled
350
+
351
+ def initialize(options = {})
352
+ @parent = options[:parent]
353
+ @task = options[:task]
354
+ @task.__context__ = self
355
+ @non_cancelling = options[:non_cancelling]
356
+ @daemon = options[:daemon]
357
+ end
358
+
359
+ # @!visibility private
360
+ def get_closest_containing_scope
361
+ @task
362
+ # @ parent
363
+ end
364
+
365
+ # @!visibility private
366
+ def alive?
367
+ @task.alive?
368
+ end
369
+
370
+ # @!visibility private
371
+ def executor
372
+ @parent.executor
373
+ end
374
+
375
+ # @!visibility private
376
+ def get_heirs
377
+ str = "I am a #{@task.class}
378
+ and my block looks like #{@task.block}"
379
+ end
380
+
381
+ # @!visibility private
382
+ def fail(this_task, error)
383
+ @parent.fail(this_task, error)
384
+ end
385
+
386
+ # @!visibility private
387
+ def remove(thread)
388
+ @parent.remove(thread)
389
+ end
390
+
391
+ # @!visibility private
392
+ def cancel(error_type)
393
+ @task.cancelled = true
394
+ @parent.cancel(self)
395
+ end
396
+
397
+ # @!visibility private
398
+ def <<(task)
399
+ @parent << task
400
+ end
401
+
402
+ end
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,41 @@
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 validate_stacktrace_content(method_to_call_on_async_backtrace, thing_to_look_for, should_it_be_there)
17
+ it "makes sure that any of #{thing_to_look_for.join", "} #{should_it_be_there} be printed when we call #{method_to_call_on_async_backtrace} on AsyncBacktrace" do
18
+ AsyncBacktrace.enable
19
+ AsyncBacktrace.send(method_to_call_on_async_backtrace)
20
+ scope = AsyncScope.new do
21
+ error_handler do |t|
22
+ t.begin { raise StandardError.new }
23
+ t.rescue(IOError) {}
24
+ end
25
+ end
26
+ begin
27
+ scope.eventLoop
28
+ rescue Exception => e
29
+ matching_lines = thing_to_look_for.select { |part| e.backtrace.to_s.include? part.to_s }
30
+
31
+ matching_lines.send(should_it_be_there, be_empty)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe AsyncBacktrace do
37
+ validate_stacktrace_content(:enable, [:continuation], :should_not)
38
+ validate_stacktrace_content(:disable, [:continuation], :should)
39
+ validate_stacktrace_content(:enable_filtering, $RUBY_FLOW_FILES, :should)
40
+ validate_stacktrace_content(:disable_filtering, $RUBY_FLOW_FILES, :should_not)
41
+ end
@@ -0,0 +1,118 @@
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
+ describe AsyncScope do
17
+
18
+ it "makes sure that basic AsyncScoping works" do
19
+ trace = []
20
+ this_scope = AsyncScope.new() do
21
+ task { trace << :inside_task}
22
+ end
23
+ trace.should == []
24
+ this_scope.eventLoop
25
+ trace.should == [:inside_task]
26
+ end
27
+
28
+ context "empty AsyncScope" do
29
+ let(:scope) { AsyncScope.new }
30
+ let(:trace) { [] }
31
+
32
+ it "adds tasks like a queue" do
33
+ scope << Task.new(scope.root_context) { trace << :task }
34
+ scope.eventLoop
35
+ trace.should == [:task]
36
+ end
37
+
38
+ it "adds a task if given one on construction" do
39
+ scope = AsyncScope.new { trace << :task }
40
+ scope.eventLoop
41
+ trace.should == [:task]
42
+ end
43
+
44
+
45
+ it "runs a task asynchronously" do
46
+ scope << Task.new(scope.root_context) { trace << :inner }
47
+ trace << :outer
48
+ scope.eventLoop
49
+ trace.should == [:outer, :inner]
50
+ end
51
+
52
+ it "runs multiple tasks in order" do
53
+ scope << Task.new(scope.root_context) { trace << :first_task }
54
+ scope << Task.new(scope.root_context) { trace << :second_task }
55
+ scope.eventLoop
56
+ trace.should == [:first_task, :second_task]
57
+ end
58
+
59
+ it "resumes tasks after a Future is ready" do
60
+ f = Future.new
61
+ scope = AsyncScope.new do
62
+ task do
63
+ trace << :first
64
+ f.get
65
+ trace << :fourth
66
+ end
67
+
68
+ task do
69
+ trace << :second
70
+ f.set(:foo)
71
+ trace << :third
72
+ end
73
+ end
74
+ scope.eventLoop
75
+ trace.should == [:first, :second, :third, :fourth]
76
+ end
77
+
78
+ it "tests to make sure that scopes complete after an event loop" do
79
+ scope = AsyncScope.new do
80
+ error_handler do |t|
81
+ t.begin do
82
+ x = Future.new
83
+ y = Future.new
84
+ error_handler do |t|
85
+ t.begin {}
86
+ t.ensure { x.set }
87
+ end
88
+ x.get
89
+ error_handler do |t|
90
+ t.begin {}
91
+ t.ensure { y.set }
92
+ end
93
+ y.get
94
+ error_handler do |t|
95
+ t.begin {}
96
+ t.ensure {}
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ completed = scope.eventLoop
103
+ completed.should == true
104
+ end
105
+ it "ensures you can cancel an async scope " do
106
+ condition = FiberConditionVariable.new
107
+ @task = nil
108
+ scope = AsyncScope.new do
109
+ task do
110
+ condition.wait
111
+ end
112
+ end
113
+ scope.eventLoop
114
+ scope.cancel(CancellationException.new)
115
+ expect { scope.eventLoop }.to raise_error CancellationException
116
+ end
117
+ end
118
+ end