async-io 1.20.0 → 1.21.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
  SHA256:
3
- metadata.gz: 69adfd1cd876361c3b94bc80a5f5c164d45df0156a37c6f9fc54542a79bc443c
4
- data.tar.gz: 32758166362e4780b7d251d4f649cdc2eb14fd4ab48a378be94968c5719053df
3
+ metadata.gz: ef50d94f2e881c86637e32d46f307a88b457537e9a649592d851e768474ffe2a
4
+ data.tar.gz: 24aac313f12b07478e080fa5713432b6e270be5d74e21688bb39efed2d4cf296
5
5
  SHA512:
6
- metadata.gz: 9fc9a5d6e563b8dbe9b2c700ce284ae905efb90a338b79ebed4daff5368912707355fe40efcbda243948e061bd0b143cd8e4bf22bf232dbc9afcf9d93aa5356b
7
- data.tar.gz: 94d7271c1c6e50a84c3120ae408989b2caeab045a8b07e54d417d4bc6159eea35138cc20cc86ba879d4492e457aee75eaa078ad23d2826c80c21fa12ed24d128
6
+ metadata.gz: '02688618b5035611cc4753acd581c0691c957ea0daefbdd4c357c52a4a31c12783b44dae77a5654aed36022abccac05a0813d300974dd94be26e97d5b25c47e9'
7
+ data.tar.gz: 811063ba4e91b823be5e8037fcf537bb40aa35bbbf7210df5fe21348b1394e579f27a7c0721114da9671c2ad108d0a70413040850d0643f5602cfc7ad5abc44f
@@ -0,0 +1,2 @@
1
+ --markup markdown
2
+
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'async'
4
+ require 'async/io'
5
+
6
+ endpoint = Async::IO::Endpoint.udp("localhost", 5678)
7
+
8
+ Async do |task|
9
+ endpoint.connect do |socket|
10
+ socket.send("Hello World")
11
+ pp socket.recv(1024)
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'async'
4
+ require 'async/io'
5
+
6
+ endpoint = Async::IO::Endpoint.udp("localhost", 5678)
7
+
8
+ Async do |task|
9
+ endpoint.bind do |socket|
10
+ while true
11
+ data, address = socket.recvfrom(1024)
12
+ socket.send(data.reverse, 0, address)
13
+ end
14
+ end
15
+ end
@@ -28,29 +28,62 @@ module Async
28
28
  # Endpoints represent a way of connecting or binding to an address.
29
29
  class Endpoint
30
30
  def initialize(**options)
31
- @options = options
31
+ @options = options.freeze
32
32
  end
33
33
 
34
- attr :options
34
+ def with(**options)
35
+ dup = self.dup
36
+
37
+ dup.options = @options.merge(options)
38
+
39
+ return dup
40
+ end
41
+
42
+ attr_accessor :options
35
43
 
44
+ # @return [String] The hostname of the bound socket.
36
45
  def hostname
37
46
  @options[:hostname]
38
47
  end
39
48
 
49
+ # If `SO_REUSEPORT` is enabled on a socket, the socket can be successfully bound even if there are existing sockets bound to the same address, as long as all prior bound sockets also had `SO_REUSEPORT` set before they were bound.
50
+ # @return [Boolean, nil] The value for `SO_REUSEPORT`.
40
51
  def reuse_port
41
52
  @options[:reuse_port]
42
53
  end
43
54
 
55
+ # If `SO_REUSEADDR` is enabled on a socket prior to binding it, the socket can be successfully bound unless there is a conflict with another socket bound to exactly the same combination of source address and port. Additionally, when set, binding a socket to the address of an existing socket in `TIME_WAIT` is not an error.
56
+ # @return [Boolean] The value for `SO_REUSEADDR`.
57
+ def reuse_address
58
+ @options[:reuse_address]
59
+ end
60
+
61
+ # Controls SO_LINGER. The amount of time the socket will stay in the `TIME_WAIT` state after being closed.
62
+ # @return [Integer, nil] The value for SO_LINGER.
63
+ def linger
64
+ @options[:linger]
65
+ end
66
+
67
+ # @return [Numeric] The default timeout for socket operations.
44
68
  def timeout
