[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:
parent
8b1e2288d8
commit
68b91d2128
4
go.mod
4
go.mod
|
@ -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
8
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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
|
// 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,
|
||||||
|
|
|
@ -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 ...
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -8,15 +8,11 @@ import (
|
||||||
"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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue