mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
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
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "" }
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
77
api/server/internal/routecache/lru.go
Normal file
77
api/server/internal/routecache/lru.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
50
api/server/singleflight.go
Normal file
50
api/server/singleflight.go
Normal file
@@ -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
|
||||
}
|
||||
33
glide.lock
generated
33
glide.lock
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user