Extract scaling from zero

- extracting this package means it can be used in other components
such as the asynchronous nats-queue-worker which may need to
invoke functions which are scaled down to zero replicas.

Ref: https://github.com/openfaas/nats-queue-worker/issues/32

Tested on Docker Swarm for scaling up, already scaled and not
found error.

Signed-off-by: Alex Ellis (VMware) <alexellis2@gmail.com>
This commit is contained in:
Alex Ellis (VMware)
2018-11-01 12:54:17 +00:00
committed by Alex Ellis
parent fb06e299cf
commit 9cea08c728
15 changed files with 208 additions and 125 deletions

View File

@ -11,30 +11,11 @@ import (
"net/http"
"github.com/openfaas/faas/gateway/requests"
)
const (
// DefaultMinReplicas is the minimal amount of replicas for a service.
DefaultMinReplicas = 1
// DefaultMaxReplicas is the amount of replicas a service will auto-scale up to.
DefaultMaxReplicas = 20
// DefaultScalingFactor is the defining proportion for the scaling increments.
DefaultScalingFactor = 20
// MinScaleLabel label indicating min scale for a function
MinScaleLabel = "com.openfaas.scale.min"
// MaxScaleLabel label indicating max scale for a function
MaxScaleLabel = "com.openfaas.scale.max"
// ScalingFactorLabel label indicates the scaling factor for a function
ScalingFactorLabel = "com.openfaas.scale.factor"
"github.com/openfaas/faas/gateway/scaling"
)
// MakeAlertHandler handles alerts from Prometheus Alertmanager
func MakeAlertHandler(service ServiceQuery) http.HandlerFunc {
func MakeAlertHandler(service scaling.ServiceQuery) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("Alert received.")
@ -76,7 +57,7 @@ func MakeAlertHandler(service ServiceQuery) http.HandlerFunc {
}
}
func handleAlerts(req *requests.PrometheusAlert, service ServiceQuery) []error {
func handleAlerts(req *requests.PrometheusAlert, service scaling.ServiceQuery) []error {
var errors []error
for _, alert := range req.Alerts {
if err := scaleService(alert, service); err != nil {
@ -88,7 +69,7 @@ func handleAlerts(req *requests.PrometheusAlert, service ServiceQuery) []error {
return errors
}
func scaleService(alert requests.PrometheusInnerAlert, service ServiceQuery) error {
func scaleService(alert requests.PrometheusInnerAlert, service scaling.ServiceQuery) error {
var err error
serviceName := alert.Labels.FunctionName

View File

@ -5,12 +5,14 @@ package handlers
import (
"testing"
"github.com/openfaas/faas/gateway/scaling"
)
func TestDisabledScale(t *testing.T) {
minReplicas := uint64(1)
scalingFactor := uint64(0)
newReplicas := CalculateReplicas("firing", DefaultMinReplicas, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("firing", scaling.DefaultMinReplicas, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != minReplicas {
t.Logf("Expected not to scale, but replicas were: %d", newReplicas)
t.Fail()
@ -20,7 +22,7 @@ func TestDisabledScale(t *testing.T) {
func TestParameterEdge(t *testing.T) {
minReplicas := uint64(0)
scalingFactor := uint64(0)
newReplicas := CalculateReplicas("firing", DefaultMinReplicas, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("firing", scaling.DefaultMinReplicas, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != 0 {
t.Log("Expected not to scale")
t.Fail()
@ -41,7 +43,7 @@ func TestScalingWithSameUpperLowerLimit(t *testing.T) {
func TestMaxScale(t *testing.T) {
minReplicas := uint64(1)
scalingFactor := uint64(100)
newReplicas := CalculateReplicas("firing", DefaultMinReplicas, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("firing", scaling.DefaultMinReplicas, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != 20 {
t.Log("Expected ceiling of 20 replicas")
t.Fail()
@ -51,7 +53,7 @@ func TestMaxScale(t *testing.T) {
func TestInitialScale(t *testing.T) {
minReplicas := uint64(1)
scalingFactor := uint64(20)
newReplicas := CalculateReplicas("firing", DefaultMinReplicas, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("firing", scaling.DefaultMinReplicas, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != 4 {
t.Log("Expected the increment to equal 4")
t.Fail()
@ -61,7 +63,7 @@ func TestInitialScale(t *testing.T) {
func TestScale(t *testing.T) {
minReplicas := uint64(1)
scalingFactor := uint64(20)
newReplicas := CalculateReplicas("firing", 4, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("firing", 4, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != 8 {
t.Log("Expected newReplicas to equal 8")
t.Fail()
@ -71,7 +73,7 @@ func TestScale(t *testing.T) {
func TestScaleCeiling(t *testing.T) {
minReplicas := uint64(1)
scalingFactor := uint64(20)
newReplicas := CalculateReplicas("firing", 20, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("firing", 20, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != 20 {
t.Log("Expected ceiling of 20 replicas")
t.Fail()
@ -81,7 +83,7 @@ func TestScaleCeiling(t *testing.T) {
func TestScaleCeilingEdge(t *testing.T) {
minReplicas := uint64(1)
scalingFactor := uint64(20)
newReplicas := CalculateReplicas("firing", 19, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("firing", 19, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != 20 {
t.Log("Expected ceiling of 20 replicas")
t.Fail()
@ -91,7 +93,7 @@ func TestScaleCeilingEdge(t *testing.T) {
func TestBackingOff(t *testing.T) {
minReplicas := uint64(1)
scalingFactor := uint64(20)
newReplicas := CalculateReplicas("resolved", 8, DefaultMaxReplicas, minReplicas, scalingFactor)
newReplicas := CalculateReplicas("resolved", 8, scaling.DefaultMaxReplicas, minReplicas, scalingFactor)
if newReplicas != 1 {
t.Log("Expected backing off to 1 replica")
t.Fail()

View File

@ -1,63 +0,0 @@
// Copyright (c) OpenFaaS Author(s). All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
package handlers
import (
"sync"
"time"
)
// FunctionMeta holds the last refresh and any other
// meta-data needed for caching.
type FunctionMeta struct {
LastRefresh time.Time
ServiceQueryResponse ServiceQueryResponse
}
// Expired find out whether the cache item has expired with
// the given expiry duration from when it was stored.
func (fm *FunctionMeta) Expired(expiry time.Duration) bool {
return time.Now().After(fm.LastRefresh.Add(expiry))
}
// FunctionCache provides a cache of Function replica counts
type FunctionCache struct {
Cache map[string]*FunctionMeta
Expiry time.Duration
Sync sync.Mutex
}
// Set replica count for functionName
func (fc *FunctionCache) Set(functionName string, serviceQueryResponse ServiceQueryResponse) {
fc.Sync.Lock()
defer fc.Sync.Unlock()
if _, exists := fc.Cache[functionName]; !exists {
fc.Cache[functionName] = &FunctionMeta{}
}
entry := fc.Cache[functionName]
entry.LastRefresh = time.Now()
entry.ServiceQueryResponse = serviceQueryResponse
}
// Get replica count for functionName
func (fc *FunctionCache) Get(functionName string) (ServiceQueryResponse, bool) {
fc.Sync.Lock()
defer fc.Sync.Unlock()
replicas := ServiceQueryResponse{
AvailableReplicas: 0,
}
hit := false
if val, exists := fc.Cache[functionName]; exists {
replicas = val.ServiceQueryResponse
hit = !val.Expired(fc.Expiry)
}
return replicas, hit
}

View File

@ -1,115 +0,0 @@
// Copyright (c) OpenFaaS Author(s). All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
package handlers
import (
"testing"
"time"
)
func Test_LastRefreshSet(t *testing.T) {
before := time.Now()
fnName := "echo"
cache := FunctionCache{
Cache: make(map[string]*FunctionMeta),
Expiry: time.Millisecond * 1,
}
if cache.Cache == nil {
t.Errorf("Expected cache map to be initialized")
t.Fail()
}
cache.Set(fnName, ServiceQueryResponse{AvailableReplicas: 1})
if _, exists := cache.Cache[fnName]; !exists {
t.Errorf("Expected entry to exist after setting %s", fnName)
t.Fail()
}
if cache.Cache[fnName].LastRefresh.Before(before) {
t.Errorf("Expected LastRefresh for function to have been after start of test")
t.Fail()
}
}
func Test_CacheExpiresIn1MS(t *testing.T) {
fnName := "echo"
cache := FunctionCache{
Cache: make(map[string]*FunctionMeta),
Expiry: time.Millisecond * 1,
}
cache.Set(fnName, ServiceQueryResponse{AvailableReplicas: 1})
time.Sleep(time.Millisecond * 2)
_, hit := cache.Get(fnName)
wantHit := false
if hit != wantHit {
t.Errorf("hit, want: %v, got %v", wantHit, hit)
}
}
func Test_CacheGivesHitWithLongExpiry(t *testing.T) {
fnName := "echo"
cache := FunctionCache{
Cache: make(map[string]*FunctionMeta),
Expiry: time.Millisecond * 500,
}
cache.Set(fnName, ServiceQueryResponse{AvailableReplicas: 1})
_, hit := cache.Get(fnName)
wantHit := true
if hit != wantHit {
t.Errorf("hit, want: %v, got %v", wantHit, hit)
}
}
func Test_CacheFunctionExists(t *testing.T) {
fnName := "echo"
cache := FunctionCache{
Cache: make(map[string]*FunctionMeta),
Expiry: time.Millisecond * 10,
}
cache.Set(fnName, ServiceQueryResponse{AvailableReplicas: 1})
time.Sleep(time.Millisecond * 2)
_, hit := cache.Get(fnName)
wantHit := true
if hit != wantHit {
t.Errorf("hit, want: %v, got %v", wantHit, hit)
}
}
func Test_CacheFunctionNotExist(t *testing.T) {
fnName := "echo"
testName := "burt"
cache := FunctionCache{
Cache: make(map[string]*FunctionMeta),
Expiry: time.Millisecond * 10,
}
cache.Set(fnName, ServiceQueryResponse{AvailableReplicas: 1})
time.Sleep(time.Millisecond * 2)
_, hit := cache.Get(testName)
wantHit := false
if hit != wantHit {
t.Errorf("hit, want: %v, got %v", wantHit, hit)
}
}

View File

@ -7,100 +7,47 @@ import (
"fmt"
"log"
"net/http"
"time"
"github.com/openfaas/faas/gateway/scaling"
)
// ScalingConfig for scaling behaviours
type ScalingConfig struct {
// MaxPollCount attempts to query a function before giving up
MaxPollCount uint
// FunctionPollInterval delay or interval between polling a function's readiness status
FunctionPollInterval time.Duration
// CacheExpiry life-time for a cache entry before considering invalid
CacheExpiry time.Duration
// ServiceQuery queries available/ready replicas for function
ServiceQuery ServiceQuery
}
// MakeScalingHandler creates handler which can scale a function from
// zero to N replica(s). After scaling the next http.HandlerFunc will
// be called. If the function is not ready after the configured
// amount of attempts / queries then next will not be invoked and a status
// will be returned to the client.
func MakeScalingHandler(next http.HandlerFunc, config ScalingConfig) http.HandlerFunc {
cache := FunctionCache{
Cache: make(map[string]*FunctionMeta),
Expiry: config.CacheExpiry,
}
func MakeScalingHandler(next http.HandlerFunc, config scaling.ScalingConfig) http.HandlerFunc {
scaler := scaling.NewFunctionScaler(config)
return func(w http.ResponseWriter, r *http.Request) {
functionName := getServiceName(r.URL.String())
res := scaler.Scale(functionName)
if serviceQueryResponse, hit := cache.Get(functionName); hit && serviceQueryResponse.AvailableReplicas > 0 {
next.ServeHTTP(w, r)
return
}
if !res.Found {
errStr := fmt.Sprintf("error finding function %s: %s", functionName, res.Error.Error())
log.Printf("Scaling: %s", errStr)
queryResponse, err := config.ServiceQuery.GetReplicas(functionName)
if err != nil {
var errStr string
errStr = fmt.Sprintf("error finding function %s: %s", functionName, err.Error())
log.Printf(errStr)
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(errStr))
return
}
cache.Set(functionName, queryResponse)
if res.Error != nil {
errStr := fmt.Sprintf("error finding function %s: %s", functionName, res.Error.Error())
log.Printf("Scaling: %s", errStr)
if queryResponse.AvailableReplicas == 0 {
minReplicas := uint64(1)
if queryResponse.MinReplicas > 0 {
minReplicas = queryResponse.MinReplicas
}
log.Printf("[Scale] function=%s 0 => %d requested", functionName, minReplicas)
scalingStartTime := time.Now()
err := config.ServiceQuery.SetReplicas(functionName, minReplicas)
if err != nil {
errStr := fmt.Errorf("unable to scale function [%s], err: %s", functionName, err)
log.Printf(errStr.Error())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errStr.Error()))
return
}
for i := 0; i < int(config.MaxPollCount); i++ {
queryResponse, err := config.ServiceQuery.GetReplicas(functionName)
cache.Set(functionName, queryResponse)
if err != nil {
errStr := fmt.Sprintf("error: %s", err.Error())
log.Printf(errStr)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errStr))
return
}
if queryResponse.AvailableReplicas > 0 {
scalingDuration := time.Since(scalingStartTime)
log.Printf("[Scale] function=%s 0 => %d successful - %f seconds", functionName, queryResponse.AvailableReplicas, scalingDuration.Seconds())
break
}
time.Sleep(config.FunctionPollInterval)
}
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errStr))
return
}
next.ServeHTTP(w, r)
if res.Available {
next.ServeHTTP(w, r)
return
}
log.Printf("[Scale] function=%s 0=>N timed-out after %f seconds", functionName, res.Duration.Seconds())
}
}

View File

@ -1,19 +0,0 @@
// Copyright (c) OpenFaaS Author(s). All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
package handlers
// ServiceQuery provides interface for replica querying/setting
type ServiceQuery interface {
GetReplicas(service string) (response ServiceQueryResponse, err error)
SetReplicas(service string, count uint64) error
}
// ServiceQueryResponse response from querying a function status
type ServiceQueryResponse struct {
Replicas uint64
MaxReplicas uint64
MinReplicas uint64
ScalingFactor uint64
AvailableReplicas uint64
}