From 54c47998d782c187146a477c07744ec1ba91a136 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Wed, 29 Jun 2016 15:13:26 +0200 Subject: [PATCH] Add caching results of API calls --- app.go | 10 +++++++++ cache/cache.go | 33 ++++++++++++++++++++++++++++ cache/inMemCache.go | 53 +++++++++++++++++++++++++++++++++++++++++++++ service_github.go | 37 ++++++++++++++++++------------- service_travis.go | 36 ++++++++++++++++++------------ 5 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 cache/cache.go create mode 100644 cache/inMemCache.go diff --git a/app.go b/app.go index 31b4ae3..3196f18 100644 --- a/app.go +++ b/app.go @@ -5,6 +5,7 @@ import ( "crypto/sha1" "errors" "fmt" + "log" "net/http" "net/url" "sort" @@ -14,6 +15,7 @@ import ( "golang.org/x/net/context" + "github.com/Luzifer/badge-gen/cache" "github.com/Luzifer/rconfig" "github.com/gorilla/mux" "github.com/tdewolff/minify" @@ -29,9 +31,11 @@ var ( cfg = struct { Port int64 `env:"PORT"` Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` + Cache string `flag:"cache" default:"mem://" description:"Where to cache query results from thirdparty APIs"` }{} serviceHandlers = map[string]serviceHandler{} version = "dev" + cacheStore cache.Cache ) type serviceHandlerDocumentation struct { @@ -72,6 +76,12 @@ func main() { cfg.Listen = fmt.Sprintf(":%d", cfg.Port) } + var err error + cacheStore, err = cache.GetCacheByURI(cfg.Cache) + if err != nil { + log.Fatalf("Unable to open cache: %s", err) + } + r := mux.NewRouter() r.HandleFunc("/v1/badge", generateBadge).Methods("GET") r.HandleFunc("/{service}/{parameters:.*}", generateServiceBadge).Methods("GET") diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..ad4c675 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,33 @@ +package cache + +import ( + "errors" + "net/url" + "time" +) + +type KeyNotFoundError struct{} + +func (k KeyNotFoundError) Error() string { + return "Requested key was not found in database" +} + +type Cache interface { + Get(namespace, key string) (value string, err error) + Set(namespace, key, value string, ttl time.Duration) (err error) + Delete(namespace, key string) (err error) +} + +func GetCacheByURI(uri string) (Cache, error) { + url, err := url.Parse(uri) + if err != nil { + return nil, err + } + + switch url.Scheme { + case "mem": + return NewInMemCache(), nil + default: + return nil, errors.New("Invalid cache scheme: " + url.Scheme) + } +} diff --git a/cache/inMemCache.go b/cache/inMemCache.go new file mode 100644 index 0000000..447c5fa --- /dev/null +++ b/cache/inMemCache.go @@ -0,0 +1,53 @@ +package cache + +import ( + "sync" + "time" +) + +type inMemCacheEntry struct { + Value string + Expires time.Time +} + +type InMemCache struct { + cache map[string]inMemCacheEntry + lock sync.RWMutex +} + +func NewInMemCache() *InMemCache { + return &InMemCache{ + cache: map[string]inMemCacheEntry{}, + } +} + +func (i InMemCache) Get(namespace, key string) (value string, err error) { + i.lock.RLock() + defer i.lock.RUnlock() + + e, ok := i.cache[namespace+"::"+key] + if !ok || e.Expires.Before(time.Now()) { + return "", KeyNotFoundError{} + } + return e.Value, nil +} + +func (i InMemCache) Set(namespace, key, value string, ttl time.Duration) (err error) { + i.lock.Lock() + defer i.lock.Unlock() + + i.cache[namespace+"::"+key] = inMemCacheEntry{ + Value: value, + Expires: time.Now().Add(ttl), + } + + return nil +} + +func (i InMemCache) Delete(namespace, key string) (err error) { + i.lock.Lock() + defer i.lock.Unlock() + + delete(i.cache, namespace+"::"+key) + return nil +} diff --git a/service_github.go b/service_github.go index b4f877f..c260034 100644 --- a/service_github.go +++ b/service_github.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "strings" + "time" "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" @@ -45,27 +46,33 @@ func (g githubServiceHandler) Handle(ctx context.Context, params []string) (titl func (g githubServiceHandler) handleLicense(ctx context.Context, params []string) (title, text, color string, err error) { path := strings.Join([]string{"repos", params[0], params[1], "license"}, "/") - req, _ := http.NewRequest("GET", "https://api.github.com/"+path, nil) - req.Header.Set("Accept", "application/vnd.github.drax-preview+json") + text, err = cacheStore.Get("github_license", path) - var resp *http.Response - resp, err = ctxhttp.Do(ctx, http.DefaultClient, req) if err != nil { - return - } - defer resp.Body.Close() + req, _ := http.NewRequest("GET", "https://api.github.com/"+path, nil) + req.Header.Set("Accept", "application/vnd.github.drax-preview+json") - r := struct { - License struct { - Name string `json:"name"` - } `json:"license"` - }{} - if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { - return + var resp *http.Response + resp, err = ctxhttp.Do(ctx, http.DefaultClient, req) + if err != nil { + return + } + defer resp.Body.Close() + + r := struct { + License struct { + Name string `json:"name"` + } `json:"license"` + }{} + if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { + return + } + + text = r.License.Name + cacheStore.Set("github_license", path, text, 10*time.Minute) } title = "license" - text = r.License.Name color = "007ec6" if text == "" { diff --git a/service_travis.go b/service_travis.go index 956955a..cda8d47 100644 --- a/service_travis.go +++ b/service_travis.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "strings" + "time" "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" @@ -36,26 +37,33 @@ func (t travisServiceHandler) Handle(ctx context.Context, params []string) (titl path := strings.Join([]string{"repos", params[0], params[1], "branches", params[2]}, "/") - var resp *http.Response - resp, err = ctxhttp.Get(ctx, http.DefaultClient, "https://api.travis-ci.org/"+path) + var state string + state, err = cacheStore.Get("travis", path) + if err != nil { - return - } - defer resp.Body.Close() + var resp *http.Response + resp, err = ctxhttp.Get(ctx, http.DefaultClient, "https://api.travis-ci.org/"+path) + if err != nil { + return + } + defer resp.Body.Close() - r := struct { - File string `json:"file"` - Branch struct { - State string `json:"state"` - } `json:"branch"` - }{} + r := struct { + File string `json:"file"` + Branch struct { + State string `json:"state"` + } `json:"branch"` + }{} - if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { - return + if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { + return + } + state = r.Branch.State + cacheStore.Set("travis", path, state, 5*time.Minute) } title = "travis" - text = r.Branch.State + text = state if text == "" { text = "unknown" }