45
69
  @options[:timeout]
46
70
  end
47
71
 
72
+ # @return [Address] the address to bind to before connecting.
73
+ def local_address
74
+ @options[:local_address]
75
+ end
76
+
77
+ # Endpoints sometimes have multiple paths.
78
+ # @yield [Endpoint] Enumerate all discrete paths as endpoints.
48
79
  def each
49
80
  return to_enum unless block_given?
50
81
 
51
82
  yield self
52
83
  end
53
84
 
85
+ # Accept connections from the specified endpoint.
86
+ # @param backlog [Integer] the number of connections to listen for.
54
87
  def accept(backlog = Socket::SOMAXCONN, &block)
55
88
  bind do |server|
56
89
  server.listen(backlog)
@@ -59,10 +92,36 @@ module Async
59
92
  end
60
93
  end
61
94
 
95
+ # Map all endpoints by invoking `#bind`.
96
+ # @yield the bound wrapper.
97
+ def bound
98
+ wrappers = []
99
+
100
+ self.each do |endpoint|
101
+ wrapper = endpoint.bind
102
+ wrappers << wrapper
103
+
104
+ yield wrapper
105
+ end
106
+
107
+ return wrappers
108
+ ensure
109
+ wrappers.each(&:close) if $!
110
+ end
111
+
112
+ # Create an Endpoint instance by URI scheme. The host and port of the URI will be passed to the Endpoint factory method, along with any options.
113
+ #
114
+ # @param string [String] URI as string. Scheme will decide implementation used.
115
+ # @param options keyword arguments passed through to {#initialize}
116
+ #
117
+ # @see Endpoint.ssl ssl - invoked when parsing a URL with the ssl scheme "ssl://127.0.0.1"
118
+ # @see Endpoint.tcp tcp - invoked when parsing a URL with the tcp scheme: "tcp://127.0.0.1"
119
+ # @see Endpoint.udp udp - invoked when parsing a URL with the udp scheme: "udp://127.0.0.1"
120
+ # @see Endpoint.unix unix - invoked when parsing a URL with the unix scheme: "unix://127.0.0.1"
62
121
  def self.parse(string, **options)
63
122
  uri = URI.parse(string)
64
123
 
65
- self.send(uri.scheme, uri.host, uri.port, **options)
124
+ self.public_send(uri.scheme, uri.host, uri.port, **options)
66
125
  end
67
126
  end
68
127
  end
@@ -41,7 +41,7 @@ module Async
41
41
  end
42
42
  end
43
43
 
44
- # Generate a list of endpoint from an array.
44
+ # Generate a list of endpoints from an array.
45
45
  def self.each(specifications, &block)
46
46
  return to_enum(:each, specifications) unless block_given?
47
47
 
@@ -43,15 +43,24 @@ module Async
43
43
  # @yield [Socket] the socket which is being connected, may be invoked more than once
44
44
  # @return [Socket] the connected socket
45
45
  # @raise if no connection could complete successfully
46
- def connect(&block)
46
+ def connect
47
47
  last_error = nil
48
48
 
49
+ task = Task.current
50
+
49
51
  Addrinfo.foreach(*@specification) do |address|
50
52
  begin
51
- return Socket.connect(address, **@options, &block)
52
- # This is a little bit risky. In theory, what it is supposed to do is catch the failed connection, and try the next address. In practice, it can catch other kinds of failures. Ideally, it only applies to `#connect`, but it also applies to what's executed in `&block`.
53
- rescue
53
+ wrapper = Socket.connect(address, **@options, task: task)
54
+ rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EAGAIN
54
55
  last_error = $!
