Merge pull request #20 from platform9/reap-idle-pods

Reap idle pods
This commit is contained in:
Soam Vasani
2016-11-05 23:33:45 -07:00
committed by GitHub
16 changed files with 611 additions and 246 deletions
+84 -14
View File
@@ -19,6 +19,7 @@ package cache
import ( import (
"time" "time"
"errors"
"fmt" "fmt"
"github.com/platform9/fission" "github.com/platform9/fission"
) )
@@ -29,6 +30,8 @@ const (
GET requestType = iota GET requestType = iota
SET SET
DELETE DELETE
EXPIRE
COPY
) )
type ( type (
@@ -38,7 +41,9 @@ type (
value interface{} value interface{}
} }
Cache struct { Cache struct {
cache map[interface{}]Value cache map[interface{}]*Value
ctimeExpiry time.Duration
atimeExpiry time.Duration
requestChannel chan *request requestChannel chan *request
} }
@@ -50,16 +55,35 @@ type (
} }
response struct { response struct {
error error
value interface{} existingValue interface{}
mapCopy map[interface{}]interface{}
value interface{}
} }
) )
func MakeCache() *Cache { func (c *Cache) IsOld(v *Value) bool {
if (c.ctimeExpiry != time.Duration(0)) && (time.Now().Sub(v.ctime) > c.ctimeExpiry) {
return true
}
if (c.atimeExpiry != time.Duration(0)) && (time.Now().Sub(v.atime) > c.atimeExpiry) {
return true
}
return false
}
func MakeCache(ctimeExpiry, atimeExpiry time.Duration) *Cache {
c := &Cache{ c := &Cache{
cache: make(map[interface{}]Value), cache: make(map[interface{}]*Value),
ctimeExpiry: ctimeExpiry,
atimeExpiry: atimeExpiry,
requestChannel: make(chan *request), requestChannel: make(chan *request),
} }
go c.service() go c.service()
if ctimeExpiry != time.Duration(0) || atimeExpiry != time.Duration(0) {
go c.expiryService()
}
return c return c
} }
@@ -73,23 +97,48 @@ func (c *Cache) service() {
if !ok { if !ok {
resp.error = fission.MakeError(fission.ErrorNotFound, resp.error = fission.MakeError(fission.ErrorNotFound,
fmt.Sprintf("key '%v' not found", req.key)) fmt.Sprintf("key '%v' not found", req.key))
} else if c.IsOld(val) {
resp.error = fission.MakeError(fission.ErrorNotFound,
fmt.Sprintf("key '%v' expired (atime %v)", req.key, val.atime))
delete(c.cache, req.key)
} else {
// update atime
val.atime = time.Now()
c.cache[req.key] = val
resp.value = val.value
} }
val.atime = time.Now()
c.cache[req.key] = val
resp.value = val.value
req.responseChannel <- resp req.responseChannel <- resp
case SET: case SET:
now := time.Now() now := time.Now()
c.cache[req.key] = Value{ if _, ok := c.cache[req.key]; ok {
value: req.value, val := c.cache[req.key]
ctime: now, val.atime = time.Now()
atime: now, resp.existingValue = val.value
resp.error = errors.New("value already exists")
} else {
c.cache[req.key] = &Value{
value: req.value,
ctime: now,
atime: now,
}
} }
req.responseChannel <- resp req.responseChannel <- resp
case DELETE: case DELETE:
delete(c.cache, req.key) delete(c.cache, req.key)
req.responseChannel <- resp req.responseChannel <- resp
case EXPIRE:
for k, v := range c.cache {
if c.IsOld(v) {
delete(c.cache, k)
}
}
// no response
case COPY:
resp.mapCopy = make(map[interface{}]interface{})
for k, v := range c.cache {
resp.mapCopy[k] = v.value
}
req.responseChannel <- resp
default: default:
resp.error = fission.MakeError(fission.ErrorInvalidArgument, resp.error = fission.MakeError(fission.ErrorInvalidArgument,
fmt.Sprintf("invalid request type: %v", req.requestType)) fmt.Sprintf("invalid request type: %v", req.requestType))
@@ -109,7 +158,9 @@ func (c *Cache) Get(key interface{}) (interface{}, error) {
return resp.value, resp.error return resp.value, resp.error
} }
func (c *Cache) Set(key interface{}, value interface{}) error { // if key exists in the cache, the new value is NOT set; instead an
// error and the old value are returned
func (c *Cache) Set(key interface{}, value interface{}) (error, interface{}) {
respChannel := make(chan *response) respChannel := make(chan *response)
c.requestChannel <- &request{ c.requestChannel <- &request{
requestType: SET, requestType: SET,
@@ -118,7 +169,7 @@ func (c *Cache) Set(key interface{}, value interface{}) error {
responseChannel: respChannel, responseChannel: respChannel,
} }
resp := <-respChannel resp := <-respChannel
return resp.error return resp.error, resp.existingValue
} }
func (c *Cache) Delete(key interface{}) error { func (c *Cache) Delete(key interface{}) error {
@@ -131,3 +182,22 @@ func (c *Cache) Delete(key interface{}) error {
resp := <-respChannel resp := <-respChannel
return resp.error return resp.error
} }
func (c *Cache) Copy() map[interface{}]interface{} {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: COPY,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.mapCopy
}
func (c *Cache) expiryService() {
for {
time.Sleep(time.Minute)
c.requestChannel <- &request{
requestType: EXPIRE,
}
}
}
+27 -11
View File
@@ -18,30 +18,46 @@ package cache
import "testing" import "testing"
import "log" import "log"
import "time"
func checkErr(err error) {
if err != nil {
log.Panicf("err: %v", err)
}
}
func TestCache(t *testing.T) { func TestCache(t *testing.T) {
c := MakeCache() c := MakeCache(100 * time.Millisecond)
err := c.Set("a", "b") err := c.Set("a", "b")
if err != nil { checkErr(err)
log.Panicf("error: %v", err) err = c.Set("p", "q")
} checkErr(err)
val, err := c.Get("a") val, err := c.Get("a")
if err != nil { checkErr(err)
log.Panicf("error: %v", err)
}
if val != "b" { if val != "b" {
log.Panicf("value %v", val) log.Panicf("value %v", val)
} }
err = c.Delete("a") cc := c.Copy()
if err != nil { if len(cc) != 2 {
log.Panicf("error: %v", err) log.Panicf("expected 2 items")
} }
err = c.Delete("a")
checkErr(err)
_, err = c.Get("a") _, err = c.Get("a")
if err == nil { if err == nil {
log.Panicf("error: %v", err) log.Panicf("found deleted element")
}
err = c.Set("expires", "42")
checkErr(err)
time.Sleep(150 * time.Millisecond)
_, err = c.Get("expires")
if err == nil {
log.Panicf("found expired element")
} }
} }
+46 -28
View File
@@ -23,6 +23,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@@ -34,27 +35,31 @@ import (
) )
type funcSvc struct { type funcSvc struct {
function *fission.Metadata // function this thing is for function *fission.Metadata // function this pod/service is for
environment *fission.Environment // env it was obtained from environment *fission.Environment // env it was obtained from
serviceName string // name of k8s svc address string // Host:Port or IP:Port that the service can be reached at.
podName string // pod name (within the function namespace)
ctime time.Time ctime time.Time
atime time.Time atime time.Time
} }
type API struct { type API struct {
poolMgr *GenericPoolManager poolMgr *GenericPoolManager
functionEnv *cache.Cache // map[fission.Metadata]fission.Environment functionEnv *cache.Cache // map[fission.Metadata]fission.Environment
functionService *cache.Cache // map[fission.Metadata]funcSvc fsCache *functionServiceCache
controller *controllerclient.Client controller *controllerclient.Client
//functionService *cache.Cache // map[fission.Metadata]*funcSvc
//urlFuncSvc *cache.Cache // map[string]*funcSvc
} }
func MakeAPI(gpm *GenericPoolManager, controller *controllerclient.Client) *API { func MakeAPI(gpm *GenericPoolManager, controller *controllerclient.Client, fsCache *functionServiceCache) *API {
return &API{ return &API{
poolMgr: gpm, poolMgr: gpm,
functionEnv: cache.MakeCache(), functionEnv: cache.MakeCache(time.Minute, 0),
functionService: cache.MakeCache(), fsCache: fsCache,
controller: controller, controller: controller,
} }
} }
@@ -87,7 +92,7 @@ func (api *API) getFunctionEnv(m *fission.Metadata) (*fission.Environment, error
var env *fission.Environment var env *fission.Environment
// Cached ? // Cached ?
result, err := api.functionEnv.Get(m) result, err := api.functionEnv.Get(*m)
if err == nil { if err == nil {
env = result.(*fission.Environment) env = result.(*fission.Environment)
return env, nil return env, nil
@@ -108,7 +113,7 @@ func (api *API) getFunctionEnv(m *fission.Metadata) (*fission.Environment, error
} }
// cache for future // cache for future
api.functionEnv.Set(m, env) api.functionEnv.Set(*m, env)
return env, nil return env, nil
} }
@@ -116,50 +121,63 @@ func (api *API) getFunctionEnv(m *fission.Metadata) (*fission.Environment, error
func (api *API) getServiceForFunction(m *fission.Metadata) (string, error) { func (api *API) getServiceForFunction(m *fission.Metadata) (string, error) {
// Check function -> svc map // Check function -> svc map
log.Printf("[%v] Checking for cached function service", m.Name) log.Printf("[%v] Checking for cached function service", m.Name)
result, err := api.functionService.Get(m) fsvc, err := api.fsCache.GetByFunction(m)
if err == nil { if err == nil {
// Ok: return svc name // Cached, return svc name
svc := result.(*funcSvc) return fsvc.address, nil
return svc.serviceName, nil
} }
// None exists, so create a new funcSvc: // None exists, so create a new funcSvc:
log.Printf("[%v] No cached function service found, creating one", m.Name) log.Printf("[%v] No cached function service found, creating one", m.Name)
// from Func -> get Env // from Func -> get Env
log.Printf("[%v] getting environment for function", m.Name) log.Printf("[%v] getting environment for function", m.Name)
env, err := api.getFunctionEnv(m) env, err := api.getFunctionEnv(m)
if err != nil { if err != nil {
return "", err return "", err
} }
// from Env -> get GenericPool // from Env -> get GenericPool
log.Printf("[%v] getting generic pool for env", m.Name) log.Printf("[%v] getting generic pool for env", m.Name)
pool, err := api.poolMgr.GetPool(env) pool, err := api.poolMgr.GetPool(env)
if err != nil { if err != nil {
return "", err return "", err
} }
// from GenericPool -> get one function container // from GenericPool -> get one function container
// (this also adds to the cache)
log.Printf("[%v] getting function service from pool", m.Name) log.Printf("[%v] getting function service from pool", m.Name)
funcSvc, err := pool.GetFuncSvc(m) funcSvc, err := pool.GetFuncSvc(m)
if err != nil { if err != nil {
return "", err return "", err
} }
// add to cache return funcSvc.address, nil
err = api.functionService.Set(m, funcSvc) }
if err != nil {
// log and ignore error
log.Printf("Error saving function service: %v", err)
}
return funcSvc.serviceName, nil // find funcSvc and update its atime
func (api *API) tapService(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request", 500)
return
}
svcName := string(body)
svcHost := strings.TrimPrefix(svcName, "http://")
err = api.fsCache.TouchByAddress(svcHost)
if err != nil {
log.Printf("funcSvc tap error: %v", err)
http.Error(w, "Not found", 404)
return
}
w.WriteHeader(http.StatusOK)
} }
func (api *API) Serve(port int) { func (api *API) Serve(port int) {
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/v1/getServiceForFunction", api.getServiceForFunctionApi).Methods("POST") r.HandleFunc("/v1/getServiceForFunction", api.getServiceForFunctionApi).Methods("POST")
r.HandleFunc("/v1/tapService", api.tapService).Methods("POST")
address := fmt.Sprintf(":%v", port) address := fmt.Sprintf(":%v", port)
log.Printf("starting poolmgr at port %v", port) log.Printf("starting poolmgr at port %v", port)
+17
View File
@@ -24,6 +24,7 @@ import (
"encoding/json" "encoding/json"
"github.com/platform9/fission" "github.com/platform9/fission"
"io/ioutil" "io/ioutil"
"net/url"
) )
type Client struct { type Client struct {
@@ -58,3 +59,19 @@ func (c *Client) GetServiceForFunction(metadata *fission.Metadata) (string, erro
return string(svcName), nil return string(svcName), nil
} }
func (c *Client) TapService(serviceUrl *url.URL) error {
url := c.poolmgrUrl + "/v1/tapService"
serviceUrlStr := serviceUrl.String()
resp, err := http.Post(url, "application/octet-stream", bytes.NewReader([]byte(serviceUrlStr)))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fission.MakeErrorFromHTTP(resp)
}
return nil
}
+236
View File
@@ -0,0 +1,236 @@
/*
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 poolmgr
import (
"log"
"time"
"github.com/platform9/fission"
"github.com/platform9/fission/cache"
)
type fscRequestType int
const (
TOUCH fscRequestType = iota
LISTOLD
LOG
DELETE_BY_POD
)
type (
functionServiceCache struct {
byFunction *cache.Cache // function -> funcSvc : map[fission.Metadata]*funcSvc
byAddress *cache.Cache // address -> function : map[string]fission.Metadata
byPod *cache.Cache // podname -> function : map[string]fission.Metadata
requestChannel chan *fscRequest
}
fscRequest struct {
requestType fscRequestType
address string
podName string
age time.Duration
responseChannel chan *fscResponse
}
fscResponse struct {
podNames []string
deleted bool
error
}
)
func MakeFunctionServiceCache() *functionServiceCache {
fsc := &functionServiceCache{
byFunction: cache.MakeCache(0, 0),
byAddress: cache.MakeCache(0, 0),
byPod: cache.MakeCache(0, 0),
requestChannel: make(chan *fscRequest),
}
go fsc.service()
return fsc
}
func (fsc *functionServiceCache) service() {
for {
req := <-fsc.requestChannel
resp := &fscResponse{}
switch req.requestType {
case TOUCH:
// update atime for this function svc
resp.error = fsc._touchByAddress(req.address)
case LISTOLD:
// get svcs idle for > req.age
byPodCopy := fsc.byPod.Copy()
pods := make([]string, 0)
for podNameI, mI := range byPodCopy {
m := mI.(fission.Metadata)
fsvcI, err := fsc.byFunction.Get(m)
if err != nil {
resp.error = err
} else {
fsvc := fsvcI.(*funcSvc)
if time.Now().Sub(fsvc.atime) > req.age {
podName := podNameI.(string)
pods = append(pods, podName)
}
}
}
resp.podNames = pods
case LOG:
funcCopy := fsc.byFunction.Copy()
log.Printf("Cache has %v entries", len(funcCopy))
for mI, fsvcI := range funcCopy {
m := mI.(fission.Metadata)
fsvc := fsvcI.(*funcSvc)
log.Printf("%v:%v\t%v", m.Name, m.Uid, fsvc.podName)
}
case DELETE_BY_POD:
resp.deleted, resp.error = fsc._deleteByPod(req.podName, req.age)
}
req.responseChannel <- resp
}
}
func (fsc *functionServiceCache) GetByFunction(m *fission.Metadata) (*funcSvc, error) {
fsvcI, err := fsc.byFunction.Get(*m)
if err != nil {
return nil, err
}
// update atime
fsvc := fsvcI.(*funcSvc)
fsvc.atime = time.Now()
fsvcCopy := *fsvc
return &fsvcCopy, nil
}
func (fsc *functionServiceCache) Add(fsvc funcSvc) (error, *funcSvc) {
err, existing := fsc.byFunction.Set(*fsvc.function, &fsvc)
if err != nil {
if existing != nil {
f := existing.(*funcSvc)
err2 := fsc.TouchByAddress(f.address)
if err2 != nil {
return err2, nil
}
fCopy := *f
return err, &fCopy
}
return err, nil
}
now := time.Now()
fsvc.ctime = now
fsvc.atime = now
err, _ = fsc.byAddress.Set(fsvc.address, *fsvc.function)
if err != nil {
log.Printf("error caching fsvc: %v", err)
return err, nil
}
err, _ = fsc.byPod.Set(fsvc.podName, *fsvc.function)
if err != nil {
log.Printf("error caching fsvc: %v", err)
return err, nil
}
return nil, nil
}
func (fsc *functionServiceCache) TouchByAddress(address string) error {
responseChannel := make(chan *fscResponse)
fsc.requestChannel <- &fscRequest{
requestType: TOUCH,
address: address,
responseChannel: responseChannel,
}
resp := <-responseChannel
return resp.error
}
func (fsc *functionServiceCache) _touchByAddress(address string) error {
mI, err := fsc.byAddress.Get(address)
if err != nil {
return err
}
m := mI.(fission.Metadata)
fsvcI, err := fsc.byFunction.Get(m)
if err != nil {
return err
}
fsvc := fsvcI.(*funcSvc)
fsvc.atime = time.Now()
return nil
}
func (fsc *functionServiceCache) DeleteByPod(podName string, minAge time.Duration) (bool, error) {
responseChannel := make(chan *fscResponse)
fsc.requestChannel <- &fscRequest{
requestType: DELETE_BY_POD,
podName: podName,
age: minAge,
responseChannel: responseChannel,
}
resp := <-responseChannel
return resp.deleted, resp.error
}
// _deleteByPod deletes the entry keyed by podName, but only if it is
// at least minAge old.
func (fsc *functionServiceCache) _deleteByPod(podName string, minAge time.Duration) (bool, error) {
mI, err := fsc.byPod.Get(podName)
if err != nil {
return false, err
}
m := mI.(fission.Metadata)
fsvcI, err := fsc.byFunction.Get(m)
if err != nil {
return false, err
}
fsvc := fsvcI.(*funcSvc)
if time.Now().Sub(fsvc.atime) < minAge {
return false, nil
}
fsc.byFunction.Delete(m)
fsc.byAddress.Delete(fsvc.address)
fsc.byPod.Delete(podName)
return true, nil
}
func (fsc *functionServiceCache) ListOld(age time.Duration) ([]string, error) {
responseChannel := make(chan *fscResponse)
fsc.requestChannel <- &fscRequest{
requestType: LISTOLD,
age: age,
responseChannel: responseChannel,
}
resp := <-responseChannel
return resp.podNames, resp.error
}
func (fsc *functionServiceCache) Log() {
log.Printf("--- FunctionService Cache Contents")
responseChannel := make(chan *fscResponse)
fsc.requestChannel <- &fscRequest{
requestType: LOG,
responseChannel: responseChannel,
}
<-responseChannel
log.Printf("--- FunctionService Cache Contents End")
}
+72
View File
@@ -0,0 +1,72 @@
package poolmgr
import (
"log"
"testing"
"github.com/platform9/fission"
"time"
)
func TestFunctionServiceCache(t *testing.T) {
fsc := MakeFunctionServiceCache()
if fsc == nil {
log.Panicf("error creating cache")
}
var fsvc *funcSvc
now := time.Now()
fsvc = &funcSvc{
function: &fission.Metadata{
Name: "foo",
Uid: "1212",
},
environment: &fission.Environment{
Metadata: fission.Metadata{
Name: "foo-env",
Uid: "2323",
},
RunContainerImageUrl: "fission/foo-env",
},
address: "xxx",
podName: "yyy",
ctime: now,
atime: now,
}
err, _ := fsc.Add(*fsvc)
if err != nil {
fsc.Log()
log.Panicf("Failed to add fsvc: %v", err)
}
f, err := fsc.GetByFunction(fsvc.function)
if err != nil {
fsc.Log()
log.Panicf("Failed to get fsvc: %v", err)
}
fsvc.atime = f.atime
fsvc.ctime = f.ctime
if *f != *fsvc {
fsc.Log()
log.Panicf("Incorrect fsvc \n(expected: %#v)\n (found: %#v)", fsvc, f)
}
err = fsc.TouchByAddress(fsvc.address)
if err != nil {
fsc.Log()
log.Panicf("Failed to touch fsvc: %v", err)
}
err = fsc.DeleteByPod(fsvc.podName)
if err != nil {
fsc.Log()
log.Panicf("Failed to delete fsvc: %v", err)
}
_, err = fsc.GetByFunction(fsvc.function)
if err == nil {
fsc.Log()
log.Panicf("found fsvc while expecting empty cache", err)
}
}
+79 -14
View File
@@ -25,8 +25,10 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/dchest/uniuri"
"k8s.io/client-go/1.4/kubernetes" "k8s.io/client-go/1.4/kubernetes"
"k8s.io/client-go/1.4/pkg/api" "k8s.io/client-go/1.4/pkg/api"
"k8s.io/client-go/1.4/pkg/api/v1" "k8s.io/client-go/1.4/pkg/api/v1"
@@ -39,14 +41,16 @@ import (
type ( type (
GenericPool struct { GenericPool struct {
env *fission.Environment env *fission.Environment
replicas int32 // num containers replicas int32 // num containers
deployment *v1beta1.Deployment // kubernetes deployment deployment *v1beta1.Deployment // kubernetes deployment
namespace string // namespace to keep our resources namespace string // namespace to keep our resources
podReadyTimeout time.Duration // timeout for generic pods to become ready podReadyTimeout time.Duration // timeout for generic pods to become ready
controllerUrl string controllerUrl string
useSvc bool idlePodReapTime time.Duration // pods unused for idlePodReapTime are deleted
fsCache *functionServiceCache // cache funcSvc's by function, address and podname
useSvc bool // create service
poolInstanceId string // small random string to uniquify pod names
kubernetesClient *kubernetes.Clientset kubernetesClient *kubernetes.Clientset
requestChannel chan *choosePodRequest requestChannel chan *choosePodRequest
} }
@@ -67,18 +71,25 @@ func MakeGenericPool(
kubernetesClient *kubernetes.Clientset, kubernetesClient *kubernetes.Clientset,
env *fission.Environment, env *fission.Environment,
initialReplicas int32, initialReplicas int32,
namespace string) (*GenericPool, error) { namespace string,
fsCache *functionServiceCache) (*GenericPool, error) {
log.Printf("Creating pool for environment %v", env.Metadata) log.Printf("Creating pool for environment %v", env.Metadata)
// TODO: in general we need to provide the user a way to configure pools. Initial
// replicas, autoscaling params, various timeouts, etc.
gp := &GenericPool{ gp := &GenericPool{
env: env, env: env,
replicas: initialReplicas, replicas: initialReplicas, // TODO make this an env param instead?
requestChannel: make(chan *choosePodRequest), requestChannel: make(chan *choosePodRequest),
kubernetesClient: kubernetesClient, kubernetesClient: kubernetesClient,
namespace: namespace, namespace: namespace,
podReadyTimeout: 5 * time.Minute, podReadyTimeout: 5 * time.Minute, // TODO make this an env param?
controllerUrl: controllerUrl, controllerUrl: controllerUrl,
useSvc: false, idlePodReapTime: 3 * time.Minute, // TODO make this configurable
fsCache: fsCache,
poolInstanceId: uniuri.NewLen(8),
useSvc: false,
} }
// create the pool // create the pool
@@ -95,6 +106,9 @@ func MakeGenericPool(
} }
go gp.choosePodService() go gp.choosePodService()
go gp.idlePodReaper()
return gp, nil return gp, nil
} }
@@ -260,7 +274,8 @@ func (gp *GenericPool) specializePod(metadata *fission.Metadata) (*v1.Pod, error
// A pool is a deployment of generic containers for an env. This // A pool is a deployment of generic containers for an env. This
// creates the pool but doesn't wait for any pods to be ready. // creates the pool but doesn't wait for any pods to be ready.
func (gp *GenericPool) createPool() error { func (gp *GenericPool) createPool() error {
poolDeploymentName := fmt.Sprintf("%v-%v", gp.env.Metadata.Name, gp.env.Metadata.Uid) poolDeploymentName := fmt.Sprintf("%v-%v-%v",
gp.env.Metadata.Name, gp.env.Metadata.Uid, strings.ToLower(gp.poolInstanceId))
podLabels := map[string]string{ podLabels := map[string]string{
"pool": poolDeploymentName, "pool": poolDeploymentName,
@@ -408,9 +423,59 @@ func (gp *GenericPool) GetFuncSvc(m *fission.Metadata) (*funcSvc, error) {
fsvc := &funcSvc{ fsvc := &funcSvc{
function: m, function: m,
environment: gp.env, environment: gp.env,
serviceName: svcHost, address: svcHost,
podName: pod.ObjectMeta.Name,
ctime: time.Now(), ctime: time.Now(),
atime: time.Now(), atime: time.Now(),
} }
err, existingFsvc := gp.fsCache.Add(*fsvc)
if err != nil {
// Some other thread beat us to it -- return the other thread's fsvc and clean up
// our own. TODO: this is grossly inefficient, improve it with some sort of state
// machine
log.Printf("func svc already exists: %v", existingFsvc.podName)
go gp.CleanupFunctionService(fsvc.podName)
return existingFsvc, nil
}
return fsvc, nil return fsvc, nil
} }
func (gp *GenericPool) CleanupFunctionService(podName string) error {
// remove ourselves from fsCache (only if we're still old)
deleted, err := gp.fsCache.DeleteByPod(podName, gp.idlePodReapTime)
if err != nil {
return err
}
if !deleted {
log.Printf("Not deleting %v, in use", podName)
return nil
}
// delete pod
err = gp.kubernetesClient.Core().Pods(gp.namespace).Delete(podName, nil)
if err != nil {
return err
}
return nil
}
func (gp *GenericPool) idlePodReaper() {
for {
time.Sleep(time.Minute)
podNames, err := gp.fsCache.ListOld(gp.idlePodReapTime)
if err != nil {
log.Printf("Error reaping idle pods: %v", err)
continue
}
for _, podName := range podNames {
log.Printf("Reaping idle pod '%v'", podName)
err := gp.CleanupFunctionService(podName)
if err != nil {
log.Printf("Error deleting idle pod '%v': %v", podName, err)
}
}
}
}
-93
View File
@@ -1,93 +0,0 @@
package poolmgr
import (
"github.com/platform9/fission"
"fmt"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"log"
"net/http"
"testing"
)
func getKubeClient() *unversioned.Client {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
panic("failed loading client config")
}
client := unversioned.NewOrDie(config)
return client
}
type staticHandler struct {
resp string
}
func (s *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(s.resp))
}
// staticHttpServer starts an http server at port and responds to any
// request with the given response. Use this to mock the controller
// raw function fetch HTTP endpoint.
func staticHttpServer(port int, response string) {
s := &staticHandler{resp: response}
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), s))
}
func TestGenericPool(t *testing.T) {
namespace := "fission-test"
client := getKubeClient()
_, err := client.Namespaces().Create(&api.Namespace{
ObjectMeta: api.ObjectMeta{
Name: namespace,
Labels: map[string]string{},
},
})
if err != nil {
log.Panicf("failed to create namespace: %v", err)
}
// destroys everything in the namespace
defer client.Namespaces().Delete(namespace)
env := &fission.Environment{
Metadata: fission.Metadata{
Name: "test-env",
Uid: "",
},
RunContainerImageUrl: "fission/testing",
}
gp, err := MakeGenericPool(client, env, 3, namespace)
if err != nil {
log.Panicf("failed to make generic pool: %v", err)
}
log.Printf("Pool created")
// test specialization
testFunc := `
module.exports = function (context, callback) {
callback(200, "Hello, world!");
}
`
go staticHttpServer(2222, testFunc)
m := fission.Metadata{
Name: "foo",
Uid: "xxx-yyy",
}
fsvc, err := gp.GetFuncSvc(&m)
if err != nil {
log.Fatalf("Error getting function svc: %v", err)
}
log.Printf("fsvc: %v", fsvc)
}
+8 -3
View File
@@ -33,6 +33,7 @@ type (
namespace string namespace string
controllerUrl string controllerUrl string
controllerClient *client.Client controllerClient *client.Client
fsCache *functionServiceCache
requestChannel chan *request requestChannel chan *request
} }
@@ -46,13 +47,14 @@ type (
} }
) )
func MakeGenericPoolManager(controllerUrl string, kubernetesClient *kubernetes.Clientset, namespace string) *GenericPoolManager { func MakeGenericPoolManager(controllerUrl string, kubernetesClient *kubernetes.Clientset, namespace string, fsCache *functionServiceCache) *GenericPoolManager {
gpm := &GenericPoolManager{ gpm := &GenericPoolManager{
pools: make(map[fission.Environment]*GenericPool), pools: make(map[fission.Environment]*GenericPool),
kubernetesClient: kubernetesClient, kubernetesClient: kubernetesClient,
namespace: namespace, namespace: namespace,
controllerUrl: controllerUrl, controllerUrl: controllerUrl,
controllerClient: client.MakeClient(controllerUrl), controllerClient: client.MakeClient(controllerUrl),
fsCache: fsCache,
requestChannel: make(chan *request), requestChannel: make(chan *request),
} }
go gpm.service() go gpm.service()
@@ -68,7 +70,7 @@ func (gpm *GenericPoolManager) service() {
var err error var err error
pool, ok := gpm.pools[*req.env] pool, ok := gpm.pools[*req.env]
if !ok { if !ok {
pool, err = MakeGenericPool(gpm.controllerUrl, gpm.kubernetesClient, req.env, 3, gpm.namespace) pool, err = MakeGenericPool(gpm.controllerUrl, gpm.kubernetesClient, req.env, 3, gpm.namespace, gpm.fsCache)
if err != nil { if err != nil {
req.responseChannel <- &response{error: err} req.responseChannel <- &response{error: err}
continue continue
@@ -108,7 +110,10 @@ func (gpm *GenericPoolManager) eagerPoolCreator() {
// to keep these eagerly created pools smaller than the ones created when there are // to keep these eagerly created pools smaller than the ones created when there are
// actual function calls. // actual function calls.
for _, env := range envs { for _, env := range envs {
gpm.GetPool(&env) _, err := gpm.GetPool(&env)
if err != nil {
log.Printf("eager-create pool failed: %v", err)
}
} }
} }
} }
+4 -2
View File
@@ -56,9 +56,11 @@ func StartPoolmgr(controllerUrl string, namespace string, port int) error {
return err return err
} }
gpm := MakeGenericPoolManager(controllerUrl, kubernetesClient, namespace) fsCache := MakeFunctionServiceCache()
gpm := MakeGenericPoolManager(controllerUrl, kubernetesClient, namespace, fsCache)
api := MakeAPI(gpm, controllerClient, fsCache)
api := MakeAPI(gpm, controllerClient)
go api.Serve(port) go api.Serve(port)
return nil return nil
+16
View File
@@ -80,7 +80,19 @@ func (rrt RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er
return http.DefaultTransport.RoundTrip(req) return http.DefaultTransport.RoundTrip(req)
} }
func (fh *functionHandler) tapService(serviceUrl *url.URL) {
if fh.poolmgr == nil {
return
}
err := fh.poolmgr.TapService(serviceUrl)
if err != nil {
log.Printf("tap service error: %v", serviceUrl.String())
}
}
func (fh *functionHandler) handler(responseWriter http.ResponseWriter, request *http.Request) { func (fh *functionHandler) handler(responseWriter http.ResponseWriter, request *http.Request) {
// cache lookup
serviceUrl, err := fh.fmap.lookup(&fh.Function) serviceUrl, err := fh.fmap.lookup(&fh.Function)
if err != nil { if err != nil {
// Cache miss: request the Pool Manager to make a new service. // Cache miss: request the Pool Manager to make a new service.
@@ -99,6 +111,10 @@ func (fh *functionHandler) handler(responseWriter http.ResponseWriter, request *
// add it to the map // add it to the map
fh.fmap.assign(&fh.Function, serviceUrl) fh.fmap.assign(&fh.Function, serviceUrl)
} else {
// if we're using our cache, asynchronously tell
// poolmgr we're using this service
go fh.tapService(serviceUrl)
} }
// Proxy off our request to the serviceUrl, and send the response back. // Proxy off our request to the serviceUrl, and send the response back.
+1 -1
View File
@@ -50,7 +50,7 @@ func TestFunctionProxying(t *testing.T) {
log.Printf("Created backend svc at %v", backendURL) log.Printf("Created backend svc at %v", backendURL)
fn := &fission.Metadata{Name: "foo", Uid: "xxx"} fn := &fission.Metadata{Name: "foo", Uid: "xxx"}
fmap := makeFunctionServiceMap() fmap := makeFunctionServiceMap(0)
fmap.assign(fn, backendURL) fmap.assign(fn, backendURL)
fh := &functionHandler{fmap: fmap, Function: *fn} fh := &functionHandler{fmap: fmap, Function: *fn}
+16 -76
View File
@@ -17,97 +17,37 @@ limitations under the License.
package router package router
import ( import (
"errors"
"log" "log"
"net/url" "net/url"
"time"
"github.com/platform9/fission" "github.com/platform9/fission"
"github.com/platform9/fission/cache"
) )
type requestType int
const (
LOOKUP requestType = iota // lookup the map
ASSIGN // assign function
NEXT_GEN // increment current generation
SWEEP // delete all but the current generation
)
type functionServiceMapResponse struct {
serviceUrl url.URL
error
}
type functionServiceMapRequest struct {
Function fission.Metadata
serviceUrl url.URL
requestType
responseChannel chan<- functionServiceMapResponse
}
type functionServiceMapEntry struct {
serviceUrl url.URL
generation uint64
}
type functionServiceMap struct { type functionServiceMap struct {
// map (funcname, uid) -> url cache *cache.Cache // map[fission.Metadata]*url.URL
svc map[fission.Metadata]functionServiceMapEntry
currentGeneration uint64
requestChannel chan *functionServiceMapRequest
} }
func makeFunctionServiceMap() *functionServiceMap { func makeFunctionServiceMap(expiry time.Duration) *functionServiceMap {
fmap := &functionServiceMap{} return &functionServiceMap{
fmap.requestChannel = make(chan *functionServiceMapRequest) cache: cache.MakeCache(expiry, 0),
fmap.svc = make(map[fission.Metadata]functionServiceMapEntry)
go fmap.functionServiceMapWork()
return fmap
}
func (fmap *functionServiceMap) functionServiceMapWork() {
for {
req := <-fmap.requestChannel
switch req.requestType {
case LOOKUP:
e, present := fmap.svc[req.Function]
if present {
req.responseChannel <- functionServiceMapResponse{serviceUrl: e.serviceUrl}
} else {
req.responseChannel <- functionServiceMapResponse{error: errors.New("not found")}
}
case ASSIGN:
fmap.svc[req.Function] =
functionServiceMapEntry{serviceUrl: req.serviceUrl, generation: fmap.currentGeneration}
// no response
case NEXT_GEN:
fmap.currentGeneration++
// no response
case SWEEP:
log.Panic("not implemented")
default:
log.Panic("bad request")
}
} }
} }
func (fmap *functionServiceMap) lookup(f *fission.Metadata) (*url.URL, error) { func (fmap *functionServiceMap) lookup(f *fission.Metadata) (*url.URL, error) {
respChannel := make(chan functionServiceMapResponse) item, err := fmap.cache.Get(*f)
fmap.requestChannel <- &functionServiceMapRequest{Function: *f, requestType: LOOKUP, responseChannel: respChannel} if err != nil {
resp := <-respChannel return nil, err
if resp.error != nil {
return nil, resp.error
} else {
return &resp.serviceUrl, nil
} }
u := item.(*url.URL)
return u, nil
} }
func (fmap *functionServiceMap) assign(f *fission.Metadata, serviceUrl *url.URL) { func (fmap *functionServiceMap) assign(f *fission.Metadata, serviceUrl *url.URL) {
fmap.requestChannel <- &functionServiceMapRequest{Function: *f, serviceUrl: *serviceUrl, requestType: ASSIGN} err, _ := fmap.cache.Set(*f, serviceUrl)
} if err != nil {
log.Printf("error caching service url for function: %v", err)
func (fmap *functionServiceMap) nextGen() { // ignore error
fmap.requestChannel <- &functionServiceMapRequest{requestType: NEXT_GEN} }
}
func (fmap *functionServiceMap) sweep() {
fmap.requestChannel <- &functionServiceMapRequest{requestType: SWEEP}
} }
+1 -1
View File
@@ -24,7 +24,7 @@ import (
) )
func TestFunctionServiceMap(t *testing.T) { func TestFunctionServiceMap(t *testing.T) {
m := makeFunctionServiceMap() m := makeFunctionServiceMap(0)
fn := &fission.Metadata{Name: "foo", Uid: "012"} fn := &fission.Metadata{Name: "foo", Uid: "012"}
u, err := url.Parse("/foo012") u, err := url.Parse("/foo012")
if err != nil { if err != nil {
+3 -2
View File
@@ -41,13 +41,14 @@ package router
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
controllerClient "github.com/platform9/fission/controller/client" controllerClient "github.com/platform9/fission/controller/client"
poolmgrClient "github.com/platform9/fission/poolmgr/client" poolmgrClient "github.com/platform9/fission/poolmgr/client"
"log"
) )
// request url ---[mux]---> Function(name,uid) ----[fmap]----> k8s service url // request url ---[mux]---> Function(name,uid) ----[fmap]----> k8s service url
@@ -68,7 +69,7 @@ func serve(port int, httpTriggerSet *HTTPTriggerSet) {
} }
func Start(port int, controllerUrl string, poolmgrUrl string) { func Start(port int, controllerUrl string, poolmgrUrl string) {
fmap := makeFunctionServiceMap() fmap := makeFunctionServiceMap(time.Minute)
controller := controllerClient.MakeClient(controllerUrl) controller := controllerClient.MakeClient(controllerUrl)
poolmgr := poolmgrClient.MakeClient(poolmgrUrl) poolmgr := poolmgrClient.MakeClient(poolmgrUrl)
+1 -1
View File
@@ -25,7 +25,7 @@ import (
) )
func TestRouter(t *testing.T) { func TestRouter(t *testing.T) {
fmap := makeFunctionServiceMap() fmap := makeFunctionServiceMap(0)
fn := &fission.Metadata{Name: "foo", Uid: "xxx"} fn := &fission.Metadata{Name: "foo", Uid: "xxx"}
testResponseString := "hi" testResponseString := "hi"