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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e540efd83cb7c67cb1c2b1a07008b33d67af97ce
4
- data.tar.gz: e10155ef52e5fc2d37da808e654e58a1b9b56286
3
+ metadata.gz: 2fbc8a7a4d1987be4082ebae4b613319a95e936f
4
+ data.tar.gz: a6eb19cd5a9485c3de75d4498e7f96b0f0755334
5
5
  SHA512:
6
- metadata.gz: 7853ec9aa1967b5375b956f6d4e2ddaa6370f8c992afb6fdd73a8a1b792a17697d90b60fcd215c1e8a77928135c41226aeac16e0e94de242ef5e9f3c2560261c
7
- data.tar.gz: 6d49f738de6c79bc363ef541549e040a8e98b74049fc346aca922560d98787df64217b148d9291b541b5ccf5e80d6cb9d90ff04cbba5fe91a061d754d579ebe7
6
+ metadata.gz: 9f53412d28ed692ca27c059371ea97bca9a7424e6dc37ad3b97e910c8031d475388de304030a8004cce54712712c59915071bff1fd281892a1ed96fb7bedbe14
7
+ data.tar.gz: 6d2691f41bdd26610261269e8c15ef210696f15dba391119437758e604ac524fb0d65c871bdcc164babd6570b3c8fbea2784b4d3cd57ef9f4d6e6ced48552f67
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gemspec
6
6
  group :development do
7
7
  gem 'pry'
8
8
  gem 'guard-rspec'
9
+ gem 'guard-yard'
9
10
 
10
11
  gem 'yard'
11
12
  end
data/Guardfile CHANGED
@@ -8,3 +8,7 @@ guard :rspec, cmd: "bundle exec rspec" do
8
8
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
9
9
  watch("spec/spec_helper.rb") { "spec" }
10
10
  end
11
+
12
+ guard 'yard', :port => '8808' do
13
+ watch(%r{^lib/(.+)\.rb$})
14
+ end
@@ -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
- def signal(value)
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
- if RUBY_VERSION >= "2.3"
38
- def wrap_blocking_method(new_name, method_name)
39
- # puts "#{self}\##{$1} -> #{method_name}"
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
- async do
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
- wrap_blocking_method($1, method_name)
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
- # Attach this node to an existing parent.
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 = true
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
- # We provide non-blocking send:
30
- alias send sendmsg
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
- module Connect
37
- def connect(*args)
38
- begin
39
- super
40
- rescue Errno::EISCONN
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
- prepend Connect
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
- def with(io)
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
@@ -21,34 +21,15 @@
21
21
  require_relative 'socket'
22
22
 
23
23
  module Async
24
- # Asynchronous TCP socket/client.
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
-
@@ -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
- wraps ::UDPSocket, :send
29
+ def_delegators :@io, :send
30
+
31
+ wrap_blocking_method :recvfrom, :recvfrom_nonblock
28
32
  end
29
33
  end
30
34
 
@@ -21,7 +21,13 @@
21
21
  require_relative 'socket'
22
22
 
23
23
  module Async
24
- module UNIXSocket < BasicSocket
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
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Async
22
- VERSION = "0.12.0"
22
+ VERSION = "0.13.0"
23
23
  end
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
- def monitor(interests)
34
- unless @monitor
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
- require_relative 'io'
21
+ require 'async/condition'
22
22
 
23
- require 'openssl'
24
-
25
- module Async
26
- # This might be better in a nested module?
27
- class SSLSocket < IO
28
- wraps OpenSSL::SSL::SSLSocket
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(:port) {6779}
25
+ let(:server_address) {Addrinfo.tcp("localhost", 6779)}
26
+ let(:data) {"The quick brown fox jumped over the lazy dog."}
24
27
 
25
- def run_echo_server
28
+ around(:each) do |example|
26
29
  # Accept a single incoming connection and then finish.
27
- subject.async(server) do |server, task|
28
- task.with(server.accept) do |peer|
29
- data = peer.read(512)
30
- peer.write(data)
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
- yield
39
+ result = example.run
35
40
 
36
- subject.run
41
+ return result if result.is_a? Exception
37
42
 
38
- server.close
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
- run_echo_server do
50
- subject.with(client) do |client|
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
- run_echo_server do
68
- subject.async do |task|
69
- Async::TCPSocket.connect("localhost", port) do |client|
70
- client.write(data)
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
- run_echo_server do
81
- subject.async do |task|
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
- subject.async(socket) do |client|
93
- client.write(data)
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
- socket = nil
103
-
104
- run_echo_server do
105
- subject.async do |task|
106
- socket = Async::TCPSocket.connect("localhost", port)
107
-
108
- # 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.
109
- expect do
110
- subject.async(socket) do |client|
111
- client.write(data)
112
- expect(client.read(512)).to be == data
113
- end
114
- end.to raise_error(ArgumentError, /already registered with selector/)
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(:port) {6778}
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
- # These may block:
27
- let(:server) {UDPSocket.new.tap{|socket| socket.bind("localhost", port)}}
28
- let(:client) {UDPSocket.new}
29
-
30
- let(:data) {"The quick brown fox jumped over the lazy dog."}
31
-
32
- after(:each) do
33
- server.close
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 echo data back to peer" do
37
- subject.async(server) do |server, task|
38
- packet, (_, remote_port, remote_host) = server.recvfrom(512)
39
-
40
- subject.async do
41
- server.send(packet, 0, remote_host, remote_port)
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(client) do |client|
46
- client.send(data, 0, "localhost", port)
47
-
48
- response, _ = client.recvfrom(512)
49
-
50
- expect(response).to be == data
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.12.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-10 00:00:00.000000000 Z
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