package fasthttp import ( "bufio" "bytes" "crypto/tls" "errors" "fmt" "io" "net" "strings" "sync" "sync/atomic" "time" ) // Do performs the given http request and fills the given http response. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // Client determines the server to be requested in the following order: // // - from RequestURI if it contains full url with scheme and host; // - from Host header otherwise. // // ErrNoFreeConns is returned if all DefaultMaxConnsPerHost connections // to the requested host are busy. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func Do(req *Request, resp *Response) error { return defaultClient.Do(req, resp) } // DoTimeout performs the given request and waits for response during // the given timeout duration. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Client determines the server to be requested in the following order: // // - from RequestURI if it contains full url with scheme and host; // - from Host header otherwise. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned during // the given timeout. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func DoTimeout(req *Request, resp *Response, timeout time.Duration) error { return defaultClient.DoTimeout(req, resp, timeout) } // DoDeadline performs the given request and waits for response until // the given deadline. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Client determines the server to be requested in the following order: // // - from RequestURI if it contains full url with scheme and host; // - from Host header otherwise. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned until // the given deadline. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func DoDeadline(req *Request, resp *Response, deadline time.Time) error { return defaultClient.DoDeadline(req, resp, deadline) } // Get appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. func Get(dst []byte, url string) (statusCode int, body []byte, err error) { return defaultClient.Get(dst, url) } // GetTimeout appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. // // ErrTimeout error is returned if url contents couldn't be fetched // during the given timeout. func GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error) { return defaultClient.GetTimeout(dst, url, timeout) } // GetDeadline appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. // // ErrTimeout error is returned if url contents couldn't be fetched // until the given deadline. func GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error) { return defaultClient.GetDeadline(dst, url, deadline) } // Post sends POST request to the given url with the given POST arguments. // // Response body is appended to dst, which is returned as body. // // New body buffer is allocated if dst is nil. // // Empty POST body is sent if postArgs is nil. func Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error) { return defaultClient.Post(dst, url, postArgs) } var defaultClient Client // Client implements http client. // // Copying Client by value is prohibited. Create new instance instead. // // It is safe calling Client methods from concurrently running goroutines. type Client struct { noCopy noCopy // Client name. Used in User-Agent request header. // // Default client name is used if not set. Name string // Callback for establishing new connections to hosts. // // Default Dial is used if not set. Dial DialFunc // Attempt to connect to both ipv4 and ipv6 addresses if set to true. // // This option is used only if default TCP dialer is used, // i.e. if Dial is blank. // // By default client connects only to ipv4 addresses, // since unfortunately ipv6 remains broken in many networks worldwide :) DialDualStack bool // TLS config for https connections. // // Default TLS config is used if not set. TLSConfig *tls.Config // Maximum number of connections per each host which may be established. // // DefaultMaxConnsPerHost is used if not set. MaxConnsPerHost int // Idle keep-alive connections are closed after this duration. // // By default idle connections are closed // after DefaultMaxIdleConnDuration. MaxIdleConnDuration time.Duration // Per-connection buffer size for responses' reading. // This also limits the maximum header size. // // Default buffer size is used if 0. ReadBufferSize int // Per-connection buffer size for requests' writing. // // Default buffer size is used if 0. WriteBufferSize int // Maximum duration for full response reading (including body). // // By default response read timeout is unlimited. ReadTimeout time.Duration // Maximum duration for full request writing (including body). // // By default request write timeout is unlimited. WriteTimeout time.Duration // Maximum response body size. // // The client returns ErrBodyTooLarge if this limit is greater than 0 // and response body is greater than the limit. // // By default response body size is unlimited. MaxResponseBodySize int // Header names are passed as-is without normalization // if this option is set. // // Disabled header names' normalization may be useful only for proxying // responses to other clients expecting case-sensitive // header names. See https://github.com/valyala/fasthttp/issues/57 // for details. // // By default request and response header names are normalized, i.e. // The first letter and the first letters following dashes // are uppercased, while all the other letters are lowercased. // Examples: // // * HOST -> Host // * content-type -> Content-Type // * cONTENT-lenGTH -> Content-Length DisableHeaderNamesNormalizing bool mLock sync.Mutex m map[string]*HostClient ms map[string]*HostClient } // Get appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. func (c *Client) Get(dst []byte, url string) (statusCode int, body []byte, err error) { return clientGetURL(dst, url, c) } // GetTimeout appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. // // ErrTimeout error is returned if url contents couldn't be fetched // during the given timeout. func (c *Client) GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error) { return clientGetURLTimeout(dst, url, timeout, c) } // GetDeadline appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. // // ErrTimeout error is returned if url contents couldn't be fetched // until the given deadline. func (c *Client) GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error) { return clientGetURLDeadline(dst, url, deadline, c) } // Post sends POST request to the given url with the given POST arguments. // // Response body is appended to dst, which is returned as body. // // New body buffer is allocated if dst is nil. // // Empty POST body is sent if postArgs is nil. func (c *Client) Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error) { return clientPostURL(dst, url, postArgs, c) } // DoTimeout performs the given request and waits for response during // the given timeout duration. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Client determines the server to be requested in the following order: // // - from RequestURI if it contains full url with scheme and host; // - from Host header otherwise. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned during // the given timeout. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *Client) DoTimeout(req *Request, resp *Response, timeout time.Duration) error { return clientDoTimeout(req, resp, timeout, c) } // DoDeadline performs the given request and waits for response until // the given deadline. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Client determines the server to be requested in the following order: // // - from RequestURI if it contains full url with scheme and host; // - from Host header otherwise. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned until // the given deadline. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *Client) DoDeadline(req *Request, resp *Response, deadline time.Time) error { return clientDoDeadline(req, resp, deadline, c) } // Do performs the given http request and fills the given http response. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // Client determines the server to be requested in the following order: // // - from RequestURI if it contains full url with scheme and host; // - from Host header otherwise. // // ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections // to the requested host are busy. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *Client) Do(req *Request, resp *Response) error { uri := req.URI() host := uri.Host() isTLS := false scheme := uri.Scheme() if bytes.Equal(scheme, strHTTPS) { isTLS = true } else if !bytes.Equal(scheme, strHTTP) { return fmt.Errorf("unsupported protocol %q. http and https are supported", scheme) } startCleaner := false c.mLock.Lock() m := c.m if isTLS { m = c.ms } if m == nil { m = make(map[string]*HostClient) if isTLS { c.ms = m } else { c.m = m } } hc := m[string(host)] if hc == nil { hc = &HostClient{ Addr: addMissingPort(string(host), isTLS), Name: c.Name, Dial: c.Dial, DialDualStack: c.DialDualStack, IsTLS: isTLS, TLSConfig: c.TLSConfig, MaxConns: c.MaxConnsPerHost, MaxIdleConnDuration: c.MaxIdleConnDuration, ReadBufferSize: c.ReadBufferSize, WriteBufferSize: c.WriteBufferSize, ReadTimeout: c.ReadTimeout, WriteTimeout: c.WriteTimeout, MaxResponseBodySize: c.MaxResponseBodySize, DisableHeaderNamesNormalizing: c.DisableHeaderNamesNormalizing, } m[string(host)] = hc if len(m) == 1 { startCleaner = true } } c.mLock.Unlock() if startCleaner { go c.mCleaner(m) } return hc.Do(req, resp) } func (c *Client) mCleaner(m map[string]*HostClient) { mustStop := false for { t := time.Now() c.mLock.Lock() for k, v := range m { if t.Sub(v.LastUseTime()) > time.Minute { delete(m, k) } } if len(m) == 0 { mustStop = true } c.mLock.Unlock() if mustStop { break } time.Sleep(10 * time.Second) } } // DefaultMaxConnsPerHost is the maximum number of concurrent connections // http client may establish per host by default (i.e. if // Client.MaxConnsPerHost isn't set). const DefaultMaxConnsPerHost = 512 // DefaultMaxIdleConnDuration is the default duration before idle keep-alive // connection is closed. const DefaultMaxIdleConnDuration = 10 * time.Second // DialFunc must establish connection to addr. // // There is no need in establishing TLS (SSL) connection for https. // The client automatically converts connection to TLS // if HostClient.IsTLS is set. // // TCP address passed to DialFunc always contains host and port. // Example TCP addr values: // // - foobar.com:80 // - foobar.com:443 // - foobar.com:8080 type DialFunc func(addr string) (net.Conn, error) // HostClient balances http requests among hosts listed in Addr. // // HostClient may be used for balancing load among multiple upstream hosts. // // It is forbidden copying HostClient instances. Create new instances instead. // // It is safe calling HostClient methods from concurrently running goroutines. type HostClient struct { noCopy noCopy // Comma-separated list of upstream HTTP server host addresses, // which are passed to Dial in round-robin manner. // // Each address may contain port if default dialer is used. // For example, // // - foobar.com:80 // - foobar.com:443 // - foobar.com:8080 Addr string // Client name. Used in User-Agent request header. Name string // Callback for establishing new connection to the host. // // Default Dial is used if not set. Dial DialFunc // Attempt to connect to both ipv4 and ipv6 host addresses // if set to true. // // This option is used only if default TCP dialer is used, // i.e. if Dial is blank. // // By default client connects only to ipv4 addresses, // since unfortunately ipv6 remains broken in many networks worldwide :) DialDualStack bool // Whether to use TLS (aka SSL or HTTPS) for host connections. IsTLS bool // Optional TLS config. TLSConfig *tls.Config // Maximum number of connections which may be established to all hosts // listed in Addr. // // DefaultMaxConnsPerHost is used if not set. MaxConns int // Keep-alive connections are closed after this duration. // // By default connection duration is unlimited. MaxConnDuration time.Duration // Idle keep-alive connections are closed after this duration. // // By default idle connections are closed // after DefaultMaxIdleConnDuration. MaxIdleConnDuration time.Duration // Per-connection buffer size for responses' reading. // This also limits the maximum header size. // // Default buffer size is used if 0. ReadBufferSize int // Per-connection buffer size for requests' writing. // // Default buffer size is used if 0. WriteBufferSize int // Maximum duration for full response reading (including body). // // By default response read timeout is unlimited. ReadTimeout time.Duration // Maximum duration for full request writing (including body). // // By default request write timeout is unlimited. WriteTimeout time.Duration // Maximum response body size. // // The client returns ErrBodyTooLarge if this limit is greater than 0 // and response body is greater than the limit. // // By default response body size is unlimited. MaxResponseBodySize int // Header names are passed as-is without normalization // if this option is set. // // Disabled header names' normalization may be useful only for proxying // responses to other clients expecting case-sensitive // header names. See https://github.com/valyala/fasthttp/issues/57 // for details. // // By default request and response header names are normalized, i.e. // The first letter and the first letters following dashes // are uppercased, while all the other letters are lowercased. // Examples: // // * HOST -> Host // * content-type -> Content-Type // * cONTENT-lenGTH -> Content-Length DisableHeaderNamesNormalizing bool clientName atomic.Value lastUseTime uint32 connsLock sync.Mutex connsCount int conns []*clientConn addrsLock sync.Mutex addrs []string addrIdx uint32 readerPool sync.Pool writerPool sync.Pool } type clientConn struct { c net.Conn createdTime time.Time lastUseTime time.Time lastReadDeadlineTime time.Time lastWriteDeadlineTime time.Time } var startTimeUnix = time.Now().Unix() // LastUseTime returns time the client was last used func (c *HostClient) LastUseTime() time.Time { n := atomic.LoadUint32(&c.lastUseTime) return time.Unix(startTimeUnix+int64(n), 0) } // Get appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. func (c *HostClient) Get(dst []byte, url string) (statusCode int, body []byte, err error) { return clientGetURL(dst, url, c) } // GetTimeout appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. // // ErrTimeout error is returned if url contents couldn't be fetched // during the given timeout. func (c *HostClient) GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error) { return clientGetURLTimeout(dst, url, timeout, c) } // GetDeadline appends url contents to dst and returns it as body. // // New body buffer is allocated if dst is nil. // // ErrTimeout error is returned if url contents couldn't be fetched // until the given deadline. func (c *HostClient) GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error) { return clientGetURLDeadline(dst, url, deadline, c) } // Post sends POST request to the given url with the given POST arguments. // // Response body is appended to dst, which is returned as body. // // New body buffer is allocated if dst is nil. // // Empty POST body is sent if postArgs is nil. func (c *HostClient) Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error) { return clientPostURL(dst, url, postArgs, c) } type clientDoer interface { Do(req *Request, resp *Response) error } func clientGetURL(dst []byte, url string, c clientDoer) (statusCode int, body []byte, err error) { req := AcquireRequest() statusCode, body, err = doRequestFollowRedirects(req, dst, url, c) ReleaseRequest(req) return statusCode, body, err } func clientGetURLTimeout(dst []byte, url string, timeout time.Duration, c clientDoer) (statusCode int, body []byte, err error) { deadline := time.Now().Add(timeout) return clientGetURLDeadline(dst, url, deadline, c) } func clientGetURLDeadline(dst []byte, url string, deadline time.Time, c clientDoer) (statusCode int, body []byte, err error) { var sleepTime time.Duration for { statusCode, body, err = clientGetURLDeadlineFreeConn(dst, url, deadline, c) if err != ErrNoFreeConns { return statusCode, body, err } sleepTime = updateSleepTime(sleepTime, deadline) time.Sleep(sleepTime) } } type clientURLResponse struct { statusCode int body []byte err error } func clientGetURLDeadlineFreeConn(dst []byte, url string, deadline time.Time, c clientDoer) (statusCode int, body []byte, err error) { timeout := -time.Since(deadline) if timeout <= 0 { return 0, dst, ErrTimeout } var ch chan clientURLResponse chv := clientURLResponseChPool.Get() if chv == nil { chv = make(chan clientURLResponse, 1) } ch = chv.(chan clientURLResponse) req := AcquireRequest() // Note that the request continues execution on ErrTimeout until // client-specific ReadTimeout exceeds. This helps limiting load // on slow hosts by MaxConns* concurrent requests. // // Without this 'hack' the load on slow host could exceed MaxConns* // concurrent requests, since timed out requests on client side // usually continue execution on the host. go func() { statusCodeCopy, bodyCopy, errCopy := doRequestFollowRedirects(req, dst, url, c) ch <- clientURLResponse{ statusCode: statusCodeCopy, body: bodyCopy, err: errCopy, } }() tc := acquireTimer(timeout) select { case resp := <-ch: ReleaseRequest(req) clientURLResponseChPool.Put(chv) statusCode = resp.statusCode body = resp.body err = resp.err case <-tc.C: body = dst err = ErrTimeout } releaseTimer(tc) return statusCode, body, err } var clientURLResponseChPool sync.Pool func clientPostURL(dst []byte, url string, postArgs *Args, c clientDoer) (statusCode int, body []byte, err error) { req := AcquireRequest() req.Header.SetMethodBytes(strPost) req.Header.SetContentTypeBytes(strPostArgsContentType) if postArgs != nil { postArgs.WriteTo(req.BodyWriter()) } statusCode, body, err = doRequestFollowRedirects(req, dst, url, c) ReleaseRequest(req) return statusCode, body, err } var ( errMissingLocation = errors.New("missing Location header for http redirect") errTooManyRedirects = errors.New("too many redirects detected when doing the request") ) const maxRedirectsCount = 16 func doRequestFollowRedirects(req *Request, dst []byte, url string, c clientDoer) (statusCode int, body []byte, err error) { resp := AcquireResponse() bodyBuf := resp.bodyBuffer() resp.keepBodyBuffer = true oldBody := bodyBuf.B bodyBuf.B = dst redirectsCount := 0 for { req.parsedURI = false req.Header.host = req.Header.host[:0] req.SetRequestURI(url) if err = c.Do(req, resp); err != nil { break } statusCode = resp.Header.StatusCode() if statusCode != StatusMovedPermanently && statusCode != StatusFound && statusCode != StatusSeeOther { break } redirectsCount++ if redirectsCount > maxRedirectsCount { err = errTooManyRedirects break } location := resp.Header.peek(strLocation) if len(location) == 0 { err = errMissingLocation break } url = getRedirectURL(url, location) } body = bodyBuf.B bodyBuf.B = oldBody resp.keepBodyBuffer = false ReleaseResponse(resp) return statusCode, body, err } func getRedirectURL(baseURL string, location []byte) string { u := AcquireURI() u.Update(baseURL) u.UpdateBytes(location) redirectURL := u.String() ReleaseURI(u) return redirectURL } var ( requestPool sync.Pool responsePool sync.Pool ) // AcquireRequest returns an empty Request instance from request pool. // // The returned Request instance may be passed to ReleaseRequest when it is // no longer needed. This allows Request recycling, reduces GC pressure // and usually improves performance. func AcquireRequest() *Request { v := requestPool.Get() if v == nil { return &Request{} } return v.(*Request) } // ReleaseRequest returns req acquired via AcquireRequest to request pool. // // It is forbidden accessing req and/or its' members after returning // it to request pool. func ReleaseRequest(req *Request) { req.Reset() requestPool.Put(req) } // AcquireResponse returns an empty Response instance from response pool. // // The returned Response instance may be passed to ReleaseResponse when it is // no longer needed. This allows Response recycling, reduces GC pressure // and usually improves performance. func AcquireResponse() *Response { v := responsePool.Get() if v == nil { return &Response{} } return v.(*Response) } // ReleaseResponse return resp acquired via AcquireResponse to response pool. // // It is forbidden accessing resp and/or its' members after returning // it to response pool. func ReleaseResponse(resp *Response) { resp.Reset() responsePool.Put(resp) } // DoTimeout performs the given request and waits for response during // the given timeout duration. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned during // the given timeout. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *HostClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error { return clientDoTimeout(req, resp, timeout, c) } // DoDeadline performs the given request and waits for response until // the given deadline. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned until // the given deadline. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *HostClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error { return clientDoDeadline(req, resp, deadline, c) } func clientDoTimeout(req *Request, resp *Response, timeout time.Duration, c clientDoer) error { deadline := time.Now().Add(timeout) return clientDoDeadline(req, resp, deadline, c) } func clientDoDeadline(req *Request, resp *Response, deadline time.Time, c clientDoer) error { var sleepTime time.Duration for { err := clientDoDeadlineFreeConn(req, resp, deadline, c) if err != ErrNoFreeConns { return err } sleepTime = updateSleepTime(sleepTime, deadline) time.Sleep(sleepTime) } } var sleepJitter uint64 func updateSleepTime(prevTime time.Duration, deadline time.Time) time.Duration { sleepTime := prevTime * 2 if sleepTime == 0 { jitter := atomic.AddUint64(&sleepJitter, 1) % 40 sleepTime = (10 + time.Duration(jitter)) * time.Millisecond } remainingTime := deadline.Sub(time.Now()) if sleepTime >= remainingTime { // Just sleep for the remaining time and then time out. // This should save CPU time for real work by other goroutines. sleepTime = remainingTime + 10*time.Millisecond if sleepTime < 0 { sleepTime = 10 * time.Millisecond } } return sleepTime } func clientDoDeadlineFreeConn(req *Request, resp *Response, deadline time.Time, c clientDoer) error { timeout := -time.Since(deadline) if timeout <= 0 { return ErrTimeout } var ch chan error chv := errorChPool.Get() if chv == nil { chv = make(chan error, 1) } ch = chv.(chan error) // Make req and resp copies, since on timeout they no longer // may be accessed. reqCopy := AcquireRequest() req.copyToSkipBody(reqCopy) swapRequestBody(req, reqCopy) respCopy := AcquireResponse() // Note that the request continues execution on ErrTimeout until // client-specific ReadTimeout exceeds. This helps limiting load // on slow hosts by MaxConns* concurrent requests. // // Without this 'hack' the load on slow host could exceed MaxConns* // concurrent requests, since timed out requests on client side // usually continue execution on the host. go func() { ch <- c.Do(reqCopy, respCopy) }() tc := acquireTimer(timeout) var err error select { case err = <-ch: if resp != nil { respCopy.copyToSkipBody(resp) swapResponseBody(resp, respCopy) } ReleaseResponse(respCopy) ReleaseRequest(reqCopy) errorChPool.Put(chv) case <-tc.C: err = ErrTimeout } releaseTimer(tc) return err } var errorChPool sync.Pool // Do performs the given http request and sets the corresponding response. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // ErrNoFreeConns is returned if all HostClient.MaxConns connections // to the host are busy. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *HostClient) Do(req *Request, resp *Response) error { retry, err := c.do(req, resp) if err != nil && retry && isIdempotent(req) { _, err = c.do(req, resp) } if err == io.EOF { err = ErrConnectionClosed } return err } func isIdempotent(req *Request) bool { return req.Header.IsGet() || req.Header.IsHead() || req.Header.IsPut() } func (c *HostClient) do(req *Request, resp *Response) (bool, error) { nilResp := false if resp == nil { nilResp = true resp = AcquireResponse() } ok, err := c.doNonNilReqResp(req, resp) if nilResp { ReleaseResponse(resp) } return ok, err } func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) { if req == nil { panic("BUG: req cannot be nil") } if resp == nil { panic("BUG: resp cannot be nil") } atomic.StoreUint32(&c.lastUseTime, uint32(time.Now().Unix()-startTimeUnix)) // Free up resources occupied by response before sending the request, // so the GC may reclaim these resources (e.g. response body). resp.Reset() cc, err := c.acquireConn() if err != nil { return false, err } conn := cc.c if c.WriteTimeout > 0 { // Optimization: update write deadline only if more than 25% // of the last write deadline exceeded. // See https://github.com/golang/go/issues/15133 for details. currentTime := time.Now() if currentTime.Sub(cc.lastWriteDeadlineTime) > (c.WriteTimeout >> 2) { if err = conn.SetWriteDeadline(currentTime.Add(c.WriteTimeout)); err != nil { c.closeConn(cc) return true, err } cc.lastWriteDeadlineTime = currentTime } } resetConnection := false if c.MaxConnDuration > 0 && time.Since(cc.createdTime) > c.MaxConnDuration && !req.ConnectionClose() { req.SetConnectionClose() resetConnection = true } userAgentOld := req.Header.UserAgent() if len(userAgentOld) == 0 { req.Header.userAgent = c.getClientName() } bw := c.acquireWriter(conn) err = req.Write(bw) if len(userAgentOld) == 0 { req.Header.userAgent = userAgentOld } if resetConnection { req.Header.ResetConnectionClose() } if err == nil { err = bw.Flush() } if err != nil { c.releaseWriter(bw) c.closeConn(cc) return true, err } c.releaseWriter(bw) if c.ReadTimeout > 0 { // Optimization: update read deadline only if more than 25% // of the last read deadline exceeded. // See https://github.com/golang/go/issues/15133 for details. currentTime := time.Now() if currentTime.Sub(cc.lastReadDeadlineTime) > (c.ReadTimeout >> 2) { if err = conn.SetReadDeadline(currentTime.Add(c.ReadTimeout)); err != nil { c.closeConn(cc) return true, err } cc.lastReadDeadlineTime = currentTime } } if !req.Header.IsGet() && req.Header.IsHead() { resp.SkipBody = true } if c.DisableHeaderNamesNormalizing { resp.Header.DisableNormalizing() } br := c.acquireReader(conn) if err = resp.ReadLimitBody(br, c.MaxResponseBodySize); err != nil { c.releaseReader(br) c.closeConn(cc) if err == io.EOF { return true, err } return false, err } c.releaseReader(br) if resetConnection || req.ConnectionClose() || resp.ConnectionClose() { c.closeConn(cc) } else { c.releaseConn(cc) } return false, err } var ( // ErrNoFreeConns is returned when no free connections available // to the given host. ErrNoFreeConns = errors.New("no free connections available to host") // ErrTimeout is returned from timed out calls. ErrTimeout = errors.New("timeout") // ErrConnectionClosed may be returned from client methods if the server // closes connection before returning the first response byte. // // If you see this error, then either fix the server by returning // 'Connection: close' response header before closing the connection // or add 'Connection: close' request header before sending requests // to broken server. ErrConnectionClosed = errors.New("the server closed connection before returning the first response byte. " + "Make sure the server returns 'Connection: close' response header before closing the connection") ) func (c *HostClient) acquireConn() (*clientConn, error) { var cc *clientConn createConn := false startCleaner := false var n int c.connsLock.Lock() n = len(c.conns) if n == 0 { maxConns := c.MaxConns if maxConns <= 0 { maxConns = DefaultMaxConnsPerHost } if c.connsCount < maxConns { c.connsCount++ createConn = true } if createConn && c.connsCount == 1 { startCleaner = true } } else { n-- cc = c.conns[n] c.conns = c.conns[:n] } c.connsLock.Unlock() if cc != nil { return cc, nil } if !createConn { return nil, ErrNoFreeConns } conn, err := c.dialHostHard() if err != nil { c.decConnsCount() return nil, err } cc = acquireClientConn(conn) if startCleaner { go c.connsCleaner() } return cc, nil } func (c *HostClient) connsCleaner() { var ( scratch []*clientConn mustStop bool maxIdleConnDuration = c.MaxIdleConnDuration ) if maxIdleConnDuration <= 0 { maxIdleConnDuration = DefaultMaxIdleConnDuration } for { currentTime := time.Now() c.connsLock.Lock() conns := c.conns n := len(conns) i := 0 for i < n && currentTime.Sub(conns[i].lastUseTime) > maxIdleConnDuration { i++ } mustStop = (c.connsCount == i) scratch = append(scratch[:0], conns[:i]...) if i > 0 { m := copy(conns, conns[i:]) for i = m; i < n; i++ { conns[i] = nil } c.conns = conns[:m] } c.connsLock.Unlock() for i, cc := range scratch { c.closeConn(cc) scratch[i] = nil } if mustStop { break } time.Sleep(maxIdleConnDuration) } } func (c *HostClient) closeConn(cc *clientConn) { c.decConnsCount() cc.c.Close() releaseClientConn(cc) } func (c *HostClient) decConnsCount() { c.connsLock.Lock() c.connsCount-- c.connsLock.Unlock() } func acquireClientConn(conn net.Conn) *clientConn { v := clientConnPool.Get() if v == nil { v = &clientConn{} } cc := v.(*clientConn) cc.c = conn cc.createdTime = time.Now() return cc } func releaseClientConn(cc *clientConn) { cc.c = nil clientConnPool.Put(cc) } var clientConnPool sync.Pool func (c *HostClient) releaseConn(cc *clientConn) { cc.lastUseTime = time.Now() c.connsLock.Lock() c.conns = append(c.conns, cc) c.connsLock.Unlock() } func (c *HostClient) acquireWriter(conn net.Conn) *bufio.Writer { v := c.writerPool.Get() if v == nil { n := c.WriteBufferSize if n <= 0 { n = defaultWriteBufferSize } return bufio.NewWriterSize(conn, n) } bw := v.(*bufio.Writer) bw.Reset(conn) return bw } func (c *HostClient) releaseWriter(bw *bufio.Writer) { c.writerPool.Put(bw) } func (c *HostClient) acquireReader(conn net.Conn) *bufio.Reader { v := c.readerPool.Get() if v == nil { n := c.ReadBufferSize if n <= 0 { n = defaultReadBufferSize } return bufio.NewReaderSize(conn, n) } br := v.(*bufio.Reader) br.Reset(conn) return br } func (c *HostClient) releaseReader(br *bufio.Reader) { c.readerPool.Put(br) } func newDefaultTLSConfig() *tls.Config { return &tls.Config{ InsecureSkipVerify: true, ClientSessionCache: tls.NewLRUClientSessionCache(0), } } func (c *HostClient) nextAddr() string { c.addrsLock.Lock() if c.addrs == nil { c.addrs = strings.Split(c.Addr, ",") } addr := c.addrs[0] if len(c.addrs) > 1 { addr = c.addrs[c.addrIdx%uint32(len(c.addrs))] c.addrIdx++ } c.addrsLock.Unlock() return addr } func (c *HostClient) dialHostHard() (conn net.Conn, err error) { // attempt to dial all the available hosts before giving up. c.addrsLock.Lock() n := len(c.addrs) c.addrsLock.Unlock() if n == 0 { // It looks like c.addrs isn't initialized yet. n = 1 } timeout := c.ReadTimeout + c.WriteTimeout if timeout <= 0 { timeout = DefaultDialTimeout } deadline := time.Now().Add(timeout) for n > 0 { addr := c.nextAddr() conn, err = dialAddr(addr, c.Dial, c.DialDualStack, c.IsTLS, c.TLSConfig) if err == nil { return conn, nil } if time.Since(deadline) >= 0 { break } n-- } return nil, err } func dialAddr(addr string, dial DialFunc, dialDualStack, isTLS bool, tlsConfig *tls.Config) (net.Conn, error) { if dial == nil { if dialDualStack { dial = DialDualStack } else { dial = Dial } addr = addMissingPort(addr, isTLS) } conn, err := dial(addr) if err != nil { return nil, err } if conn == nil { panic("BUG: DialFunc returned (nil, nil)") } if isTLS { if tlsConfig == nil { tlsConfig = newDefaultTLSConfig() } conn = tls.Client(conn, tlsConfig) } return conn, nil } func (c *HostClient) getClientName() []byte { v := c.clientName.Load() var clientName []byte if v == nil { clientName = []byte(c.Name) if len(clientName) == 0 { clientName = defaultUserAgent } c.clientName.Store(clientName) } else { clientName = v.([]byte) } return clientName } func addMissingPort(addr string, isTLS bool) string { n := strings.Index(addr, ":") if n >= 0 { return addr } port := 80 if isTLS { port = 443 } return fmt.Sprintf("%s:%d", addr, port) } // PipelineClient pipelines requests over a single connection to the given Addr. // // This client may be used in highly loaded HTTP-based RPC systems for reducing // context switches and network level overhead. // See https://en.wikipedia.org/wiki/HTTP_pipelining for details. // // It is forbidden copying PipelineClient instances. Create new instances // instead. // // It is safe calling PipelineClient methods from concurrently running // goroutines. type PipelineClient struct { noCopy noCopy // Address of the host to connect to. Addr string // The maximum number of pending pipelined requests to the server. // // DefaultMaxPendingRequests is used by default. MaxPendingRequests int // The maximum delay before sending pipelined requests as a batch // to the server. // // By default requests are sent immediately to the server. MaxBatchDelay time.Duration // Callback for connection establishing to the host. // // Default Dial is used if not set. Dial DialFunc // Attempt to connect to both ipv4 and ipv6 host addresses // if set to true. // // This option is used only if default TCP dialer is used, // i.e. if Dial is blank. // // By default client connects only to ipv4 addresses, // since unfortunately ipv6 remains broken in many networks worldwide :) DialDualStack bool // Whether to use TLS (aka SSL or HTTPS) for host connections. IsTLS bool // Optional TLS config. TLSConfig *tls.Config // Idle connection to the host is closed after this duration. // // By default idle connection is closed after // DefaultMaxIdleConnDuration. MaxIdleConnDuration time.Duration // Buffer size for responses' reading. // This also limits the maximum header size. // // Default buffer size is used if 0. ReadBufferSize int // Buffer size for requests' writing. // // Default buffer size is used if 0. WriteBufferSize int // Maximum duration for full response reading (including body). // // By default response read timeout is unlimited. ReadTimeout time.Duration // Maximum duration for full request writing (including body). // // By default request write timeout is unlimited. WriteTimeout time.Duration // Logger for logging client errors. // // By default standard logger from log package is used. Logger Logger workPool sync.Pool chLock sync.Mutex chW chan *pipelineWork chR chan *pipelineWork } type pipelineWork struct { reqCopy Request respCopy Response req *Request resp *Response t *time.Timer deadline time.Time err error done chan struct{} } // DoTimeout performs the given request and waits for response during // the given timeout duration. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned during // the given timeout. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *PipelineClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error { return c.DoDeadline(req, resp, time.Now().Add(timeout)) } // DoDeadline performs the given request and waits for response until // the given deadline. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // ErrTimeout is returned if the response wasn't returned until // the given deadline. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *PipelineClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error { c.init() timeout := -time.Since(deadline) if timeout < 0 { return ErrTimeout } w := acquirePipelineWork(&c.workPool, timeout) w.req = &w.reqCopy w.resp = &w.respCopy // Make a copy of the request in order to avoid data races on timeouts req.copyToSkipBody(&w.reqCopy) swapRequestBody(req, &w.reqCopy) // Put the request to outgoing queue select { case c.chW <- w: // Fast path: len(c.ch) < cap(c.ch) default: // Slow path select { case c.chW <- w: case <-w.t.C: releasePipelineWork(&c.workPool, w) return ErrTimeout } } // Wait for the response var err error select { case <-w.done: if resp != nil { w.respCopy.copyToSkipBody(resp) swapResponseBody(resp, &w.respCopy) } err = w.err releasePipelineWork(&c.workPool, w) case <-w.t.C: err = ErrTimeout } return err } // Do performs the given http request and sets the corresponding response. // // Request must contain at least non-zero RequestURI with full url (including // scheme and host) or non-zero Host header + RequestURI. // // Response is ignored if resp is nil. // // ErrNoFreeConns is returned if all HostClient.MaxConns connections // to the host are busy. // // It is recommended obtaining req and resp via AcquireRequest // and AcquireResponse in performance-critical code. func (c *PipelineClient) Do(req *Request, resp *Response) error { c.init() w := acquirePipelineWork(&c.workPool, 0) w.req = req if resp != nil { w.resp = resp } else { w.resp = &w.respCopy } // Put the request to outgoing queue select { case c.chW <- w: default: // Try substituting the oldest w with the current one. select { case wOld := <-c.chW: wOld.err = ErrPipelineOverflow wOld.done <- struct{}{} default: } select { case c.chW <- w: default: releasePipelineWork(&c.workPool, w) return ErrPipelineOverflow } } // Wait for the response <-w.done err := w.err releasePipelineWork(&c.workPool, w) return err } // ErrPipelineOverflow may be returned from PipelineClient.Do // if the requests' queue is overflown. var ErrPipelineOverflow = errors.New("pipelined requests' queue has been overflown. Increase MaxPendingRequests") // DefaultMaxPendingRequests is the default value // for PipelineClient.MaxPendingRequests. const DefaultMaxPendingRequests = 1024 func (c *PipelineClient) init() { c.chLock.Lock() if c.chR == nil { maxPendingRequests := c.MaxPendingRequests if maxPendingRequests <= 0 { maxPendingRequests = DefaultMaxPendingRequests } c.chR = make(chan *pipelineWork, maxPendingRequests) if c.chW == nil { c.chW = make(chan *pipelineWork, maxPendingRequests) } go func() { if err := c.worker(); err != nil { c.logger().Printf("error in PipelineClient(%q): %s", c.Addr, err) if netErr, ok := err.(net.Error); ok && netErr.Temporary() { // Throttle client reconnections on temporary errors time.Sleep(time.Second) } } c.chLock.Lock() // Do not reset c.chW to nil, since it may contain // pending requests, which could be served on the next // connection to the host. c.chR = nil c.chLock.Unlock() }() } c.chLock.Unlock() } func (c *PipelineClient) worker() error { conn, err := dialAddr(c.Addr, c.Dial, c.DialDualStack, c.IsTLS, c.TLSConfig) if err != nil { return err } // Start reader and writer stopW := make(chan struct{}) doneW := make(chan error) go func() { doneW <- c.writer(conn, stopW) }() stopR := make(chan struct{}) doneR := make(chan error) go func() { doneR <- c.reader(conn, stopR) }() // Wait until reader and writer are stopped select { case err = <-doneW: conn.Close() close(stopR) <-doneR case err = <-doneR: conn.Close() close(stopW) <-doneW } // Notify pending readers for len(c.chR) > 0 { w := <-c.chR w.err = errPipelineClientStopped w.done <- struct{}{} } return err } func (c *PipelineClient) writer(conn net.Conn, stopCh <-chan struct{}) error { writeBufferSize := c.WriteBufferSize if writeBufferSize <= 0 { writeBufferSize = defaultWriteBufferSize } bw := bufio.NewWriterSize(conn, writeBufferSize) defer bw.Flush() chR := c.chR chW := c.chW writeTimeout := c.WriteTimeout maxIdleConnDuration := c.MaxIdleConnDuration if maxIdleConnDuration <= 0 { maxIdleConnDuration = DefaultMaxIdleConnDuration } maxBatchDelay := c.MaxBatchDelay var ( stopTimer = time.NewTimer(time.Hour) flushTimer = time.NewTimer(time.Hour) flushTimerCh <-chan time.Time instantTimerCh = make(chan time.Time) w *pipelineWork err error lastWriteDeadlineTime time.Time ) close(instantTimerCh) for { againChW: select { case w = <-chW: // Fast path: len(chW) > 0 default: // Slow path stopTimer.Reset(maxIdleConnDuration) select { case w = <-chW: case <-stopTimer.C: return nil case <-stopCh: return nil case <-flushTimerCh: if err = bw.Flush(); err != nil { return err } flushTimerCh = nil goto againChW } } if !w.deadline.IsZero() && time.Since(w.deadline) >= 0 { w.err = ErrTimeout w.done <- struct{}{} continue } if writeTimeout > 0 { // Optimization: update write deadline only if more than 25% // of the last write deadline exceeded. // See https://github.com/golang/go/issues/15133 for details. currentTime := time.Now() if currentTime.Sub(lastWriteDeadlineTime) > (writeTimeout >> 2) { if err = conn.SetWriteDeadline(currentTime.Add(writeTimeout)); err != nil { w.err = err w.done <- struct{}{} return err } lastWriteDeadlineTime = currentTime } } if err = w.req.Write(bw); err != nil { w.err = err w.done <- struct{}{} return err } if flushTimerCh == nil && (len(chW) == 0 || len(chR) == cap(chR)) { if maxBatchDelay > 0 { flushTimer.Reset(maxBatchDelay) flushTimerCh = flushTimer.C } else { flushTimerCh = instantTimerCh } } againChR: select { case chR <- w: // Fast path: len(chR) < cap(chR) default: // Slow path select { case chR <- w: case <-stopCh: w.err = errPipelineClientStopped w.done <- struct{}{} return nil case <-flushTimerCh: if err = bw.Flush(); err != nil { w.err = err w.done <- struct{}{} return err } flushTimerCh = nil goto againChR } } } } func (c *PipelineClient) reader(conn net.Conn, stopCh <-chan struct{}) error { readBufferSize := c.ReadBufferSize if readBufferSize <= 0 { readBufferSize = defaultReadBufferSize } br := bufio.NewReaderSize(conn, readBufferSize) chR := c.chR readTimeout := c.ReadTimeout var ( w *pipelineWork err error lastReadDeadlineTime time.Time ) for { select { case w = <-chR: // Fast path: len(chR) > 0 default: // Slow path select { case w = <-chR: case <-stopCh: return nil } } if readTimeout > 0 { // Optimization: update read deadline only if more than 25% // of the last read deadline exceeded. // See https://github.com/golang/go/issues/15133 for details. currentTime := time.Now() if currentTime.Sub(lastReadDeadlineTime) > (readTimeout >> 2) { if err = conn.SetReadDeadline(currentTime.Add(readTimeout)); err != nil { w.err = err w.done <- struct{}{} return err } lastReadDeadlineTime = currentTime } } if err = w.resp.Read(br); err != nil { w.err = err w.done <- struct{}{} return err } w.done <- struct{}{} } } func (c *PipelineClient) logger() Logger { if c.Logger != nil { return c.Logger } return defaultLogger } // PendingRequests returns the current number of pending requests pipelined // to the server. // // This number may exceed MaxPendingRequests by up to two times, since // the client may keep up to MaxPendingRequests requests in the queue before // sending them to the server. func (c *PipelineClient) PendingRequests() int { c.init() c.chLock.Lock() n := len(c.chR) + len(c.chW) c.chLock.Unlock() return n } var errPipelineClientStopped = errors.New("pipeline client has been stopped") func acquirePipelineWork(pool *sync.Pool, timeout time.Duration) *pipelineWork { v := pool.Get() if v == nil { v = &pipelineWork{ done: make(chan struct{}, 1), } } w := v.(*pipelineWork) if timeout > 0 { if w.t == nil { w.t = time.NewTimer(timeout) } else { w.t.Reset(timeout) } w.deadline = time.Now().Add(timeout) } else { w.deadline = zeroTime } return w } func releasePipelineWork(pool *sync.Pool, w *pipelineWork) { if w.t != nil { w.t.Stop() } w.reqCopy.Reset() w.respCopy.Reset() w.req = nil w.resp = nil w.err = nil pool.Put(w) }