await 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'eventmachine'
4
+
5
+ group :development do
6
+ gem 'bundler'
7
+ gem 'jeweler'
8
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Scott Tadman, The Working Group Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,107 @@
1
+ # await
2
+
3
+ This implements the await/defer pattern in Ruby using fibers within the
4
+ EventMachine environment.
5
+
6
+ In general terms, `await` is used to define a group of operations that must
7
+ be completed before processing can continue, and `defer` is used to define
8
+ these individual operations.
9
+
10
+ This can be used to simplify otherwise complicated callback structures where
11
+ a number of operations can be performed in parallel. Further complexity arises
12
+ when some of these operations have optional steps.
13
+
14
+ ## Examples
15
+
16
+ In order to execute properly, an `await` call must be created within a fiber
17
+ that can yield. Since the root fiber cannot yield, a secondary fiber must be
18
+ created.
19
+
20
+ A very simple example shows how a number of timers can be set to simulate
21
+ some long-running asynchronous operations:
22
+
23
+ ```ruby
24
+ require 'await'
25
+ require 'eventmachine'
26
+
27
+ EventMachine.run do
28
+ Fiber.new do
29
+ include Await
30
+
31
+ now = Time.now
32
+ await do
33
+ EventMachine::Timer.new(5, &defer)
34
+ EventMachine::Timer.new(2, &defer)
35
+ EventMachine::Timer.new(3, &defer)
36
+ end
37
+
38
+ puts "Took %.1fs to complete" % (Time.now - now).to_f
39
+
40
+ EventMachine.stop_event_loop
41
+ end.resume
42
+ end
43
+ ```
44
+
45
+ In the end you should see that it took only as long as the longest timer.
46
+
47
+ In its default state, `defer` is simply used as a trigger. It can also wrap
48
+ around a callback block to add additional functionality:
49
+
50
+ ```ruby
51
+ require 'await'
52
+ require 'eventmachine'
53
+
54
+ EventMachine.run do
55
+ Fiber.new do
56
+ include Await
57
+
58
+ now = Time.now
59
+ await do
60
+ trigger = defer do |x, y|
61
+ puts "Received arguments: #{[ x, y ].inspect}"
62
+ end
63
+
64
+ EventMachine::Timer.new(4) do
65
+ trigger.call(1, '2')
66
+ end
67
+
68
+ EventMachine::Timer.new(
69
+ 3,
70
+ &defer do
71
+ EventMachine::Timer.new(
72
+ 2,
73
+ &defer
74
+ )
75
+ end
76
+ )
77
+ end
78
+
79
+ puts "Took %.1fs to complete" % (Time.now - now).to_f
80
+
81
+ EventMachine.stop_event_loop
82
+ end.resume
83
+ end
84
+ ```
85
+
86
+ Since the `defer` method returns a standard Proc callback, it is possible to
87
+ pass it through as a callback method or to trigger it at an arbitrary time
88
+ with `call`.
89
+
90
+ More examples of callback structures are available in the various unit tests.
91
+
92
+ ## Troubleshooting
93
+
94
+ If the secondary fiber has not been created you will see errors like:
95
+
96
+ FiberError: can't yield from root fiber
97
+
98
+ Keep in mind that unless a `defer` call is made within a callback then the
99
+ operation is considered completed. If an additional asynchronous operation
100
+ must be performed before it is complete, be sure to wrap any and all of these
101
+ calls with `defer` as well to tag and track them properly.
102
+
103
+ ## Copyright
104
+
105
+ Copyright (c) 2012 Scott Tadman, The Working Group Inc.
106
+ See LICENSE.txt for further details.
107
+
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ require 'rake'
15
+ require 'jeweler'
16
+
17
+ Jeweler::Tasks.new do |gem|
18
+ gem.name = "await"
19
+ gem.homepage = "http://github.com/twg/await"
20
+ gem.license = "MIT"
21
+ gem.summary = %Q{Asynchronous await/defer methods for organizing callbacks}
22
+ gem.description = %Q{Implements the await/defer pattern for event-driven or asynchronous Ruby}
23
+ gem.email = "scott@twg.ca"
24
+ gem.authors = [ "Scott Tadman" ]
25
+ end
26
+
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rake/testtask'
30
+
31
+ Rake::TestTask.new(:test) do |test|
32
+ test.libs << 'lib' << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,55 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "await"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Scott Tadman"]
12
+ s.date = "2012-11-13"
13
+ s.description = "Implements the await/defer pattern for event-driven or asynchronous Ruby"
14
+ s.email = "scott@twg.ca"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "LICENSE.txt",
23
+ "README.md",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "await.gemspec",
27
+ "lib/await.rb",
28
+ "test/helper.rb",
29
+ "test/test_await.rb"
30
+ ]
31
+ s.homepage = "http://github.com/twg/await"
32
+ s.licenses = ["MIT"]
33
+ s.require_paths = ["lib"]
34
+ s.rubygems_version = "1.8.24"
35
+ s.summary = "Asynchronous await/defer methods for organizing callbacks"
36
+
37
+ if s.respond_to? :specification_version then
38
+ s.specification_version = 3
39
+
40
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
41
+ s.add_runtime_dependency(%q<eventmachine>, [">= 0"])
42
+ s.add_development_dependency(%q<bundler>, [">= 0"])
43
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
44
+ else
45
+ s.add_dependency(%q<eventmachine>, [">= 0"])
46
+ s.add_dependency(%q<bundler>, [">= 0"])
47
+ s.add_dependency(%q<jeweler>, [">= 0"])
48
+ end
49
+ else
50
+ s.add_dependency(%q<eventmachine>, [">= 0"])
51
+ s.add_dependency(%q<bundler>, [">= 0"])
52
+ s.add_dependency(%q<jeweler>, [">= 0"])
53
+ end
54
+ end
55
+
@@ -0,0 +1,94 @@
1
+ require 'thread'
2
+ require 'fiber'
3
+
4
+ module Await
5
+ module ThreadExtension
6
+ def await
7
+ # Capture the current context to properly chain this await in
8
+ parent = @deferred
9
+
10
+ deferred = @deferred = {
11
+ :fiber => Fiber.current
12
+ }
13
+
14
+ if (parent)
15
+ parent[deferred] = true
16
+ end
17
+
18
+ yield if (block_given?)
19
+
20
+ # So long as there is at least one outstanding defer block, this fiber
21
+ # must continue to yield.
22
+ while (deferred.size > 1)
23
+ fiber, trigger, block, args = Fiber.yield
24
+
25
+ deferred.delete(trigger)
26
+
27
+ if (block)
28
+ # Always introduce the correct context here by setting the
29
+ # thread-local @deferred instance variable.
30
+ @deferred = deferred
31
+ block.call(*args)
32
+ end
33
+ end
34
+
35
+ # If this was part of an existing await call then remove the current
36
+ # await operation from the list of pending entries.
37
+ if (parent)
38
+ if (deferred[:fiber] == Fiber.current)
39
+ parent.delete(deferred)
40
+ else
41
+ parent[:fiber].transfer([ Fiber.current, deferred ])
42
+ end
43
+ end
44
+
45
+ true
46
+ end
47
+
48
+ def defer(&block)
49
+ deferred = @deferred
50
+
51
+ trigger = lambda do |*args|
52
+ if (deferred[:fiber] == Fiber.current)
53
+ # Within the same fiber, remove this from the pending list
54
+ deferred.delete(trigger)
55
+
56
+ block.call(*args)
57
+ else
58
+ # If this is executing in a different fiber, transfer control back
59
+ # to the original fiber for reasons of continuity.
60
+ deferred[:fiber].transfer([ Fiber.current, trigger, block, args ])
61
+ end
62
+ end
63
+
64
+ deferred[trigger] = true
65
+
66
+ trigger
67
+ end
68
+ end
69
+
70
+ # Define a group of operations that need to be completed before execution
71
+ # will continue. The current fiber is suspended until all of the defer
72
+ # operations have completed.
73
+ def await(&block)
74
+ Thread.current.await(&block)
75
+ end
76
+
77
+ # Used to define a deferred operation. The return value is a Proc that can
78
+ # be passed through as a callback argument to any method. Once the callback
79
+ # has been executed it is considered complete unless within that callback
80
+ # other defer or await calls are made.
81
+ def defer(*args, &block)
82
+ Thread.current.defer(*args, &block)
83
+ end
84
+
85
+ # Import all mixin methods here as module methods so they can be used
86
+ # directly without requiring an import.
87
+ extend self
88
+ end
89
+
90
+ # == Hook Installation ======================================================
91
+
92
+ class Thread
93
+ include Await::ThreadExtension
94
+ end
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ begin
5
+ Bundler.setup(:default, :development)
6
+ rescue Bundler::BundlerError => e
7
+ $stderr.puts e.message
8
+ $stderr.puts "Run `bundle install` to install missing gems"
9
+ exit e.status_code
10
+ end
11
+
12
+ require 'test/unit'
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+
17
+ require 'await'
18
+
19
+ require 'eventmachine'
20
+
21
+ class Test::Unit::TestCase
22
+ def em
23
+ EventMachine.run do
24
+ Fiber.new do
25
+ yield
26
+ end.resume
27
+
28
+ EventMachine.stop_event_loop
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,259 @@
1
+ require File.expand_path('helper', File.dirname(__FILE__))
2
+
3
+ class TestAwait < Test::Unit::TestCase
4
+ include Await
5
+
6
+ def test_module
7
+ assert_equal true, Await.respond_to?(:await)
8
+ assert_equal true, Await.respond_to?(:defer)
9
+ end
10
+
11
+ def test_await_default_state
12
+ triggered = false
13
+
14
+ em do
15
+ await do
16
+ triggered = true
17
+ end
18
+ end
19
+
20
+ assert_equal true, triggered
21
+ end
22
+
23
+ def test_await_block_object
24
+ trigger = nil
25
+ exited = false
26
+
27
+ em do
28
+ await do
29
+ trigger = defer
30
+ end
31
+
32
+ exited = true
33
+ end
34
+
35
+ assert_equal false, exited
36
+
37
+ trigger.call
38
+
39
+ assert_equal true, exited
40
+ end
41
+
42
+ def test_await_single_defer
43
+ triggered = false
44
+
45
+ em do
46
+ await do
47
+ defer do
48
+ triggered = true
49
+ end.call
50
+ end
51
+ end
52
+
53
+ assert_equal true, triggered
54
+ end
55
+
56
+ def test_await_nested
57
+ triggered = 0
58
+
59
+ em do
60
+ await do
61
+ defer do
62
+ await do
63
+ defer do
64
+ triggered += 1
65
+ end.call
66
+ end
67
+
68
+ assert_equal 1, triggered
69
+
70
+ await do
71
+ defer do
72
+ triggered += 2
73
+ end.call
74
+ end
75
+ end.call
76
+ end
77
+
78
+ assert_equal 3, triggered
79
+ end
80
+
81
+ assert_equal 3, triggered
82
+ end
83
+
84
+ def test_await_delayed_defer
85
+ triggered = false
86
+ defer_block = nil
87
+
88
+ em do
89
+ await do
90
+ defer_block = defer do
91
+ triggered = true
92
+ end
93
+ end
94
+ end
95
+
96
+ assert defer_block
97
+ assert_equal false, triggered
98
+
99
+ assert defer_block
100
+ defer_block.call
101
+
102
+ assert_equal true, triggered
103
+ end
104
+
105
+ def test_await_delayed_double_defer
106
+ outer_trigger = nil
107
+ inner_trigger = nil
108
+ triggered = false
109
+ continued = false
110
+
111
+ em do
112
+ await do
113
+ outer_trigger = defer do
114
+ inner_trigger = defer do
115
+ triggered = true
116
+ end
117
+ end
118
+ end
119
+
120
+ continued = true
121
+ end
122
+
123
+ assert_equal false, triggered
124
+ assert_equal false, continued
125
+
126
+ assert !inner_trigger
127
+
128
+ outer_trigger.call
129
+
130
+ assert_equal false, triggered
131
+ assert_equal false, continued
132
+
133
+ assert inner_trigger
134
+
135
+ inner_trigger.call
136
+
137
+ assert_equal true, triggered
138
+ assert_equal true, continued
139
+ end
140
+
141
+ def test_await_nested_delayed_trigger
142
+ outer_trigger = nil
143
+ inner_trigger = nil
144
+ triggered = false
145
+ continued = false
146
+
147
+ em do
148
+ await do
149
+ outer_trigger = defer do
150
+ await do
151
+ inner_trigger = defer do
152
+ triggered = true
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ continued = true
159
+ end
160
+
161
+ assert_equal false, triggered
162
+ assert_equal false, continued
163
+
164
+ assert !inner_trigger
165
+
166
+ outer_trigger.call
167
+
168
+ assert_equal false, triggered
169
+ assert_equal false, continued
170
+
171
+ assert inner_trigger
172
+
173
+ inner_trigger.call
174
+
175
+ assert_equal true, triggered
176
+ assert_equal true, continued
177
+ end
178
+
179
+ def test_await_delayed_defer_callback
180
+ triggered = false
181
+ trigger_callback = nil
182
+ results = nil
183
+
184
+ em do
185
+ await do
186
+ trigger_callback = defer do |*args|
187
+ results = args
188
+ end
189
+ end
190
+
191
+ triggered = true
192
+ end
193
+
194
+ assert_equal false, triggered
195
+
196
+ assert trigger_callback
197
+ trigger_callback.call(1, :two, '3')
198
+
199
+ assert_equal true, triggered
200
+ assert_equal [ 1, :two, '3' ], results
201
+ end
202
+
203
+ def test_await_multiple_defer
204
+ triggered = false
205
+ triggers = [ ]
206
+ count = 10
207
+
208
+ em do
209
+ await do
210
+ count.times do
211
+ triggers << defer
212
+ end
213
+ end
214
+
215
+ triggered = true
216
+ end
217
+
218
+ assert_equal false, triggered
219
+
220
+ while (triggers.length > 1)
221
+ triggers.pop.call
222
+ end
223
+
224
+ assert_equal false, triggered
225
+
226
+ triggers.pop.call
227
+
228
+ assert_equal true, triggered
229
+ end
230
+
231
+ def test_await_chained_defer
232
+ triggered = false
233
+ trigger_block_outer = nil
234
+ trigger_block_inner = nil
235
+
236
+ em do
237
+ await do
238
+ trigger_block_outer = defer do
239
+ trigger_block_inner = defer do
240
+ end
241
+ end
242
+ end
243
+
244
+ triggered = true
245
+ end
246
+
247
+ assert_equal false, triggered
248
+
249
+ assert trigger_block_outer
250
+ trigger_block_outer.call
251
+
252
+ assert_equal false, triggered
253
+
254
+ assert trigger_block_inner
255
+ trigger_block_inner.call
256
+
257
+ assert_equal true, triggered
258
+ end
259
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: await
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Scott Tadman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: jeweler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Implements the await/defer pattern for event-driven or asynchronous Ruby
63
+ email: scott@twg.ca
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files:
67
+ - LICENSE.txt
68
+ - README.md
69
+ files:
70
+ - .document
71
+ - Gemfile
72
+ - LICENSE.txt
73
+ - README.md
74
+ - Rakefile
75
+ - VERSION
76
+ - await.gemspec
77
+ - lib/await.rb
78
+ - test/helper.rb
79
+ - test/test_await.rb
80
+ homepage: http://github.com/twg/await
81
+ licenses:
82
+ - MIT
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 1.8.24
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Asynchronous await/defer methods for organizing callbacks
105
+ test_files: []