lauantai 29. huhtikuuta 2017

Cache on the background with golang, Part 2

Part one can be found here.

Expiring cache


First, we're still using the var cache cmap.ConcurrentMap
Second, we need a new field to store, the expire information. So, let's save a struct as a value for the cache item.
type CacheItem struct {
    Key    string
    Value  []byte
    Expire time.Time
}
Adding an item to cache is almost the same, let's just take the expiring into account and also the set maximum size of the cache.
func AddItemToCache(key string, value []byte, expire time.Duration) {
    if cache.Count() <= cacheSize {
        cache.Set(key, CacheItem{Key: key, Value: value, Expire: time.Now().Add(expire)})
    } else {
        panic("cacheSize reached")
    }
}
Getting an item from the cache is trivial. Again, let's take the expired items into account by removing them.
func GetItemFromCache(key string) *CacheItem {
    if cache.Has(key) {
        if tmp, ok := cache.Get(key); ok {
            item := tmp.(CacheItem)
            if item.Expire.After(time.Now()) {
                return &item
            }
            removeItem(item.Key)
        }
    }
    return nil
}

Usages of an expiring cache

You can fill the cache with a scheduler on the background, or by filling the cache when you get a nil from a get. You can also take this further by fetching the almost expired item on the background and still giving the item from the cache. This way there is not so much cache misses when there is less traffic.
cacheItem := GetItemFromCache(weatherURLs[1])
if cacheItem != nil {
    w.Write(cacheItem.Value)
} else {
    if weather := fetchWeather(weatherURLs[1]); weather != nil {
        j, _ := json.Marshal(&weather.Query.Results.Channel.Item)
        AddItemToCache(weatherURLs[1], j, time.Minute)
        w.Write(j)
    } else {
        json.NewEncoder(w).Encode("not found")
    }
}

Removing stale items

In addition to removing stale items when bumping into them while getting an item from cache, we can remove stale items by running
func removeStaleCacheItems() {
    for _, item := range cache.Items() {
        cItem := item.(CacheItem)
        if time.Now().After(cItem.Expire) {
            removeItem(cItem.Key)
        }
    }
}
once in a minute for example. We loop all items and check if the current time is after expiration time. This is started as a background job in the init function.
func init() {
    go doEvery(time.Minute, removeStaleCacheItems)
}

func doEvery(d time.Duration, f func()) {
    for range time.Tick(d) {
        f()
    }
}

Testing

Testing is otherwise about the same, but we have the new expiring test. Easy enough, just give a little time for an added item and then sleep a bit more than that. When getting the first added item after expiring, you get nil back as supposed to.
func TestExpire(t *testing.T) {
    assert := assert.New(t)
    AddItemToCache("item", []byte("value"), time.Second)
    assert.Equal(string(GetItemFromCache("item").Value), "value", "should be equal")
    time.Sleep(time.Second * 2)
    assert.True(GetItemFromCache("item") == nil, "should be equal")
}

The whole source code can be found from github.