56
+ else
57
+ return wrapper unless block_given?
58
+
59
+ begin
60
+ return yield wrapper, task
61
+ ensure
62
+ wrapper.close
63
+ end
55
64
  end
56
65
  end
57
66
 
@@ -78,13 +87,20 @@ module Async
78
87
  end
79
88
 
80
89
  class Endpoint
81
- # args: nodename, service, family, socktype, protocol, flags
90
+ # @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM.
91
+ # @param options keyword arguments passed on to {HostEndpoint#initialize}
92
+ #
93
+ # @return [HostEndpoint]
82
94
  def self.tcp(*args, **options)
83
95
  args[3] = ::Socket::SOCK_STREAM
84
96
 
85
97
  HostEndpoint.new(args, **options)
86
98
  end
87
99
 
100
+ # @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM.
101
+ # @param options keyword arguments passed on to {HostEndpoint#initialize}
102
+ #
103
+ # @return [HostEndpoint]
88
104
  def self.udp(*args, **options)
89
105
  args[3] = ::Socket::SOCK_DGRAM
90
106
 
@@ -22,31 +22,27 @@ require_relative 'endpoint'
22
22
 
23
23
  module Async
24
24
  module IO
25
+ # Pre-connect and pre-bind sockets so that it can be used between processes.
25
26
  class SharedEndpoint < Endpoint
27
+ # Create a new `SharedEndpoint` by binding to the given endpoint.
26
28
  def self.bound(endpoint, backlog = Socket::SOMAXCONN)
27
- wrappers = []
28
-
29
- endpoint.each do |endpoint|
30
- server = endpoint.bind
31
-
29
+ wrappers = endpoint.bound do |server|
32
30
  server.listen(backlog)
33
-
34
31
  server.close_on_exec = false
35
32
  server.reactor = nil
36
-
37
- wrappers << server
38
33
  end
39
34
 
40
- self.new(endpoint, wrappers)
35
+ return self.new(endpoint, wrappers)
41
36
  end
42
37
 
38
+ # Create a new `SharedEndpoint` by connecting to the given endpoint.
43
39
  def self.connected(endpoint)
44
- peer = endpoint.connect
40
+ wrapper = endpoint.connect
45
41
 
46
- peer.close_on_exec = false
47
- peer.reactor = nil
42
+ wrapper.close_on_exec = false
43
+ wrapper.reactor = nil
48
44
 
49
- self.new(endpoint, [peer])
45
+ return self.new(endpoint, [wrapper])
50
46
  end
51
47
 
52
48
  def initialize(endpoint, wrappers, **options)
@@ -59,8 +55,10 @@ module Async
59
55
  attr :endpoint
60
56
  attr :wrappers
61
57
 
58
+ # Close all the internal wrappers.
62
59
  def close
63
60
  @wrappers.each(&:close)
61
+ @wrappers.clear
64
62
  end
65
63
 
66
64
  def bind
@@ -108,6 +108,7 @@ module Async
108
108
 
109
109
  include ::Socket::Constants
110
110
 
111
+ # @raise Errno::EAGAIN the connection failed due to the remote end being overloaded.
111
112
  def connect(*args)
112
113
  begin
113
114
  async_send(:connect_nonblock, *args)
@@ -142,9 +143,23 @@ module Async
142
143
  alias sysaccept accept
143
144
 
144
145
  # Build and wrap the underlying io.
145
- def self.build(*args, timeout: nil, task: Task.current)
146
+ # @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
147
+ # @option reuse_address [Boolean] Allow this port to be bound in multiple processes.
148
+ def self.build(*args, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil, task: Task.current)
146
149
  socket = wrapped_klass.new(*args)
147
150
 
151
+ if reuse_address
152
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
153
+ end
154
+
155
+ if reuse_port
156
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
157
+ end
158
+
159
+ if linger
160
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_LINGER, linger)
161
+ end
162
+
148
163
  yield socket
149
164
 
150
165
  wrapper = self.new(socket, task.reactor)
