web: add feature flag PropagateCancels (#7778)

This allow client-initiated cancels to propagate through gRPC.

IN-10803 tracks the SRE-side changes to enable this flag.
This commit is contained in:
Jacob Hoffman-Andrews 2024-11-04 14:37:29 -08:00 committed by GitHub
parent 21bc647fa5
commit 02685602a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 54 additions and 5 deletions

View File

@ -103,6 +103,15 @@ type Config struct {
// This flag should only be used in conjunction with UseKvLimitsForNewOrder. // This flag should only be used in conjunction with UseKvLimitsForNewOrder.
DisableLegacyLimitWrites bool DisableLegacyLimitWrites bool
// PropagateCancels controls whether the WFE and ocsp-responder allows
// cancellation of an inbound request to cancel downstream gRPC and other
// queries. In practice, cancellation of an inbound request is achieved by
// Nginx closing the connection on which the request was happening. This may
// help shed load in overcapacity situations. However, note that in-progress
// database queries (for instance, in the SA) are not cancelled. Database
// queries waiting for an available connection may be cancelled.
PropagateCancels bool
// InsertAuthzsIndividually causes the SA's NewOrderAndAuthzs method to // InsertAuthzsIndividually causes the SA's NewOrderAndAuthzs method to
// create each new authz one at a time, rather than using MultiInserter. // create each new authz one at a time, rather than using MultiInserter.
// Although this is expected to be a performance penalty, it is necessary to // Although this is expected to be a performance penalty, it is necessary to

View File

@ -127,6 +127,7 @@
"Overrides": "test/config-next/wfe2-ratelimit-overrides.yml" "Overrides": "test/config-next/wfe2-ratelimit-overrides.yml"
}, },
"features": { "features": {
"PropagateCancels": true,
"ServeRenewalInfo": true, "ServeRenewalInfo": true,
"CheckIdentifiersPaused": true, "CheckIdentifiersPaused": true,
"UseKvLimitsForNewOrder": true, "UseKvLimitsForNewOrder": true,

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
) )
@ -127,11 +128,13 @@ func (th *TopHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Origin: r.Header.Get("Origin"), Origin: r.Header.Get("Origin"),
Extra: make(map[string]interface{}), Extra: make(map[string]interface{}),
} }
// We specifically override the default r.Context() because we would prefer if !features.Get().PropagateCancels {
// for clients to not be able to cancel our operations in arbitrary places. // We specifically override the default r.Context() because we would prefer
// Instead we start a new context, and apply timeouts in our various RPCs. // for clients to not be able to cancel our operations in arbitrary places.
ctx := context.WithoutCancel(r.Context()) // Instead we start a new context, and apply timeouts in our various RPCs.
r = r.WithContext(ctx) ctx := context.WithoutCancel(r.Context())
r = r.WithContext(ctx)
}
// Some clients will send a HTTP Host header that includes the default port // Some clients will send a HTTP Host header that includes the default port
// for the scheme that they are using. Previously when we were fronted by // for the scheme that they are using. Previously when we were fronted by

View File

@ -2,13 +2,16 @@ package web
import ( import (
"bytes" "bytes"
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"time"
"github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
) )
@ -117,3 +120,36 @@ func TestHostHeaderRewrite(t *testing.T) {
req.Host = "localhost:123" req.Host = "localhost:123"
th.ServeHTTP(httptest.NewRecorder(), req) th.ServeHTTP(httptest.NewRecorder(), req)
} }
type cancelHandler struct {
res chan string
}
func (ch cancelHandler) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http.Request) {
select {
case <-r.Context().Done():
ch.res <- r.Context().Err().Error()
case <-time.After(300 * time.Millisecond):
ch.res <- "300 ms passed"
}
}
func TestPropagateCancel(t *testing.T) {
mockLog := blog.UseMock()
res := make(chan string)
features.Set(features.Config{PropagateCancels: true})
th := NewTopHandler(mockLog, cancelHandler{res})
ctx, cancel := context.WithCancel(context.Background())
go func() {
req, err := http.NewRequestWithContext(ctx, "GET", "/thisisignored", &bytes.Reader{})
if err != nil {
t.Error(err)
}
th.ServeHTTP(httptest.NewRecorder(), req)
}()
cancel()
result := <-res
if result != "context canceled" {
t.Errorf("expected 'context canceled', got %q", result)
}
}