async 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.travis.yml +2 -0
- data/README.md +56 -38
- data/Rakefile +4 -0
- data/async.gemspec +4 -3
- data/lib/async/node.rb +47 -0
- data/lib/async/reactor.rb +27 -34
- data/lib/async/task.rb +35 -65
- data/lib/async/version.rb +1 -1
- data/lib/async/wrapper.rb +35 -19
- data/spec/async/condition_spec.rb +1 -1
- data/spec/async/node_spec.rb +37 -0
- data/spec/async/reactor/nested_spec.rb +1 -1
- data/spec/async/reactor_spec.rb +22 -1
- data/spec/async/task_spec.rb +53 -1
- data/{lib/async/udp_socket.rb → spec/async/wrapper_spec.rb} +25 -9
- data/spec/spec_helper.rb +2 -35
- metadata +14 -25
- data/examples/aio.rb +0 -42
- data/examples/echo.rb +0 -40
- data/lib/async/io.rb +0 -109
- data/lib/async/socket.rb +0 -109
- data/lib/async/tcp_socket.rb +0 -35
- data/lib/async/unix_socket.rb +0 -33
- data/spec/async/tcp_socket_spec.rb +0 -106
- data/spec/async/udp_socket_spec.rb +0 -71
- data/spec/async/unix_socket_spec.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 881ace206b49720782f4bf53c50cea4997efe5ac
|
4
|
+
data.tar.gz: f9b1ecbd174370ce7b62c6f4fdbd6a0dddda858c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef14bfef3d114ef2ee95f15fc239efad2b1f91efe5b0e99d8243dc3d5d487188cf41a7c5e36708c07d8f007db0b5fd7dfaf3ba731ab2a3fb98751bf0a6938b8c
|
7
|
+
data.tar.gz: fc1eeaadb11ec7dafe4e21ea2afa251508d1eaab8820d86b4e8a93aa7eca1828caa3c9bf42384cae7aec11afca8a0159b608fd732e26dcdf25118832fbcaa748
|
data/.rspec
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Async
|
2
2
|
|
3
|
-
|
3
|
+
Async is a composable asynchronous I/O framework for Ruby based on [nio4r] and [timers].
|
4
4
|
|
5
5
|
[timers]: https://github.com/socketry/timers
|
6
6
|
[nio4r]: https://github.com/socketry/nio4r
|
@@ -39,53 +39,69 @@ Or install it yourself as:
|
|
39
39
|
|
40
40
|
## Usage
|
41
41
|
|
42
|
-
|
42
|
+
`Async::Reactor` is the top level IO reactor, and runs `Async::Task`s asynchronously. The reactor itself is not thread-safe, so you'd typically have one reactor per thread.
|
43
|
+
|
44
|
+
An `Async::Task` runs using a `Fiber` and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can succeed.
|
45
|
+
|
46
|
+
The design of this core library is deliberately simple in scope. Additional libraries provide asynchronous networking, process management, etc. It's likely you will prefer to depend on `async-io` for actual wrappers around `IO` and `Socket`.
|
47
|
+
|
48
|
+
### Main Entry Points
|
49
|
+
|
50
|
+
#### `Async::Reactor.run`
|
51
|
+
|
52
|
+
The highest level entry point is `Async::Reactor.run`. It's useful if you are building a library and you want well defined asynchronous semantics.
|
43
53
|
|
44
54
|
```ruby
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
def echo_server
|
51
|
-
Async::Reactor.run do |task|
|
52
|
-
# This is a synchronous block within the current task:
|
53
|
-
task.with(TCPServer.new('localhost', 9000)) do |server|
|
54
|
-
|
55
|
-
# This is an asynchronous block within the current reactor:
|
56
|
-
task.reactor.with(server.accept) do |client|
|
57
|
-
data = client.read(512)
|
58
|
-
|
59
|
-
task.sleep(rand)
|
60
|
-
|
61
|
-
client.write(data)
|
62
|
-
end while true
|
63
|
-
end
|
64
|
-
end
|
55
|
+
def run_server
|
56
|
+
Async::Reactor.run do |task|
|
57
|
+
# ... acccept connections
|
58
|
+
end
|
65
59
|
end
|
60
|
+
```
|
61
|
+
|
62
|
+
If `Async::Reactor.run(&block)` happens within an existing reactor, it will schedule an asynchronous task and return. If `Async::Reactor.run(&block)` happens outside of an existing reactor, it will create a reactor, schedule the asynchronous task, and block until it completes. The task is scheduled by calling `Async::Reactor.async(&block)`.
|
63
|
+
|
64
|
+
This puts the power into the hands of the client, who can either have blocking or non-blocking behaviour by explicitly wrapping the call in a reactor (or not). The cost of using `Async::Reactor.run` is minimal for initialization/server setup, but is not ideal for per-connection tasks.
|
65
|
+
|
66
|
+
#### `Async::Task#async`
|
67
|
+
|
68
|
+
If you can guarantee you are running within a task, and have access to it (e.g. via an argument), you can efficiently schedule new tasks using the `Async::Task#async(&block)` method.
|
66
69
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
73
|
-
end
|
70
|
+
```ruby
|
71
|
+
def do_request(task: Task.current)
|
72
|
+
task.async do
|
73
|
+
# ... do some actual work
|
74
|
+
end
|
74
75
|
end
|
76
|
+
```
|
77
|
+
|
78
|
+
This method effectively creates a child task. It's the most efficient way to schedule a task. The task is executed until the first blocking operation, at which point it will yield control and `#async` will return. The result of this method is the task itself.
|
79
|
+
|
80
|
+
### Reactor Tree
|
81
|
+
|
82
|
+
`Async::Reactor` and `Async::Task` form nodes in a tree. Reactors and tasks can spawn children tasks. When you invoke `Async::Reactor#async`, the parent task is determined by calling `Async::Task.current?` which uses fiber local storage. A slightly more efficient method is to use `Async::Task#async`, which uses `self` as the parent task.
|
75
83
|
|
84
|
+
When invoking `Async::Reactor#stop`, you will stop *all* children tasks of that reactor. Tasks will raise `Async::Interrupt` if they are in a blocking operation. In addition, it's possible to only stop a sub-tree by issuing `Async::Task#stop`, which will stop that task and all it's children (recursively). When you design a server, you should return the task back to the caller. They can use this task to stop the server if needed, independently of any other unrelated tasks within the reactor, and it will correctly clean up all related tasks.
|
85
|
+
|
86
|
+
### Resource Management
|
87
|
+
|
88
|
+
In order to ensure your resources are cleaned up correctly, make sure you wrap resources appropriately, e.g.:
|
89
|
+
|
90
|
+
```ruby
|
76
91
|
Async::Reactor.run do
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
server.stop
|
92
|
+
begin
|
93
|
+
socket = connect(remote_address) # May raise Async::Interrupt so socket could be nil
|
94
|
+
|
95
|
+
socket.write(...) # May raise Async::Interrupt
|
96
|
+
socket.read(...) # May raise Async::Interrupt
|
97
|
+
ensure
|
98
|
+
socket.close if socket
|
99
|
+
end
|
86
100
|
end
|
87
101
|
```
|
88
102
|
|
103
|
+
As tasks run synchronously until they yield back to the reactor, you can guarantee this model works correctly. While in theory `IO#autoclose` allows you to automatically close file descriptors when they go out of scope via the GC, it may produce unpredictable behavour (exhaustion of file descriptors, flushing data at odd times), so it's not recommended.
|
104
|
+
|
89
105
|
## Supported Ruby Versions
|
90
106
|
|
91
107
|
This library aims to support and is [tested against][travis] the following Ruby
|
@@ -119,7 +135,9 @@ dropped.
|
|
119
135
|
|
120
136
|
## See Also
|
121
137
|
|
138
|
+
- [async-io](https://github.com/socketry/async-io) — Asynchronous networking and sockets.
|
122
139
|
- [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server.
|
140
|
+
- [async-rspec](https://github.com/socketry/async-rspec) — Shared contexts for running async specs.
|
123
141
|
- [rubydns](https://github.com/ioquatix/rubydns) — A easy to use Ruby DNS server.
|
124
142
|
|
125
143
|
## License
|
data/Rakefile
CHANGED
data/async.gemspec
CHANGED
@@ -21,13 +21,14 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
spec.has_rdoc = "yard"
|
23
23
|
|
24
|
-
spec.required_ruby_version = "
|
24
|
+
spec.required_ruby_version = "~> 2.0"
|
25
25
|
|
26
26
|
spec.add_runtime_dependency "nio4r"
|
27
27
|
spec.add_runtime_dependency "timers", "~> 4.1"
|
28
28
|
|
29
|
+
spec.add_development_dependency "async-rspec", "~> 1.0"
|
30
|
+
|
29
31
|
spec.add_development_dependency "bundler", "~> 1.3"
|
30
|
-
spec.add_development_dependency "
|
31
|
-
spec.add_development_dependency "rspec", "~> 3.4.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
32
33
|
spec.add_development_dependency "rake"
|
33
34
|
end
|
data/lib/async/node.rb
CHANGED
@@ -29,6 +29,9 @@ module Async
|
|
29
29
|
@children = Set.new
|
30
30
|
@parent = nil
|
31
31
|
|
32
|
+
@annotation = nil
|
33
|
+
@object_name = nil
|
34
|
+
|
32
35
|
if parent
|
33
36
|
self.parent = parent
|
34
37
|
end
|
@@ -40,6 +43,34 @@ module Async
|
|
40
43
|
# @attr children [Set<Node>]
|
41
44
|
attr :children
|
42
45
|
|
46
|
+
# A useful identifier for the current node.
|
47
|
+
attr :annotation
|
48
|
+
|
49
|
+
def annotate(annotation)
|
50
|
+
if block_given?
|
51
|
+
previous_annotation = @annotation
|
52
|
+
@annotation = annotation
|
53
|
+
yield
|
54
|
+
@annotation = previous_annotation
|
55
|
+
else
|
56
|
+
@annotation = annotation
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def description
|
61
|
+
@object_name ||= "#{self.class}:0x#{object_id.to_s(16)}"
|
62
|
+
|
63
|
+
if @annotation
|
64
|
+
"#{@object_name} #{@annotation}"
|
65
|
+
else
|
66
|
+
@object_name
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"\#<#{description}>"
|
72
|
+
end
|
73
|
+
|
43
74
|
# Change the parent of this node.
|
44
75
|
# @param parent [Node, nil] the parent to attach to, or nil to detach.
|
45
76
|
# @return [self]
|
@@ -81,5 +112,21 @@ module Async
|
|
81
112
|
def reap(child)
|
82
113
|
@children.delete(child)
|
83
114
|
end
|
115
|
+
|
116
|
+
# Traverse the tree.
|
117
|
+
# @yield [node, level] The node and the level relative to the given root.
|
118
|
+
def traverse(level = 0, &block)
|
119
|
+
yield self, level
|
120
|
+
|
121
|
+
@children.each do |child|
|
122
|
+
child.traverse(level + 1, &block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def print_hierarchy(out = $stdout)
|
127
|
+
self.traverse do |node, level|
|
128
|
+
out.puts "#{"\t" * level}#{node}"
|
129
|
+
end
|
130
|
+
end
|
84
131
|
end
|
85
132
|
end
|
data/lib/async/reactor.rb
CHANGED
@@ -35,7 +35,7 @@ module Async
|
|
35
35
|
class Reactor < Node
|
36
36
|
extend Forwardable
|
37
37
|
|
38
|
-
# The preferred method to invoke asynchronous behavior.
|
38
|
+
# The preferred method to invoke asynchronous behavior at the top level.
|
39
39
|
#
|
40
40
|
# - When invoked within an existing reactor task, it will run the given block
|
41
41
|
# asynchronously. Will return the task once it has been scheduled.
|
@@ -59,43 +59,35 @@ module Async
|
|
59
59
|
return reactor
|
60
60
|
end
|
61
61
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
super(nil)
|
66
|
-
|
67
|
-
@wrappers = wrappers
|
62
|
+
|
63
|
+
def initialize
|
64
|
+
super
|
68
65
|
|
69
66
|
@selector = NIO::Selector.new
|
70
67
|
@timers = Timers::Group.new
|
71
68
|
|
72
69
|
@stopped = true
|
73
70
|
end
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
"<#{self.description} stopped=#{@stopped}>"
|
74
|
+
end
|
75
|
+
|
76
|
+
# @attr stopped [Boolean]
|
78
77
|
attr :stopped
|
79
78
|
|
80
79
|
def_delegators :@timers, :every, :after
|
81
|
-
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
def
|
91
|
-
|
92
|
-
task.with(io, &block)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# @return [Task]
|
97
|
-
def async(*ios, &block)
|
98
|
-
task = Task.new(ios, self, &block)
|
80
|
+
|
81
|
+
# Start an asynchronous task within the specified reactor. The task will be
|
82
|
+
# executed until the first blocking call, at which point it will yield and
|
83
|
+
# and this method will return.
|
84
|
+
#
|
85
|
+
# This is the main entry point for scheduling asynchronus tasks.
|
86
|
+
#
|
87
|
+
# @yield [Task] Executed within the asynchronous task.
|
88
|
+
# @return [Task] The task that was
|
89
|
+
def async(*args, &block)
|
90
|
+
task = Task.new(self, &block)
|
99
91
|
|
100
92
|
# I want to take a moment to explain the logic of this.
|
101
93
|
# When calling an async block, we deterministically execute it until the
|
@@ -104,7 +96,7 @@ module Async
|
|
104
96
|
# - Fail at the point of call where possible.
|
105
97
|
# - Execute determinstically where possible.
|
106
98
|
# - Avoid overhead if no blocking operation is performed.
|
107
|
-
task.run
|
99
|
+
task.run(*args)
|
108
100
|
|
109
101
|
# Async.logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
|
110
102
|
return task
|
@@ -141,7 +133,6 @@ module Async
|
|
141
133
|
interval = 0 if interval && interval < 0
|
142
134
|
|
143
135
|
Async.logger.debug{"[#{self} Pre] Updating #{@children.count} children..."}
|
144
|
-
Async.logger.debug{@children.collect{|child| [child.to_s, child.alive?]}.inspect}
|
145
136
|
# As timeouts may have been updated, and caused fibers to complete, we should check this.
|
146
137
|
|
147
138
|
# If there is nothing to do, then finish:
|
@@ -161,16 +152,18 @@ module Async
|
|
161
152
|
|
162
153
|
return self
|
163
154
|
ensure
|
164
|
-
Async.logger.debug{"[#{self} Ensure] Exiting run-loop (stopped: #{@stopped} exception: #{
|
165
|
-
Async.logger.debug{@children.collect{|child| [child.to_s, child.alive?]}.inspect}
|
155
|
+
Async.logger.debug{"[#{self} Ensure] Exiting run-loop (stopped: #{@stopped} exception: #{$!.inspect})..."}
|
166
156
|
@stopped = true
|
167
157
|
end
|
168
158
|
|
169
|
-
#
|
159
|
+
# Stop each of the children tasks and close the selector.
|
160
|
+
#
|
170
161
|
# @return [void]
|
171
162
|
def close
|
172
163
|
@children.each(&:stop)
|
173
164
|
|
165
|
+
# TODO Should we also clear all timers?
|
166
|
+
|
174
167
|
@selector.close
|
175
168
|
@selector = nil
|
176
169
|
end
|
data/lib/async/task.rb
CHANGED
@@ -53,34 +53,25 @@ module Async
|
|
53
53
|
return result
|
54
54
|
end
|
55
55
|
end
|
56
|
-
|
56
|
+
|
57
57
|
# Create a new task.
|
58
|
-
# @param
|
59
|
-
# @param
|
60
|
-
|
61
|
-
|
62
|
-
if parent = Task.current?
|
63
|
-
super(parent)
|
64
|
-
else
|
65
|
-
super(reactor)
|
66
|
-
end
|
67
|
-
|
68
|
-
@ios = Hash[
|
69
|
-
ios.collect{|io| [io.fileno, reactor.wrap(io, self)]}
|
70
|
-
]
|
58
|
+
# @param reactor [Async::Reactor] the reactor this task will run within.
|
59
|
+
# @param parent [Async::Task] the parent task.
|
60
|
+
def initialize(reactor, parent = Task.current?)
|
61
|
+
super(parent || reactor)
|
71
62
|
|
72
63
|
@reactor = reactor
|
73
64
|
|
74
|
-
@status = :
|
65
|
+
@status = :initialized
|
75
66
|
@result = nil
|
76
67
|
|
77
68
|
@condition = nil
|
78
69
|
|
79
|
-
@fiber = Fiber.new do
|
70
|
+
@fiber = Fiber.new do |args|
|
80
71
|
set!
|
81
72
|
|
82
73
|
begin
|
83
|
-
@result = yield(
|
74
|
+
@result = yield(self, *args)
|
84
75
|
@status = :complete
|
85
76
|
# Async.logger.debug("Task #{self} completed normally.")
|
86
77
|
rescue Interrupt
|
@@ -93,19 +84,14 @@ module Async
|
|
93
84
|
raise
|
94
85
|
ensure
|
95
86
|
# Async.logger.debug("Task #{self} closing: #{$!}")
|
96
|
-
|
87
|
+
finish!
|
97
88
|
end
|
98
89
|
end
|
99
90
|
end
|
100
91
|
|
101
|
-
# Show the current status of the task as a string.
|
102
|
-
# @todo (picat) Add test for this method?
|
103
92
|
def to_s
|
104
|
-
"
|
93
|
+
"<#{self.description} status=#{@status}>"
|
105
94
|
end
|
106
|
-
|
107
|
-
# @attr ios [Array<IO>] All wrappers associated with this task.
|
108
|
-
attr :ios
|
109
95
|
|
110
96
|
# @attr ios [Reactor] The reactor the task was created within.
|
111
97
|
attr :reactor
|
@@ -119,10 +105,23 @@ module Async
|
|
119
105
|
attr :status
|
120
106
|
|
121
107
|
# Resume the execution of the task.
|
122
|
-
def run
|
123
|
-
@
|
108
|
+
def run(*args)
|
109
|
+
if @status == :initialized
|
110
|
+
@status = :running
|
111
|
+
@fiber.resume(*args)
|
112
|
+
else
|
113
|
+
raise RuntimeError, "Task already running!"
|
114
|
+
end
|
124
115
|
end
|
125
|
-
|
116
|
+
|
117
|
+
def async(*args, &block)
|
118
|
+
task = Task.new(@reactor, self, &block)
|
119
|
+
|
120
|
+
task.run(*args)
|
121
|
+
|
122
|
+
return task
|
123
|
+
end
|
124
|
+
|
126
125
|
# Retrieve the current result of the task. Will cause the caller to wait until result is available.
|
127
126
|
# @raise [RuntimeError] if the task's fiber is the current fiber.
|
128
127
|
# @return [Object]
|
@@ -150,62 +149,35 @@ module Async
|
|
150
149
|
end
|
151
150
|
end
|
152
151
|
|
153
|
-
# Provide a wrapper to an IO object with a Reactor.
|
154
|
-
# @yield [Async::Wrapper] a wrapped object.
|
155
|
-
def with(io, *args)
|
156
|
-
wrapper = @reactor.wrap(io, self)
|
157
|
-
yield wrapper, *args
|
158
|
-
ensure
|
159
|
-
wrapper.close if wrapper
|
160
|
-
io.close if io
|
161
|
-
end
|
162
|
-
|
163
|
-
# Wrap and bind the given object to the reactor.
|
164
|
-
# @param io the native object to bind to this task.
|
165
|
-
# @return [Wrapper] The wrapped object.
|
166
|
-
def bind(io)
|
167
|
-
@ios[io.fileno] ||= @reactor.wrap(io, self)
|
168
|
-
end
|
169
|
-
|
170
|
-
# Register a given IO with given interests to be able to monitor it.
|
171
|
-
# @param io [IO] a native io object.
|
172
|
-
# @param interests [Symbol] One of `:r`, `:w` or `:rw`.
|
173
|
-
# @return [NIO::Monitor]
|
174
|
-
def register(io, interests)
|
175
|
-
@reactor.register(io, interests)
|
176
|
-
end
|
177
|
-
|
178
152
|
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
179
153
|
# @return [Async::Task]
|
180
154
|
# @raise [RuntimeError] if task was not {set!} for the current fiber.
|
181
155
|
def self.current
|
182
156
|
Thread.current[:async_task] or raise RuntimeError, "No async task available!"
|
183
157
|
end
|
184
|
-
|
185
|
-
|
158
|
+
|
186
159
|
# Check if there is a task defined for the current fiber.
|
187
160
|
# @return [Async::Task, nil]
|
188
161
|
def self.current?
|
189
162
|
Thread.current[:async_task]
|
190
163
|
end
|
191
|
-
|
164
|
+
|
192
165
|
# Check if the task is running.
|
193
166
|
# @return [Boolean]
|
194
167
|
def running?
|
195
168
|
@status == :running
|
196
169
|
end
|
197
|
-
|
170
|
+
|
198
171
|
# Whether we can remove this node from the reactor graph.
|
199
172
|
# @return [Boolean]
|
200
173
|
def finished?
|
201
174
|
super && @status != :running
|
202
175
|
end
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
# Finish the current task, and all bound bound IO objects.
|
180
|
+
def finish!
|
209
181
|
# Attempt to remove this node from the task tree.
|
210
182
|
consume
|
211
183
|
|
@@ -214,9 +186,7 @@ module Async
|
|
214
186
|
@condition.signal(@result)
|
215
187
|
end
|
216
188
|
end
|
217
|
-
|
218
|
-
private
|
219
|
-
|
189
|
+
|
220
190
|
# Set the current fiber's `:async_task` to this task.
|
221
191
|
def set!
|
222
192
|
# This is actually fiber-local:
|