@@ -160,19 +175,14 @@ module Async
160
175
  # Establish a connection to a given `remote_address`.
161
176
  # @example
162
177
  # socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53))
163
- # @param remote_address [Addrinfo] The remote address to connect to.
164
- # @param local_address [Addrinfo] The local address to bind to before connecting.
165
- # @option protcol [Integer] The socket protocol to use.
166
- def self.connect(remote_address, local_address = nil, reuse_port: nil, task: Task.current, **options)
178
+ # @param remote_address [Address] The remote address to connect to.
179
+ # @option local_address [Address] The local address to bind to before connecting.
180
+ def self.connect(remote_address, local_address: nil, task: Task.current, **options)
167
181
  Async.logger.debug(self) {"Connecting to #{remote_address.inspect}"}
168
182
 
169
183
  task.annotate "connecting to #{remote_address.inspect}"
170
184
 
171
185
  wrapper = build(remote_address.afamily, remote_address.socktype, remote_address.protocol, **options) do |socket|
172
- if reuse_port
173
- socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
174
- end
175
-
176
186
  if local_address
177
187
  socket.bind(local_address.to_sockaddr)
178
188
  end
@@ -200,19 +210,10 @@ module Async
200
210
  # socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090))
201
211
  # @param local_address [Address] The local address to bind to.
202
212
  # @option protocol [Integer] The socket protocol to use.
203
- # @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
204
- def self.bind(local_address, protocol: 0, reuse_port: nil, reuse_address: true, task: Task.current, **options, &block)
213
+ def self.bind(local_address, protocol: 0, task: Task.current, **options, &block)
205
214
  Async.logger.debug(self) {"Binding to #{local_address.inspect}"}
206
215
 
207
216
  wrapper = build(local_address.afamily, local_address.socktype, protocol, **options) do |socket|
208
- if reuse_address
209
- socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, 1)
210
- end
211
-
212
- if reuse_port
213
- socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
214
- end
215
-
216
217
  socket.bind(local_address.to_sockaddr)
217
218
  end
218
219
 
@@ -90,7 +90,7 @@ module Async
90
90
  return to_enum unless block_given?
91
91
 
92
92
  @endpoint.each do |endpoint|
93
- yield self.class.new(endpoint, @options)
93
+ yield self.class.new(endpoint, **@options)
94
94
  end
95
95
  end
96
96
  end
@@ -99,6 +99,12 @@ module Async
99
99
  SecureEndpoint = SSLEndpoint
100
100
 
101
101
  class Endpoint
102
+ # @param args
103
+ # @param ssl_context [OpenSSL::SSL::SSLContext, nil]
104
+ # @param hostname [String, nil]
105
+ # @param options keyword arguments passed through to {Endpoint.tcp}
106
+ #
107
+ # @return [SSLEndpoint]
102
108
  def self.ssl(*args, ssl_context: nil, hostname: nil, **options)
103
109
  SSLEndpoint.new(self.tcp(*args, **options), ssl_context: ssl_context, hostname: hostname)
104
110
  end
@@ -22,8 +22,26 @@ require_relative 'generic'
22
22
 
23
23
  module Async
24
24
  module IO
25
- STDIN = Generic.new($stdin)
26
- STDOUT = Generic.new($stdout)
27
- STDERR = Generic.new($stderr)
25
+ class StandardInput < Generic
26
+ def initialize(io = $stdin)
27
+ super(io)
28
+ end
29
+ end
30
+
31
+ class StandardOutput < Generic
32
+ def initialize(io = $stdout)
33
+ super(io)
34
+ end
35
+ end
36
+
37
+ class StandardError < Generic
38
+ def initialize(io = $stderr)
39
+ super(io)
40
+ end
41
+ end
42
+
43
+ STDIN = StandardInput.new
44
+ STDOUT = StandardOutput.new
45
+ STDERR = StandardError.new
28
46
  end
29
47
  end
