-
Notifications
You must be signed in to change notification settings - Fork 4
/
cache.go
140 lines (118 loc) · 3.49 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
package main
import (
"encoding/json"
"errors"
fmt "fmt"
"strings"
"time"
"github.com/tidwall/buntdb"
)
// Multiple read-only transactions can be opened at the same time but
// there can only be one read/write transaction at a time.
// Attempting to open a read/write transactions while another one is
// in progress will result in blocking until the current read/write
// transaction is completed.
// Use sync.Mutex underneath. Will come up with something else later.
type Cache struct {
db *buntdb.DB
}
// Item that is internally saved to the database.
// Don't expect the Uid to be generated on Insert.
type result struct {
data json.RawMessage
}
func (r *result) Exists() bool {
return r.data != nil
}
func (r *result) Unmarshal(obj interface{}) error {
if r.data == nil {
return errors.New("Empty result set cannot be unmarshalled")
}
return json.Unmarshal(r.data, obj)
}
func makeKey(table, uid string) string {
// Combine to attach a table prefix and convert everything to lowercase
// Hello and hELLo are the same tables.
return fmt.Sprintf("%s-%s", strings.ToLower(table), uid)
}
func (c *Cache) Get(table, uid string) (*result, error) {
key := makeKey(table, uid)
r := &result{}
return r, c.db.View(func(tx *buntdb.Tx) error {
val, err := tx.Get(key) //IgnoreExpiredValue
if err != nil {
// NOTE: Weird syntax by buntDB where it returns a pre-constructed
// value rather than an error type.
if err == buntdb.ErrNotFound {
r.data = nil
return nil
}
return err
}
r.data = []byte(val)
return nil
})
}
// Set is just a wrapper around ExpireSet with a no-expiration Intent.
func (c *Cache) Set(table, uid string, obj interface{}) error {
return c.ExpireSet(table, uid, obj, 0)
}
// Table is just a prefix on which an Index exists.
// This is a way to emulate buckets in buntDB. In a key-value DB a query
// like find all keys that are of the same type are challenging.
// In absence of buckets and partial Indexes, prefix can be exploited
// to deliver the same feature.
// Set an object in table with Expiry, and create a new Index if none exists
func (c *Cache) ExpireSet(table, uid string, obj interface{}, expires int) error {
b, err := json.Marshal(obj)
if err != nil {
return err
}
opts := &buntdb.SetOptions{}
if expires > 0 {
opts.Expires = true
opts.TTL = time.Duration(expires) * time.Second
}
return c.db.Update(func(tx *buntdb.Tx) error {
indices, err := tx.Indexes()
if err != nil {
return err
}
var indexExists bool
for _, ix := range indices {
if table == ix {
indexExists = true
break
}
}
if !indexExists {
if err := tx.CreateIndex(table, makeKey(table, "*"), buntdb.IndexString); err != nil {
return err
}
}
tx.Set(makeKey(table, uid), string(b), opts)
return nil
})
}
// Use the Index to List all the Keys in the Index.
// If the index doesn't exist. the method raises an IndexNotFound error.
func (c *Cache) List(table string) ([]string, error) {
var values []string
index := makeKey(table, "")
return values, c.db.View(func(tx *buntdb.Tx) error {
return tx.Ascend(table, func(key, value string) bool {
values = append(values, strings.SplitN(key, index, 2)[1])
return true
})
})
}
type Cachier interface {
Set(table, uid string, obj interface{}) error
ExpireSet(table, uid string, obj interface{}, expires int) error
Get(table, uid string) (*result, error)
List(table string) ([]string, error)
}
func newCache() (Cachier, error) {
db, err := buntdb.Open(":memory:")
return &Cache{db}, err
}