async-http 0.49.1 → 0.50.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: 234d08278fa38ecca5449b3c94220d956f53c564291fe6a71e5b5152e0f5bace
4
- data.tar.gz: a60712a3e7483dc7bb60d220b439b6f749f15ca0ca70375d3aa10d1460849168
3
+ metadata.gz: 5db10f65827c5a336ba0d827f659f81411e87a65de7b2cf0ef512ce52bf4950e
4
+ data.tar.gz: 50461d80e6da099e33f71a4d6a476fa25609d99c69f401374509d6daf5f29116
5
5
  SHA512:
6
- metadata.gz: 63f4a410af4f4d6777f6c1c00ca96855c8d8c6057ae4ce43a2f277798f29bf1385dc2bf7d4a3d1eaa76bb16d260d93cf8da1b1679b83defd46b5c7fb91508817
7
- data.tar.gz: 136a88be9c5e3de646e7249a905627a1f9621d1218d3f4a36fc522aa31d60278b410262b5c7667dc03ac89dfce40ad7e53978f2d798860f9deef6ab0f74e8bd2
6
+ metadata.gz: d61522536ff249da6c1fbecbc6493d1f401cb005ce280c1a6ce278f107399a1a2fc43bce170123a52be21da4f88940f0e8783b949bda132609dbf41400c10628
7
+ data.tar.gz: fba11fe6d4b10036f288e120416efb85e6d7704ae556eeb1eb51f1d7eae7b2cb0ea3106677470d9f6024e5b9dfc0278ba54aab691f7982162f1c363bed0d1d24
@@ -13,17 +13,17 @@ addons:
13
13
 
14
14
  matrix:
15
15
  include:
16
- - rvm: 2.3
17
16
  - rvm: 2.4
18
17
  - rvm: 2.5
19
18
  - rvm: 2.6
19
+ - rvm: 2.7
20
20
  - rvm: 2.6
21
- env: COVERAGE=Summary,Coveralls
21
+ env: COVERAGE=PartialSummary,Coveralls
22
22
  - rvm: truffleruby
23
23
  - rvm: jruby-head
24
24
  env: JRUBY_OPTS="--debug -X+O"
25
25
  - rvm: ruby-head
26
- - rvm: 2.6
26
+ - rvm: 2.7
27
27
  os: osx
28
28
  allow_failures:
29
29
  - rvm: truffleruby
data/Gemfile CHANGED
@@ -9,8 +9,3 @@ gemspec
9
9
  # gem "protocol-http1", path: "../protocol-http1"
10
10
  # gem "protocol-http2", path: "../protocol-http2"
11
11
  # gem "protocol-hpack", path: "../protocol-hpack"
12
-
13
- group :development do
14
- gem 'pry'
15
- gem 'ruby-prof', '~> 0.18'
16
- end
data/README.md CHANGED
@@ -62,6 +62,72 @@ end
62
62
 