@@ -54,7 +54,7 @@ module Async
54
54
  end
55
55
  end
56
56
 
57
- def initialize(remote_host, remote_port = nil, local_host = nil, local_port = nil)
57
+ def initialize(remote_host, remote_port = nil, local_host = nil, local_port = 0)
58
58
  if remote_host.is_a? ::TCPSocket
59
59
  super(remote_host)
60
60
  else
@@ -62,7 +62,7 @@ module Async
62
62
  local_address = Addrinfo.tcp(local_host, local_port) if local_host
63
63
 
64
64
  # We do this unusual dance to avoid leaking an "open" socket instance.
65
- socket = Socket.connect(remote_address, local_address)
65
+ socket = Socket.connect(remote_address, local_address: local_address)
66
66
  fd = socket.fcntl(Fcntl::F_DUPFD)
67
67
  Async.logger.debug(self) {"Connected to #{remote_address.inspect}: #{fd}"}
68
68
  socket.close
@@ -59,6 +59,11 @@ module Async
59
59
  end
60
60
 
61
61
  class Endpoint
62
+ # @param path [String]
63
+ # @param type Socket type
64
+ # @param options keyword arguments passed through to {UNIXEndpoint#initialize}
65
+ #
66
+ # @return [UNIXEndpoint]
62
67
  def self.unix(path, type = ::Socket::SOCK_STREAM, **options)
63
68
  UNIXEndpoint.new(path, type, **options)
64
69
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module IO
23
- VERSION = "1.20.0"
23
+ VERSION = "1.21.0"
24
24
  end
25
25
  end
@@ -57,6 +57,11 @@ RSpec.describe Async::IO::Endpoint do
57
57
  expect(subject.hostname).to be == '0.0.0.0'
58
58
  end
59
59
 
60
+ it "has local address" do
61
+ address = Async::IO::Address.tcp('127.0.0.1', 8080)
62
+ expect(subject.with(local_address: address).local_address).to be == address
63
+ end
64
+
60
65
  let(:message) {"Hello World!"}
61
66
 
62
67
  it "can connect to bound server" do
@@ -31,7 +31,7 @@ end
31
31
  RSpec.describe Async::IO::STDOUT do
32
32
  include_context Async::RSpec::Reactor
33
33
 
34
- it "should be able to read" do
34
+ it "should be able to write" do
35
35
  expect(subject.write("")).to be == 0
36
36
  end
37
37
  end
@@ -39,7 +39,7 @@ end
39
39
  RSpec.describe Async::IO::STDERR do
40
40
  include_context Async::RSpec::Reactor
41
41
 
42
- it "should be able to read" do
42
+ it "should be able to write" do
43
43
  expect(subject.write("")).to be == 0
44
44
  end
45
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-io
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.20.0
4
+ version: 1.21.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: 2019-03-04 00:00:00.000000000 Z
11
+ date: 2019-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -119,6 +119,7 @@ files:
119
119
  - ".gitignore"
120
120
  - ".rspec"
121
121
  - ".travis.yml"
122
+ - ".yardopts"
122
123
  - Gemfile
123
124
  - README.md
124
125
  - Rakefile
@@ -131,6 +132,8 @@ files:
131
132
  - examples/issues/broken_ssl.rb
132
133
  - examples/millions/client.rb
133
134
  - examples/millions/server.rb
135
+ - examples/udp/client.rb
136
+ - examples/udp/server.rb
134
137
  - lib/async/io.rb
135
138
  - lib/async/io/address.rb
136
139
  - lib/async/io/address_endpoint.rb
@@ -201,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
204
  - !ruby/object:Gem::Version
202
205
  version: '0'
203
206
  requirements: []
204
- rubygems_version: 3.0.2
207
+ rubygems_version: 3.0.3
205
208
  signing_key:
206
209
  specification_version: 4
207
210
  summary: Provides support for asynchonous TCP, UDP, UNIX and SSL sockets.