async 0.12.0 → 0.13.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.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/Gemfile +1 -0
- data/Guardfile +4 -0
- data/lib/async/condition.rb +11 -2
- data/lib/async/io.rb +34 -18
- data/lib/async/logger.rb +6 -2
- data/lib/async/node.rb +21 -4
- data/lib/async/reactor.rb +46 -12
- data/lib/async/socket.rb +66 -8
- data/lib/async/task.rb +63 -18
- data/lib/async/tcp_socket.rb +4 -23
- data/lib/async/udp_socket.rb +6 -2
- data/lib/async/unix_socket.rb +7 -1
- data/lib/async/version.rb +1 -1
- data/lib/async/wrapper.rb +29 -15
- data/{lib/async/ssl_socket.rb → spec/async/condition_spec.rb} +18 -7
- data/spec/async/tcp_socket_spec.rb +48 -61
- data/spec/async/udp_socket_spec.rb +36 -21
- data/spec/async/unix_socket_spec.rb +53 -0
- data/spec/spec_helper.rb +25 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fbc8a7a4d1987be4082ebae4b613319a95e936f
|
4
|
+
data.tar.gz: a6eb19cd5a9485c3de75d4498e7f96b0f0755334
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f53412d28ed692ca27c059371ea97bca9a7424e6dc37ad3b97e910c8031d475388de304030a8004cce54712712c59915071bff1fd281892a1ed96fb7bedbe14
|
7
|
+
data.tar.gz: 6d2691f41bdd26610261269e8c15ef210696f15dba391119437758e604ac524fb0d65c871bdcc164babd6570b3c8fbea2784b4d3cd57ef9f4d6e6ced48552f67
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/lib/async/condition.rb
CHANGED
@@ -20,25 +20,34 @@
|
|
20
20
|
|
21
21
|
require 'fiber'
|
22
22
|
require 'forwardable'
|
23
|
-
|
24
23
|
require_relative 'node'
|
25
24
|
|
26
25
|
module Async
|
26
|
+
# A synchronization primative, which allows fibers to wait until a particular condition is triggered.
|
27
27
|
class Condition
|
28
28
|
def initialize
|
29
29
|
@waiting = []
|
30
30
|
end
|
31
31
|
|
32
|
+
# Queue up the current fiber and wait on yielding the task.
|
33
|
+
# @return [Object]
|
32
34
|
def wait
|
33
35
|
@waiting << Fiber.current
|
34
36
|
|
35
37
|
Task.yield
|
36
38
|
end
|
37
39
|
|
38
|
-
|
40
|
+
# Signal to a given task that it should resume operations.
|
41
|
+
# @param value The value to return to the waiting fibers.
|
42
|
+
# @see Task.yield which is responsible for handling value.
|
43
|
+
# @return [void]
|
44
|
+
def signal(value = nil)
|
45
|
+
# TODO: Should we hot-swap @waiting - so that tasks can wait on this condition again?
|
39
46
|
while task = @waiting.pop
|
40
47
|
task.resume(value)
|
41
48
|
end
|
49
|
+
|
50
|
+
return nil
|
42
51
|
end
|
43
52
|
end
|
44
53
|
end
|
data/lib/async/io.rb
CHANGED
@@ -29,27 +29,21 @@ module Async
|
|
29
29
|
|
30
30
|
WRAPPERS = {}
|
31
31
|
|
32
|
+
# Return the wrapper for a given native IO instance.
|
32
33
|
def self.[] instance
|
33
34
|
WRAPPERS[instance.class]
|
34
35
|
end
|
35
36
|
|
36
37
|
class << self
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
# @!macro [attach] wrap_blocking_method
|
39
|
+
# @method $1
|
40
|
+
# Invokes `$2` on the underlying {io}. If the operation would block, the current task is paused until the operation can succeed, at which point it's resumed and the operation is completed.
|
41
|
+
def wrap_blocking_method(new_name, method_name, &block)
|
42
|
+
if block_given?
|
43
|
+
define_method(new_name, &block)
|
44
|
+
else
|
40
45
|
define_method(new_name) do |*args|
|
41
|
-
|
42
|
-
@io.__send__(method_name, *args, exception: false)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
else
|
47
|
-
def wrap_blocking_method(new_name, method_name)
|
48
|
-
# puts "#{self}\##{$1} -> #{method_name}"
|
49
|
-
define_method(new_name) do |*args|
|
50
|
-
async do
|
51
|
-
@io.__send__(method_name, *args)
|
52
|
-
end
|
46
|
+
async_send(method_name, *args)
|
53
47
|
end
|
54
48
|
end
|
55
49
|
end
|
@@ -57,9 +51,9 @@ module Async
|
|
57
51
|
def wraps(klass, *additional_methods)
|
58
52
|
WRAPPERS[klass] = self
|
59
53
|
|
60
|
-
klass.instance_methods(false).grep(/(.*)_nonblock/) do |method_name|
|
61
|
-
|
62
|
-
end
|
54
|
+
# klass.instance_methods(false).grep(/(.*)_nonblock/) do |method_name|
|
55
|
+
# wrap_blocking_method($1, method_name)
|
56
|
+
# end
|
63
57
|
|
64
58
|
def_delegators :@io, *(additional_methods - instance_methods(false))
|
65
59
|
end
|
@@ -67,8 +61,30 @@ module Async
|
|
67
61
|
|
68
62
|
wraps ::IO
|
69
63
|
|
64
|
+
# @example
|
65
|
+
# data = io.read(512)
|
66
|
+
wrap_blocking_method :read, :read_nonblock
|
67
|
+
|
68
|
+
# @example
|
69
|
+
# io.write("Hello World")
|
70
|
+
wrap_blocking_method :write, :write_nonblock
|
71
|
+
|
70
72
|
protected
|
71
73
|
|
74
|
+
if RUBY_VERSION >= "2.3"
|
75
|
+
def async_send(*args)
|
76
|
+
async do
|
77
|
+
@io.__send__(*args, exception: false)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
else
|
81
|
+
def async_send(*args)
|
82
|
+
async do
|
83
|
+
@io.__send__(*args)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
72
88
|
def async
|
73
89
|
while true
|
74
90
|
begin
|
data/lib/async/logger.rb
CHANGED
@@ -21,9 +21,12 @@
|
|
21
21
|
require 'logger'
|
22
22
|
|
23
23
|
module Async
|
24
|
+
# The Async Logger class.
|
24
25
|
class << self
|
26
|
+
# @attr logger [Logger] the global logger instance used by `Async`.
|
25
27
|
attr :logger
|
26
|
-
|
28
|
+
|
29
|
+
# Set the default log level based on `$DEBUG` and `$VERBOSE`.
|
27
30
|
def default_log_level
|
28
31
|
if $DEBUG
|
29
32
|
Logger::DEBUG
|
@@ -34,6 +37,7 @@ module Async
|
|
34
37
|
end
|
35
38
|
end
|
36
39
|
end
|
37
|
-
|
40
|
+
|
41
|
+
# Create the logger instance.
|
38
42
|
@logger = Logger.new($stderr, level: default_log_level)
|
39
43
|
end
|
data/lib/async/node.rb
CHANGED
@@ -21,7 +21,10 @@
|
|
21
21
|
require 'set'
|
22
22
|
|
23
23
|
module Async
|
24
|
+
# Represents a node in a tree, used for nested {Task} instances.
|
24
25
|
class Node
|
26
|
+
# Create a new node in the tree.
|
27
|
+
# @param parent [Node, nil] This node will attach to the given parent.
|
25
28
|
def initialize(parent = nil)
|
26
29
|
@children = Set.new
|
27
30
|
@parent = nil
|
@@ -31,10 +34,15 @@ module Async
|
|
31
34
|
end
|
32
35
|
end
|
33
36
|
|
37
|
+
# @attr parent [Node, nil]
|
34
38
|
attr :parent
|
39
|
+
|
40
|
+
# @attr children [Set<Node>]
|
35
41
|
attr :children
|
36
42
|
|
37
|
-
#
|
43
|
+
# Change the parent of this node.
|
44
|
+
# @param parent [Node, nil] the parent to attach to, or nil to detach.
|
45
|
+
# @return [self]
|
38
46
|
def parent=(parent)
|
39
47
|
return if @parent.equal?(parent)
|
40
48
|
|
@@ -47,12 +55,19 @@ module Async
|
|
47
55
|
@parent = parent
|
48
56
|
@parent.children << self
|
49
57
|
end
|
58
|
+
|
59
|
+
return self
|
50
60
|
end
|
51
|
-
|
61
|
+
|
62
|
+
# Whether the node can be consumed safely. By default, checks if the
|
63
|
+
# children set is empty.
|
64
|
+
# @return [Boolean]
|
52
65
|
def finished?
|
53
66
|
@children.empty?
|
54
67
|
end
|
55
|
-
|
68
|
+
|
69
|
+
# If the node has a parent, and is {finished?}, then remove this node from
|
70
|
+
# the parent.
|
56
71
|
def consume
|
57
72
|
if @parent && finished?
|
58
73
|
@parent.reap(self)
|
@@ -60,7 +75,9 @@ module Async
|
|
60
75
|
@parent = nil
|
61
76
|
end
|
62
77
|
end
|
63
|
-
|
78
|
+
|
79
|
+
# Remove a given child node.
|
80
|
+
# @param child [Node]
|
64
81
|
def reap(child)
|
65
82
|
@children.delete(child)
|
66
83
|
end
|
data/lib/async/reactor.rb
CHANGED
@@ -27,12 +27,21 @@ require 'timers'
|
|
27
27
|
require 'forwardable'
|
28
28
|
|
29
29
|
module Async
|
30
|
+
# Raised if a timeout occurs on a specific Fiber. Handled gracefully by {Task}.
|
30
31
|
class TimeoutError < RuntimeError
|
31
32
|
end
|
32
|
-
|
33
|
+
|
34
|
+
# An asynchronous, cooperatively scheduled event reactor.
|
33
35
|
class Reactor < Node
|
34
36
|
extend Forwardable
|
35
37
|
|
38
|
+
# The preferred method to invoke asynchronous behavior.
|
39
|
+
#
|
40
|
+
# - When invoked within an existing reactor task, it will run the given block
|
41
|
+
# asynchronously. Will return the task once it has been scheduled.
|
42
|
+
# - When invoked at the top level, will create and run a reactor, and invoke
|
43
|
+
# the block as an asynchronous task. Will block until the reactor finishes
|
44
|
+
# running.
|
36
45
|
def self.run(*args, &block)
|
37
46
|
if current = Task.current?
|
38
47
|
reactor = current.reactor
|
@@ -50,7 +59,8 @@ module Async
|
|
50
59
|
return reactor
|
51
60
|
end
|
52
61
|
end
|
53
|
-
|
62
|
+
|
63
|
+
# @param wrappers [Hash] A mapping for wrapping pre-existing IO objects.
|
54
64
|
def initialize(wrappers: IO)
|
55
65
|
super(nil)
|
56
66
|
|
@@ -61,22 +71,29 @@ module Async
|
|
61
71
|
|
62
72
|
@stopped = true
|
63
73
|
end
|
64
|
-
|
74
|
+
|
75
|
+
# @attr wrappers [Object]
|
65
76
|
attr :wrappers
|
77
|
+
# @attr stopped [Boolean]
|
66
78
|
attr :stopped
|
67
79
|
|
68
80
|
def_delegators :@timers, :every, :after
|
69
|
-
|
81
|
+
|
82
|
+
# Wrap a given IO object and associate it with a specific task.
|
83
|
+
# @param io The `IO` instance to wrap.
|
84
|
+
# @param task [Task] The task which manages the wrapper.
|
85
|
+
# @return [Wrapper]
|
70
86
|
def wrap(io, task)
|
71
87
|
@wrappers[io].new(io, task)
|
72
88
|
end
|
73
|
-
|
89
|
+
|
74
90
|
def with(io, &block)
|
75
91
|
async do |task|
|
76
92
|
task.with(io, &block)
|
77
93
|
end
|
78
94
|
end
|
79
|
-
|
95
|
+
|
96
|
+
# @return [Task]
|
80
97
|
def async(*ios, &block)
|
81
98
|
task = Task.new(ios, self, &block)
|
82
99
|
|
@@ -96,11 +113,18 @@ module Async
|
|
96
113
|
def register(*args)
|
97
114
|
@selector.register(*args)
|
98
115
|
end
|
99
|
-
|
116
|
+
|
117
|
+
# Stop the reactor at the earliest convenience.
|
118
|
+
# @return [void]
|
100
119
|
def stop
|
101
|
-
@stopped
|
120
|
+
unless @stopped
|
121
|
+
@stopped = true
|
122
|
+
@selector.wakeup
|
123
|
+
end
|
102
124
|
end
|
103
|
-
|
125
|
+
|
126
|
+
# Run the reactor until either all tasks complete or {#stop} is invoked.
|
127
|
+
# Proxies arguments to {#async} immediately before entering the loop.
|
104
128
|
def run(*args, &block)
|
105
129
|
raise RuntimeError, 'Reactor has been closed' if @selector.nil?
|
106
130
|
|
@@ -129,7 +153,7 @@ module Async
|
|
129
153
|
monitors.each do |monitor|
|
130
154
|
if fiber = monitor.value
|
131
155
|
# Async.logger.debug "Resuming task #{task} due to IO..."
|
132
|
-
fiber.resume
|
156
|
+
fiber.resume # if fiber.alive?
|
133
157
|
end
|
134
158
|
end
|
135
159
|
end
|
@@ -141,7 +165,9 @@ module Async
|
|
141
165
|
Async.logger.debug{@children.collect{|child| [child.to_s, child.alive?]}.inspect}
|
142
166
|
@stopped = true
|
143
167
|
end
|
144
|
-
|
168
|
+
|
169
|
+
# Close each of the children tasts and selector.
|
170
|
+
# @return [void]
|
145
171
|
def close
|
146
172
|
@children.each(&:stop)
|
147
173
|
|
@@ -149,10 +175,14 @@ module Async
|
|
149
175
|
@selector = nil
|
150
176
|
end
|
151
177
|
|
178
|
+
# Check if the selector has been closed.
|
179
|
+
# @return [Boolean]
|
152
180
|
def closed?
|
153
181
|
@selector.nil?
|
154
182
|
end
|
155
|
-
|
183
|
+
|
184
|
+
# Put the calling fiber to sleep for a given ammount of time.
|
185
|
+
# @param duration [Numeric] The time in seconds, to sleep for.
|
156
186
|
def sleep(duration)
|
157
187
|
task = Fiber.current
|
158
188
|
|
@@ -167,6 +197,10 @@ module Async
|
|
167
197
|
timer.cancel if timer
|
168
198
|
end
|
169
199
|
|
200
|
+
# Invoke the block, but after the timeout, raise {TimeoutError} in any
|
201
|
+
# currenly blocking operation.
|
202
|
+
# @param duration [Integer] The time in seconds, in which the task should
|
203
|
+
# complete.
|
170
204
|
def timeout(duration)
|
171
205
|
backtrace = caller
|
172
206
|
task = Fiber.current
|
data/lib/async/socket.rb
CHANGED
@@ -26,23 +26,81 @@ module Async
|
|
26
26
|
class BasicSocket < IO
|
27
27
|
wraps ::BasicSocket
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
wrap_blocking_method :recv, :recv_nonblock
|
30
|
+
wrap_blocking_method :recvmsg, :recvmsg_nonblock
|
31
|
+
|
32
|
+
wrap_blocking_method :recvfrom, :recvfrom_nonblock
|
33
|
+
|
34
|
+
wrap_blocking_method :send, :sendmsg_nonblock
|
35
|
+
wrap_blocking_method :sendmsg, :sendmsg_nonblock
|
31
36
|
end
|
32
37
|
|
33
38
|
class Socket < BasicSocket
|
34
39
|
wraps ::Socket
|
35
40
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
+
wrap_blocking_method :accept, :accept_nonblock
|
42
|
+
|
43
|
+
wrap_blocking_method :connect, :connect_nonblock do |*args|
|
44
|
+
begin
|
45
|
+
async_send(:connect_nonblock, *args)
|
46
|
+
rescue Errno::EISCONN
|
47
|
+
# We are now connected.
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Establish a connection to a given `remote_address`.
|
52
|
+
# @example
|
53
|
+
# socket = Async::Socket.connect(Addrinfo.tcp("8.8.8.8", 53))
|
54
|
+
# @param remote_address [Addrinfo] The remote address to connect to.
|
55
|
+
# @param local_address [Addrinfo] The local address to bind to before connecting.
|
56
|
+
# @option protcol [Integer] The socket protocol to use.
|
57
|
+
def self.connect(remote_address, local_address = nil, protocol: 0, task: Task.current)
|
58
|
+
socket = ::Socket.new(remote_address.afamily, remote_address.socktype, protocol)
|
59
|
+
|
60
|
+
if local_address
|
61
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true)
|
62
|
+
socket.bind(local_address) if local_address
|
63
|
+
end
|
64
|
+
|
65
|
+
if block_given?
|
66
|
+
task.with(socket) do |wrapper|
|
67
|
+
wrapper.connect(remote_address.to_sockaddr)
|
68
|
+
|
69
|
+
yield wrapper
|
41
70
|
end
|
71
|
+
else
|
72
|
+
task.bind(socket).connect(remote_address.to_sockaddr)
|
73
|
+
|
74
|
+
return socket
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Bind to a local address.
|
79
|
+
# @example
|
80
|
+
# socket = Async::Socket.bind(Addrinfo.tcp("0.0.0.0", 9090))
|
81
|
+
# @param local_address [Addrinfo] The local address to bind to.
|
82
|
+
# @option protcol [Integer] The socket protocol to use.
|
83
|
+
def self.bind(local_address, backlog: nil, protocol: 0, task: Task.current, &block)
|
84
|
+
socket = ::Socket.new(local_address.afamily, local_address.socktype, protocol)
|
85
|
+
|
86
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true)
|
87
|
+
socket.bind(local_address)
|
88
|
+
|
89
|
+
socket.listen(backlog) if backlog
|
90
|
+
|
91
|
+
if block_given?
|
92
|
+
task.with(socket, &block)
|
93
|
+
else
|
94
|
+
return socket
|
42
95
|
end
|
43
96
|
end
|
44
97
|
|
45
|
-
|
98
|
+
# Bind to a local address and accept connections in a loop.
|
99
|
+
def self.accept(*args, task: Task.current, &block)
|
100
|
+
bind(*args, task: task) do |wrapper|
|
101
|
+
task.with(*wrapper.accept, &block) while true
|
102
|
+
end
|
103
|
+
end
|
46
104
|
end
|
47
105
|
|
48
106
|
class IPSocket < BasicSocket
|
data/lib/async/task.rb
CHANGED
@@ -25,12 +25,21 @@ require_relative 'node'
|
|
25
25
|
require_relative 'condition'
|
26
26
|
|
27
27
|
module Async
|
28
|
+
# Raised when a task is explicitly stopped.
|
28
29
|
class Interrupt < Exception
|
29
30
|
end
|
30
|
-
|
31
|
+
|
32
|
+
# A task represents the state associated with the execution of an asynchronous
|
33
|
+
# block.
|
31
34
|
class Task < Node
|
32
35
|
extend Forwardable
|
33
|
-
|
36
|
+
|
37
|
+
# Yield the unerlying `result` for the task. If the result
|
38
|
+
# is an Exception, then that result will be raised an its
|
39
|
+
# exception.
|
40
|
+
# @return [Object] result of the task
|
41
|
+
# @raise [Exception] if the result is an exception
|
42
|
+
# @yield [result] result of the task if a block if given.
|
34
43
|
def self.yield
|
35
44
|
if block_given?
|
36
45
|
result = yield
|
@@ -44,7 +53,11 @@ module Async
|
|
44
53
|
return result
|
45
54
|
end
|
46
55
|
end
|
47
|
-
|
56
|
+
|
57
|
+
# Create a new task.
|
58
|
+
# @param ios [Array] an array of `IO` objects such as `TCPServer`, `Socket`, etc.
|
59
|
+
# @param reactor [Async::Reactor]
|
60
|
+
# @return [void]
|
48
61
|
def initialize(ios, reactor)
|
49
62
|
if parent = Task.current?
|
50
63
|
super(parent)
|
@@ -85,25 +98,34 @@ module Async
|
|
85
98
|
end
|
86
99
|
end
|
87
100
|
|
101
|
+
# Show the current status of the task as a string.
|
102
|
+
# @todo (picat) Add test for this method?
|
88
103
|
def to_s
|
89
104
|
"#{super}[#{@status}]"
|
90
105
|
end
|
91
|
-
|
106
|
+
|
107
|
+
# @attr ios [Array<IO>] All wrappers associated with this task.
|
92
108
|
attr :ios
|
93
109
|
|
110
|
+
# @attr ios [Reactor] The reactor the task was created within.
|
94
111
|
attr :reactor
|
95
112
|
def_delegators :@reactor, :timeout, :sleep
|
96
113
|
|
114
|
+
# @attr fiber [Fiber] The fiber which is being used for the execution of this task.
|
97
115
|
attr :fiber
|
98
116
|
def_delegators :@fiber, :alive?
|
99
117
|
|
118
|
+
# @attr status [Symbol] The status of the execution of the fiber, one of `:running`, `:complete`, `:interrupted`, or `:failed`.
|
100
119
|
attr :status
|
101
|
-
attr :result
|
102
120
|
|
121
|
+
# Resume the execution of the task.
|
103
122
|
def run
|
104
123
|
@fiber.resume
|
105
124
|
end
|
106
|
-
|
125
|
+
|
126
|
+
# Retrieve the current result of the task. Will cause the caller to wait until result is available.
|
127
|
+
# @raise [RuntimeError] if the task's fiber is the current fiber.
|
128
|
+
# @return [Object]
|
107
129
|
def result
|
108
130
|
raise RuntimeError.new("Cannot wait on own fiber") if Fiber.current.equal?(@fiber)
|
109
131
|
|
@@ -116,7 +138,9 @@ module Async
|
|
116
138
|
end
|
117
139
|
|
118
140
|
alias wait result
|
119
|
-
|
141
|
+
|
142
|
+
# Stop the task and all of its children.
|
143
|
+
# @return [void]
|
120
144
|
def stop
|
121
145
|
@children.each(&:stop)
|
122
146
|
|
@@ -125,47 +149,67 @@ module Async
|
|
125
149
|
@fiber.resume(exception)
|
126
150
|
end
|
127
151
|
end
|
128
|
-
|
129
|
-
|
152
|
+
|
153
|
+
# Provide a wrapper to an IO object with a Reactor.
|
154
|
+
# @yield [Async::Wrapper] a wrapped object.
|
155
|
+
def with(io, *args)
|
130
156
|
wrapper = @reactor.wrap(io, self)
|
131
|
-
|
132
|
-
yield wrapper
|
157
|
+
yield wrapper, *args
|
133
158
|
ensure
|
134
|
-
wrapper.close
|
135
|
-
io.close
|
159
|
+
wrapper.close if wrapper
|
160
|
+
io.close if io
|
136
161
|
end
|
137
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.
|
138
166
|
def bind(io)
|
139
|
-
@ios[io.fileno] ||= reactor.wrap(io, self)
|
167
|
+
@ios[io.fileno] ||= @reactor.wrap(io, self)
|
140
168
|
end
|
141
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]
|
142
174
|
def register(io, interests)
|
143
175
|
@reactor.register(io, interests)
|
144
176
|
end
|
145
|
-
|
177
|
+
|
178
|
+
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
179
|
+
# @return [Async::Task]
|
180
|
+
# @raise [RuntimeError] if task was not {set!} for the current fiber.
|
146
181
|
def self.current
|
147
182
|
Thread.current[:async_task] or raise RuntimeError, "No async task available!"
|
148
183
|
end
|
149
|
-
|
184
|
+
|
185
|
+
|
186
|
+
# Check if there is a task defined for the current fiber.
|
187
|
+
# @return [Async::Task, nil]
|
150
188
|
def self.current?
|
151
189
|
Thread.current[:async_task]
|
152
190
|
end
|
153
|
-
|
191
|
+
|
192
|
+
# Check if the task is running.
|
193
|
+
# @return [Boolean]
|
154
194
|
def running?
|
155
195
|
@status == :running
|
156
196
|
end
|
157
197
|
|
158
198
|
# Whether we can remove this node from the reactor graph.
|
199
|
+
# @return [Boolean]
|
159
200
|
def finished?
|
160
201
|
super && @status != :running
|
161
202
|
end
|
162
203
|
|
204
|
+
# Close all bound IO objects.
|
163
205
|
def close
|
164
206
|
@ios.each_value(&:close)
|
165
|
-
@ios =
|
207
|
+
@ios = nil
|
166
208
|
|
209
|
+
# Attempt to remove this node from the task tree.
|
167
210
|
consume
|
168
211
|
|
212
|
+
# If this task was being used as a future, signal completion here:
|
169
213
|
if @condition
|
170
214
|
@condition.signal(@result)
|
171
215
|
end
|
@@ -173,6 +217,7 @@ module Async
|
|
173
217
|
|
174
218
|
private
|
175
219
|
|
220
|
+
# Set the current fiber's `:async_task` to this task.
|
176
221
|
def set!
|
177
222
|
# This is actually fiber-local:
|
178
223
|
Thread.current[:async_task] = self
|
data/lib/async/tcp_socket.rb
CHANGED
@@ -21,34 +21,15 @@
|
|
21
21
|
require_relative 'socket'
|
22
22
|
|
23
23
|
module Async
|
24
|
-
# Asynchronous TCP socket
|
24
|
+
# Asynchronous TCP socket wrapper.
|
25
25
|
class TCPSocket < IPSocket
|
26
26
|
wraps ::TCPSocket
|
27
|
-
|
28
|
-
def self.connect(remote_address, remote_port, local_address = nil, local_port = nil, task: Task.current, &block)
|
29
|
-
# This may block if remote_address is a hostname
|
30
|
-
remote = Addrinfo.tcp(remote_address, remote_port)
|
31
|
-
|
32
|
-
socket = ::Socket.new(remote.afamily, ::Socket::SOCK_STREAM, 0)
|
33
|
-
socket.bind Addrinfo.tcp(local_address, local_port) if local_address
|
34
|
-
|
35
|
-
if block_given?
|
36
|
-
task.with(socket) do |wrapper|
|
37
|
-
wrapper.connect(remote.to_sockaddr)
|
38
|
-
|
39
|
-
yield wrapper
|
40
|
-
end
|
41
|
-
else
|
42
|
-
task.bind(socket).connect(remote.to_sockaddr)
|
43
|
-
|
44
|
-
return socket
|
45
|
-
end
|
46
|
-
end
|
47
27
|
end
|
48
28
|
|
49
|
-
# Asynchronous TCP server
|
29
|
+
# Asynchronous TCP server wrappper.
|
50
30
|
class TCPServer < TCPSocket
|
51
31
|
wraps ::TCPServer
|
32
|
+
|
33
|
+
wrap_blocking_method :accept, :accept_nonblock
|
52
34
|
end
|
53
35
|
end
|
54
|
-
|
data/lib/async/udp_socket.rb
CHANGED
@@ -21,10 +21,14 @@
|
|
21
21
|
require_relative 'socket'
|
22
22
|
|
23
23
|
module Async
|
24
|
-
# Asynchronous UDP socket.
|
24
|
+
# Asynchronous UDP socket wrapper.
|
25
25
|
class UDPSocket < IPSocket
|
26
|
+
wraps ::UDPSocket
|
27
|
+
|
26
28
|
# We pass `send` through directly, but in theory it might block. Internally, it uses sendto.
|
27
|
-
|
29
|
+
def_delegators :@io, :send
|
30
|
+
|
31
|
+
wrap_blocking_method :recvfrom, :recvfrom_nonblock
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
data/lib/async/unix_socket.rb
CHANGED
@@ -21,7 +21,13 @@
|
|
21
21
|
require_relative 'socket'
|
22
22
|
|
23
23
|
module Async
|
24
|
-
|
24
|
+
class UNIXServer < BasicSocket
|
25
|
+
wraps ::UNIXServer
|
26
|
+
|
27
|
+
wrap_blocking_method :accept, :accept_nonblock
|
28
|
+
end
|
29
|
+
|
30
|
+
class UNIXSocket < BasicSocket
|
25
31
|
wraps ::UNIXSocket
|
26
32
|
end
|
27
33
|
end
|
data/lib/async/version.rb
CHANGED
data/lib/async/wrapper.rb
CHANGED
@@ -21,47 +21,61 @@
|
|
21
21
|
module Async
|
22
22
|
# Represents an asynchronous IO within a reactor.
|
23
23
|
class Wrapper
|
24
|
+
# @param io the native object to wrap.
|
25
|
+
# @param task [Task] the task that is managing this wrapper.
|
24
26
|
def initialize(io, task)
|
25
27
|
@io = io
|
26
28
|
@task = task
|
27
29
|
@monitor = nil
|
28
30
|
end
|
29
31
|
|
32
|
+
# The underlying native `io`.
|
30
33
|
attr :io
|
31
|
-
attr :task
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
@monitor = @task.register(@io, interests)
|
36
|
-
else
|
37
|
-
@monitor.interests = interests
|
38
|
-
end
|
39
|
-
|
40
|
-
@monitor.value = Fiber.current
|
41
|
-
|
42
|
-
yield
|
43
|
-
|
44
|
-
ensure
|
45
|
-
@monitor.value = nil if @monitor
|
46
|
-
end
|
35
|
+
# The task this wrapper is associated with.
|
36
|
+
attr :task
|
47
37
|
|
38
|
+
# Wait for the io to become readable.
|
48
39
|
def wait_readable
|
49
40
|
wait_any(:r)
|
50
41
|
end
|
51
42
|
|
43
|
+
# Wait for the io to become writable.
|
52
44
|
def wait_writable
|
53
45
|
wait_any(:w)
|
54
46
|
end
|
55
47
|
|
48
|
+
# Wait fo the io to become either readable or writable.
|
49
|
+
# @param interests [:r | :w | :rw] what events to wait for.
|
56
50
|
def wait_any(interests = :rw)
|
57
51
|
monitor(interests) do
|
58
52
|
Task.yield
|
59
53
|
end
|
60
54
|
end
|
61
55
|
|
56
|
+
# Close the monitor.
|
62
57
|
def close
|
63
58
|
@monitor.close if @monitor
|
64
59
|
@monitor = nil
|
65
60
|
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Monitor the io for the given events
|
65
|
+
# @param interests [:r | :w | :rw] what events to wait for.
|
66
|
+
def monitor(interests)
|
67
|
+
unless @monitor
|
68
|
+
@monitor = @task.register(@io, interests)
|
69
|
+
else
|
70
|
+
@monitor.interests = interests
|
71
|
+
end
|
72
|
+
|
73
|
+
@monitor.value = Fiber.current
|
74
|
+
|
75
|
+
yield
|
76
|
+
|
77
|
+
ensure
|
78
|
+
@monitor.value = nil if @monitor
|
79
|
+
end
|
66
80
|
end
|
67
81
|
end
|
@@ -18,13 +18,24 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
21
|
+
require 'async/condition'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
RSpec.describe Async::Condition do
|
24
|
+
include_context 'reactor'
|
25
|
+
|
26
|
+
it 'should continue after condition is signalled' do
|
27
|
+
task = reactor.async do
|
28
|
+
subject.wait
|
29
|
+
#puts "Got #{value}"
|
30
|
+
end
|
31
|
+
|
32
|
+
expect(task.status).to be :running
|
33
|
+
|
34
|
+
# This will cause the task to exit:
|
35
|
+
subject.signal
|
36
|
+
|
37
|
+
expect(task.status).to be :complete
|
38
|
+
|
39
|
+
task.stop
|
29
40
|
end
|
30
41
|
end
|
@@ -19,57 +19,47 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
RSpec.describe Async::Reactor do
|
22
|
+
include_context "closes all io"
|
23
|
+
|
22
24
|
# Shared port for localhost network tests.
|
23
|
-
let(:
|
25
|
+
let(:server_address) {Addrinfo.tcp("localhost", 6779)}
|
26
|
+
let(:data) {"The quick brown fox jumped over the lazy dog."}
|
24
27
|
|
25
|
-
|
28
|
+
around(:each) do |example|
|
26
29
|
# Accept a single incoming connection and then finish.
|
27
|
-
subject.async
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
subject.async do |task|
|
31
|
+
Async::Socket.bind(server_address, backlog: 128) do |server|
|
32
|
+
task.with(*server.accept) do |peer, address|
|
33
|
+
data = peer.read(512)
|
34
|
+
peer.write(data)
|
35
|
+
end
|
31
36
|
end
|
32
37
|
end
|
33
38
|
|
34
|
-
|
39
|
+
result = example.run
|
35
40
|
|
36
|
-
|
41
|
+
return result if result.is_a? Exception
|
37
42
|
|
38
|
-
|
43
|
+
subject.run
|
39
44
|
end
|
40
45
|
|
41
46
|
describe 'basic tcp server' do
|
42
|
-
# These may block:
|
43
|
-
let(:server) {TCPServer.new("localhost", port)}
|
44
|
-
let(:client) {TCPSocket.new("localhost", port)}
|
45
|
-
|
46
|
-
let(:data) {"The quick brown fox jumped over the lazy dog."}
|
47
|
-
|
48
47
|
it "should start server and send data" do
|
49
|
-
|
50
|
-
|
48
|
+
subject.async do
|
49
|
+
Async::Socket.connect(server_address) do |client|
|
51
50
|
client.write(data)
|
52
51
|
expect(client.read(512)).to be == data
|
53
52
|
end
|
54
53
|
end
|
55
|
-
|
56
|
-
expect(client).to be_closed
|
57
54
|
end
|
58
55
|
end
|
59
56
|
|
60
57
|
describe 'non-blocking tcp connect' do
|
61
|
-
# These may block:
|
62
|
-
let(:server) {TCPServer.new("localhost", port)}
|
63
|
-
|
64
|
-
let(:data) {"The quick brown fox jumped over the lazy dog."}
|
65
|
-
|
66
58
|
it "should start server and send data" do
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
expect(client.read(512)).to be == data
|
72
|
-
end
|
59
|
+
subject.async do |task|
|
60
|
+
Async::Socket.connect(server_address) do |client|
|
61
|
+
client.write(data)
|
62
|
+
expect(client.read(512)).to be == data
|
73
63
|
end
|
74
64
|
end
|
75
65
|
end
|
@@ -77,42 +67,39 @@ RSpec.describe Async::Reactor do
|
|
77
67
|
it "can connect socket and read/write in a different task" do
|
78
68
|
socket = nil
|
79
69
|
|
80
|
-
|
81
|
-
|
82
|
-
socket = Async::TCPSocket.connect("localhost", port)
|
83
|
-
|
84
|
-
# Stop the reactor once the connection was made.
|
85
|
-
subject.stop
|
86
|
-
end
|
87
|
-
|
88
|
-
subject.run
|
89
|
-
|
90
|
-
expect(socket).to_not be_nil
|
70
|
+
subject.async do |task|
|
71
|
+
socket = Async::Socket.connect(server_address)
|
91
72
|
|
92
|
-
|
93
|
-
|
94
|
-
expect(client.read(512)).to be == data
|
95
|
-
end
|
96
|
-
|
97
|
-
subject.run
|
73
|
+
# Stop the reactor once the connection was made.
|
74
|
+
subject.stop
|
98
75
|
end
|
76
|
+
|
77
|
+
subject.run
|
78
|
+
|
79
|
+
expect(socket).to_not be_nil
|
80
|
+
|
81
|
+
subject.async(socket) do |client|
|
82
|
+
client.write(data)
|
83
|
+
expect(client.read(512)).to be == data
|
84
|
+
end
|
85
|
+
|
86
|
+
subject.run
|
99
87
|
end
|
100
88
|
|
101
89
|
it "can't use a socket in nested tasks" do
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
90
|
+
subject.async do |task|
|
91
|
+
socket = Async::Socket.connect(server_address)
|
92
|
+
|
93
|
+
# I'm not sure if this is the right behaviour or not. Without a significant amont of work, async sockets are tied to the task that creates them.
|
94
|
+
expect do
|
95
|
+
subject.async(socket) do |client|
|
96
|
+
client.write(data)
|
97
|
+
expect(client.read(512)).to be == data
|
98
|
+
end
|
99
|
+
end.to raise_error(ArgumentError, /already registered with selector/)
|
100
|
+
|
101
|
+
# We need to explicitly close the socket, since we explicitly opened it.
|
102
|
+
socket.close
|
116
103
|
end
|
117
104
|
end
|
118
105
|
end
|
@@ -19,35 +19,50 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
RSpec.describe Async::Reactor do
|
22
|
+
include_context "closes all io"
|
23
|
+
|
22
24
|
# Shared port for localhost network tests.
|
23
|
-
let(:
|
25
|
+
let(:server_address) {Addrinfo.udp("127.0.0.1", 6778)}
|
26
|
+
let(:data) {"The quick brown fox jumped over the lazy dog."}
|
24
27
|
|
25
28
|
describe 'basic udp server' do
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
it "should echo data back to peer" do
|
30
|
+
subject.async do
|
31
|
+
Async::Socket.bind(server_address) do |server|
|
32
|
+
packet, address = server.recvfrom(512)
|
33
|
+
|
34
|
+
server.send(packet, 0, address)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
subject.async do
|
39
|
+
Async::Socket.connect(server_address) do |client|
|
40
|
+
client.send(data)
|
41
|
+
response = client.recv(512)
|
42
|
+
|
43
|
+
expect(response).to be == data
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
subject.run
|
34
48
|
end
|
35
49
|
|
36
|
-
it "should
|
37
|
-
subject.async
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
server.send(packet, 0,
|
50
|
+
it "should use unconnected socket" do
|
51
|
+
subject.async do
|
52
|
+
Async::Socket.bind(server_address) do |server|
|
53
|
+
packet, address = server.recvfrom(512)
|
54
|
+
|
55
|
+
server.send(packet, 0, address)
|
42
56
|
end
|
43
57
|
end
|
44
58
|
|
45
|
-
subject.async
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
subject.async do |task|
|
60
|
+
task.with(UDPSocket.new(server_address.afamily)) do |client|
|
61
|
+
client.send(data, 0, server_address)
|
62
|
+
response, address = client.recvfrom(512)
|
63
|
+
|
64
|
+
expect(response).to be == data
|
65
|
+
end
|
51
66
|
end
|
52
67
|
|
53
68
|
subject.run
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'async/unix_socket'
|
22
|
+
|
23
|
+
RSpec.describe Async::Reactor do
|
24
|
+
include_context "closes all io"
|
25
|
+
|
26
|
+
let(:path) {File.join(__dir__, "unix-socket")}
|
27
|
+
let(:data) {"The quick brown fox jumped over the lazy dog."}
|
28
|
+
|
29
|
+
before(:each) do
|
30
|
+
FileUtils.rm_f path
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'basic unix socket' do
|
34
|
+
it "should echo data back to peer" do
|
35
|
+
subject.async do |task|
|
36
|
+
task.with(UNIXServer.new(path)) do |server|
|
37
|
+
task.with(server.accept) do |peer|
|
38
|
+
peer.send(peer.recv(512))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
subject.with(UNIXSocket.new(path)) do |socket|
|
44
|
+
socket.send(data)
|
45
|
+
response = socket.recv(512)
|
46
|
+
|
47
|
+
expect(response).to be == data
|
48
|
+
end
|
49
|
+
|
50
|
+
subject.run
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -21,14 +21,38 @@ require "async"
|
|
21
21
|
require "async/tcp_socket"
|
22
22
|
require "async/udp_socket"
|
23
23
|
|
24
|
+
RSpec.shared_context "closes all io" do
|
25
|
+
def current_ios(gc: GC.start)
|
26
|
+
all_ios = ObjectSpace.each_object(IO).to_a.sort_by(&:object_id)
|
27
|
+
|
28
|
+
# We are not interested in ios that have been closed already:
|
29
|
+
return all_ios.reject{|io| io.closed?}
|
30
|
+
end
|
31
|
+
|
32
|
+
# We use around(:each) because it's the highest priority.
|
33
|
+
around(:each) do |example|
|
34
|
+
@system_ios = current_ios
|
35
|
+
|
36
|
+
result = example.run
|
37
|
+
|
38
|
+
expect(current_ios).to be == @system_ios
|
39
|
+
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
24
44
|
RSpec.shared_context "reactor" do
|
25
45
|
let(:reactor) {Async::Task.current.reactor}
|
26
46
|
|
27
47
|
around(:each) do |example|
|
28
48
|
Async::Reactor.run do
|
29
|
-
example.run
|
49
|
+
result = example.run
|
50
|
+
|
51
|
+
return result if result.is_a? Exception
|
30
52
|
end
|
31
53
|
end
|
54
|
+
|
55
|
+
include_context "closes all io"
|
32
56
|
end
|
33
57
|
|
34
58
|
RSpec.configure do |config|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-04-
|
11
|
+
date: 2017-04-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nio4r
|
@@ -107,6 +107,7 @@ files:
|
|
107
107
|
- ".gitignore"
|
108
108
|
- ".rspec"
|
109
109
|
- ".travis.yml"
|
110
|
+
- ".yardopts"
|
110
111
|
- Gemfile
|
111
112
|
- Guardfile
|
112
113
|
- README.md
|
@@ -121,19 +122,20 @@ files:
|
|
121
122
|
- lib/async/node.rb
|
122
123
|
- lib/async/reactor.rb
|
123
124
|
- lib/async/socket.rb
|
124
|
-
- lib/async/ssl_socket.rb
|
125
125
|
- lib/async/task.rb
|
126
126
|
- lib/async/tcp_socket.rb
|
127
127
|
- lib/async/udp_socket.rb
|
128
128
|
- lib/async/unix_socket.rb
|
129
129
|
- lib/async/version.rb
|
130
130
|
- lib/async/wrapper.rb
|
131
|
+
- spec/async/condition_spec.rb
|
131
132
|
- spec/async/node_spec.rb
|
132
133
|
- spec/async/reactor/nested_spec.rb
|
133
134
|
- spec/async/reactor_spec.rb
|
134
135
|
- spec/async/task_spec.rb
|
135
136
|
- spec/async/tcp_socket_spec.rb
|
136
137
|
- spec/async/udp_socket_spec.rb
|
138
|
+
- spec/async/unix_socket_spec.rb
|
137
139
|
- spec/spec_helper.rb
|
138
140
|
homepage: https://github.com/socketry/async
|
139
141
|
licenses:
|
@@ -160,10 +162,12 @@ signing_key:
|
|
160
162
|
specification_version: 4
|
161
163
|
summary: Async is an asynchronous I/O framework based on nio4r.
|
162
164
|
test_files:
|
165
|
+
- spec/async/condition_spec.rb
|
163
166
|
- spec/async/node_spec.rb
|
164
167
|
- spec/async/reactor/nested_spec.rb
|
165
168
|
- spec/async/reactor_spec.rb
|
166
169
|
- spec/async/task_spec.rb
|
167
170
|
- spec/async/tcp_socket_spec.rb
|
168
171
|
- spec/async/udp_socket_spec.rb
|
172
|
+
- spec/async/unix_socket_spec.rb
|
169
173
|
- spec/spec_helper.rb
|