Skip to content

TT-14085: Remove the pmylund/go-cache dependency #6871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions dnscache/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (

"fmt"

cache "github.com/pmylund/go-cache"
"github.com/sirupsen/logrus"

"github.com/TykTechnologies/tyk/internal/cache"
)

// DnsCacheItem represents single record in cache
Expand All @@ -21,8 +22,10 @@ type DnsCacheStorage struct {
}

func NewDnsCacheStorage(expiration, checkInterval time.Duration) *DnsCacheStorage {
storage := DnsCacheStorage{cache.New(expiration, checkInterval)}
return &storage
storage := &DnsCacheStorage{
cache: cache.NewCache(expiration, checkInterval),
}
return storage
}

// Items returns map of non expired dns cache items
Expand Down
46 changes: 29 additions & 17 deletions gateway/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,7 @@ func NewGateway(config config.Config, ctx context.Context) *Gateway {
}
gw.ConnectionWatcher = httputil.NewConnectionWatcher()

gw.SessionCache = cache.New(10, 5)
gw.ExpiryCache = cache.New(600, 10*60)
gw.UtilCache = cache.New(3600, 10*60)

var timeout = int64(config.ServiceDiscovery.DefaultCacheTimeout)
if timeout <= 0 {
timeout = 120 // 2 minutes
}

gw.ServiceCache = cache.New(timeout, 15)
gw.cacheCreate()

gw.apisByID = map[string]*APISpec{}
gw.apisHandlesByID = new(sync.Map)
Expand All @@ -256,6 +247,34 @@ func NewGateway(config config.Config, ctx context.Context) *Gateway {
return gw
}

// cacheCreate will create the caches in *Gateway.
func (gw *Gateway) cacheCreate() {
conf := gw.GetConfig()

gw.SessionCache = cache.New(10, 5)
gw.ExpiryCache = cache.New(600, 10*60)
gw.UtilCache = cache.New(3600, 10*60)

var timeout = int64(conf.ServiceDiscovery.DefaultCacheTimeout)
if timeout <= 0 {
timeout = 120 // 2 minutes
}
gw.ServiceCache = cache.New(timeout, 15)

gw.RPCGlobalCache = cache.New(int64(conf.SlaveOptions.RPCGlobalCacheExpiration), 15)
gw.RPCCertCache = cache.New(int64(conf.SlaveOptions.RPCCertCacheExpiration), 15)
}

// cacheClose will close the caches in *Gateway, cleaning up the goroutines.
func (gw *Gateway) cacheClose() {
gw.SessionCache.Close()
gw.ServiceCache.Close()
gw.ExpiryCache.Close()
gw.UtilCache.Close()
gw.RPCGlobalCache.Close()
gw.RPCCertCache.Close()
}

