Ensure handling for specialization failure in pool manager (#2788)

* Add fixes for failure in specialization
* reduce specialization in progress and remove expired requests from queue when specialization is timed out
* rename markSpecializationFailure and remove logger from the queue
* refactor clean up code in api.go and add test case for queue

Details:

- Cleanup svc waiting for the counter in the pool manager if specialization fails
- Cleanup active requests counter in pool manager if client exists the demand for function service while we have allocated function service
- Consider specialization timeout if pod ready timeout > specialization timeout in waiting for ready pod. We also consider if the request to choosePod is cancelled.
- We ensure if we have requests waiting for service requests but if there is no pod in the specialization we clean up those.
---------

Signed-off-by: Sanket Sudake <sanketsudake@gmail.com>
Co-authored-by: Pranoy Kundu <pranoy1998k@gmail.com>
This commit is contained in:
Sanket Sudake
2023-05-11 21:03:57 +05:30
committed by GitHub
co-authored by Pranoy Kundu
parent 31c81e132e
commit 6c431e4d9b
10 changed files with 177 additions and 2 deletions
+19
View File
@@ -31,8 +31,10 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
fv1 "github.com/fission/fission/pkg/apis/core/v1" fv1 "github.com/fission/fission/pkg/apis/core/v1"
"github.com/fission/fission/pkg/crd"
ferror "github.com/fission/fission/pkg/error" ferror "github.com/fission/fission/pkg/error"
"github.com/fission/fission/pkg/executor/client" "github.com/fission/fission/pkg/executor/client"
"github.com/fission/fission/pkg/executor/fscache"
"github.com/fission/fission/pkg/utils/httpserver" "github.com/fission/fission/pkg/utils/httpserver"
"github.com/fission/fission/pkg/utils/metrics" "github.com/fission/fission/pkg/utils/metrics"
otelUtils "github.com/fission/fission/pkg/utils/otel" otelUtils "github.com/fission/fission/pkg/utils/otel"
@@ -148,7 +150,24 @@ func (executor *Executor) getServiceForFunction(ctx context.Context, fn *fv1.Fun
respChan: respChan, respChan: respChan,
} }
resp := <-respChan resp := <-respChan
cleanUp := func(funcSvc *fscache.FuncSvc) {
et, ok := executor.executorTypes[fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType]
if !ok {
executor.logger.Error("unknown executor type received in function service", zap.Any("executor", funcSvc.Executor))
return
}
if funcSvc != nil {
et.UnTapService(ctx, crd.CacheKey(funcSvc.Function), resp.funcSvc.Address)
} else {
et.MarkSpecializationFailure(ctx, crd.CacheKey(&fn.ObjectMeta))
}
}
if errors.Is(ctx.Err(), context.Canceled) {
cleanUp(resp.funcSvc)
return "", ferror.MakeError(499, "client leave early in the process of getServiceForFunction")
}
if resp.err != nil { if resp.err != nil {
cleanUp(resp.funcSvc)
return "", resp.err return "", resp.err
} }
return resp.funcSvc.Address, resp.err return resp.funcSvc.Address, resp.err
@@ -177,6 +177,11 @@ func (caaf *Container) UnTapService(ctx context.Context, key string, svcHost str
// Not Implemented for CaaF. // Not Implemented for CaaF.
} }
// MarkSpecializationFailure has not been implemented for CaaF.
func (caaf *Container) MarkSpecializationFailure(ctx context.Context, key string) {
// Not Implemented for CaaF.
}
// GetFuncSvc returns a function service; error otherwise. // GetFuncSvc returns a function service; error otherwise.
func (caaf *Container) GetFuncSvc(ctx context.Context, fn *fv1.Function) (*fscache.FuncSvc, error) { func (caaf *Container) GetFuncSvc(ctx context.Context, fn *fv1.Function) (*fscache.FuncSvc, error) {
return caaf.createFunction(ctx, fn) return caaf.createFunction(ctx, fn)
@@ -48,6 +48,9 @@ type ExecutorType interface {
// UnTapService updates the isActive to false // UnTapService updates the isActive to false
UnTapService(ctx context.Context, key string, svcHost string) UnTapService(ctx context.Context, key string, svcHost string)
// ReduceSpecializationInProgress updates the svcWaiting count in funcSvcGroup
MarkSpecializationFailure(ctx context.Context, key string)
// IsValid returns true if a function service is valid. Different executor types // IsValid returns true if a function service is valid. Different executor types
// use distinct ways to examine the function service. // use distinct ways to examine the function service.
IsValid(context.Context, *fscache.FuncSvc) bool IsValid(context.Context, *fscache.FuncSvc) bool
@@ -199,6 +199,11 @@ func (deploy *NewDeploy) UnTapService(ctx context.Context, key string, svcHost s
// Not Implemented for NewDeployment. Will be used when support of concurrent specialization of same function is added. // Not Implemented for NewDeployment. Will be used when support of concurrent specialization of same function is added.
} }
// MarkSpecializationFailure has not been implemented for NewDeployment.
func (deploy *NewDeploy) MarkSpecializationFailure(ctx context.Context, key string) {
// Not Implemented for NewDeployment. Will be used when support of concurrent specialization of same function is added.
}
// TapService makes a TouchByAddress request to the cache. // TapService makes a TouchByAddress request to the cache.
func (deploy *NewDeploy) TapService(ctx context.Context, svcHost string) error { func (deploy *NewDeploy) TapService(ctx context.Context, svcHost string) error {
otelUtils.SpanTrackEvent(ctx, "TapService") otelUtils.SpanTrackEvent(ctx, "TapService")
+14 -2
View File
@@ -226,6 +226,14 @@ func (gp *GenericPool) updateCPUUtilizationSvc(ctx context.Context) {
// returns the key and pod API object. // returns the key and pod API object.
func (gp *GenericPool) choosePod(ctx context.Context, newLabels map[string]string) (string, *apiv1.Pod, error) { func (gp *GenericPool) choosePod(ctx context.Context, newLabels map[string]string) (string, *apiv1.Pod, error) {
startTime := time.Now() startTime := time.Now()
podTimeout := startTime.Add(gp.podReadyTimeout)
deadline, ok := ctx.Deadline()
if ok {
deadline = deadline.Add(-1 * time.Second)
if deadline.Before(podTimeout) {
podTimeout = deadline
}
}
expoDelay := 100 * time.Millisecond expoDelay := 100 * time.Millisecond
logger := otelUtils.LoggerWithTraceID(ctx, gp.logger) logger := otelUtils.LoggerWithTraceID(ctx, gp.logger)
if !cache.WaitForCacheSync(ctx.Done(), gp.readyPodListerSynced) { if !cache.WaitForCacheSync(ctx.Done(), gp.readyPodListerSynced) {
@@ -234,10 +242,14 @@ func (gp *GenericPool) choosePod(ctx context.Context, newLabels map[string]strin
} }
for { for {
// Retries took too long, error out. // Retries took too long, error out.
if time.Since(startTime) > gp.podReadyTimeout { if time.Now().After(podTimeout) {
logger.Error("timed out waiting for pod", zap.Any("labels", newLabels), zap.Duration("timeout", gp.podReadyTimeout)) logger.Error("timed out waiting for pod", zap.Any("labels", newLabels), zap.Duration("timeout", podTimeout.Sub(startTime)))
return "", nil, errors.New("timeout: waited too long to get a ready pod") return "", nil, errors.New("timeout: waited too long to get a ready pod")
} }
if ctx.Err() != nil {
logger.Error("context canceled while waiting for pod", zap.Any("labels", newLabels), zap.Duration("timeout", podTimeout.Sub(startTime)))
return "", nil, fmt.Errorf("context canceled while waiting for pod: %w", ctx.Err())
}
var chosenPod *apiv1.Pod var chosenPod *apiv1.Pod
var key string var key string
+8
View File
@@ -252,6 +252,14 @@ func (gpm *GenericPoolManager) TapService(ctx context.Context, svcHost string) e
return nil return nil
} }
func (gpm *GenericPoolManager) MarkSpecializationFailure(ctx context.Context, key string) {
otelUtils.SpanTrackEvent(ctx, "MarkSpecializationFailure",
attribute.KeyValue{Key: "key", Value: attribute.StringValue(key)})
logger := otelUtils.LoggerWithTraceID(ctx, gpm.logger)
logger.Info("marking specialization failure", zap.Any("key", key))
gpm.fsCache.MarkSpecializationFailure(key)
}
// IsValid checks if pod is not deleted and that it has the address passed as the argument. Also checks that all the // IsValid checks if pod is not deleted and that it has the address passed as the argument. Also checks that all the
// containers in it are reporting a ready status for the healthCheck. // containers in it are reporting a ready status for the healthCheck.
func (gpm *GenericPoolManager) IsValid(ctx context.Context, fsvc *fscache.FuncSvc) bool { func (gpm *GenericPoolManager) IsValid(ctx context.Context, fsvc *fscache.FuncSvc) bool {
@@ -244,6 +244,10 @@ func (fsc *FunctionServiceCache) MarkAvailable(key string, svcHost string) {
fsc.connFunctionCache.MarkAvailable(key, svcHost) fsc.connFunctionCache.MarkAvailable(key, svcHost)
} }
func (fsc *FunctionServiceCache) MarkSpecializationFailure(key string) {
fsc.connFunctionCache.MarkSpecializationFailure(key)
}
// Add adds a function service to cache if it does not exist already. // Add adds a function service to cache if it does not exist already.
func (fsc *FunctionServiceCache) Add(fsvc FuncSvc) (*FuncSvc, error) { func (fsc *FunctionServiceCache) Add(fsvc FuncSvc) (*FuncSvc, error) {
existing, err := fsc.byFunction.Set(crd.CacheKey(fsvc.Function), &fsvc) existing, err := fsc.byFunction.Set(crd.CacheKey(fsvc.Function), &fsvc)
+18
View File
@@ -36,6 +36,7 @@ const (
markAvailable markAvailable
deleteValue deleteValue
setCPUUtilization setCPUUtilization
markSpecializationFailure
) )
type ( type (
@@ -232,6 +233,14 @@ func (c *PoolCache) service() {
} }
} }
} }
case markSpecializationFailure:
if c.cache[req.function].svcWaiting > c.cache[req.function].queue.Len() {
c.cache[req.function].svcWaiting--
if c.cache[req.function].svcWaiting == c.cache[req.function].queue.Len() {
expiredRequests := c.cache[req.function].queue.Expired()
c.cache[req.function].svcWaiting = c.cache[req.function].svcWaiting - expiredRequests
}
}
case deleteValue: case deleteValue:
delete(c.cache[req.function].svcs, req.address) delete(c.cache[req.function].svcs, req.address)
req.responseChannel <- resp req.responseChannel <- resp
@@ -328,3 +337,12 @@ func (c *PoolCache) DeleteValue(ctx context.Context, function, address string) e
resp := <-respChannel resp := <-respChannel
return resp.error return resp.error
} }
// ReduceSpecializationInProgress reduces the svcWaiting count
func (c *PoolCache) MarkSpecializationFailure(function string) {
c.requestChannel <- &request{
requestType: markSpecializationFailure,
function: function,
responseChannel: make(chan *response),
}
}
+25
View File
@@ -38,6 +38,31 @@ func (q *Queue) Pop() *svcWait {
return svcWait return svcWait
} }
func (q *Queue) Expired() int {
q.mutex.Lock()
defer q.mutex.Unlock()
expired := 0
svcExpired := []*list.Element{}
for item := q.items.Front(); item != nil; item = item.Next() {
svcWait, ok := item.Value.(*svcWait)
if !ok {
continue
}
if svcWait.ctx.Err() != nil {
close(svcWait.svcChannel)
svcExpired = append(svcExpired, item)
expired = expired + 1
}
}
for _, item := range svcExpired {
q.items.Remove(item)
}
return expired
}
func (q *Queue) Len() int { func (q *Queue) Len() int {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()
+76
View File
@@ -1,6 +1,7 @@
package fscache package fscache
import ( import (
"context"
"sync" "sync"
"testing" "testing"
) )
@@ -113,3 +114,78 @@ func TestQueueLen(t *testing.T) {
t.Errorf("Expected queue length to be 1, got %d", q.Len()) t.Errorf("Expected queue length to be 1, got %d", q.Len())
} }
} }
func TestExpiredWhenAllItemsExpired(t *testing.T) {
q := NewQueue()
if q.Expired() != 0 {
t.Errorf("Expected Expired to return 0, got %d", q.Expired())
}
ctx, cancel := context.WithCancel(context.Background())
item := &svcWait{
svcChannel: make(chan *FuncSvc),
ctx: ctx,
}
q.Push(item)
if q.Len() != 1 {
t.Errorf("Expected queue length to be 1, got %d", q.Len())
}
cancel()
if q.Expired() != 1 {
t.Errorf("Expected Expired to return 1, got %d", q.Expired())
}
if q.Len() != 0 {
t.Errorf("Expected queue length to be 0, got %d", q.Len())
}
}
func TestExpiredWhenFewItemsExpired(t *testing.T) {
q := NewQueue()
if q.Expired() != 0 {
t.Errorf("Expected Expired to return 0, got %d", q.Expired())
}
ctx, cancel := context.WithCancel(context.Background())
q.Push(&svcWait{
svcChannel: make(chan *FuncSvc),
ctx: ctx,
})
q.Push(&svcWait{
svcChannel: make(chan *FuncSvc),
ctx: context.Background(),
})
if q.Len() != 2 {
t.Errorf("Expected queue length to be 1, got %d", q.Len())
}
cancel()
if q.Expired() != 1 {
t.Errorf("Expected Expired to return 1, got %d", q.Expired())
}
if q.Len() != 1 {
t.Errorf("Expected queue length to be 0, got %d", q.Len())
}
}
func TestExpiredWhenNoItemsExpired(t *testing.T) {
q := NewQueue()
if q.Expired() != 0 {
t.Errorf("Expected Expired to return 0, got %d", q.Expired())
}
q.Push(&svcWait{
svcChannel: make(chan *FuncSvc),
ctx: context.Background(),
})
q.Push(&svcWait{
svcChannel: make(chan *FuncSvc),
ctx: context.Background(),
})
if q.Len() != 2 {
t.Errorf("Expected queue length to be 1, got %d", q.Len())
}
if q.Expired() != 0 {
t.Errorf("Expected Expired to return 1, got %d", q.Expired())
}
if q.Len() != 2 {
t.Errorf("Expected queue length to be 0, got %d", q.Len())
}
}