[performance] tweak http client error handling (#1718)

* update errors library, check for more TLS type error in http client

Signed-off-by: kim <grufwub@gmail.com>

* bump cache library version to match errors library

Signed-off-by: kim <grufwub@gmail.com>

---------

Signed-off-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2023-04-29 17:44:20 +01:00 committed by GitHub
parent 8b1e2288d8
commit 68b91d2128
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 115 additions and 49 deletions

4
go.mod
View file

@ -5,9 +5,9 @@ go 1.20
require ( require (
codeberg.org/gruf/go-bytesize v1.0.2 codeberg.org/gruf/go-bytesize v1.0.2
codeberg.org/gruf/go-byteutil v1.1.2 codeberg.org/gruf/go-byteutil v1.1.2
codeberg.org/gruf/go-cache/v3 v3.2.5 codeberg.org/gruf/go-cache/v3 v3.2.6
codeberg.org/gruf/go-debug v1.3.0 codeberg.org/gruf/go-debug v1.3.0
codeberg.org/gruf/go-errors/v2 v2.1.1 codeberg.org/gruf/go-errors/v2 v2.2.0
codeberg.org/gruf/go-fastcopy v1.1.2 codeberg.org/gruf/go-fastcopy v1.1.2
codeberg.org/gruf/go-kv v1.6.1 codeberg.org/gruf/go-kv v1.6.1
codeberg.org/gruf/go-logger/v2 v2.2.1 codeberg.org/gruf/go-logger/v2 v2.2.1

8
go.sum
View file

@ -49,13 +49,13 @@ codeberg.org/gruf/go-bytesize v1.0.2/go.mod h1:n/GU8HzL9f3UNp/mUKyr1qVmTlj7+xacp
codeberg.org/gruf/go-byteutil v1.0.0/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU= codeberg.org/gruf/go-byteutil v1.0.0/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU=
codeberg.org/gruf/go-byteutil v1.1.2 h1:TQLZtTxTNca9xEfDIndmo7nBYxeS94nrv/9DS3Nk5Tw= codeberg.org/gruf/go-byteutil v1.1.2 h1:TQLZtTxTNca9xEfDIndmo7nBYxeS94nrv/9DS3Nk5Tw=
codeberg.org/gruf/go-byteutil v1.1.2/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU= codeberg.org/gruf/go-byteutil v1.1.2/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU=
codeberg.org/gruf/go-cache/v3 v3.2.5 h1:C+JwTR4uxjuE6qwqB48d3NCRJejsbzxRpfFEBReaViA= codeberg.org/gruf/go-cache/v3 v3.2.6 h1:PtAGOvCTGwhqOqIEFBP4M0F6xbaAWYe3t/7QYGNzulI=
codeberg.org/gruf/go-cache/v3 v3.2.5/go.mod h1:up7za8FtNdtttcx6AJ8ffqkrSkXDGTilsd9yJ0IyhfM= codeberg.org/gruf/go-cache/v3 v3.2.6/go.mod h1:pTeVPEb9DshXUkd8Dg76UcsLpU6EC/tXQ2qb+JrmxEc=
codeberg.org/gruf/go-debug v1.3.0 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs= codeberg.org/gruf/go-debug v1.3.0 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs=
codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg= codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg=
codeberg.org/gruf/go-errors/v2 v2.0.0/go.mod h1:ZRhbdhvgoUA3Yw6e56kd9Ox984RrvbEFC2pOXyHDJP4= codeberg.org/gruf/go-errors/v2 v2.0.0/go.mod h1:ZRhbdhvgoUA3Yw6e56kd9Ox984RrvbEFC2pOXyHDJP4=
codeberg.org/gruf/go-errors/v2 v2.1.1 h1:oj7JUIvUBafF60HrwN74JrCMol1Ouh3gq1ggrH5hGTw= codeberg.org/gruf/go-errors/v2 v2.2.0 h1:CxnTtR4+BqRGeBHuG/FdCKM4m3otMdfPVez6ReBebkM=
codeberg.org/gruf/go-errors/v2 v2.1.1/go.mod h1:LfzD9nkAAJpEDbkUqOZQ2jdaQ8VrK0pnR36zLOMFq6Y= codeberg.org/gruf/go-errors/v2 v2.2.0/go.mod h1:LfzD9nkAAJpEDbkUqOZQ2jdaQ8VrK0pnR36zLOMFq6Y=
codeberg.org/gruf/go-fastcopy v1.1.2 h1:YwmYXPsyOcRBxKEE2+w1bGAZfclHVaPijFsOVOcnNcw= codeberg.org/gruf/go-fastcopy v1.1.2 h1:YwmYXPsyOcRBxKEE2+w1bGAZfclHVaPijFsOVOcnNcw=
codeberg.org/gruf/go-fastcopy v1.1.2/go.mod h1:GDDYR0Cnb3U/AIfGM3983V/L+GN+vuwVMvrmVABo21s= codeberg.org/gruf/go-fastcopy v1.1.2/go.mod h1:GDDYR0Cnb3U/AIfGM3983V/L+GN+vuwVMvrmVABo21s=
codeberg.org/gruf/go-fastpath v1.0.1/go.mod h1:edveE/Kp3Eqi0JJm0lXYdkVrB28cNUkcb/bRGFTPqeI= codeberg.org/gruf/go-fastpath v1.0.1/go.mod h1:edveE/Kp3Eqi0JJm0lXYdkVrB28cNUkcb/bRGFTPqeI=

View file

@ -35,7 +35,7 @@
// ignoreErrors is an error ignoring function capable of being passed to // ignoreErrors is an error ignoring function capable of being passed to
// caches, which specifically catches and ignores our sentinel error type. // caches, which specifically catches and ignores our sentinel error type.
func ignoreErrors(err error) bool { func ignoreErrors(err error) bool {
return errorsv2.Is( return errorsv2.Comparable(
SentinelError, SentinelError,
context.DeadlineExceeded, context.DeadlineExceeded,
context.Canceled, context.Canceled,

View file

@ -149,7 +149,7 @@ func New(cfg Config) *Client {
// Initiate outgoing bad hosts lookup cache. // Initiate outgoing bad hosts lookup cache.
c.badHosts = cache.New[string, struct{}](0, 1000, 0) c.badHosts = cache.New[string, struct{}](0, 1000, 0)
c.badHosts.SetTTL(15*time.Minute, false) c.badHosts.SetTTL(time.Hour, false)
if !c.badHosts.Start(time.Minute) { if !c.badHosts.Start(time.Minute) {
log.Panic(nil, "failed to start transport controller cache") log.Panic(nil, "failed to start transport controller cache")
} }
@ -165,7 +165,7 @@ func (c *Client) Do(r *http.Request) (*http.Response, error) {
} }
// DoSigned ... // DoSigned ...
func (c *Client) DoSigned(r *http.Request, sign SignFunc) (*http.Response, error) { func (c *Client) DoSigned(r *http.Request, sign SignFunc) (rsp *http.Response, err error) {
const ( const (
// max no. attempts. // max no. attempts.
maxRetries = 5 maxRetries = 5
@ -182,10 +182,16 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (*http.Response, error
if !fastFail { if !fastFail {
// Check if recently reached max retries for this host // Check if recently reached max retries for this host
// so we don't bother with a retry-backoff loop. The only // so we don't bother with a retry-backoff loop. The only
// errors that are retried upon are server failure and // errors that are retried upon are server failure, TLS
// domain resolution type errors, so this cached result // and domain resolution type errors, so this cached result
// indicates this server is likely having issues. // indicates this server is likely having issues.
fastFail = c.badHosts.Has(host) fastFail = c.badHosts.Has(host)
defer func() {
if err != nil {
// On error return mark as bad-host.
c.badHosts.Set(host, struct{}{})
}
}()
} }
// Start a log entry for this request // Start a log entry for this request
@ -218,7 +224,7 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (*http.Response, error
l.Infof("performing request") l.Infof("performing request")
// Perform the request. // Perform the request.
rsp, err := c.do(r) rsp, err = c.do(r)
if err == nil { //nolint:gocritic if err == nil { //nolint:gocritic
// TooManyRequest means we need to slow // TooManyRequest means we need to slow
@ -249,20 +255,27 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (*http.Response, error
} }
} }
} else if errorsv2.Is(err, // Ensure unset.
rsp = nil
} else if errorsv2.Comparable(err,
context.DeadlineExceeded, context.DeadlineExceeded,
context.Canceled, context.Canceled,
ErrBodyTooLarge, ErrBodyTooLarge,
ErrReservedAddr, ErrReservedAddr,
) { ) {
// Return on non-retryable errors // Non-retryable errors.
return nil, err
} else if errorsv2.Assignable(err,
(*x509.CertificateInvalidError)(nil),
(*x509.HostnameError)(nil),
(*x509.UnknownAuthorityError)(nil),
) {
// Non-retryable TLS errors.
return nil, err return nil, err
} else if strings.Contains(err.Error(), "stopped after 10 redirects") { } else if strings.Contains(err.Error(), "stopped after 10 redirects") {
// Don't bother if net/http returned after too many redirects // Don't bother if net/http returned after too many redirects
return nil, err return nil, err
} else if errors.As(err, &x509.UnknownAuthorityError{}) {
// Unknown authority errors we do NOT recover from
return nil, err
} else if dnserr := (*net.DNSError)(nil); // nocollapse } else if dnserr := (*net.DNSError)(nil); // nocollapse
errors.As(err, &dnserr) && dnserr.IsNotFound { errors.As(err, &dnserr) && dnserr.IsNotFound {
// DNS lookup failure, this domain does not exist // DNS lookup failure, this domain does not exist
@ -292,10 +305,9 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (*http.Response, error
} }
} }
// Add "bad" entry for this host. // Set error return to trigger setting "bad host".
c.badHosts.Set(host, struct{}{}) err = errors.New("transport reached max retries")
return
return nil, errors.New("transport reached max retries")
} }
// do ... // do ...

View file

@ -95,7 +95,7 @@ func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, erro
defer func() { defer func() {
// This is only done when ctx NOT cancelled. // This is only done when ctx NOT cancelled.
done = err == nil || !errors.Is(err, done = err == nil || !errors.Comparable(err,
context.Canceled, context.Canceled,
context.DeadlineExceeded, context.DeadlineExceeded,
) )

View file

@ -95,7 +95,7 @@ func (p *ProcessingMedia) load(ctx context.Context) (*gtsmodel.MediaAttachment,
defer func() { defer func() {
// This is only done when ctx NOT cancelled. // This is only done when ctx NOT cancelled.
done = err == nil || !errors.Is(err, done = err == nil || !errors.Comparable(err,
context.Canceled, context.Canceled,
context.DeadlineExceeded, context.DeadlineExceeded,
) )

View file

@ -138,7 +138,7 @@ func (c *Cache[Value]) SetInvalidateCallback(hook func(Value)) {
func (c *Cache[Value]) IgnoreErrors(ignore func(error) bool) { func (c *Cache[Value]) IgnoreErrors(ignore func(error) bool) {
if ignore == nil { if ignore == nil {
ignore = func(err error) bool { ignore = func(err error) bool {
return errors.Is( return errors.Comparable(
err, err,
context.Canceled, context.Canceled,
context.DeadlineExceeded, context.DeadlineExceeded,

View file

@ -1,6 +1,7 @@
package errors package errors
import ( import (
"errors"
"fmt" "fmt"
) )
@ -29,7 +30,7 @@ func Stacktrace(err error) Callers {
var e interface { var e interface {
Stacktrace() Callers Stacktrace() Callers
} }
if !As(err, &e) { if !errors.As(err, &e) {
return nil return nil
} }
return e.Stacktrace() return e.Stacktrace()

View file

@ -8,15 +8,11 @@
"codeberg.org/gruf/go-bitutil" "codeberg.org/gruf/go-bitutil"
) )
// Is reports whether any error in err's chain matches any of targets // errtype is a ptr to the error interface type.
// (up to a max of 64 targets). var errtype = reflect.TypeOf((*error)(nil)).Elem()
//
// The chain consists of err itself followed by the sequence of errors obtained by // Comparable is functionally equivalent to calling errors.Is() on multiple errors (up to a max of 64).
// repeatedly calling Unwrap. func Comparable(err error, targets ...error) bool {
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
func Is(err error, targets ...error) bool {
var flags bitutil.Flags64 var flags bitutil.Flags64
// Flags only has 64 bit-slots // Flags only has 64 bit-slots
@ -24,17 +20,15 @@ func Is(err error, targets ...error) bool {
panic("too many targets") panic("too many targets")
} }
// Check if error is nil so we can catch
// the fast-case where a target is nil
isNil := (err == nil)
for i := 0; i < len(targets); { for i := 0; i < len(targets); {
// Drop nil targets
if targets[i] == nil { if targets[i] == nil {
if isNil /* match! */ { if err == nil {
return true return true
} }
targets = append(targets[:i], targets[i+1:]...)
// Drop nil targets from slice.
copy(targets[i:], targets[i+1:])
targets = targets[:len(targets)-1]
continue continue
} }
@ -81,11 +75,68 @@ func Is(err error, targets ...error) bool {
return false return false
} }
// As finds the first error in err's chain that matches target, and if one is found, sets // Assignable is functionally equivalent to calling errors.As() on multiple errors,
// except that it only checks assignability as opposed to setting the target.
func Assignable(err error, targets ...error) bool {
if err == nil {
// Fastest case.
return false
}
for i := 0; i < len(targets); {
if targets[i] == nil {
// Drop nil targets from slice.
copy(targets[i:], targets[i+1:])
targets = targets[:len(targets)-1]
continue
}
i++
}
for err != nil {
// Check if this layer supports .As interface
as, ok := err.(interface{ As(any) bool })
// Get reflected err type.
te := reflect.TypeOf(err)
if !ok {
// Error does not support interface.
//
// Check assignability using reflection.
for i := 0; i < len(targets); i++ {
tt := reflect.TypeOf(targets[i])
if te.AssignableTo(tt) {
return true
}
}
} else {
// Error supports the .As interface.
//
// Check using .As() and reflection.
for i := 0; i < len(targets); i++ {
if as.As(targets[i]) {
return true
} else if tt := reflect.TypeOf(targets[i]); // nocollapse
te.AssignableTo(tt) {
return true
}
}
}
// Unwrap to next layer.
err = errors.Unwrap(err)
}
return false
}
// As finds the first error in err's tree that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false. // target to that error value and returns true. Otherwise, it returns false.
// //
// The chain consists of err itself followed by the sequence of errors obtained by // The tree consists of err itself, followed by the errors obtained by repeatedly
// repeatedly calling Unwrap. // calling Unwrap. When err wraps multiple errors, As examines err followed by a
// depth-first traversal of its children.
// //
// An error matches target if the error's concrete value is assignable to the value // An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that // pointed to by target, or if the error has a method As(interface{}) bool such that
@ -99,7 +150,7 @@ func Is(err error, targets ...error) bool {
// error, or to any interface type. // error, or to any interface type.
// //
//go:linkname As errors.As //go:linkname As errors.As
func As(err error, target interface{}) bool func As(err error, target any) bool
// Unwrap returns the result of calling the Unwrap method on err, if err's // Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error. Otherwise, Unwrap returns nil. // type contains an Unwrap method returning error. Otherwise, Unwrap returns nil.

View file

@ -1,5 +1,7 @@
package errors package errors
import "errors"
// WithValue wraps err to store given key-value pair, accessible via Value() function. // WithValue wraps err to store given key-value pair, accessible via Value() function.
func WithValue(err error, key any, value any) error { func WithValue(err error, key any, value any) error {
if err == nil { if err == nil {
@ -16,7 +18,7 @@ func WithValue(err error, key any, value any) error {
func Value(err error, key any) any { func Value(err error, key any) any {
var e *errWithValue var e *errWithValue
if !As(err, &e) { if !errors.As(err, &e) {
return nil return nil
} }
@ -47,7 +49,7 @@ func (e *errWithValue) Value(key any) any {
return e.val return e.val
} }
if !As(e.err, &e) { if !errors.As(e.err, &e) {
return nil return nil
} }
} }

4
vendor/modules.txt vendored
View file

@ -13,7 +13,7 @@ codeberg.org/gruf/go-bytesize
# codeberg.org/gruf/go-byteutil v1.1.2 # codeberg.org/gruf/go-byteutil v1.1.2
## explicit; go 1.16 ## explicit; go 1.16
codeberg.org/gruf/go-byteutil codeberg.org/gruf/go-byteutil
# codeberg.org/gruf/go-cache/v3 v3.2.5 # codeberg.org/gruf/go-cache/v3 v3.2.6
## explicit; go 1.19 ## explicit; go 1.19
codeberg.org/gruf/go-cache/v3 codeberg.org/gruf/go-cache/v3
codeberg.org/gruf/go-cache/v3/result codeberg.org/gruf/go-cache/v3/result
@ -21,7 +21,7 @@ codeberg.org/gruf/go-cache/v3/ttl
# codeberg.org/gruf/go-debug v1.3.0 # codeberg.org/gruf/go-debug v1.3.0
## explicit; go 1.16 ## explicit; go 1.16
codeberg.org/gruf/go-debug codeberg.org/gruf/go-debug
# codeberg.org/gruf/go-errors/v2 v2.1.1 # codeberg.org/gruf/go-errors/v2 v2.2.0
## explicit; go 1.19 ## explicit; go 1.19
codeberg.org/gruf/go-errors/v2 codeberg.org/gruf/go-errors/v2
# codeberg.org/gruf/go-fastcopy v1.1.2 # codeberg.org/gruf/go-fastcopy v1.1.2