// SetupNewRelic creates new newrelic.Application instance.
func (gw *Gateway) SetupNewRelic() (app *newrelic.Application) {
var (
Expand Down Expand Up @@ -290,12 +309,6 @@ func (gw *Gateway) MarshalJSON() ([]byte, error) {
return json.Marshal(struct{}{})
}

func (gw *Gateway) initRPCCache() {
conf := gw.GetConfig()
gw.RPCGlobalCache = cache.New(int64(conf.SlaveOptions.RPCGlobalCacheExpiration), 15)
gw.RPCCertCache = cache.New(int64(conf.SlaveOptions.RPCCertCacheExpiration), 15)
}

// SetNodeID writes NodeID safely.
func (gw *Gateway) SetNodeID(nodeID string) {
gw.nodeIDMu.Lock()
Expand Down Expand Up @@ -1384,7 +1397,6 @@ func (gw *Gateway) initSystem() error {
gw.SetConfig(gwConfig)
config.Global = gw.GetConfig
gw.getHostDetails()
gw.initRPCCache()
gw.setupInstrumentation()

// cleanIdleMemConnProviders checks memconn.Provider (a part of internal API handling)
Expand Down
2 changes: 2 additions & 0 deletions gateway/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,8 @@ func (s *Test) Close() {
if err != nil {
log.Error("could not remove apis")
}

s.Gw.cacheClose()
}

// RemoveApis clean all the apis from a living gw
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ require (
github.com/oschwald/maxminddb-golang v1.13.1
github.com/paulbellamy/ratecounter v0.2.0
github.com/pires/go-proxyproto v0.8.0
github.com/pmylund/go-cache v2.1.0+incompatible
github.com/robertkrimen/otto v0.5.1
github.com/rs/cors v1.11.1
github.com/sirupsen/logrus v1.9.3
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -674,8 +674,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmylund/go-cache v2.1.0+incompatible h1:n+7K51jLz6a3sCvff3BppuCAkixuDHuJ/C57Vw/XjTE=
github.com/pmylund/go-cache v2.1.0+incompatible/go.mod h1:hmz95dGvINpbRZGsqPcd7B5xXY5+EKb5PpGhQY3NTHk=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
Expand Down
34 changes: 34 additions & 0 deletions internal/cache/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
version: "3"

vars:
coverage: cache.cov

tasks:
default:
desc: "Run everything"
cmds:
- task: fmt
- task: test

fmt:
desc: "Run formatters"
cmds:
- goimports -local github.com/TykTechnologies,github.com/TykTechnologies/tyk/internal -w .
- go fmt ./...

test:
desc: "Build/run tests"
cmds:
- go test -bench=. -benchtime=10s -race -cpu 1,2,4 -cover -coverprofile {{.coverage}} -coverpkg=$(go list .) -v .

cover:
desc: "Show source coverage"
aliases: [coverage, cov]
cmds:
- go tool cover -func={{.coverage}}

uncover:
desc: "Show uncovered source"
cmds:
- uncover {{.coverage}}
165 changes: 117 additions & 48 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
@@ -1,73 +1,142 @@
package cache

import (
"sync"
"time"

"github.com/pmylund/go-cache"
)

// DefaultExpiration is a helper value that uses the repository defaults.
const DefaultExpiration int64 = 0
// Cache holds key-value pairs with a TTL.
type Cache struct {
// expiration (<= 0 means never expire).
expiration time.Duration

// janitor holds a clean up goroutine
janitor *Janitor

// Repository interface is the API signature for an object cache.
type Repository interface {
Get(string) (interface{}, bool)
Set(string, interface{}, int64)
Delete(string)
Count() int
Flush()
// cache items and protecting mutex
mu sync.RWMutex
items map[string]Item
}

// New creates a new cache instance.
func New(defaultExpiration, cleanupInterval int64) Repository {
var (
defaultExpirationDuration = time.Duration(defaultExpiration) * time.Second
cleanupIntervalDuration = time.Duration(cleanupInterval) * time.Second
)

return &repository{
defaultExpiration: defaultExpiration,
cleanupInterval: cleanupInterval,
cache: cache.New(defaultExpirationDuration, cleanupIntervalDuration),
// NewCache creates a new *Cache for storing items with a TTL.
func NewCache(expiration, cleanupInterval time.Duration) *Cache {
if expiration == 0 {
expiration = -1
}
}

type repository struct {
// Default expiration interval, in seconds.
defaultExpiration int64
cache := &Cache{
items: make(map[string]Item),
expiration: expiration,
}

// Clean up interval, in seconds.
cleanupInterval int64
if cleanupInterval > 0 {
// Cache with a cleanup janitor.
cache.janitor = NewJanitor(cleanupInterval, cache.Cleanup)
}

return cache
}

// The underlying cache driver.
cache *cache.Cache
// Close implements an io.Closer; Invoke it to cancel the cleanup goroutine.
func (c *Cache) Close() {
if c.janitor != nil {
c.janitor.Close()
c.janitor = nil
}
c.Flush()
}

// Add an item to the cache, replacing any existing item. If the duration is 0,
// the cache's expiration time is used. If it is -1, the item never expires.
func (c *Cache) Set(k string, x any, d time.Duration) {
var e int64
if d == 0 {
d = c.expiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
c.mu.Lock()
c.items[k] = Item{
Object: x,
Expiration: e,
}
c.mu.Unlock()
}

// Get retrieves a cache item by key.
func (r *repository) Get(key string) (interface{}, bool) {
return r.cache.Get(key)
// Get an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found.
func (c *Cache) Get(k string) (any, bool) {
c.mu.RLock()

item, found := c.items[k]
if !found {
c.mu.RUnlock()
return nil, false
}

if item.Expiration > 0 {
if time.Now().UnixNano() > item.Expiration {
c.mu.RUnlock()
return nil, false
}
}

c.mu.RUnlock()
return item.Object, true
}

// Set writes a cache item with a timeout in seconds. If timeout is zero,
// the default expiration for the repository instance will be used.
func (r *repository) Set(key string, value interface{}, timeout int64) {
if timeout <= 0 {
timeout = r.defaultExpiration
// Items copies all unexpired items in the cache into a new map and returns it.
func (c *Cache) Items() map[string]Item {
c.mu.RLock()
defer c.mu.RUnlock()

m := make(map[string]Item, len(c.items))

now := time.Now().UnixNano()
for k, v := range c.items {
if v.Expiration > 0 && now > v.Expiration {
continue
}
m[k] = v
}
r.cache.Set(key, value, time.Duration(timeout)*time.Second)

return m
}

// Delete an item from the cache. Does nothing if the key is not in the cache.
func (c *Cache) Delete(k string) {
c.mu.Lock()
delete(c.items, k)
c.mu.Unlock()
}

// Delete cache item by key.
func (r *repository) Delete(key string) {
r.cache.Delete(key)
// Cleanup will delete all expired items from the cache map.
func (c *Cache) Cleanup() {
now := time.Now().UnixNano()

c.mu.Lock()
for k, v := range c.items {
if v.Expiration > 0 && now > v.Expiration {
delete(c.items, k)
}
}
c.mu.Unlock()
}

// Count returns number of items in the cache.
func (r *repository) Count() int {
return r.cache.ItemCount()
// Count returns the number of items in cache, including expired items.
// Expired items get cleaned up by the janitor periodically.
func (c *Cache) Count() int {
c.mu.RLock()
n := len(c.items)
c.mu.RUnlock()

return n
}

// Flush flushes all the items from the cache.
func (r *repository) Flush() {
r.cache.Flush()
// Flush deletes all items from the cache.
func (c *Cache) Flush() {
c.mu.Lock()
c.items = map[string]Item{}
c.mu.Unlock()
}
Loading
Loading