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.
@@ -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