# frozen_string_literal: true## Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com># # Permission is hereby granted, free of charge, to any person obtaining a copy# of this software and associated documentation files (the "Software"), to deal# in the Software without restriction, including without limitation the rights# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell# copies of the Software, and to permit persons to whom the Software is# furnished to do so, subject to the following conditions:# # The above copyright notice and this permission notice shall be included in# all copies or substantial portions of the Software.# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN# THE SOFTWARE.require'async/io/endpoint'require'async/io/stream'require'async/pool/controller'require'protocol/http/body/streamable'require'protocol/http/methods'require_relative'protocol'moduleAsyncmoduleHTTPDEFAULT_RETRIES=3DEFAULT_CONNECTION_LIMIT=nilclassClient<::Protocol::HTTP::Methods# Provides a robust interface to a server.# * If there are no connections, it will create one.# * If there are already connections, it will reuse it.# * If a request fails, it will retry it up to N times if it was idempotent.# The client object will never become unusable. It internally manages persistent connections (or non-persistent connections if that's required).# @param endpoint [Endpoint] the endpoint to connnect to.# @param protocol [Protocol::HTTP1 | Protocol::HTTP2 | Protocol::HTTPS] the protocol to use.# @param scheme [String] The default scheme to set to requests.# @param authority [String] The default authority to set to requests.definitialize(endpoint,protocol=endpoint.protocol,scheme=endpoint.scheme,authority=endpoint.authority,retries: DEFAULT_RETRIES,connection_limit: DEFAULT_CONNECTION_LIMIT)@endpoint=endpoint@protocol=protocol@retries=retries@pool=make_pool(connection_limit)@scheme=scheme@authority=authorityendattr:endpointattr:protocolattr:retriesattr:poolattr:schemeattr:authoritydefsecure?@endpoint.secure?enddefself.open(*arguments,**options,&block)client=self.new(*arguments,**options)returnclientunlessblock_given?beginyieldclientensureclient.closeendenddefclosewhile@pool.busy?Async.logger.warn(self){"Waiting for pool to drain: #{@pool}"}@pool.waitend@pool.closeenddefcall(request)request.scheme||=self.schemerequest.authority||=self.authorityattempt=0# We may retry the request if it is possible to do so. https://tools.ietf.org/html/draft-nottingham-httpbis-retry-01 is a good guide for how retrying requests should work.beginattempt+=1# As we cache pool, it's possible these pool go bad (e.g. closed by remote host). In this case, we need to try again. It's up to the caller to impose a timeout on this. If this is the last attempt, we force a new connection.connection=@pool.acquireresponse=make_response(request,connection)# This signals that the ensure block below should not try to release the connection, because it's bound into the response which will be returned:connection=nilreturnresponserescueProtocol::RequestFailed# This is a specific case where the entire request wasn't sent before a failure occurred. So, we can even resend non-idempotent requests.ifconnection@pool.release(connection)connection=nilendifattempt<@retriesretryelseraiseendrescueErrno::ECONNRESET,Errno::EPIPE,IOErrorifconnection@pool.release(connection)connection=nilendifrequest.idempotent?andattempt<@retriesretryelseraiseendensure@pool.release(connection)ifconnectionendendprotecteddefmake_response(request,connection)response=request.call(connection)# The connection won't be released until the body is completely read/released.::Protocol::HTTP::Body::Streamable.wrap(response)do@pool.release(connection)endreturnresponseenddefmake_pool(connection_limit)Async::Pool::Controller.wrap(limit: connection_limit)doAsync.logger.debug(self){"Making connection to #{@endpoint.inspect}"}@protocol.client(@endpoint.connect)endendendendend