thinkoner/thinkgo

View on GitHub
cache/memory_store.go

Summary

Maintainability
B
5 hrs
Test Coverage
package cache

import (
    "errors"
    "fmt"
    "reflect"
    "sync"
    "time"
)

type item struct {
    Object     interface{}
    Expiration int64
}

// Expired Returns true if the item has expired.
func (item item) Expired() bool {
    if item.Expiration < 0 {
        return false
    }
    return time.Now().UnixNano() > item.Expiration
}

type MemoryStore struct {
    prefix       string
    items        map[string]item
    mu           sync.RWMutex
    cleanupTimer *time.Timer
}

// NewStore Create a memory cache store
func NewMemoryStore(prefix string) *MemoryStore {
    s := &MemoryStore{
        items: make(map[string]item),
    }
    return s.SetPrefix(prefix)
}

// Get get cached value by key.
// func (s *Store) Get(key string) (interface{}, error) {
func (s *MemoryStore) Get(key string, val interface{}) error {
    s.mu.RLock()
    defer s.mu.RUnlock()

    item, ok := s.items[s.prefix+key]
    if !ok {
        return errors.New("not found")
    }

    if item.Expired() {
        return errors.New("expired")
    }

    rv := reflect.ValueOf(val)
    if rv.Kind() != reflect.Ptr || rv.IsNil() {
        return errors.New("invalid unmarshal")
    }

    rv = rv.Elem()

    rv.Set(reflect.ValueOf(item.Object))

    return nil
}

// Put set cached value with key and expire time.
func (s *MemoryStore) Put(key string, val interface{}, timeout time.Duration) error {
    var e int64 = -1
    if timeout >= 0 {
        e = time.Now().Add(timeout).UnixNano()
    }

    s.mu.RLock()
    defer s.mu.RUnlock()

    s.items[s.prefix+key] = item{
        Object:     val,
        Expiration: e,
    }

    if e >= 0 {
        s.DeleteExpired()
    }

    return nil
}

// Increment the value of an item in the cache.
func (s *MemoryStore) Increment(key string, value ...int) (int, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    var by = 1
    if len(value) > 0 {
        by = value[0]
    }

    exist, ok := s.items[s.prefix+key]
    if !ok {
        s.items[s.prefix+key] = item{
            Object: 1 + by,
        }
    } else {
        by = exist.Object.(int) + by
        exist.Object = by
        s.items[s.prefix+key] = exist
    }

    return by, nil
}

// Decrement the value of an item in the cache.
func (s *MemoryStore) Decrement(key string, value ...int) (int, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    var by = 1
    if len(value) > 0 {
        by = value[0]
    }

    exist, ok := s.items[s.prefix+key]
    if !ok {
        s.items[s.prefix+key] = item{
            Object: 0 - by,
        }
    } else {
        by = exist.Object.(int) - by
        exist.Object = by
        s.items[s.prefix+key] = exist
    }

    return by, nil
}

// Forever Store an item in the cache indefinitely.
func (s *MemoryStore) Forever(key string, val interface{}) error {
    return s.Put(key, val, 0)
}

// Exist check cache's existence in memory.
func (s *MemoryStore) Exist(key string) bool {
    s.mu.RLock()
    defer s.mu.RUnlock()

    item, ok := s.items[s.prefix+key]

    if item.Expired() {
        return false
    }

    return ok
}

// Expire set value expire time.
func (s *MemoryStore) Expire(key string, timeout time.Duration) error {
    var e int64 = -1
    if timeout >= 0 {
        e = time.Now().Add(timeout).UnixNano()
    }

    s.mu.RLock()
    defer s.mu.RUnlock()

    if !s.Exist(key) {
        return errors.New("key not exist")
    }

    item := s.items[s.prefix+key]
    item.Expiration = e
    s.items[s.prefix+key] = item

    if e >= 0 {
        s.DeleteExpired()
    }

    return nil
}

// Forget Remove an item from the cache.
func (s *MemoryStore) Forget(key string) error {
    delete(s.items, s.prefix+key)
    return nil
}

// Remove all items from the cache.
func (s *MemoryStore) Flush() error {
    s.mu.RLock()
    defer s.mu.RUnlock()

    s.items = map[string]item{}

    return nil
}

func (s *MemoryStore) Tags(names ...string) Store {
    // tags not be supported
    return s
}

func (s *MemoryStore) TTL(key string) (int64, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    item, ok := s.items[s.prefix+key]
    if !ok {
        return 0, errors.New("not found")
    }

    if item.Expired() {
        return 0, errors.New("not found")
    }

    return item.Expiration - time.Now().UnixNano(), nil
}

// GetPrefix Get the cache key prefix.
func (s *MemoryStore) GetPrefix() string {
    return s.prefix
}

// SetPrefix Set the cache key prefix.
func (s *MemoryStore) SetPrefix(prefix string) *MemoryStore {
    if len(prefix) != 0 {
        s.prefix = fmt.Sprintf("%s:", prefix)
    } else {
        s.prefix = ""
    }
    return s
}

// Delete all expired items from the cache.
func (s *MemoryStore) DeleteExpired() {
    s.mu.RLock()
    defer s.mu.RUnlock()

    if s.cleanupTimer != nil {
        s.cleanupTimer.Stop()
    }

    smallestDuration := 0 * time.Nanosecond
    for key, item := range s.items {
        if item.Expiration < 0 {
            continue
        }
        // "Inlining" of expired
        if item.Expired() {
            delete(s.items, key)
        } else {
            // Find the item chronologically closest to its end-of-lifespan.
            sub := item.Expiration - time.Now().UnixNano()

            if smallestDuration == 0 {
                smallestDuration = time.Duration(sub) * time.Nanosecond
            } else {
                if time.Duration(sub)*time.Nanosecond < smallestDuration {
                    smallestDuration = time.Duration(sub) * time.Nanosecond
                }
            }
        }
    }

    if smallestDuration > 0 {
        s.cleanupTimer = time.AfterFunc(smallestDuration, func() {
            go s.DeleteExpired()
        })
    }
}