[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 (
codeberg.org/gruf/go-bytesize v1.0.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-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-kv v1.6.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.1.2 h1:TQLZtTxTNca9xEfDIndmo7nBYxeS94nrv/9DS3Nk5Tw=
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.5/go.mod h1:up7za8FtNdtttcx6AJ8ffqkrSkXDGTilsd9yJ0IyhfM=
codeberg.org/gruf/go-cache/v3 v3.2.6 h1:PtAGOvCTGwhqOqIEFBP4M0F6xbaAWYe3t/7QYGNzulI=
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/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.1.1 h1:oj7JUIvUBafF60HrwN74JrCMol1Ouh3gq1ggrH5hGTw=
codeberg.org/gruf/go-errors/v2 v2.1.1/go.mod h1:LfzD9nkAAJpEDbkUqOZQ2jdaQ8VrK0pnR36zLOMFq6Y=
codeberg.org/gruf/go-errors/v2 v2.2.0 h1:CxnTtR4+BqRGeBHuG/FdCKM4m3otMdfPVez6ReBebkM=
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/go.mod h1:GDDYR0Cnb3U/AIfGM3983V/L+GN+vuwVMvrmVABo21s=
codeberg.org/gruf/go-fastpath v1.0.1/go.mod h1:edveE/Kp3Eqi0JJm0lXYdkVrB28cNUkcb/bRGFTPqeI=

View file

@ -35,7 +35,7 @@ var SentinelError = errors.New("BUG: error should not be returned") //nolint:rev
// ignoreErrors is an error ignoring function capable of being passed to
// caches, which specifically catches and ignores our sentinel error type.
func ignoreErrors(err error) bool {
return errorsv2.Is(
return errorsv2.Comparable(
SentinelError,
context.DeadlineExceeded,
context.Canceled,

View file

@ -149,7 +149,7 @@ func New(cfg Config) *Client {
// Initiate outgoing bad hosts lookup cache.
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) {
log.Panic(nil, "failed to start transport controller cache")
}
@ -165,7 +165,7 @@ func (c *Client) Do(r *http.Request) (*http.Response, error) {
}
// 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 (
// max no. attempts.
maxRetries = 5
@ -182,10 +182,16 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (*http.Response, error
if !fastFail {
// Check if recently reached max retries for this host
// so we don't bother with a retry-backoff loop. The only
// errors that are retried upon are server failure and
// domain resolution type errors, so this cached result
// errors that are retried upon are server failure, TLS
// and domain resolution type errors, so this cached result
// indicates this server is likely having issues.
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
@ -218,7 +224,7 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (*http.Response, error
l.Infof("performing request")
// Perform the request.
rsp, err := c.do(r)
rsp, err = c.do(r)
if err == nil { //nolint:gocritic
// 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.Canceled,
ErrBodyTooLarge,
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
} else if strings.Contains(err.Error(), "stopped after 10 redirects") {
// Don't bother if net/http returned after too many redirects
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
errors.As(err, &dnserr) && dnserr.IsNotFound {
// 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.
c.badHosts.Set(host, struct{}{})
return nil, errors.New("transport reached max retries")
// Set error return to trigger setting "bad host".
err = errors.New("transport reached max retries")
return
}
// do ...

View file

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

View file

@ -95,7 +95,7 @@ func (p *ProcessingMedia) load(ctx context.Context) (*gtsmodel.MediaAttachment,
defer func() {
// This is only done when ctx NOT cancelled.
done = err == nil || !errors.Is(err,
done = err == nil || !errors.Comparable(err,
context.Canceled,
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) {
if ignore == nil {
ignore = func(err error) bool {
return errors.Is(
return errors.Comparable(
err,
context.Canceled,
context.DeadlineExceeded,

View file

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

View file

@ -8,15 +8,11 @@ import (
"codeberg.org/gruf/go-bitutil"
)
// Is reports whether any error in err's chain matches any of targets
// (up to a max of 64 targets).
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// 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 {
// errtype is a ptr to the error interface type.
var errtype = reflect.TypeOf((*error)(nil)).Elem()
// Comparable is functionally equivalent to calling errors.Is() on multiple errors (up to a max of 64).
func Comparable(err error, targets ...error) bool {
var flags bitutil.Flags64
// Flags only has 64 bit-slots
@ -24,17 +20,15 @@ func Is(err error, targets ...error) bool {
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); {
// Drop nil targets
if targets[i] == nil {
if isNil /* match! */ {
if err == nil {
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
}
@ -81,11 +75,68 @@ func Is(err error, targets ...error) bool {
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.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
// The tree consists of err itself, followed by the errors obtained by repeatedly
// 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
// 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.
//
//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
// type contains an Unwrap method returning error. Otherwise, Unwrap returns nil.

View file

@ -1,5 +1,7 @@
package errors
import "errors"
// WithValue wraps err to store given key-value pair, accessible via Value() function.
func WithValue(err error, key any, value any) error {
if err == nil {
@ -16,7 +18,7 @@ func WithValue(err error, key any, value any) error {
func Value(err error, key any) any {
var e *errWithValue
if !As(err, &e) {
if !errors.As(err, &e) {
return nil
}
@ -47,7 +49,7 @@ func (e *errWithValue) Value(key any) any {
return e.val
}
if !As(e.err, &e) {
if !errors.As(e.err, &e) {
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
## explicit; go 1.16
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
codeberg.org/gruf/go-cache/v3
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
## explicit; go 1.16
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
codeberg.org/gruf/go-errors/v2
# codeberg.org/gruf/go-fastcopy v1.1.2