Signed-off-by: Sanket Sudake <sanketsudake@gmail.com> Co-authored-by: Vishal <vishal-biyani@users.noreply.github.com>
737 lines
26 KiB
Go
737 lines
26 KiB
Go
/*
|
|
Copyright 2016 The Fission Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package router
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"go.opencensus.io/plugin/ochttp"
|
|
"go.uber.org/zap"
|
|
k8stypes "k8s.io/apimachinery/pkg/types"
|
|
|
|
fv1 "github.com/fission/fission/pkg/apis/core/v1"
|
|
"github.com/fission/fission/pkg/crd"
|
|
ferror "github.com/fission/fission/pkg/error"
|
|
"github.com/fission/fission/pkg/error/network"
|
|
executorClient "github.com/fission/fission/pkg/executor/client"
|
|
"github.com/fission/fission/pkg/throttler"
|
|
"github.com/fission/fission/pkg/utils"
|
|
)
|
|
|
|
const (
|
|
// FORWARDED represents the 'Forwarded' request header
|
|
FORWARDED = "Forwarded"
|
|
|
|
// X_FORWARDED_HOST represents the 'X_FORWARDED_HOST' request header
|
|
X_FORWARDED_HOST = "X-Forwarded-Host"
|
|
)
|
|
|
|
type (
|
|
functionHandler struct {
|
|
logger *zap.Logger
|
|
fmap *functionServiceMap
|
|
executor *executorClient.Client
|
|
function *fv1.Function
|
|
httpTrigger *fv1.HTTPTrigger
|
|
functionMap map[string]*fv1.Function
|
|
fnWeightDistributionList []functionWeightDistribution
|
|
tsRoundTripperParams *tsRoundTripperParams
|
|
isDebugEnv bool
|
|
svcAddrUpdateThrottler *throttler.Throttler
|
|
functionTimeoutMap map[k8stypes.UID]int
|
|
unTapServiceTimeout time.Duration
|
|
}
|
|
|
|
tsRoundTripperParams struct {
|
|
timeout time.Duration
|
|
timeoutExponent int
|
|
disableKeepAlive bool
|
|
keepAliveTime time.Duration
|
|
|
|
// maxRetires is the max times for RetryingRoundTripper to retry a request.
|
|
// Default maxRetries is 10, which means router will retry for
|
|
// up to 10 times and abort it if still not succeeded.
|
|
maxRetries int
|
|
|
|
// svcAddrRetryCount is the max times for RetryingRoundTripper to retry with a specific service address
|
|
// Router sends requests to a specific service address for each function.
|
|
// A service address is considered as an invalid one if amount of non-network
|
|
// errors router received is higher than svcAddrRetryCount.
|
|
// Try to get a new one from executor.
|
|
// Default svcAddrRetryCount is 5.
|
|
svcAddrRetryCount int
|
|
}
|
|
|
|
// RetryingRoundTripper is a layer on top of http.DefaultTransport, with retries.
|
|
RetryingRoundTripper struct {
|
|
logger *zap.Logger
|
|
funcHandler *functionHandler
|
|
funcTimeout time.Duration
|
|
closeContextFunc *context.CancelFunc
|
|
serviceURL *url.URL
|
|
urlFromCache bool
|
|
totalRetry int
|
|
}
|
|
|
|
// To keep the request body open during retries, we create an interface with Close operation being a no-op.
|
|
// Details : https://github.com/flynn/flynn/pull/875
|
|
fakeCloseReadCloser struct {
|
|
io.ReadCloser
|
|
}
|
|
|
|
svcEntryRecord struct {
|
|
svcURL *url.URL
|
|
cacheHit bool
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
// just seeding the random number for getting the canary function
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
func (w *fakeCloseReadCloser) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (w *fakeCloseReadCloser) RealClose() error {
|
|
if w.ReadCloser == nil {
|
|
return nil
|
|
}
|
|
return w.ReadCloser.Close()
|
|
}
|
|
|
|
// RoundTrip is a custom transport with retries for http requests that forwards the request to the right serviceUrl, obtained
|
|
// from router's cache or from executor if router entry is stale.
|
|
//
|
|
// It first checks if the service address for this function came from router's cache.
|
|
// If it didn't, it makes a request to executor to get a new service for function. If that succeeds, it adds the address
|
|
// to it's cache and makes a request to that address with transport.RoundTrip call.
|
|
// Initial requests to new k8s services sometimes seem to fail, but retries work. So, it retries with an exponential
|
|
// back-off for maxRetries times.
|
|
//
|
|
// Else if it came from the cache, it makes a transport.RoundTrip with that cached address. If the response received is
|
|
// a network dial error (which means that the pod doesn't exist anymore), it removes the cache entry and makes a request
|
|
// to executor to get a new service for function. It then retries transport.RoundTrip with the new address.
|
|
//
|
|
// At any point in time, if the response received from transport.RoundTrip is other than dial network error, it is
|
|
// relayed as-is to the user, without any retries.
|
|
//
|
|
// While this RoundTripper handles the case where a previously cached address of the function pod isn't valid anymore
|
|
// (probably because the pod got deleted somehow), by making a request to executor to get a new service for this function,
|
|
// it doesn't handle a case where a newly specialized pod gets deleted just after the GetServiceForFunction succeeds.
|
|
// In such a case, the RoundTripper will retry requests against the new address and give up after maxRetries.
|
|
// However, the subsequent http call for this function will ensure the cache is invalidated.
|
|
//
|
|
// If GetServiceForFunction returns an error or if RoundTripper exits with an error, it get's translated into 502
|
|
// inside ServeHttp function of the reverseProxy.
|
|
// Earlier, GetServiceForFunction was called inside handler function and fission explicitly set http status code to 500
|
|
// if it returned an error.
|
|
func (roundTripper *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
// set the timeout for transport context
|
|
roundTripper.addForwardedHostHeader(req)
|
|
transport := roundTripper.getDefaultTransport()
|
|
ocRoundTripper := &ochttp.Transport{Base: transport}
|
|
|
|
executingTimeout := roundTripper.funcHandler.tsRoundTripperParams.timeout
|
|
|
|
// wrap the req.Body with another ReadCloser interface.
|
|
if req.Body != nil {
|
|
req.Body = &fakeCloseReadCloser{req.Body}
|
|
}
|
|
|
|
// close req body
|
|
defer func() {
|
|
if req.Body != nil {
|
|
req.Body.(*fakeCloseReadCloser).RealClose()
|
|
}
|
|
}()
|
|
|
|
roundTripper.logger.Debug("request headers", zap.Any("headers", req.Header))
|
|
|
|
// The reason for request failure may vary from case to case.
|
|
// After some investigation, found most of the failure are due to
|
|
// network timeout or target function is under heavy workload. In
|
|
// such cases, if router keeps trying to get new function service
|
|
// will increase executor burden and cause 502 error.
|
|
//
|
|
// The "retryCounter" was introduced to solve this problem by retrying
|
|
// requests for "limited threshold". Once a request's retryCounter higher
|
|
// than the predefined threshold, reset retryCounter and remove service
|
|
// cache, then retry to get new svc record from executor again.
|
|
var retryCounter int
|
|
var err error
|
|
var fnMeta = &roundTripper.funcHandler.function.ObjectMeta
|
|
|
|
for i := 0; i < roundTripper.funcHandler.tsRoundTripperParams.maxRetries; i++ {
|
|
// set service url of target service of request only when
|
|
// trying to get new service url from cache/executor.
|
|
if retryCounter == 0 {
|
|
// get function service url from cache or executor
|
|
roundTripper.serviceURL, roundTripper.urlFromCache, err = roundTripper.funcHandler.getServiceEntry()
|
|
if err != nil {
|
|
// We might want a specific error code or header for fission failures as opposed to
|
|
// user function bugs.
|
|
statusCode, errMsg := ferror.GetHTTPError(err)
|
|
if statusCode == http.StatusTooManyRequests {
|
|
return nil, err
|
|
}
|
|
if roundTripper.funcHandler.isDebugEnv {
|
|
return &http.Response{
|
|
StatusCode: statusCode,
|
|
Proto: req.Proto,
|
|
ProtoMajor: req.ProtoMajor,
|
|
ProtoMinor: req.ProtoMinor,
|
|
Body: ioutil.NopCloser(bytes.NewBufferString(errMsg)),
|
|
ContentLength: int64(len(errMsg)),
|
|
Request: req,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}
|
|
return nil, ferror.MakeError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
if roundTripper.serviceURL == nil {
|
|
roundTripper.logger.Warn("serviceURL is empty for function, retrying", zap.Duration("executingTimeout", executingTimeout))
|
|
time.Sleep(executingTimeout)
|
|
executingTimeout = executingTimeout * time.Duration(roundTripper.funcHandler.tsRoundTripperParams.timeoutExponent)
|
|
continue
|
|
}
|
|
if roundTripper.funcHandler.function.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType == fv1.ExecutorTypePoolmgr {
|
|
defer func(fn *fv1.Function, serviceURL *url.URL) {
|
|
go roundTripper.funcHandler.unTapService(fn, serviceURL) //nolint errcheck
|
|
}(roundTripper.funcHandler.function, roundTripper.serviceURL)
|
|
}
|
|
|
|
// modify the request to reflect the service url
|
|
// this service url comes from executor response
|
|
req.URL.Scheme = roundTripper.serviceURL.Scheme
|
|
req.URL.Host = roundTripper.serviceURL.Host
|
|
|
|
// With addition of routing support from functions if function supports routing,
|
|
// 1. we trim prefix url and forward request
|
|
// 2. otherwise we just keep default request to root path
|
|
// We leave the query string intact (req.URL.RawQuery) where as we manipuate
|
|
// req.URL.Path according to httpTrigger specification.
|
|
prefixTrim := ""
|
|
functionURL := utils.UrlForFunction(fnMeta.Name, fnMeta.Namespace)
|
|
if roundTripper.funcHandler.httpTrigger != nil && roundTripper.funcHandler.httpTrigger.Spec.Prefix != nil && *roundTripper.funcHandler.httpTrigger.Spec.Prefix != "" {
|
|
prefixTrim = *roundTripper.funcHandler.httpTrigger.Spec.Prefix
|
|
} else if strings.HasPrefix(req.URL.Path, functionURL) {
|
|
prefixTrim = functionURL
|
|
}
|
|
if prefixTrim != "" {
|
|
req.URL.Path = strings.TrimPrefix(req.URL.Path, prefixTrim)
|
|
if !strings.HasPrefix(req.URL.Path, "/") {
|
|
req.URL.Path = "/" + req.URL.Path
|
|
}
|
|
} else {
|
|
req.URL.Path = "/"
|
|
}
|
|
|
|
// Overwrite request host with internal host,
|
|
// or request will be blocked in some situations
|
|
// (e.g. istio-proxy)
|
|
req.Host = roundTripper.serviceURL.Host
|
|
}
|
|
|
|
// over-riding default settings.
|
|
transport.DialContext = (&net.Dialer{
|
|
Timeout: executingTimeout,
|
|
KeepAlive: roundTripper.funcHandler.tsRoundTripperParams.keepAliveTime,
|
|
}).DialContext
|
|
|
|
// Do NOT assign returned request to "req"
|
|
// because the request used in the last round
|
|
// will be canceled when calling setContext.
|
|
newReq := roundTripper.setContext(req)
|
|
|
|
// forward the request to the function service
|
|
resp, err := ocRoundTripper.RoundTrip(newReq)
|
|
if err == nil {
|
|
// return response back to user
|
|
return resp, nil
|
|
}
|
|
|
|
roundTripper.totalRetry++
|
|
|
|
if i >= roundTripper.funcHandler.tsRoundTripperParams.maxRetries-1 {
|
|
// return here if we are in the last round
|
|
roundTripper.logger.Error("error getting response from function",
|
|
zap.String("function_name", fnMeta.Name),
|
|
zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
// if transport.RoundTrip returns a non-network dial error, then relay it back to user
|
|
netErr := network.Adapter(err)
|
|
|
|
// dial timeout or dial network errors goes here
|
|
var isNetDialErr, isNetTimeoutErr bool
|
|
if netErr != nil {
|
|
isNetDialErr = netErr.IsDialError()
|
|
isNetTimeoutErr = netErr.IsTimeoutError()
|
|
}
|
|
|
|
// if transport.RoundTrip returns a non-network dial error (e.g. "context canceled"), then relay it back to user
|
|
if !isNetDialErr {
|
|
roundTripper.logger.Error("encountered non-network dial error", zap.Error(err))
|
|
return resp, err
|
|
}
|
|
|
|
// close response body before entering next loop
|
|
if resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
|
|
// Check whether an error is an timeout error ("dial tcp i/o timeout").
|
|
if isNetTimeoutErr {
|
|
roundTripper.logger.Debug("request errored out - backing off before retrying",
|
|
zap.String("url", req.URL.Host),
|
|
zap.String("function_name", fnMeta.Name),
|
|
zap.Error(err))
|
|
retryCounter++
|
|
}
|
|
|
|
// If it's not a timeout error or retryCounter exceeded pre-defined threshold,
|
|
if retryCounter >= roundTripper.funcHandler.tsRoundTripperParams.svcAddrRetryCount {
|
|
roundTripper.logger.Debug(fmt.Sprintf(
|
|
"retry counter exceeded pre-defined threshold of %v",
|
|
roundTripper.funcHandler.tsRoundTripperParams.svcAddrRetryCount))
|
|
if roundTripper.urlFromCache {
|
|
roundTripper.funcHandler.removeServiceEntryFromCache()
|
|
}
|
|
retryCounter = 0
|
|
}
|
|
|
|
roundTripper.logger.Debug("Backing off before retrying", zap.Any("backoff_time", executingTimeout), zap.Error(err))
|
|
time.Sleep(executingTimeout)
|
|
executingTimeout = executingTimeout * time.Duration(roundTripper.funcHandler.tsRoundTripperParams.timeoutExponent)
|
|
}
|
|
|
|
e := errors.New("Unable to get service url for connection")
|
|
roundTripper.logger.Error(e.Error(), zap.String("function_name", fnMeta.Name))
|
|
return nil, e
|
|
}
|
|
|
|
// getDefaultTransport returns a pointer to new copy of http.Transport object to prevent
|
|
// the value of http.DefaultTransport from being changed by goroutines.
|
|
func (roundTripper RetryingRoundTripper) getDefaultTransport() *http.Transport {
|
|
// The transport setup here follows the configurations of http.DefaultTransport
|
|
// but without Dialer since we will change it later.
|
|
return &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
// Default disables caching, Please refer to issue and specifically comment:
|
|
// https://github.com/fission/fission/issues/723#issuecomment-398781995
|
|
// You can change it by setting environment variable "ROUTER_ROUND_TRIP_DISABLE_KEEP_ALIVE"
|
|
// of router or helm variable "disableKeepAlive" before installation to false.
|
|
DisableKeepAlives: roundTripper.funcHandler.tsRoundTripperParams.disableKeepAlive,
|
|
}
|
|
}
|
|
|
|
// setContext returns a shallow copy of request with a new timeout context.
|
|
func (roundTripper *RetryingRoundTripper) setContext(req *http.Request) *http.Request {
|
|
if roundTripper.closeContextFunc != nil {
|
|
(*roundTripper.closeContextFunc)()
|
|
}
|
|
// pass request context as parent context for the case
|
|
// that user aborts connection before timeout. Otherwise,
|
|
// the request won't be canceled until the deadline exceeded
|
|
// which may be a potential security issue.
|
|
ctx, closeCtx := context.WithTimeout(req.Context(), roundTripper.funcTimeout)
|
|
roundTripper.closeContextFunc = &closeCtx
|
|
|
|
return req.WithContext(ctx)
|
|
}
|
|
|
|
// closeContext closes the context to release resources.
|
|
func (roundTripper *RetryingRoundTripper) closeContext() {
|
|
if roundTripper.closeContextFunc != nil {
|
|
(*roundTripper.closeContextFunc)()
|
|
}
|
|
}
|
|
|
|
func (fh *functionHandler) tapService(fn *fv1.Function, serviceURL *url.URL) {
|
|
if fh.executor == nil {
|
|
return
|
|
}
|
|
fh.executor.TapService(fn.ObjectMeta, fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType, serviceURL)
|
|
}
|
|
|
|
func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *http.Request) {
|
|
if fh.httpTrigger != nil && fh.httpTrigger.Spec.FunctionReference.Type == fv1.FunctionReferenceTypeFunctionWeights {
|
|
// canary deployment. need to determine the function to send request to now
|
|
fn := getCanaryBackend(fh.functionMap, fh.fnWeightDistributionList)
|
|
if fn == nil {
|
|
fh.logger.Error("could not get canary backend",
|
|
zap.Any("fnMap", fh.functionMap),
|
|
zap.Any("distributionList", fh.fnWeightDistributionList))
|
|
// TODO : write error to responseWrite and return response
|
|
return
|
|
}
|
|
fh.function = fn
|
|
fh.logger.Debug("chosen function backend's metadata", zap.Any("metadata", fh.function))
|
|
}
|
|
|
|
// url path
|
|
setPathInfoToHeader(request)
|
|
|
|
// system params
|
|
setFunctionMetadataToHeader(&fh.function.ObjectMeta, request)
|
|
|
|
director := func(req *http.Request) {
|
|
if _, ok := req.Header["User-Agent"]; !ok {
|
|
// explicitly disable User-Agent so it's not set to default value
|
|
req.Header.Set("User-Agent", "")
|
|
}
|
|
}
|
|
|
|
fnTimeout := fh.functionTimeoutMap[fh.function.ObjectMeta.GetUID()]
|
|
if fnTimeout == 0 {
|
|
fnTimeout = fv1.DEFAULT_FUNCTION_TIMEOUT
|
|
}
|
|
|
|
rrt := &RetryingRoundTripper{
|
|
logger: fh.logger.Named("roundtripper"),
|
|
funcHandler: &fh,
|
|
funcTimeout: time.Duration(fnTimeout) * time.Second,
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
proxy := &httputil.ReverseProxy{
|
|
Director: director,
|
|
Transport: rrt,
|
|
ErrorHandler: fh.getProxyErrorHandler(start, rrt),
|
|
ModifyResponse: func(resp *http.Response) error {
|
|
go fh.collectFunctionMetric(start, rrt, request, resp)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
defer func() {
|
|
// If the context is closed when RoundTrip returns, client may receive
|
|
// truncated response body due to "context canceled" error. To avoid
|
|
// this, we need to close request context after proxy.ServeHTTP finished.
|
|
//
|
|
// NOTE: rrt.closeContext() must be put in the defer function; otherwise,
|
|
// reverseProxy may panic when failed to write response and the context
|
|
// will not be closed.
|
|
//
|
|
// ref: https://github.com/golang/go/issues/28239
|
|
rrt.closeContext()
|
|
}()
|
|
|
|
proxy.ServeHTTP(responseWriter, request)
|
|
}
|
|
|
|
// findCeil picks a function from the functionWeightDistribution list based on the
|
|
// random number generated. It uses the prefix calculated for the function weights.
|
|
func findCeil(randomNumber int, wtDistrList []functionWeightDistribution) string {
|
|
low := 0
|
|
high := len(wtDistrList) - 1
|
|
|
|
for {
|
|
if low >= high {
|
|
break
|
|
}
|
|
|
|
mid := low + high/2
|
|
if randomNumber >= wtDistrList[mid].sumPrefix {
|
|
low = mid + 1
|
|
} else {
|
|
high = mid
|
|
}
|
|
}
|
|
|
|
if wtDistrList[low].sumPrefix >= randomNumber {
|
|
return wtDistrList[low].name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// picks a function to route to based on a random number generated
|
|
func getCanaryBackend(fnMap map[string]*fv1.Function, fnWtDistributionList []functionWeightDistribution) *fv1.Function {
|
|
randomNumber := rand.Intn(fnWtDistributionList[len(fnWtDistributionList)-1].sumPrefix + 1)
|
|
fnName := findCeil(randomNumber, fnWtDistributionList)
|
|
return fnMap[fnName]
|
|
}
|
|
|
|
// addForwardedHostHeader add "forwarded host" to request header
|
|
func (roundTripper RetryingRoundTripper) addForwardedHostHeader(req *http.Request) {
|
|
// for more detailed information, please visit:
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
|
|
|
|
if len(req.Header.Get(FORWARDED)) > 0 || len(req.Header.Get(X_FORWARDED_HOST)) > 0 {
|
|
// forwarded headers were set by external proxy, leave them intact
|
|
return
|
|
}
|
|
|
|
// Format of req.Host is <host>:<port>
|
|
// We need to extract hostname from it, than
|
|
// check whether a host is ipv4 or ipv6 or FQDN
|
|
reqURL := fmt.Sprintf("%s://%s", req.Proto, req.Host)
|
|
u, err := url.Parse(reqURL)
|
|
if err != nil {
|
|
roundTripper.logger.Error("error parsing request url while adding forwarded host headers",
|
|
zap.Error(err),
|
|
zap.String("url", reqURL))
|
|
return
|
|
}
|
|
|
|
var host string
|
|
|
|
// ip will be nil if the Hostname is a FQDN string
|
|
ip := net.ParseIP(u.Hostname())
|
|
|
|
// ip == nil -> hostname is FQDN instead of ip address
|
|
// The order of To4() and To16() here matters, To16() will
|
|
// converts an IPv4 address to IPv6 format address and may
|
|
// cause router append wrong host value to header. To prevent
|
|
// this we need to check whether To4() is nil first.
|
|
if ip == nil || (ip != nil && ip.To4() != nil) {
|
|
host = fmt.Sprintf(`host=%s;`, req.Host)
|
|
} else if ip != nil && ip.To16() != nil {
|
|
// For the "Forwarded" header, if a host is an IPv6 address it should be quoted
|
|
host = fmt.Sprintf(`host="%s";`, req.Host)
|
|
}
|
|
|
|
req.Header.Set(FORWARDED, host)
|
|
req.Header.Set(X_FORWARDED_HOST, req.Host)
|
|
}
|
|
|
|
// unTapservice marks the serviceURL in executor's cache as inactive, so that it can be reused
|
|
func (fh functionHandler) unTapService(fn *fv1.Function, serviceUrl *url.URL) error {
|
|
fh.logger.Info("UnTapService Called")
|
|
ctx, cancel := context.WithTimeout(context.Background(), fh.unTapServiceTimeout)
|
|
defer cancel()
|
|
err := fh.executor.UnTapService(ctx, fn.ObjectMeta, fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType, serviceUrl)
|
|
if err != nil {
|
|
statusCode, errMsg := ferror.GetHTTPError(err)
|
|
fh.logger.Error("error from UnTapService",
|
|
zap.Error(err),
|
|
zap.String("error_message", errMsg),
|
|
zap.Any("function", fh.function),
|
|
zap.Int("status_code", statusCode))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getServiceEntryFromCache returns service url entry returns from cache
|
|
func (fh functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err error) {
|
|
// cache lookup to get serviceUrl
|
|
serviceUrl, err = fh.fmap.lookup(&fh.function.ObjectMeta)
|
|
if err != nil {
|
|
var errMsg string
|
|
|
|
e, ok := err.(ferror.Error)
|
|
if !ok {
|
|
errMsg = fmt.Sprintf("Unknown error when looking up service entry: %v", err)
|
|
} else {
|
|
// Ignore ErrorNotFound error here, it's an expected error,
|
|
// roundTripper will try to get service url later.
|
|
if e.Code == ferror.ErrorNotFound {
|
|
return nil, nil
|
|
}
|
|
errMsg = fmt.Sprintf("Error getting function %v;s service entry from cache: %v", fh.function.ObjectMeta.Name, err)
|
|
}
|
|
return nil, ferror.MakeError(http.StatusInternalServerError, errMsg)
|
|
}
|
|
return serviceUrl, nil
|
|
}
|
|
|
|
// addServiceEntryToCache add service url entry to cache
|
|
func (fn functionHandler) addServiceEntryToCache(serviceUrl *url.URL) {
|
|
fn.fmap.assign(&fn.function.ObjectMeta, serviceUrl)
|
|
}
|
|
|
|
// removeServiceEntryFromCache removes service url entry from cache
|
|
func (fn functionHandler) removeServiceEntryFromCache() {
|
|
fn.fmap.remove(&fn.function.ObjectMeta)
|
|
}
|
|
|
|
func (fh functionHandler) getServiceEntryFromExecutor() (serviceUrl *url.URL, err error) {
|
|
// send a request to executor to specialize a new pod
|
|
fh.logger.Debug("function timeout specified", zap.Int("timeout", fh.function.Spec.FunctionTimeout))
|
|
timeout := 30 * time.Second
|
|
if fh.function.Spec.FunctionTimeout > 0 {
|
|
timeout = time.Second * time.Duration(fh.function.Spec.FunctionTimeout)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
service, err := fh.executor.GetServiceForFunction(ctx, fh.function)
|
|
if err != nil {
|
|
statusCode, errMsg := ferror.GetHTTPError(err)
|
|
fh.logger.Error("error from GetServiceForFunction",
|
|
zap.Error(err),
|
|
zap.String("error_message", errMsg),
|
|
zap.Any("function", fh.function),
|
|
zap.Int("status_code", statusCode))
|
|
return nil, err
|
|
}
|
|
// parse the address into url
|
|
svcURL, err := url.Parse(fmt.Sprintf("http://%v", service))
|
|
if err != nil {
|
|
fh.logger.Error("error parsing service url",
|
|
zap.Error(err),
|
|
zap.String("service_url", svcURL.String()))
|
|
return nil, err
|
|
}
|
|
return svcURL, err
|
|
}
|
|
|
|
// getServiceEntryFromExecutor returns service url entry returns from executor
|
|
func (fh functionHandler) getServiceEntry() (svcURL *url.URL, cacheHit bool, err error) {
|
|
if fh.function.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType == fv1.ExecutorTypePoolmgr {
|
|
svcURL, err = fh.getServiceEntryFromExecutor()
|
|
return svcURL, false, err
|
|
}
|
|
// Check if service URL present in cache
|
|
svcURL, err = fh.getServiceEntryFromCache()
|
|
if err == nil && svcURL != nil {
|
|
return svcURL, true, nil
|
|
} else if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
fnMeta := &fh.function.ObjectMeta
|
|
recordObj, err := fh.svcAddrUpdateThrottler.RunOnce(
|
|
crd.CacheKey(fnMeta),
|
|
func(firstToTheLock bool) (interface{}, error) {
|
|
if !firstToTheLock {
|
|
svcURL, err := fh.getServiceEntryFromCache()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return svcEntryRecord{svcURL: svcURL, cacheHit: true}, err
|
|
}
|
|
svcURL, err = fh.getServiceEntryFromExecutor()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fh.addServiceEntryToCache(svcURL)
|
|
return svcEntryRecord{
|
|
svcURL: svcURL,
|
|
cacheHit: false,
|
|
}, nil
|
|
},
|
|
)
|
|
|
|
record, ok := recordObj.(svcEntryRecord)
|
|
if !ok {
|
|
return nil, false, fmt.Errorf("received unknown service record type")
|
|
}
|
|
return record.svcURL, record.cacheHit, err
|
|
}
|
|
|
|
// getProxyErrorHandler returns a reverse proxy error handler
|
|
func (fh functionHandler) getProxyErrorHandler(start time.Time, rrt *RetryingRoundTripper) func(rw http.ResponseWriter, req *http.Request, err error) {
|
|
return func(rw http.ResponseWriter, req *http.Request, err error) {
|
|
var status int
|
|
var msg string
|
|
switch err {
|
|
case context.Canceled:
|
|
// 499 CLIENT CLOSED REQUEST
|
|
// A non-standard status code introduced by nginx for the case
|
|
// when a client closes the connection while nginx is processing the request.
|
|
// Reference: https://httpstatuses.com/499
|
|
status = 499
|
|
msg = "client closes the connection"
|
|
fh.logger.Debug(msg, zap.Any("function", fh.function), zap.Any("request_header", req.Header))
|
|
case context.DeadlineExceeded:
|
|
status = http.StatusGatewayTimeout
|
|
msg = "function not responses before the timeout"
|
|
fh.logger.Error(msg, zap.Any("function", fh.function), zap.Any("request_header", req.Header))
|
|
default:
|
|
code, msg := ferror.GetHTTPError(err)
|
|
status = code
|
|
msg = "error sending request to function"
|
|
fh.logger.Error(msg, zap.Error(err), zap.Any("function", fh.function), zap.Any("request_header", req.Header), zap.Any("code", code))
|
|
}
|
|
|
|
go fh.collectFunctionMetric(start, rrt, req, &http.Response{
|
|
StatusCode: status,
|
|
ContentLength: req.ContentLength,
|
|
})
|
|
|
|
// TODO: return error message that contains traceable UUID back to user. Issue #693
|
|
rw.WriteHeader(status)
|
|
_, err = rw.Write([]byte(msg))
|
|
if err != nil {
|
|
fh.logger.Error(
|
|
"error writing HTTP response",
|
|
zap.Error(err),
|
|
zap.Any("function", fh.function),
|
|
zap.Any("request_header", req.Header),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fh functionHandler) collectFunctionMetric(start time.Time, rrt *RetryingRoundTripper, req *http.Request, resp *http.Response) {
|
|
duration := time.Since(start)
|
|
|
|
// Metrics stuff
|
|
funcMetricLabels := &functionLabels{
|
|
namespace: fh.function.ObjectMeta.Namespace,
|
|
name: fh.function.ObjectMeta.Name,
|
|
}
|
|
httpMetricLabels := &httpLabels{
|
|
method: req.Method,
|
|
}
|
|
if fh.httpTrigger != nil {
|
|
httpMetricLabels.host = fh.httpTrigger.Spec.Host
|
|
if fh.httpTrigger.Spec.Prefix != nil && *fh.httpTrigger.Spec.Prefix != "" {
|
|
httpMetricLabels.path = *fh.httpTrigger.Spec.Prefix
|
|
} else {
|
|
httpMetricLabels.path = fh.httpTrigger.Spec.RelativeURL
|
|
}
|
|
}
|
|
|
|
// Track metrics
|
|
httpMetricLabels.code = resp.StatusCode
|
|
funcMetricLabels.cached = rrt.urlFromCache
|
|
|
|
functionCallCompleted(funcMetricLabels, httpMetricLabels,
|
|
duration, duration, resp.ContentLength)
|
|
|
|
// tapService before invoking roundTrip for the serviceUrl
|
|
if rrt.urlFromCache {
|
|
fh.tapService(fh.function, rrt.serviceURL)
|
|
}
|
|
|
|
fh.logger.Debug("Request complete", zap.String("function", fh.function.ObjectMeta.Name),
|
|
zap.Int("retry", rrt.totalRetry), zap.Duration("total-time", duration),
|
|
zap.Int64("content-length", resp.ContentLength))
|
|
}
|