├── README.md ├── LICENSE ├── ramcache_test.go └── ramcache.go /README.md: -------------------------------------------------------------------------------- 1 | # ramcache 2 | 3 | go get [-u] stathat.com/c/ramcache 4 | 5 | http://godoc.org/stathat.com/c/ramcache 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, stathat 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of ramcache nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /ramcache_test.go: -------------------------------------------------------------------------------- 1 | package ramcache 2 | 3 | import ( 4 | "testing" 5 | "testing/quick" 6 | "time" 7 | ) 8 | 9 | func TestSet(t *testing.T) { 10 | r := New() 11 | err := r.Set("asdfwqer", "qwerqwer") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | count := r.Count() 16 | if count != 1 { 17 | t.Errorf("expected count == 1, got %d", count) 18 | } 19 | } 20 | 21 | func TestGet(t *testing.T) { 22 | r := New() 23 | r.Set("asdfqwer", "qwerqwer") 24 | x, err := r.Get("asdfqwer") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | if x.(string) != "qwerqwer" { 29 | t.Errorf("expected get to return 'qwerqwer', got %q", x.(string)) 30 | } 31 | } 32 | 33 | func TestCreatedAt(t *testing.T) { 34 | now := time.Now() 35 | key := "asdfqwer" 36 | r := New() 37 | r.Set(key, "qwerqwer") 38 | created, err := r.CreatedAt(key) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | if !created.After(now) { 43 | t.Errorf("expected created to be after test start time. test started at %s, created = %s", now, created) 44 | } 45 | } 46 | 47 | func TestCreatedAtUnchangedByGet(t *testing.T) { 48 | key := "asdfqwer" 49 | r := New() 50 | r.Set(key, "qwerqwer") 51 | before, err := r.CreatedAt(key) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | _, err = r.Get(key) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | after, err := r.CreatedAt(key) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | if before.Equal(after) == false { 64 | t.Errorf("created at changed with get. before = %s, after = %s", before, after) 65 | } 66 | } 67 | 68 | func TestDelete(t *testing.T) { 69 | key := "asdfqwer" 70 | r := New() 71 | r.Set(key, "qwerqwer") 72 | before := r.Count() 73 | err := r.Delete(key) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | after := r.Count() 78 | if after-before != -1 { 79 | t.Errorf("expected count to decrease by 1, got %d", after-before) 80 | } 81 | } 82 | 83 | func TestRemove(t *testing.T) { 84 | key := "asdfqwer" 85 | r := New() 86 | r.Set(key, "qwerqwer") 87 | before := r.Count() 88 | obj, err := r.Remove(key) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | after := r.Count() 93 | if after-before != -1 { 94 | t.Errorf("expected count to decrease by 1, got %d", after-before) 95 | } 96 | s, ok := obj.(string) 97 | if !ok { 98 | t.Fatalf("returned object not a string") 99 | } 100 | if s != "qwerqwer" { 101 | t.Errorf("expected Remove to return object 'qwerqwer', got %q", s) 102 | } 103 | } 104 | 105 | func TestClean(t *testing.T) { 106 | r := New() 107 | r.Set("asdfqwer", "qwerqwer") 108 | before := r.Count() 109 | r.clean(time.Now()) 110 | if (r.Count() - before) != 0 { 111 | t.Errorf("expected no deletions by clean, got: %d", r.Count()-before) 112 | } 113 | r.clean(time.Now().Add(10 * time.Minute)) 114 | if (r.Count() - before) != -1 { 115 | t.Errorf("expected clean to remove one elt, got %d", r.Count()-before) 116 | } 117 | 118 | } 119 | 120 | func TestGetUpdatesAccessedAt(t *testing.T) { 121 | key := "asdfqwer" 122 | r := New() 123 | r.Set(key, "qwerqwer") 124 | i := r.cache[key] 125 | before := i.accessedAt 126 | _, err := r.Get(key) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | j := r.cache[key] 131 | after := j.accessedAt 132 | if !after.After(before) { 133 | t.Errorf("Get didn't make accessedAt newer. before: %s, after: %s", before, after) 134 | } 135 | } 136 | 137 | func TestGetNoAccessDoesntUpdateAccessedAt(t *testing.T) { 138 | key := "asdfqwer" 139 | r := New() 140 | r.Set(key, "qwerqwer") 141 | i := r.cache[key] 142 | before := i.accessedAt 143 | _, err := r.GetNoAccess(key) 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | j := r.cache[key] 148 | after := j.accessedAt 149 | if after.After(before) { 150 | t.Errorf("GetNoAccess made accessedAt newer. before: %s, after: %s", before, after) 151 | } 152 | } 153 | 154 | func TestSetReplace(t *testing.T) { 155 | key := "asdfqwer" 156 | r := New() 157 | r.Set(key, "qwerqwer") 158 | if len(r.cache) != 1 { 159 | t.Errorf("expected 1 elt in cache, got %d", len(r.cache)) 160 | } 161 | if len(r.tqueue) != 1 { 162 | t.Errorf("expected 1 elt in tqueue, got %d", len(r.tqueue)) 163 | } 164 | r.Set(key, "qwerqwer") 165 | if len(r.cache) != 1 { 166 | t.Errorf("expected 1 elt in cache, got %d", len(r.cache)) 167 | } 168 | if len(r.tqueue) != 1 { 169 | t.Errorf("expected 1 elt in tqueue, got %d", len(r.tqueue)) 170 | } 171 | r.Set(key, "different") 172 | if len(r.cache) != 1 { 173 | t.Errorf("expected 1 elt in cache, got %d", len(r.cache)) 174 | } 175 | if len(r.tqueue) != 1 { 176 | t.Errorf("expected 1 elt in tqueue, got %d", len(r.tqueue)) 177 | } 178 | 179 | } 180 | 181 | func TestSetGetQuick(t *testing.T) { 182 | r := New() 183 | f := func(key, value string) bool { 184 | r.Set(key, value) 185 | v, err := r.Get(key) 186 | if err != nil { 187 | return false 188 | } 189 | s, ok := v.(string) 190 | if !ok { 191 | return false 192 | } 193 | if s != value { 194 | return false 195 | } 196 | return true 197 | } 198 | if err := quick.Check(f, nil); err != nil { 199 | t.Error(err) 200 | } 201 | } 202 | 203 | func TestSetRemoveQuick(t *testing.T) { 204 | r := New() 205 | f := func(key, value string) bool { 206 | r.Set(key, value) 207 | v, err := r.Remove(key) 208 | if err != nil { 209 | return false 210 | } 211 | s, ok := v.(string) 212 | if !ok { 213 | return false 214 | } 215 | if s != value { 216 | return false 217 | } 218 | if r.Count() != 0 { 219 | return false 220 | } 221 | return true 222 | } 223 | if err := quick.Check(f, nil); err != nil { 224 | t.Error(err) 225 | } 226 | } 227 | 228 | func TestSetDeleteQuick(t *testing.T) { 229 | r := New() 230 | f := func(key, value string) bool { 231 | r.Set(key, value) 232 | err := r.Delete(key) 233 | if err != nil { 234 | return false 235 | } 236 | if r.Count() != 0 { 237 | return false 238 | } 239 | return true 240 | } 241 | if err := quick.Check(f, nil); err != nil { 242 | t.Error(err) 243 | } 244 | } 245 | 246 | func TestBool(t *testing.T) { 247 | r := New() 248 | r.Set("asdfqwer", true) 249 | r.Set("zxcvzxcv", false) 250 | x, err := Bool(r.Get("asdfqwer")) 251 | if err != nil { 252 | t.Fatal(err) 253 | } 254 | if x != true { 255 | t.Errorf("expected get to return true, got %v", x) 256 | } 257 | y, err := Bool(r.Get("zxcvzxcv")) 258 | if err != nil { 259 | t.Fatal(err) 260 | } 261 | if y != false { 262 | t.Errorf("expected get to return false, got %v", y) 263 | } 264 | } 265 | 266 | func TestShutdownMultiple(t *testing.T) { 267 | r := New() 268 | r.Set("asdfqwer", true) 269 | r.Set("zxcvzxcv", false) 270 | r.Shutdown() 271 | r.Shutdown() 272 | r.Shutdown() 273 | r.Shutdown() 274 | r.Shutdown() 275 | } 276 | 277 | func BenchmarkSet(b *testing.B) { 278 | r := New() 279 | for i := 0; i < b.N; i++ { 280 | r.Set("asdfqwer", "zxcvxczv") 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /ramcache.go: -------------------------------------------------------------------------------- 1 | // Package ramcache implements an in-memory key/value cache with 2 | // expirations based on access and insertion times. It is safe 3 | // for concurrent use by multiple goroutines. 4 | package ramcache // import "stathat.com/c/ramcache" 5 | 6 | import ( 7 | "container/heap" 8 | "errors" 9 | "fmt" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // ErrNotFound is returned when a key isn't found in the cache. 15 | var ErrNotFound = errors.New("ramcache: key not found in cache") 16 | 17 | // Ramcache is an in-memory key/value store. It has two 18 | // configuration durations: TTL (time to live) and MaxAge. 19 | // Ramcache removes any objects that haven't been accessed in the 20 | // TTL duration. 21 | // Ramcache removes (on get) any objects that were created more 22 | // than MaxAge time ago. 23 | // This allows you to keep recently accessed objects cached but 24 | // also delete them once they have been in the cache for MaxAge 25 | // duration. 26 | type Ramcache struct { 27 | cache map[string]*item 28 | tqueue timeQueue 29 | TTL time.Duration 30 | MaxAge time.Duration 31 | frozen bool 32 | done chan bool 33 | sync.RWMutex 34 | shutdownOnce sync.Once 35 | } 36 | 37 | // New creates a Ramcache with a TTL of 5 minutes. You can change 38 | // this by setting the result's TTL to any time.Duration you want. 39 | // You can also set the MaxAge on the result. 40 | func New() *Ramcache { 41 | tq := make(timeQueue, 0) 42 | c := make(map[string]*item) 43 | d := make(chan bool) 44 | r := &Ramcache{cache: c, tqueue: tq, TTL: 5 * time.Minute, done: d} 45 | go r.cleanup() 46 | return r 47 | } 48 | 49 | // Get retrieves a value from the cache. 50 | func (rc *Ramcache) Get(key string) (interface{}, error) { 51 | rc.Lock() 52 | defer rc.Unlock() 53 | i, ok := rc.cache[key] 54 | if !ok { 55 | return nil, ErrNotFound 56 | } 57 | if rc.MaxAge > 0 && time.Since(i.createdAt) > rc.MaxAge { 58 | heap.Remove(&rc.tqueue, i.index) 59 | delete(rc.cache, key) 60 | return nil, ErrNotFound 61 | } 62 | 63 | rc.tqueue.Access(i) 64 | return i.value, nil 65 | } 66 | 67 | // GetNoAccess retrieves a value from the cache, but does not 68 | // update the access time. 69 | func (rc *Ramcache) GetNoAccess(key string) (interface{}, error) { 70 | rc.Lock() 71 | defer rc.Unlock() 72 | i, ok := rc.cache[key] 73 | if !ok { 74 | return nil, ErrNotFound 75 | } 76 | if rc.MaxAge > 0 && time.Since(i.createdAt) > rc.MaxAge { 77 | heap.Remove(&rc.tqueue, i.index) 78 | delete(rc.cache, key) 79 | return nil, ErrNotFound 80 | } 81 | return i.value, nil 82 | } 83 | 84 | // Set inserts a value in the cache. If an object already exists, 85 | // it will be replaced, but the createdAt timestamp won't change. 86 | func (rc *Ramcache) Set(key string, obj interface{}) error { 87 | rc.Lock() 88 | defer rc.Unlock() 89 | i := newItem(key, obj) 90 | existing, ok := rc.cache[key] 91 | if ok { 92 | heap.Remove(&rc.tqueue, existing.index) 93 | i.createdAt = existing.createdAt 94 | } 95 | rc.cache[key] = i 96 | rc.tqueue.Insert(i) 97 | return nil 98 | } 99 | 100 | // Delete deletes an item from the cache. 101 | func (rc *Ramcache) Delete(key string) error { 102 | rc.Lock() 103 | defer rc.Unlock() 104 | i, ok := rc.cache[key] 105 | if !ok { 106 | return ErrNotFound 107 | } 108 | heap.Remove(&rc.tqueue, i.index) 109 | delete(rc.cache, key) 110 | return nil 111 | } 112 | 113 | // Remove deletes an item from the cache and returns it. 114 | func (rc *Ramcache) Remove(key string) (interface{}, error) { 115 | rc.Lock() 116 | defer rc.Unlock() 117 | i, ok := rc.cache[key] 118 | if !ok { 119 | return nil, ErrNotFound 120 | } 121 | heap.Remove(&rc.tqueue, i.index) 122 | delete(rc.cache, key) 123 | return i.value, nil 124 | } 125 | 126 | // CreatedAt returns the time the key was inserted into the cache. 127 | func (rc *Ramcache) CreatedAt(key string) (t time.Time, err error) { 128 | rc.RLock() 129 | defer rc.RUnlock() 130 | i, ok := rc.cache[key] 131 | if !ok { 132 | err = ErrNotFound 133 | return 134 | } 135 | t = i.createdAt 136 | return 137 | } 138 | 139 | // Count returns the number of elements in the cache. 140 | func (rc *Ramcache) Count() int { 141 | rc.RLock() 142 | defer rc.RUnlock() 143 | return len(rc.cache) 144 | } 145 | 146 | // Keys returns all the keys in the cache. 147 | func (rc *Ramcache) Keys() []string { 148 | rc.RLock() 149 | defer rc.RUnlock() 150 | var result []string 151 | for k := range rc.cache { 152 | result = append(result, k) 153 | } 154 | return result 155 | } 156 | 157 | // Shutdown cleanly stops any background work, allowing Ramcache 158 | // to be garbage collected. 159 | func (rc *Ramcache) Shutdown() { 160 | rc.shutdownOnce.Do(func() { close(rc.done) }) 161 | } 162 | 163 | func (rc *Ramcache) cleanup() { 164 | for { 165 | select { 166 | case <-time.After(10 * time.Second): 167 | rc.clean(time.Now()) 168 | case <-rc.done: 169 | return 170 | } 171 | } 172 | } 173 | 174 | func (rc *Ramcache) clean(now time.Time) { 175 | rc.Lock() 176 | defer rc.Unlock() 177 | if rc.frozen { 178 | return 179 | } 180 | for i := 0; i < 10000; i++ { 181 | if rc.tqueue.Len() == 0 { 182 | return 183 | } 184 | top := heap.Pop(&rc.tqueue).(*item) 185 | if now.Sub(top.accessedAt) > rc.TTL { 186 | delete(rc.cache, top.key) 187 | } else { 188 | heap.Push(&rc.tqueue, top) 189 | return 190 | } 191 | } 192 | } 193 | 194 | // Freeze stops Ramcache from removing any expired entries. 195 | func (rc *Ramcache) Freeze() { 196 | rc.Lock() 197 | rc.frozen = true 198 | rc.Unlock() 199 | } 200 | 201 | // Each will call f for every entry in the cache. 202 | func (rc *Ramcache) Each(f func(key string, value interface{})) { 203 | rc.RLock() 204 | defer rc.Unlock() 205 | for k, v := range rc.cache { 206 | f(k, v.value) 207 | } 208 | } 209 | 210 | // An item is something cached in the Ramcache, and managed in the timeQueue. 211 | type item struct { 212 | key string 213 | value interface{} 214 | createdAt time.Time 215 | accessedAt time.Time 216 | index int 217 | } 218 | 219 | func newItem(key string, val interface{}) *item { 220 | now := time.Now() 221 | return &item{key: key, value: val, createdAt: now, accessedAt: now, index: -1} 222 | } 223 | 224 | // A timeQueue implements heap.Interface and holds items 225 | type timeQueue []*item 226 | 227 | func (tq timeQueue) Len() int { return len(tq) } 228 | 229 | func (tq timeQueue) Less(i, j int) bool { 230 | return tq[i].accessedAt.Before(tq[j].accessedAt) 231 | } 232 | 233 | func (tq timeQueue) Swap(i, j int) { 234 | tq[i], tq[j] = tq[j], tq[i] 235 | tq[i].index = i 236 | tq[j].index = j 237 | } 238 | 239 | func (tq *timeQueue) Push(x interface{}) { 240 | a := *tq 241 | n := len(a) 242 | // a = a[0 : n+1] 243 | itm := x.(*item) 244 | itm.index = n 245 | // a[n] = itm 246 | a = append(a, itm) 247 | *tq = a 248 | } 249 | 250 | func (tq *timeQueue) Pop() interface{} { 251 | a := *tq 252 | n := len(a) 253 | itm := a[n-1] 254 | itm.index = -1 255 | *tq = a[0 : n-1] 256 | return itm 257 | } 258 | 259 | func (tq *timeQueue) Access(itm *item) { 260 | heap.Remove(tq, itm.index) 261 | itm.accessedAt = time.Now() 262 | heap.Push(tq, itm) 263 | } 264 | 265 | func (tq *timeQueue) Insert(itm *item) { 266 | heap.Push(tq, itm) 267 | } 268 | 269 | // Bool is a convenience method to type assert a Ramcache reply 270 | // into a boolean value. 271 | func Bool(reply interface{}, err error) (bool, error) { 272 | if err != nil { 273 | return false, err 274 | } 275 | b, ok := reply.(bool) 276 | if !ok { 277 | return false, fmt.Errorf("ramcache: unexpected type for Bool, got %T", reply) 278 | } 279 | return b, nil 280 | } 281 | --------------------------------------------------------------------------------