Skip to content

Commit 2b037a7

Browse files
committed
configurable cache ttl
1 parent 8856f12 commit 2b037a7

File tree

7 files changed

+98
-24
lines changed

7 files changed

+98
-24
lines changed

api/handlers/handlers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ func GetHandlers(cfg *config.Config) *http.ServeMux {
2727

2828
func GetNewCacheStore(cfg *config.Config) cache.Cache {
2929
if cfg.CacheBackend == "redis" {
30-
cache.NewRedisCache(cfg.Redis.Host, strconv.Itoa(cfg.Redis.Port))
30+
cache.NewRedisCache(cfg.Redis.Host, strconv.Itoa(cfg.Redis.Port), cfg.CacheTTL)
3131
}
32-
return cache.NewInMemoryCache()
32+
return cache.NewInMemoryCache(cfg.CacheTTL)
3333
}
3434

3535
func GetCacheOptions(cfg *config.Config, values []interface{}) *graphcache.GraphCacheOptions {

cache/inmemory_cache.go

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,54 @@ package cache
33
import (
44
"encoding/json"
55
"errors"
6-
"fmt"
76
"graphql_cache/utils/file_utils"
87
"regexp"
98
"strings"
9+
"sync"
10+
"time"
1011
)
1112

1213
type InMemoryCache struct {
13-
data map[string]interface{}
14+
data map[string]interface{}
15+
expiration map[string]*time.Time
16+
ttl int
17+
mu sync.Mutex
1418
}
1519

16-
func NewInMemoryCache() *InMemoryCache {
17-
return &InMemoryCache{
18-
data: make(map[string]interface{}),
20+
func NewInMemoryCache(ttl int) *InMemoryCache {
21+
cache := &InMemoryCache{
22+
data: make(map[string]interface{}),
23+
expiration: make(map[string]*time.Time),
24+
ttl: ttl,
1925
}
26+
go cache.cleanup()
27+
return cache
2028
}
2129

2230
func (c *InMemoryCache) Key(key string) string {
2331
return key
2432
}
2533

2634
func (c *InMemoryCache) Set(key string, value interface{}) error {
35+
c.mu.Lock()
36+
defer c.mu.Unlock()
2737
c.data[c.Key(key)] = deepCopy(value)
38+
if value == nil {
39+
c.expiration[c.Key(key)] = nil
40+
} else {
41+
t := time.Now().Add(time.Duration(c.ttl) * time.Second)
42+
c.expiration[c.Key(key)] = &t
43+
}
2844
return nil
2945
}
3046

3147
func (c *InMemoryCache) Get(key string) (interface{}, error) {
48+
c.mu.Lock()
49+
defer c.mu.Unlock()
50+
expiration, exists := c.expiration[c.Key(key)]
51+
if !exists || expiration == nil || time.Now().After(*expiration) {
52+
return nil, errors.New("key not found")
53+
}
3254
value, exists := c.data[c.Key(key)]
3355
if !exists {
3456
return nil, errors.New("key not found")
@@ -37,28 +59,51 @@ func (c *InMemoryCache) Get(key string) (interface{}, error) {
3759
}
3860

3961
func (c *InMemoryCache) Del(key string) error {
62+
c.mu.Lock()
63+
defer c.mu.Unlock()
4064
c.Set(c.Key(key), nil)
4165
return nil
4266
}
4367

4468
func (c *InMemoryCache) Exists(key string) (bool, error) {
45-
_, exists := c.data[c.Key(key)]
46-
return exists, nil
69+
c.mu.Lock()
70+
defer c.mu.Unlock()
71+
expiration, exists := c.expiration[c.Key(key)]
72+
if !exists || expiration == nil || time.Now().After(*expiration) {
73+
return false, nil
74+
}
75+
return true, nil
4776
}
4877

4978
func (c *InMemoryCache) Map() (map[string]interface{}, error) {
79+
c.mu.Lock()
80+
defer c.mu.Unlock()
5081
copy := make(map[string]interface{})
82+
now := time.Now()
5183
for k, v := range c.data {
52-
copy[k] = v
84+
if expiration, exists := c.expiration[k]; exists && expiration != nil && now.Before(*expiration) {
85+
copy[k] = v
86+
}
5387
}
5488
return copy, nil
5589
}
5690

5791
func (c *InMemoryCache) JSON() ([]byte, error) {
58-
return json.Marshal(c.data)
92+
c.mu.Lock()
93+
defer c.mu.Unlock()
94+
copy := make(map[string]interface{})
95+
now := time.Now()
96+
for k, v := range c.data {
97+
if expiration, exists := c.expiration[k]; exists && expiration != nil && now.Before(*expiration) {
98+
copy[k] = v
99+
}
100+
}
101+
return json.Marshal(copy)
59102
}
60103

61104
func (c *InMemoryCache) Debug(identifier string) error {
105+
c.mu.Lock()
106+
defer c.mu.Unlock()
62107
f := file_utils.NewFile("../" + identifier + ".cache.json")
63108
defer f.Close()
64109
jsonContent, _ := c.JSON()
@@ -67,23 +112,40 @@ func (c *InMemoryCache) Debug(identifier string) error {
67112
}
68113

69114
func (c *InMemoryCache) Flush() error {
115+
c.mu.Lock()
116+
defer c.mu.Unlock()
70117
c.data = make(map[string]interface{})
118+
c.expiration = make(map[string]*time.Time)
71119
return nil
72120
}
73121

74122
func (c *InMemoryCache) DeleteByPrefix(prefix string) error {
123+
c.mu.Lock()
124+
defer c.mu.Unlock()
75125
var re = regexp.MustCompile(`(?m)` + strings.ReplaceAll(c.Key(prefix), "*", ".*"))
76126
for k := range c.data {
77-
// regex match the prefix to the key
78-
// if the key is gql:* then delete all keys which start with gql
79127
if re.Match([]byte(k)) {
80-
fmt.Println("deleting key: ", k)
81128
c.Del(k)
82129
}
83130
}
84131
return nil
85132
}
86133

134+
func (c *InMemoryCache) cleanup() {
135+
for {
136+
time.Sleep(time.Duration(c.ttl) * time.Second)
137+
c.mu.Lock()
138+
now := time.Now()
139+
for key, expiration := range c.expiration {
140+
if expiration != nil && now.After(*expiration) {
141+
delete(c.data, key)
142+
delete(c.expiration, key)
143+
}
144+
}
145+
c.mu.Unlock()
146+
}
147+
}
148+
87149
func deepCopy(v interface{}) interface{} {
88150
if v == nil {
89151
return nil

cache/redis_cache.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"reflect"
7+
"time"
78

89
"github.com/go-redis/redis/v8"
910
)
@@ -13,20 +14,22 @@ var ctx = context.Background()
1314
// RedisCache implements the Cache interface and uses Redis as the cache store
1415
type RedisCache struct {
1516
cache *redis.Client
17+
ttl int
1618
}
1719

1820
func (c *RedisCache) Key(key string) string {
1921
return key
2022
}
2123

22-
func NewRedisCache(host, port string) Cache {
24+
func NewRedisCache(host, port string, ttl int) Cache {
2325
c := redis.NewClient(&redis.Options{
2426
Addr: host + ":" + port,
2527
Password: "", // no password set
2628
DB: 0, // use default DB
2729
})
2830
return &RedisCache{
2931
cache: c,
32+
ttl: ttl,
3033
}
3134
}
3235

@@ -35,14 +38,14 @@ func (c *RedisCache) Set(key string, value interface{}) error {
3538
switch valueType.Kind() {
3639
case reflect.Map:
3740
br, _ := json.Marshal(value)
38-
c.cache.Set(ctx, c.Key(key), string(br), 0)
39-
c.cache.Set(ctx, c.Key(key+"_type"), "reflect.Map", 0)
41+
c.cache.Set(ctx, c.Key(key), string(br), time.Second*time.Duration(c.ttl))
42+
c.cache.Set(ctx, c.Key(key+"_type"), "reflect.Map", time.Second*time.Duration(c.ttl))
4043
case reflect.Slice:
4144
br, _ := json.Marshal(value)
42-
c.cache.Set(ctx, c.Key(key), string(br), 0)
43-
c.cache.Set(ctx, c.Key(key+"_type"), "reflect.Slice", 0)
45+
c.cache.Set(ctx, c.Key(key), string(br), time.Second*time.Duration(c.ttl))
46+
c.cache.Set(ctx, c.Key(key+"_type"), "reflect.Slice", time.Second*time.Duration(c.ttl))
4447
default:
45-
c.cache.Set(ctx, c.Key(key), value, 0)
48+
c.cache.Set(ctx, c.Key(key), value, time.Second*time.Duration(c.ttl))
4649
}
4750
return nil
4851
}

config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ port=9090
1616
# The returns cache hit or cache miss in the header you specify here. Default is X-Orbit-Cache
1717
cache_header_name="X-Orbit-Cache"
1818

19+
# TTL of the graphql cache in seconds - default is 60 minutes
20+
cache_ttl=3600
21+
1922
# If you have an authenticated API and want to separate the cache based on what the headers are, you can configure the headers here.
2023
# The system will scope the cache based on the unique values of the headers you provide
2124
# For example, if you want to scope the cache based on the Authorization header, you can set the following:

config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Config struct {
2626
Port int `toml:"port"`
2727
CacheBackend string `toml:"cache_backend"`
2828
CacheHeaderName string `toml:"cache_header_name"`
29+
CacheTTL int `toml:"cache_ttl"`
2930
ScopeHeaders []string `toml:"scope_headers"`
3031
PrimaryKeyField string `toml:"primary_key_field"`
3132
Handlers HandlersConfig `toml:"handlers"`
@@ -85,5 +86,10 @@ func NewConfig() *Config {
8586
cfg.CacheHeaderName = "x-orbit-cache"
8687
}
8788

89+
if cfg.CacheTTL == 0 {
90+
// default cache TTL is 1 hour
91+
cfg.CacheTTL = 3600
92+
}
93+
8894
return &cfg
8995
}

graphcache/graphcache.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ const CacheBackendInMemory CacheBackend = "in_memory"
3939

4040
func NewGraphCache() *GraphCache {
4141
return NewGraphCacheWithOptions(context.Background(), &GraphCacheOptions{
42-
ObjectStore: cache.NewInMemoryCache(),
43-
QueryStore: cache.NewInMemoryCache(),
42+
ObjectStore: cache.NewInMemoryCache(300),
43+
QueryStore: cache.NewInMemoryCache(300),
4444
})
4545
}
4646

graphcache/graphcache_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ func TestNewGraphCache(t *testing.T) {
1919

2020
func TestNewGraphCacheWithOptions(t *testing.T) {
2121
opts := &GraphCacheOptions{
22-
ObjectStore: cache.NewInMemoryCache(),
23-
QueryStore: cache.NewInMemoryCache(),
22+
ObjectStore: cache.NewInMemoryCache(300),
23+
QueryStore: cache.NewInMemoryCache(300),
2424
Prefix: "test::",
2525
IDField: "customID",
2626
}

0 commit comments

Comments
 (0)