async 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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