-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcache.go
148 lines (135 loc) · 4.59 KB
/
cache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package cache
import (
"context"
"errors"
"fmt"
"path/filepath"
"reflect"
"runtime"
"strings"
"time"
)
var (
cacheConfig map[string]Config
)
// Storage is an interface used to denote the caching store (i.e. where to cache).
type Storage interface {
GetName() string
Read(ctx context.Context, key string, opType reflect.Type) (interface{}, error)
Write(ctx context.Context, key string, expiration time.Duration, res interface{}, err error) error
Delete(ctx context.Context, key string) error
}
// Config contains parameters for all cache related configs.
type Config struct {
TtlInSeconds int64 `json:"ttlInSeconds"`
Enabled bool `json:"enabled"`
}
func Init(cfg map[string]Config) {
cacheConfig = cfg
}
// isCachingEnabled checks if a particular method is cacheable from config
func isCachingEnabled(fnName string) (bool, time.Duration) {
conf, ok := cacheConfig[fnName]
if !ok {
return false, 0
}
return conf.Enabled, time.Duration(conf.TtlInSeconds) * time.Second
}
// Cache function takes a func as param along with its arguments, cache key and store.
// It calls the passed fn with given args and caches the result with given storage and returns the result.
// Only works with func having 2 return values, with the second one being an interface.
// Caching will only work if it is enabled in config.
func Cache(ctx context.Context, key string, store Storage, fnc interface{}, args ...interface{}) (interface{}, error) {
fnType, err := fetchFuncType(fnc)
if err != nil {
return nil, err
}
fnName := getFuncName(fnc)
key = fnName + "_" + key
err = checkForArgsError(args, fnName, fnType)
if err != nil {
return nil, err
}
if fnType.NumOut() != 2 {
return nil, errors.New("func: " + fnName + " should have exactly two return values")
}
err = checkArgIndexForError(fnType, 1, fnName)
if err != nil {
return nil, err
}
enabled, expiration := isCachingEnabled(fnName)
if !enabled {
return invoke(fnc, args...)
}
if store == nil {
panic(fmt.Sprintf("invalid cache store:%v", store))
}
opType := reflect.TypeOf(fnc).Out(0)
res, err := store.Read(ctx, key, opType)
if err == nil {
return res, err
}
res, err = invoke(fnc, args...)
store.Write(ctx, key, expiration, res, err)
return res, err
}
// Delete func deletes the given cache key
func Delete(ctx context.Context, key string, store Storage) {
if store == nil {
panic(fmt.Sprintf("invalid cache store:%v", store))
}
store.Delete(ctx, key)
}
// checkArgIndexForError looks for all the values returned by ProcessResponse and checks if the index specified is error.
func checkArgIndexForError(fnType reflect.Type, index int, fnName string) error {
if !fnType.Out(index).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
return fmt.Errorf("second return type should be error, fn:" + fnName)
}
return nil
}
// invoke calls the function with relevant args
// Note: This function would panic in case there is a mismatch in the arguments in the source
func invoke(fnc interface{}, args ...interface{}) (interface{}, error) {
fn := reflect.ValueOf(fnc)
inputs := make([]reflect.Value, len(args))
for i := range args {
inputs[i] = reflect.ValueOf(args[i])
}
res := fn.Call(inputs)
err, _ := res[1].Interface().(error)
return res[0].Interface(), err
}
// getFuncName takes function as input and returns the name of the function as string
// This won't work if functions is defined like this var abc = func()
func getFuncName(fnc interface{}) string {
fullName := runtime.FuncForPC(reflect.ValueOf(fnc).Pointer()).Name()
endName := filepath.Ext(fullName)
name := strings.TrimPrefix(endName, ".")
//this is to remove `-fm` from struct func
if len(name) > 3 && name[len(name)-3:] == "-fm" {
name = name[:len(name)-3]
}
return name
}
// fetchFuncType checks whether the func passed as a parameter is of type function or not and returns its type
func fetchFuncType(fnc interface{}) (fnType reflect.Type, err error) {
fn := reflect.ValueOf(fnc)
fnType = reflect.TypeOf(fnc)
if fn.Kind() != reflect.Func {
err = fmt.Errorf("function %v is not of type reflect Func", fnc)
}
return
}
// checkForArgsError checks for all possible args error
func checkForArgsError(args []interface{}, fnName string, fnType reflect.Type) error {
noOfArgs := fnType.NumIn()
if noOfArgs != len(args) {
return fmt.Errorf("mismatch in the no of args expected:%v, provided:%v, fn:%v", noOfArgs, len(args), fnName)
}
for i := 0; i < noOfArgs; i++ {
if !reflect.TypeOf(args[i]).AssignableTo(fnType.In(i)) {
return fmt.Errorf("mismatch in the type of arg no %v, expected:%v, provided:%v, fn:%v", i+1, fnType.In(i), reflect.TypeOf(args[i]), fnName)
}
}
return nil
}