63
63
  Consider using [async-rest](https://github.com/socketry/async-rest) instead.
64
64
 
65
+ ### Multiple Requests
66
+
67
+ To issue multiple requests concurrently, you should use a barrier, e.g.
68
+
69
+ ```ruby
70
+ #!/usr/bin/env ruby
71
+
72
+ require 'async'
73
+ require 'async/barrier'
74
+ require 'async/http/internet'
75
+
76
+ TOPICS = ["ruby", "python", "rust"]
77
+
78
+ Async do
79
+ internet = Async::HTTP::Internet.new
80
+ barrier = Async::Barrier.new
81
+
82
+ # Spawn an asynchronous task for each topic:
83
+ TOPICS.each do |topic|
84
+ barrier.async do
85
+ response = internet.get "https://www.google.com/search?q=#{topic}"
86
+ puts "Found #{topic}: #{response.read.scan(topic).size} times."
87
+ end
88
+ end
89
+
90
+ # Ensure we wait for all requests to complete before continuing:
91
+ barrier.wait
92
+ ensure
93
+ internet&.close
94
+ end
95
+ ```
96
+
97
+ #### Limiting Requests
98
+
99
+ If you need to limit the number of simultaneous requests, use a semaphore.
100
+
101
+ ```ruby
102
+ #!/usr/bin/env ruby
103
+
104
+ require 'async'
105
+ require 'async/barrier'
106
+ require 'async/semaphore'
107
+ require 'async/http/internet'
108
+
109
+ TOPICS = ["ruby", "python", "rust"]
110
+
111
+ Async do
112
+ internet = Async::HTTP::Internet.new
113
+ barrier = Async::Barrier.new
114
+ semaphore = Async::Semaphore.new(2, parent: barrier)
115
+
116
+ # Spawn an asynchronous task for each topic:
117
+ TOPICS.each do |topic|
118
+ semaphore.async do
119
+ response = internet.get "https://www.google.com/search?q=#{topic}"
120
+ puts "Found #{topic}: #{response.read.scan(topic).size} times."
121
+ end
122
+ end
123
+
124
+ # Ensure we wait for all requests to complete before continuing:
125
+ barrier.wait
126
+ ensure
127
+ internet&.close
128
+ end
129
+ ```
130
+
65
131
  ### Downloading a File
66
132
 
67
133
  Here is an example showing how to download a file and save it to a local path:
data/Rakefile CHANGED
@@ -1,9 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
 
4
- RSpec::Core::RakeTask.new(:test)
4
+ RSpec::Core::RakeTask.new
5
5
 
6
- task :default => :test
6
+ task :default => :spec
7
7
 
8
8
  # Load all rake tasks:
9
9
  import(*Dir.glob('tasks/**/*.rake'))
@@ -4,6 +4,7 @@ require_relative 'lib/async/http/version'
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "async-http"
6
6
  spec.version = Async::HTTP::VERSION
7
+ spec.licenses = ["MIT"]
7
8
  spec.authors = ["Samuel Williams"]
8
9
  spec.email = ["samuel.williams@oriontransfer.co.nz"]
9
10
 
@@ -18,6 +19,7 @@ Gem::Specification.new do |spec|
18
19
 
19
20
  spec.add_dependency("async", "~> 1.23")
20
21
  spec.add_dependency("async-io", "~> 1.27.0")
22
+ spec.add_dependency("async-pool", "~> 0.2")
21
23
 
22
24
  spec.add_dependency("protocol-http", "~> 0.13.0")
23
25
  spec.add_dependency("protocol-http1", "~> 0.10.0")
@@ -23,6 +23,8 @@
23
23
  require 'async/io/endpoint'
24
24
  require 'async/io/stream'
25
25
 
26
+ require 'async/pool/controller'
27
+
26
28
  require 'protocol/http/body/streamable'
27
29
  require 'protocol/http/methods'
28
30
 
@@ -30,6 +32,9 @@ require_relative 'protocol'
30
32
 
31
33
  module Async
32
34
  module HTTP
35
+ DEFAULT_RETRIES = 3
36
+ DEFAULT_CONNECTION_LIMIT = nil
37
+
33
38
  class Client < ::Protocol::HTTP::Methods
34
39
  # Provides a robust interface to a server.
35
40
  # * If there are no connections, it will create one.
@@ -40,7 +45,7 @@ module Async
40
45
  # @param protocol [Protocol::HTTP1 | Protocol::HTTP2 | Protocol::HTTPS] the protocol to use.
41
46
  # @param scheme [String] The default scheme to set to requests.
42
47
  # @param authority [String] The default authority to set to requests.
43
- def initialize(endpoint, protocol = endpoint.protocol, scheme = endpoint.scheme, authority = endpoint.authority, retries: 3, connection_limit: nil)
48
+ def initialize(endpoint, protocol = endpoint.protocol, scheme = endpoint.scheme, authority = endpoint.authority, retries: DEFAULT_RETRIES, connection_limit: DEFAULT_CONNECTION_LIMIT)
44
49
  @endpoint = endpoint
45
50
  @protocol = protocol
46
51
 
@@ -129,7 +134,7 @@ module Async
129
134
  protected
130
135
 
131
136
  def make_pool(connection_limit)
132
- Pool.new(connection_limit) do
137
+ Async::Pool::Controller.wrap(limit: connection_limit) do
133
138
  Async.logger.debug(self) {"Making connection to #{@endpoint.inspect}"}
134
139
 
135
140
  @protocol.client(@endpoint.connect)
@@ -37,6 +37,17 @@ module Async
37
37
  return self.new(url, endpoint, **options)
38
38
  end
39
39
 
40
+ # Construct an endpoint with a specified scheme, hostname, and options.
41
+ def self.for(scheme, hostname, **options)
42
+ # TODO: Consider using URI.for once it becomes available:
43
+ uri_klass = URI.scheme_list[scheme.upcase] || URI::HTTP
44
+
45
+ self.new(
46
+ uri_klass.new(scheme, nil, hostname, nil, nil, nil, nil, nil, nil),
47
+ **options
48
+ )
49
+ end
50
+
40
51
  # @option scheme [String] the scheme to use, overrides the URL scheme.
41
52
  # @option hostname [String] the hostname to connect to (or bind to), overrides the URL hostname (used for SNI).
42
53
  # @option port [Integer] the port to bind to, overrides the URL port.
@@ -81,7 +92,7 @@ module Async
81
92
  end
82
93
 
83
94
  def secure?
84
- ['https', 'wss'].include?(@url.scheme)
95
+ ['https', 'wss'].include?(self.scheme)
85
96
  end
86
97
 
87
98
  def protocol
@@ -28,8 +28,9 @@ require 'protocol/http/body/buffered'
28
28
  module Async
29
29
  module HTTP
30
30
  class Internet
31
- def initialize
31
+ def initialize(**options)
32
32
  @clients = {}
33
+ @options = options
33
34
  end
34
35
 
35
36
  def call(method, url, headers = [], body = nil)
@@ -48,7 +49,7 @@ module Async
48
49
  end
49
50
 
50
51
  def client_for(endpoint)
51
- Client.new(endpoint)
52
+ Client.new(endpoint, **@options)
52
53
  end
53
54
 
54
55
  def close
@@ -35,10 +35,10 @@ module Async
35
35
 
36
36
  # A connection must implement the following interface:
37
37
  # class Connection
38
- # def multiplex -> can invoke call 1 or more times simultaneously.
38
+ # def concurrency -> can invoke call 1 or more times simultaneously.
39
39
  # def reusable? -> can be used again/persistent connection.
40
40
 
41
- # def connected? -> Boolean
41
+ # def viable? -> Boolean
42
42
 
43
43
  # def call(request) -> Response
44
44
  # def each -> (yield(request) -> Response)
@@ -62,12 +62,12 @@ module Async
62
62
 
63
63
  attr :count
64
64
 
65
- def multiplex
65
+ def concurrency
66
66
  1
67
67
  end
68
68
 
69
69
  # Can we use this connection to make requests?
70
- def connected?
70
+ def viable?
71
71
  @stream&.connected?
72
72
  end
73
73
 
@@ -113,12 +113,12 @@ module Async
113
113
 
114
114
  attr :count
115
115
 
116
- def multiplex
116
+ def concurrency
117
117
  self.maximum_concurrent_streams
118
118
  end
119
119
 
120
120
  # Can we use this connection to make requests?
121
- def connected?
121
+ def viable?
122
122
  @stream.connected?
123
123
  end
124
124
 
@@ -25,8 +25,6 @@ require_relative 'http11'
25
25
 
26
26
  require_relative 'http2'
27
27
 
28
- require_relative '../pool'
29
-
30
28
  require 'openssl'
31
29
 
32
30
  unless OpenSSL::SSL::SSLContext.instance_methods.include? :alpn_protocols=
@@ -66,7 +64,7 @@ module Async
66
64
  if protocol = HANDLERS[name]
67
65
  return protocol
68
66
  else
69
- throw ArgumentError.new("Could not determine protocol for connection (#{name.inspect}).")
67
+ raise ArgumentError, "Could not determine protocol for connection (#{name.inspect})."
70
68
  end
71
69
  end
72
70
 
@@ -26,6 +26,9 @@ require 'protocol/http/middleware'
26
26
 
27
27
  module Async
28
28
  module HTTP
29
+ class TooManyRedirects < StandardError
30
+ end
31
+
29
32
  # A client wrapper which transparently handles both relative and absolute redirects to a given maximum number of hops.
30
33
  class RelativeLocation < ::Protocol::HTTP::Middleware
31
34
  DEFAULT_METHOD = GET
@@ -69,7 +72,7 @@ module Async
69
72
  end
70
73
  end
71
74
 
72
- raise ArgumentError, "Redirected #{hops} times, exceeded maximum!"
75
+ raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!"
73
76
  end
74
77
  end
75
78
  end
@@ -29,8 +29,8 @@ require 'protocol/http/middleware'
29
29
  module Async
30
30
  module HTTP
31
31
  class Server < ::Protocol::HTTP::Middleware
32
- def self.for(*args, &block)
33
- self.new(block, *args)
32
+ def self.for(*arguments, &block)
33
+ self.new(block, *arguments)
34
34
  end
35
35
 
36
36
  def initialize(app, endpoint, protocol = endpoint.protocol, scheme = endpoint.scheme)
@@ -22,6 +22,6 @@
22
22
 
23
23
  module Async
24
24
  module HTTP
25
- VERSION = "0.49.1"
25
+ VERSION = "0.50.0"
26
26
  end
27
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.49.1
4
+ version: 0.50.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-11-23 00:00:00.000000000 Z
11
+ date: 2019-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.27.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: async-pool
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.2'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: protocol-http
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -218,7 +232,6 @@ files:
218
232
  - lib/async/http/client.rb
219
233
  - lib/async/http/endpoint.rb
220
234
  - lib/async/http/internet.rb
221
- - lib/async/http/pool.rb
222
235
  - lib/async/http/protocol.rb
223
236
  - lib/async/http/protocol/http1.rb
224
237
  - lib/async/http/protocol/http1/client.rb
@@ -247,7 +260,8 @@ files:
247
260
  - tasks/h2spec.rake
248
261
  - tasks/server.rake
249
262
  homepage: https://github.com/socketry/async-http
250
- licenses: []
263
+ licenses:
264
+ - MIT
251
265
  metadata: {}
252
266
  post_install_message:
253
267
  rdoc_options: []
@@ -264,7 +278,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
264
278
  - !ruby/object:Gem::Version
265
279
  version: '0'
266
280
  requirements: []
267
- rubygems_version: 3.0.6
281
+ rubygems_version: 3.1.2
268
282
  signing_key:
269
283
  specification_version: 4
270
284
  summary: A HTTP client and server library.
@@ -1,187 +0,0 @@
1
- # frozen_string_literal: true
2
- #
3
- # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- # THE SOFTWARE.
22
-
23
- require 'async/logger'
24
- require 'async/notification'
25
-
26
- module Async
27
- module HTTP
28
- # Pool behaviours
29
- #
30
- # - Single request per connection (HTTP/1 without keep-alive)
31
- # - Multiple sequential requests per connection (HTTP1 with keep-alive)
32
- # - Multiplex requests per connection (HTTP2)
33
- #
34
- # In general we don't know the policy until connection is established.
35
- #
36
- # This pool doesn't impose a maximum number of open resources, but it WILL block if there are no available resources and trying to allocate another one fails.
37
- #
38
- # Resources must respond to
39
- # #multiplex -> 1 or more.
40
- # #reusable? -> can be used again.
41
- #
42
- class Pool
43
- def initialize(limit = nil, &block)
44
- @resources = {} # resource => count
45
- @available = Async::Notification.new
46
-
47
- @limit = limit
48
-
49
- @constructor = block
50
- @guard = Async::Semaphore.new(1)
51
- end
52
-
53
- # The number of allocated resources.
54
- def active
55
- @resources.count
56
- end
57
-
58
- # Whether there are resources which are currently in use.
59
- def busy?
60
- @resources.collect do |_, usage|
61
- return true if usage > 0
62
- end
63
-
64
- return false
65
- end
66
-
67
- # Wait until a pool resource has been freed.
68
- def wait
69
- @available.wait
70
- end
71
-
72
- # All allocated resources.
73
- attr :resources
74
-
75
- def empty?
76
- @resources.empty?
77
- end
78
-
79
- def acquire
80
- resource = wait_for_resource
81
-
82
- return resource unless block_given?
83
-
84
- begin
85
- yield resource
86
- ensure
87
- release(resource)
88
- end
89
- end
90
-
91
- # Make the resource resources and let waiting tasks know that there is something resources.
92
- def release(resource)
93
- # A resource that is not good should also not be reusable.
94
- if resource.reusable?
95
- reuse(resource)
96
- else
97
- retire(resource)
98
- end
99
- end
100
-
101
- def close
102
- @resources.each_key(&:close)
103
- @resources.clear
104
- end
105
-
106
- def to_s
107
- "\#<#{self.class} resources=#{availability_string} limit=#{@limit.inspect}>"
108
- end
109
-
110
- protected
111
-
112
- def availability_string
113
- @resources.collect do |resource,usage|
114
- "#{usage}/#{resource.multiplex}#{resource.connected? ? '' : '*'}/#{resource.count}"
115
- end.join(";")
116
- end
117
-
118
- def reuse(resource)
119
- Async.logger.debug(self) {"Reuse #{resource}"}
120
-
121
- @resources[resource] -= 1
122
-
123
- @available.signal
124
- end
125
-
126
- def retire(resource)
127
- Async.logger.debug(self) {"Retire #{resource}"}
128
-
129
- @resources.delete(resource)
130
-
131
- resource.close
132
-
133
- @available.signal
134
- end
135
-
136
- def wait_for_resource
137
- # If we fail to create a resource (below), we will end up waiting for one to become resources.
138
- until resource = available_resource
139
- @available.wait
140
- end
141
-
142
- Async.logger.debug(self) {"Wait for resource #{resource}"}
143
-
144
- if resource.multiplex
145
- @available.signal
146
- end
147
-
148
- return resource
149
- end
150
-
151
- def create
152
- # This might return nil, which means creating the resource failed.
153
- if resource = @constructor.call
154
- @resources[resource] = 1
155
- end
156
-
157
- return resource
158
- end
159
-
160
- def available_resource
161
- # TODO This is a linear search... not ideal, but simple for now.
162
- @resources.each do |resource, count|
163
- if count < resource.multiplex
164
- # We want to use this resource... but is it connected?
165
- if resource.connected?
166
- @resources[resource] += 1
167
-
168
- return resource
169
- else
170
- retire(resource)
171
- end
172
- end
173
- end
174
-
175
- @guard.acquire do
176
- if @limit.nil? or self.active < @limit
177
- Async.logger.debug(self) {"No resources resources, allocating new one..."}
178
-
179
- return create
180
- end
181
- end
182
-
183
- return nil
184
- end
185
- end
186
- end
187
- end