From f6d19c3cc9f6008d6d406dbf00c5ac0ac8655184 Mon Sep 17 00:00:00 2001 From: C Cirello Date: Mon, 21 Nov 2016 19:48:11 +0100 Subject: [PATCH] functions: performance improvements - LRU & singleflight DB calls (#322) * functions: add cache and singleflight to ease database load * runner: upgrade * deps: upgrade glide files * license: add third party notifications * functions: fix handling of implicitly created apps * functions: code deduplication * functions: fix missing variable --- THIRD_PARTY | 4 ++ api/datastore/mock.go | 2 +- api/runner/async_runner.go | 28 ++------- api/runner/async_runner_test.go | 2 +- api/runner/task.go | 3 +- api/server/apps_create.go | 4 +- api/server/internal/routecache/lru.go | 77 ++++++++++++++++++++++++ api/server/routes_create.go | 4 +- api/server/routes_delete.go | 4 +- api/server/runner.go | 61 +++++++++++++------ api/server/server.go | 85 +++++++++++++++++++++++++-- api/server/singleflight.go | 50 ++++++++++++++++ glide.lock | 33 +++++------ main.go | 1 + 14 files changed, 289 insertions(+), 69 deletions(-) create mode 100644 api/server/internal/routecache/lru.go create mode 100644 api/server/singleflight.go diff --git a/THIRD_PARTY b/THIRD_PARTY index 032503233..903b20a59 100644 --- a/THIRD_PARTY +++ b/THIRD_PARTY @@ -2,3 +2,7 @@ This software uses third-party software. For: api/server/tree.go Copyright 2013 Julien Schmidt. All rights reserved. BSD-license + +For: api/server/internal/routecache/lru.go +For: api/server/singleflight.go +Copyright 2012 Google Inc. All rights reserved. Apache 2 license \ No newline at end of file diff --git a/api/datastore/mock.go b/api/datastore/mock.go index 10a9d128d..197244852 100644 --- a/api/datastore/mock.go +++ b/api/datastore/mock.go @@ -65,7 +65,7 @@ func (m *Mock) GetRoutesByApp(appName string, routeFilter *models.RouteFilter) ( route := m.FakeRoute if route == nil && m.FakeRoutes != nil { for _, r := range m.FakeRoutes { - if r.AppName == appName && r.Path == routeFilter.Path && r.AppName == routeFilter.AppName { + if r.AppName == appName && (routeFilter.Path == "" || r.Path == routeFilter.Path) && (routeFilter.AppName == "" || r.AppName == routeFilter.AppName) { routes = append(routes, r) } } diff --git a/api/runner/async_runner.go b/api/runner/async_runner.go index 95597c386..6aa02d576 100644 --- a/api/runner/async_runner.go +++ b/api/runner/async_runner.go @@ -86,24 +86,12 @@ func deleteTask(url string, task *models.Task) error { // RunAsyncRunner pulls tasks off a queue and processes them func RunAsyncRunner(ctx context.Context, tasksrv string, tasks chan TaskRequest, rnr *Runner) { - u, h := tasksrvURL(tasksrv) - if isHostOpen(h) { - return - } + u := tasksrvURL(tasksrv) startAsyncRunners(ctx, u, tasks, rnr) <-ctx.Done() } -func isHostOpen(host string) bool { - conn, err := net.Dial("tcp", host) - available := err == nil - if available { - conn.Close() - } - return available -} - func startAsyncRunners(ctx context.Context, url string, tasks chan TaskRequest, rnr *Runner) { var wg sync.WaitGroup ctx, log := common.LoggerWithFields(ctx, logrus.Fields{"runner": "async"}) @@ -159,15 +147,11 @@ func startAsyncRunners(ctx context.Context, url string, tasks chan TaskRequest, } } -func tasksrvURL(tasksrv string) (parsedURL, host string) { +func tasksrvURL(tasksrv string) string { parsed, err := url.Parse(tasksrv) if err != nil { - logrus.WithError(err).Fatalln("cannot parse TASKSRV endpoint") + logrus.WithError(err).Fatalln("cannot parse API_URL endpoint") } - // host, port, err := net.SplitHostPort(parsed.Host) - // if err != nil { - // log.WithError(err).Fatalln("net.SplitHostPort") - // } if parsed.Scheme == "" { parsed.Scheme = "http" @@ -177,9 +161,5 @@ func tasksrvURL(tasksrv string) (parsedURL, host string) { parsed.Path = "/tasks" } - // if _, _, err := net.SplitHostPort(parsed.Host); err != nil { - // parsed.Host = net.JoinHostPort(parsed.Host, parsed) - // } - - return parsed.String(), parsed.Host + return parsed.String() } diff --git a/api/runner/async_runner_test.go b/api/runner/async_runner_test.go index 2cadea350..9b7584553 100644 --- a/api/runner/async_runner_test.go +++ b/api/runner/async_runner_test.go @@ -182,7 +182,7 @@ func TestTasksrvURL(t *testing.T) { } for _, tt := range tests { - if got, _ := tasksrvURL(tt.in); got != tt.out { + if got := tasksrvURL(tt.in); got != tt.out { t.Errorf("tasksrv: %s\texpected: %s\tgot: %s\t", tt.in, tt.out, got) } } diff --git a/api/runner/task.go b/api/runner/task.go index 5147ad2db..701ef057a 100644 --- a/api/runner/task.go +++ b/api/runner/task.go @@ -3,6 +3,7 @@ package runner import ( "context" "io" + "time" "github.com/fsouza/go-dockerclient" "github.com/iron-io/runner/drivers" @@ -32,7 +33,7 @@ func (t *containerTask) Labels() map[string]string { func (t *containerTask) Id() string { return t.cfg.ID } func (t *containerTask) Route() string { return "" } func (t *containerTask) Image() string { return t.cfg.Image } -func (t *containerTask) Timeout() uint { return uint(t.cfg.Timeout.Seconds()) } +func (t *containerTask) Timeout() time.Duration { return t.cfg.Timeout } func (t *containerTask) Logger() (stdout, stderr io.Writer) { return t.cfg.Stdout, t.cfg.Stderr } func (t *containerTask) Volumes() [][2]string { return [][2]string{} } func (t *containerTask) WorkDir() string { return "" } diff --git a/api/server/apps_create.go b/api/server/apps_create.go index e12c3b378..e71c7809c 100644 --- a/api/server/apps_create.go +++ b/api/server/apps_create.go @@ -9,7 +9,7 @@ import ( "github.com/iron-io/runner/common" ) -func handleAppCreate(c *gin.Context) { +func (s *Server) handleAppCreate(c *gin.Context) { ctx := c.MustGet("ctx").(context.Context) log := common.Logger(ctx) @@ -55,5 +55,7 @@ func handleAppCreate(c *gin.Context) { return } + s.resetcache(wapp.App.Name, 1) + c.JSON(http.StatusCreated, appResponse{"App successfully created", wapp.App}) } diff --git a/api/server/internal/routecache/lru.go b/api/server/internal/routecache/lru.go new file mode 100644 index 000000000..c26f49eca --- /dev/null +++ b/api/server/internal/routecache/lru.go @@ -0,0 +1,77 @@ +// Package routecache is meant to assist in resolving the most used routes at +// an application. Implemented as a LRU, it returns always its full context for +// iteration at the router handler. +package routecache + +// based on groupcache's LRU + +import ( + "container/list" + + "github.com/iron-io/functions/api/models" +) + +// Cache holds an internal linkedlist for hotness management. It is not safe +// for concurrent use, must be guarded externally. +type Cache struct { + MaxEntries int + + ll *list.List + cache map[string]*list.Element +} + +// New returns a route cache. +func New(maxentries int) *Cache { + return &Cache{ + MaxEntries: maxentries, + ll: list.New(), + cache: make(map[string]*list.Element), + } +} + +// Refresh updates internal linkedlist either adding a new route to the front, +// or moving it to the front when used. It will discard seldom used routes. +func (c *Cache) Refresh(route *models.Route) { + if c.cache == nil { + return + } + + if ee, ok := c.cache[route.Path]; ok { + c.ll.MoveToFront(ee) + ee.Value = route + return + } + + ele := c.ll.PushFront(route) + c.cache[route.Path] = ele + if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { + c.removeOldest() + } +} + +// Get looks up a path's route from the cache. +func (c *Cache) Get(path string) (route *models.Route, ok bool) { + if c.cache == nil { + return + } + if ele, hit := c.cache[path]; hit { + c.ll.MoveToFront(ele) + return ele.Value.(*models.Route), true + } + return +} + +func (c *Cache) removeOldest() { + if c.cache == nil { + return + } + if ele := c.ll.Back(); ele != nil { + c.removeElement(ele) + } +} + +func (c *Cache) removeElement(e *list.Element) { + c.ll.Remove(e) + kv := e.Value.(*models.Route) + delete(c.cache, kv.Path) +} diff --git a/api/server/routes_create.go b/api/server/routes_create.go index 7374110ee..b1ae16f18 100644 --- a/api/server/routes_create.go +++ b/api/server/routes_create.go @@ -10,7 +10,7 @@ import ( "github.com/iron-io/runner/common" ) -func handleRouteCreate(c *gin.Context) { +func (s *Server) handleRouteCreate(c *gin.Context) { ctx := c.MustGet("ctx").(context.Context) log := common.Logger(ctx) @@ -80,5 +80,7 @@ func handleRouteCreate(c *gin.Context) { return } + s.resetcache(wroute.Route.AppName, 1) + c.JSON(http.StatusCreated, routeResponse{"Route successfully created", wroute.Route}) } diff --git a/api/server/routes_delete.go b/api/server/routes_delete.go index c82fae7f2..a49b1ae1a 100644 --- a/api/server/routes_delete.go +++ b/api/server/routes_delete.go @@ -9,7 +9,7 @@ import ( "github.com/iron-io/runner/common" ) -func handleRouteDelete(c *gin.Context) { +func (s *Server) handleRouteDelete(c *gin.Context) { ctx := c.MustGet("ctx").(context.Context) log := common.Logger(ctx) @@ -23,5 +23,7 @@ func handleRouteDelete(c *gin.Context) { return } + s.resetcache(appName, 0) + c.JSON(http.StatusOK, gin.H{"message": "Route deleted"}) } diff --git a/api/server/runner.go b/api/server/runner.go index 5ee69cd36..3f2b600ff 100644 --- a/api/server/runner.go +++ b/api/server/runner.go @@ -76,13 +76,11 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) { c.JSON(http.StatusBadRequest, simpleError(models.ErrAppsNotFound)) return } - route := c.Param("route") - if route == "" { - route = c.Request.URL.Path + path := c.Param("route") + if path == "" { + path = c.Request.URL.Path } - log.WithFields(logrus.Fields{"app": appName, "path": route}).Debug("Finding route on datastore") - app, err := Api.Datastore.GetApp(appName) if err != nil || app == nil { log.WithError(err).Error(models.ErrAppsNotFound) @@ -90,30 +88,56 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) { return } - routes, err := Api.Datastore.GetRoutesByApp(appName, &models.RouteFilter{AppName: appName, Path: route}) + log.WithFields(logrus.Fields{"app": appName, "path": path}).Debug("Finding route on LRU cache") + route, ok := s.cacheget(appName, path) + if ok && s.serve(c, log, appName, route, app, path, reqID, payload, enqueue) { + s.refreshcache(appName, route) + return + } + + log.WithFields(logrus.Fields{"app": appName, "path": path}).Debug("Finding route on datastore") + routes, err := s.loadroutes(models.RouteFilter{AppName: appName, Path: path}) if err != nil { log.WithError(err).Error(models.ErrRoutesList) c.JSON(http.StatusInternalServerError, simpleError(models.ErrRoutesList)) return } - log.WithField("routes", routes).Debug("Got routes from datastore") - if len(routes) == 0 { log.WithError(err).Error(models.ErrRunnerRouteNotFound) c.JSON(http.StatusNotFound, simpleError(models.ErrRunnerRouteNotFound)) return } - found := routes[0] - log = log.WithFields(logrus.Fields{ - "app": appName, "route": found.Path, "image": found.Image}) + log.WithField("routes", len(routes)).Debug("Got routes from datastore") + route = routes[0] + log = log.WithFields(logrus.Fields{"app": appName, "path": route.Path, "image": route.Image}) + + if s.serve(c, log, appName, route, app, path, reqID, payload, enqueue) { + s.refreshcache(appName, route) + return + } + + log.Error(models.ErrRunnerRouteNotFound) + c.JSON(http.StatusNotFound, simpleError(models.ErrRunnerRouteNotFound)) +} + +func (s *Server) loadroutes(filter models.RouteFilter) ([]*models.Route, error) { + resp, err := s.singleflight.do( + filter, + func() (interface{}, error) { + return Api.Datastore.GetRoutesByApp(filter.AppName, &filter) + }, + ) + return resp.([]*models.Route), err +} + +func (s *Server) serve(c *gin.Context, log logrus.FieldLogger, appName string, found *models.Route, app *models.App, route, reqID string, payload io.Reader, enqueue models.Enqueue) (ok bool) { + log = log.WithFields(logrus.Fields{"app": appName, "route": found.Path, "image": found.Image}) params, match := matchRoute(found.Path, route) if !match { - log.WithError(err).Error(models.ErrRunnerRouteNotFound) - c.JSON(http.StatusNotFound, simpleError(models.ErrRunnerRouteNotFound)) - return + return false } var stdout bytes.Buffer // TODO: should limit the size of this, error if gets too big. akin to: https://golang.org/pkg/io/#LimitReader @@ -162,7 +186,7 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) { if err != nil { log.WithError(err).Error(models.ErrInvalidPayload) c.JSON(http.StatusBadRequest, simpleError(models.ErrInvalidPayload)) - return + return true } // Create Task @@ -176,13 +200,12 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) { task.EnvVars = cfg.Env task.Payload = string(pl) // Push to queue - enqueue(ctx, s.MQ, task) + enqueue(c, s.MQ, task) log.Info("Added new task to queue") c.JSON(http.StatusAccepted, map[string]string{"call_id": task.ID}) default: - - result, err := runner.RunTask(s.tasks, ctx, cfg) + result, err := runner.RunTask(s.tasks, c, cfg) if err != nil { break } @@ -197,6 +220,8 @@ func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) { } } + + return true } var fakeHandler = func(http.ResponseWriter, *http.Request, Params) {} diff --git a/api/server/server.go b/api/server/server.go index fe9d2c38f..f5180b380 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -5,14 +5,17 @@ import ( "encoding/json" "errors" "io/ioutil" + "math" "net/http" "path" + "sync" "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" "github.com/iron-io/functions/api/ifaces" "github.com/iron-io/functions/api/models" "github.com/iron-io/functions/api/runner" + "github.com/iron-io/functions/api/server/internal/routecache" "github.com/iron-io/runner/common" ) @@ -23,13 +26,18 @@ var Api *Server type Server struct { Runner *runner.Runner Router *gin.Engine - Datastore models.Datastore MQ models.MessageQueue AppListeners []ifaces.AppListener SpecialHandlers []ifaces.SpecialHandler Enqueue models.Enqueue tasks chan runner.TaskRequest + + mu sync.Mutex // protects hotroutes + hotroutes map[string]*routecache.Cache + + singleflight singleflight // singleflight assists Datastore + Datastore models.Datastore } func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, r *runner.Runner, tasks chan runner.TaskRequest, enqueue models.Enqueue) *Server { @@ -38,6 +46,7 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, r *ru Router: gin.New(), Datastore: ds, MQ: mq, + hotroutes: make(map[string]*routecache.Cache), tasks: tasks, Enqueue: enqueue, } @@ -47,10 +56,43 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, r *ru c.Set("ctx", ctx) c.Next() }) + Api.primeCache() return Api } +func (s *Server) primeCache() { + logrus.Info("priming cache with known routes") + apps, err := s.Datastore.GetApps(nil) + if err != nil { + logrus.WithError(err).Error("cannot prime cache - could not load application list") + return + } + for _, app := range apps { + routes, err := s.Datastore.GetRoutesByApp(app.Name, &models.RouteFilter{AppName: app.Name}) + if err != nil { + logrus.WithError(err).WithField("appName", app.Name).Error("cannot prime cache - could not load routes") + continue + } + + entries := len(routes) + // The idea here is to prevent both extremes: cache being too small that is ineffective, + // or too large that it takes too much memory. Up to 1k routes, the cache will try to hold + // all routes in the memory, thus taking up to 48K per application. After this threshold, + // it will keep 1024 routes + 20% of the total entries - in a hybrid incarnation of Pareto rule + // 1024+20% of the remaining routes will likelly be responsible for 80% of the workload. + if entries > cacheParetoThreshold { + entries = int(math.Ceil(float64(entries-1024)*0.2)) + 1024 + } + s.hotroutes[app.Name] = routecache.New(entries) + + for i := 0; i < entries; i++ { + s.refreshcache(app.Name, routes[i]) + } + } + logrus.Info("cached prime") +} + // AddAppListener adds a listener that will be notified on App changes. func (s *Server) AddAppListener(listener ifaces.AppListener) { s.AppListeners = append(s.AppListeners, listener) @@ -105,6 +147,41 @@ func (s *Server) handleRunnerRequest(c *gin.Context) { s.handleRequest(c, s.Enqueue) } +// cacheParetoThreshold is both the mark from which the LRU starts caching only +// the most likely hot routes, and also as a stopping mark for the cache priming +// during start. +const cacheParetoThreshold = 1024 + +func (s *Server) cacheget(appname, path string) (*models.Route, bool) { + s.mu.Lock() + cache, ok := s.hotroutes[appname] + if !ok { + s.mu.Unlock() + return nil, false + } + route, ok := cache.Get(path) + s.mu.Unlock() + return route, ok +} + +func (s *Server) refreshcache(appname string, route *models.Route) { + s.mu.Lock() + cache := s.hotroutes[appname] + cache.Refresh(route) + s.mu.Unlock() +} + +func (s *Server) resetcache(appname string, delta int) { + s.mu.Lock() + hr, ok := s.hotroutes[appname] + if !ok { + s.hotroutes[appname] = routecache.New(0) + hr = s.hotroutes[appname] + } + s.hotroutes[appname] = routecache.New(hr.MaxEntries + delta) + s.mu.Unlock() +} + func (s *Server) handleTaskRequest(c *gin.Context) { ctx, _ := common.LoggerWithFields(c, nil) switch c.Request.Method { @@ -164,7 +241,7 @@ func (s *Server) bindHandlers() { v1 := engine.Group("/v1") { v1.GET("/apps", handleAppList) - v1.POST("/apps", handleAppCreate) + v1.POST("/apps", s.handleAppCreate) v1.GET("/apps/:app", handleAppGet) v1.PUT("/apps/:app", handleAppUpdate) @@ -175,10 +252,10 @@ func (s *Server) bindHandlers() { apps := v1.Group("/apps/:app") { apps.GET("/routes", handleRouteList) - apps.POST("/routes", handleRouteCreate) + apps.POST("/routes", s.handleRouteCreate) apps.GET("/routes/*route", handleRouteGet) apps.PUT("/routes/*route", handleRouteUpdate) - apps.DELETE("/routes/*route", handleRouteDelete) + apps.DELETE("/routes/*route", s.handleRouteDelete) } } diff --git a/api/server/singleflight.go b/api/server/singleflight.go new file mode 100644 index 000000000..ed39a6e3a --- /dev/null +++ b/api/server/singleflight.go @@ -0,0 +1,50 @@ +package server + +// Imported from https://github.com/golang/groupcache/blob/master/singleflight/singleflight.go + +import ( + "sync" + + "github.com/iron-io/functions/api/models" +) + +// call is an in-flight or completed do call +type call struct { + wg sync.WaitGroup + val interface{} + err error +} + +type singleflight struct { + mu sync.Mutex // protects m + m map[models.RouteFilter]*call // lazily initialized +} + +// do executes and returns the results of the given function, making +// sure that only one execution is in-flight for a given key at a +// time. If a duplicate comes in, the duplicate caller waits for the +// original to complete and receives the same results. +func (g *singleflight) do(key models.RouteFilter, fn func() (interface{}, error)) (interface{}, error) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[models.RouteFilter]*call) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err + } + c := new(call) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err +} diff --git a/glide.lock b/glide.lock index 41da7110d..6592136ed 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 4acb4372011661fa4731e89ffd27714eb7ca6285a3d53c2f16f87c3a564d4d4e -updated: 2016-11-02T11:42:34.9563341-07:00 +hash: a35b8b9511d2059122e72773c169b4dbbf9554dd9075ad55352facf7eae09895 +updated: 2016-11-20T20:18:19.564468442+01:00 imports: - name: github.com/amir/raidman version: c74861fe6a7bb8ede0a010ce4485bdbb4fc4c985 @@ -25,16 +25,11 @@ imports: - aws/signer/v4 - private/endpoints - private/protocol - - private/protocol/query - - private/protocol/query/queryutil + - private/protocol/json/jsonutil + - private/protocol/jsonrpc - private/protocol/rest - - private/protocol/restxml - - private/protocol/xml/xmlutil - - private/waiter - - service/cloudfront/sign - - service/s3 - - vendor/github.com/go-ini/ini - - vendor/github.com/jmespath/go-jmespath + - private/protocol/restjson + - service/lambda - name: github.com/Azure/go-ansiterm version: fa152c58bc15761d0200cb75fe958b89a9d4888e subpackages: @@ -61,8 +56,10 @@ imports: version: 4385816142116aade2d97d0f320f9d3666e74cd9 - name: github.com/dghubble/sling version: c961a4334054e64299d16f8a31bd686ee2565ae4 +- name: github.com/dgrijalva/jwt-go + version: 9ed569b5d1ac936e6494082958d63a6aa4fff99a - name: github.com/docker/distribution - version: 6edf9c507051be36d82afbd71a3f2a7cdbbf4394 + version: 99cb7c0946d2f5a38015443e515dc916295064d7 subpackages: - context - digest @@ -106,7 +103,7 @@ imports: - name: github.com/fsnotify/fsnotify version: fd9ec7deca8bf46ecd2a795baaacf2b3a9be1197 - name: github.com/fsouza/go-dockerclient - version: 5cfde1d138cd2cdc13e4aa36af631beb19dcbe9c + version: ece08f96ac5f26f4073ab5c38f198c3e5000c554 - name: github.com/garyburd/redigo version: 80f7de34463b0ed3d7c61303e5619efe1b227f92 subpackages: @@ -177,8 +174,6 @@ imports: version: 2a2e6b9e3eed0a98d438f111ba7469744c07281d subpackages: - registry -- name: github.com/iron-io/functions_go - version: 584f4a6e13b53370f036012347cf0571128209f0 - name: github.com/iron-io/iron_go3 version: b50ecf8ff90187fc5fabccd9d028dd461adce4ee subpackages: @@ -191,7 +186,7 @@ imports: subpackages: - lambda - name: github.com/iron-io/runner - version: 272e153e728eb4e0c1c92d908c463424dec78a73 + version: 4101fd406ada3497c832d6877653262e23a84f1f repo: https://github.com/iron-io/runner.git vcs: git subpackages: @@ -200,6 +195,8 @@ imports: - drivers - drivers/docker - drivers/mock +- name: github.com/jmespath/go-jmespath + version: 3433f3ea46d9f8019119e7dd41274e112a2359a9 - name: github.com/juju/errgo version: 08cceb5d0b5331634b9826762a8fd53b29b86ad8 subpackages: @@ -233,6 +230,8 @@ imports: version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d - name: github.com/pelletier/go-toml version: 45932ad32dfdd20826f5671da37a5f3ce9f26a8d +- name: github.com/pivotal-golang/bytefmt + version: b12c1522f4cbb5f35861bd5dd2c39a4fa996441a - name: github.com/pkg/errors version: 248dadf4e9068a0b3e79f02ed0a610d935de5302 - name: github.com/pkg/sftp @@ -244,7 +243,7 @@ imports: - name: github.com/satori/go.uuid version: 879c5887cd475cd7864858769793b2ceb0d44feb - name: github.com/Sirupsen/logrus - version: 4b6ea7319e214d98c938f12692336f7ca9348d6b + version: d26492970760ca5d33129d2d799e34be5c4782eb subpackages: - hooks/syslog - name: github.com/spf13/afero diff --git a/main.go b/main.go index f3da4303e..25cb9fd09 100644 --- a/main.go +++ b/main.go @@ -76,6 +76,7 @@ func main() { } svr := &supervisor.Supervisor{ + MaxRestarts: supervisor.AlwaysRestart, Log: func(msg interface{}) { log.Debug("supervisor: ", msg) },