Invalidate stale router cache entry with podIP's for deleted pods. (#546)

The router's cache entry for a function might become stale if the pod that had the function specialized gets deleted somehow. In such a case, we'd retry getting a new service for the function from executor and retry forwarding the user request to the newly created service.
This commit is contained in:
smruthi2187
2018-04-05 13:47:47 -07:00
committed by GitHub
parent db2f620121
commit 8014c83b02
10 changed files with 302 additions and 90 deletions
+125 -80
View File
@@ -28,6 +28,7 @@ import (
"github.com/gorilla/mux"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fission/fission"
executorClient "github.com/fission/fission/executor/client"
)
@@ -37,45 +38,139 @@ type functionHandler struct {
function *metav1.ObjectMeta
}
func (fh *functionHandler) getServiceForFunction() (*url.URL, error) {
// call executor, get a url for a function
svcName, err := fh.executor.GetServiceForFunction(fh.function)
if err != nil {
return nil, err
}
svcUrl, err := url.Parse(fmt.Sprintf("http://%v", svcName))
if err != nil {
return nil, err
}
return svcUrl, nil
}
// A layer on top of http.DefaultTransport, with retries.
type RetryingRoundTripper struct {
maxRetries int
initalTimeout time.Duration
maxRetries int
initialTimeout time.Duration
funcHandler *functionHandler
}
func (rrt RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
timeout := rrt.initalTimeout
transport := http.DefaultTransport.(*http.Transport)
// 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) (resp *http.Response, err error) {
var needExecutor, serviceUrlFromExecutor bool
var serviceUrl *url.URL
// Do max-1 retries; the last one uses default transport timeouts
for i := rrt.maxRetries - 1; i > 0; i-- {
// update timeout in transport
// set the timeout for transport context
timeout := roundTripper.initialTimeout
transport := http.DefaultTransport.(*http.Transport)
defer transport.CloseIdleConnections()
// cache lookup to get serviceUrl
serviceUrl, err = roundTripper.funcHandler.fmap.lookup(roundTripper.funcHandler.function)
if err != nil || serviceUrl == nil {
// cache miss or nil entry in cache
needExecutor = true
}
for i := 0; i < roundTripper.maxRetries-1; i++ {
if needExecutor {
log.Printf("Calling getServiceForFunction for function: %s", roundTripper.funcHandler.function.Name)
// send a request to executor to specialize a new pod
service, err := roundTripper.funcHandler.executor.GetServiceForFunction(
roundTripper.funcHandler.function)
if err != nil {
// We might want a specific error code or header for fission failures as opposed to
// user function bugs.
return nil, err
}
// parse the address into url
serviceUrl, err = url.Parse(fmt.Sprintf("http://%v", service))
if err != nil {
return nil, err
}
// add the address in router's cache
roundTripper.funcHandler.fmap.assign(roundTripper.funcHandler.function, serviceUrl)
// flag denotes that service was not obtained from cache, instead, created just now by executor
serviceUrlFromExecutor = true
}
// modify the request to reflect the service url
// this service url may have come from the cache lookup or from executor response
req.URL.Scheme = serviceUrl.Scheme
req.URL.Host = serviceUrl.Host
// To keep the function run container simple, it
// doesn't do any routing. In the future if we have
// multiple functions per container, we could use the
// function metadata here.
// leave the query string intact (req.URL.RawQuery)
req.URL.Path = "/"
// Overwrite request host with internal host,
// or request will be blocked in some situations
// (e.g. istio-proxy)
req.Host = serviceUrl.Host
// over-riding default settings.
transport.DialContext = (&net.Dialer{
Timeout: timeout,
KeepAlive: 30 * time.Second,
}).DialContext
resp, err := transport.RoundTrip(req)
// forward the request to the function service
resp, err = transport.RoundTrip(req)
if err == nil {
// if transport.RoundTrip succeeds and it was a cached entry, then tapService
if !serviceUrlFromExecutor {
go roundTripper.funcHandler.tapService(serviceUrl)
}
// return response back to user
return resp, nil
}
timeout *= time.Duration(2)
log.Printf("Retrying request to %v in %v", req.URL.Host, timeout)
time.Sleep(timeout)
// if transport.RoundTrip returns a non-network dial error, then relay it back to user
if !fission.IsNetworkDialError(err) {
return resp, err
}
// means its a newly created service and it returned a network dial error.
// just retry after backing off for timeout period.
if serviceUrlFromExecutor {
log.Printf("request to %s errored out. backing off for %v before retrying",
req.URL.Host, timeout)
timeout *= time.Duration(2)
time.Sleep(timeout)
needExecutor = false
continue
} else {
// if transport.RoundTrip returns a network dial error and serviceUrl was from cache,
// it means, the entry in router cache is stale, so invalidate it.
// also set needExecutor to true so a new service can be requested for function.
log.Printf("request to %s errored out. removing function : %s from router's cache "+
"and requesting a new service for function",
req.URL.Host, roundTripper.funcHandler.function.Name)
roundTripper.funcHandler.fmap.remove(roundTripper.funcHandler.function)
needExecutor = true
}
}
// finally, one more retry with the default timeout
@@ -90,8 +185,6 @@ func (fh *functionHandler) tapService(serviceUrl *url.URL) {
}
func (fh *functionHandler) handler(responseWriter http.ResponseWriter, request *http.Request) {
reqStartTime := time.Now()
// retrieve url params and add them to request header
vars := mux.Vars(request)
for k, v := range vars {
@@ -101,71 +194,23 @@ func (fh *functionHandler) handler(responseWriter http.ResponseWriter, request *
// System Params
MetadataToHeaders(HEADERS_FISSION_FUNCTION_PREFIX, fh.function, request)
// cache lookup
serviceUrl, err := fh.fmap.lookup(fh.function)
if err != nil {
// Cache miss: request the Pool Manager to make a new service.
log.Printf("Not cached, getting new service for %v", fh.function)
var poolErr error
serviceUrl, poolErr = fh.getServiceForFunction()
if poolErr != nil {
log.Printf("Failed to get service for function %v: %v", fh.function.Name, poolErr)
// We might want a specific error code or header for fission
// failures as opposed to user function bugs.
http.Error(responseWriter, "Internal server error (fission)", 500)
return
}
// add it to the map
fh.fmap.assign(fh.function, serviceUrl)
} else {
// if we're using our cache, asynchronously tell
// executor we're using this service
go fh.tapService(serviceUrl)
}
// Proxy off our request to the serviceUrl, and send the response back.
// TODO: As an optimization we may want to cache proxies too -- this might get us
// connection reuse and possibly better performance
director := func(req *http.Request) {
log.Printf("Proxying request for %v to %v", req.URL, serviceUrl.Host)
// send this request to serviceurl
req.URL.Scheme = serviceUrl.Scheme
req.URL.Host = serviceUrl.Host
// To keep the function run container simple, it
// doesn't do any routing. In the future if we have
// multiple functions per container, we could use the
// function metadata here.
req.URL.Path = "/"
// Overwrite request host with internal host,
// or request will be blocked in some situations
// (e.g. istio-proxy)
req.Host = serviceUrl.Host
// leave the query string intact (req.URL.RawQuery)
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
}
// Initial requests to new k8s services sometimes seem to
// fail, but retries work. So use a transport that does retries.
proxy := &httputil.ReverseProxy{
Director: director,
Transport: RetryingRoundTripper{
maxRetries: 10,
initalTimeout: 50 * time.Millisecond,
Transport: &RetryingRoundTripper{
initialTimeout: 50 * time.Millisecond,
maxRetries: 10,
funcHandler: fh,
},
}
delay := time.Since(reqStartTime)
if delay > 100*time.Millisecond {
log.Printf("Request delay for %v: %v", serviceUrl, delay)
}
proxy.ServeHTTP(responseWriter, request)
}