async-http 0.49.1 → 0.50.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
  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