[feature/performance] Fail fast when doing remote transport calls inside incoming request contexts (#1119)

* [feature/performance] Fail fast when doing remote transport calls inside incoming request contexts

* [chore] Reduce outgoing request timeout to 15s

* log error messages when fastfailing

* use context.Value() instead of wrapped context, wrap error with fastfail instead of extra log entry

* add fast-fail context key test

Signed-off-by: kim <grufwub@gmail.com>
Co-authored-by: kim <grufwub@gmail.com>
This commit is contained in:
tobi 2022-11-23 22:40:07 +01:00 committed by GitHub
commit c9d893fec1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 141 additions and 24 deletions

View file

@ -0,0 +1,43 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package transport
import "context"
// ctxkey is our own unique context key type to prevent setting outside package.
type ctxkey string
// fastfailkey is our unique context key to indicate fast-fail is enabled.
var fastfailkey = ctxkey("ff")
// WithFastfail returns a Context which indicates that any http requests made
// with it should return after the first failed attempt, instead of retrying.
//
// This can be used to fail quickly when you're making an outgoing http request
// inside the context of an incoming http request, and you want to be able to
// provide a snappy response to the user, instead of retrying + backing off.
func WithFastfail(parent context.Context) context.Context {
return context.WithValue(parent, fastfailkey, struct{}{})
}
// IsFastfail returns true if the given context was created by WithFastfail.
func IsFastfail(ctx context.Context) bool {
_, ok := ctx.Value(fastfailkey).(struct{})
return ok
}

View file

@ -0,0 +1,34 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package transport_test
import (
"context"
"testing"
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
func TestFastFailContext(t *testing.T) {
ctx := context.Background()
ctx = transport.WithFastfail(ctx)
if !transport.IsFastfail(ctx) {
t.Fatal("failed to set fast-fail context key")
}
}

View file

@ -23,6 +23,7 @@ import (
"crypto"
"crypto/x509"
"errors"
"fmt"
"io"
"net/http"
"net/url"
@ -33,25 +34,41 @@ import (
errorsv2 "codeberg.org/gruf/go-errors/v2"
"codeberg.org/gruf/go-kv"
"github.com/go-fed/httpsig"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/httpclient"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
// Transport wraps the pub.Transport interface with some additional functionality for fetching remote media.
// Transport implements the pub.Transport interface with some additional functionality for fetching remote media.
//
// Since the transport has the concept of 'shortcuts' for fetching data locally rather than remotely, it is
// not *always* the case that calling a Transport function does an http call, but it usually will for remote
// hosts or resources for which a shortcut isn't provided by the transport controller (also in this package).
//
// For any of the transport functions, if a Fastfail context is passed in as the first parameter, the function
// will return after the first transport failure, instead of retrying + backing off.
type Transport interface {
pub.Transport
/*
POST functions
*/
// Deliver sends an ActivityStreams object.
Deliver(ctx context.Context, b []byte, to *url.URL) error
// BatchDeliver sends an ActivityStreams object to multiple recipients.
BatchDeliver(ctx context.Context, b []byte, recipients []*url.URL) error
/*
GET functions
*/
// Dereference fetches the ActivityStreams object located at this IRI with a GET request.
Dereference(ctx context.Context, iri *url.URL) ([]byte, error)
// DereferenceMedia fetches the given media attachment IRI, returning the reader and filesize.
DereferenceMedia(ctx context.Context, iri *url.URL) (io.ReadCloser, int64, error)
// DereferenceInstance dereferences remote instance information, first by checking /api/v1/instance, and then by checking /.well-known/nodeinfo.
DereferenceInstance(ctx context.Context, iri *url.URL) (*gtsmodel.Instance, error)
// Finger performs a webfinger request with the given username and domain, and returns the bytes from the response body.
Finger(ctx context.Context, targetUsername string, targetDomains string) ([]byte, error)
Finger(ctx context.Context, targetUsername string, targetDomain string) ([]byte, error)
}
// transport implements the Transport interface
@ -106,6 +123,10 @@ func (t *transport) do(r *http.Request, signer func(*http.Request) error, retryO
return nil, errors.New("too many failed attempts")
}
// Check whether request should fast fail, we check this
// before loop as each context.Value() requires mutex lock.
fastFail := IsFastfail(r.Context())
// Start a log entry for this request
l := log.WithFields(kv.Fields{
{"pubKeyID", t.pubKeyID},
@ -156,6 +177,9 @@ func (t *transport) do(r *http.Request, signer func(*http.Request) error, retryO
} else if errors.As(err, &x509.UnknownAuthorityError{}) {
// Unknown authority errors we do NOT recover from
return nil, err
} else if fastFail {
// on fast-fail, don't bother backoff/retry
return nil, fmt.Errorf("%w (fast fail)", err)
}
l.Errorf("backing off for %s after http request error: %v", backoff.String(), err)