Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
c3mb0 committed Jul 3, 2020
1 parent ca68b26 commit 0091cfa
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
# niftycache

```go
cb := func(k string, v interface{}) {
fmt.Printf("key: %s | value: %+v\n", k, v)
}

cache := niftycache.New(10*time.Minute,
niftycache.ExpireCallback(cb),
niftycache.RemoveCallback(cb),
niftycache.SetCallback(cb),
niftycache.UpdateCallback(cb),
// controls whether a successful cache hit extends the item's ttl (default false)
niftycache.ExtendTTLOnHit(),
// controls the max amount of items that can be expired at once (default 10000)
niftycache.MaxExpires(10000),
// controls the max amount of concurrent callbacks that can be fired (default 1000)
niftycache.MaxCallbacks(1000),
)
```
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/c3mb0/niftycache

go 1.13

require github.com/eapache/queue v1.1.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
34 changes: 34 additions & 0 deletions item.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package niftycache

import "time"

type item struct {
key string
value interface{}
ttl time.Duration
expireAt time.Time
index int
}

func newItem(key string, value interface{}, ttl time.Duration) *item {
item := &item{
value: value,
ttl: ttl,
key: key,
}
item.touch()
return item
}

func (item *item) update(value interface{}) {
item.value = value
item.touch()
}

func (item *item) touch() {
item.expireAt = time.Now().Add(item.ttl)
}

func (item *item) expired() bool {
return item.expireAt.Before(time.Now())
}
66 changes: 66 additions & 0 deletions items_heap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package niftycache

import "container/heap"

func newItemsHeap() *itemsHeap {
ih := new(itemsHeap)
heap.Init(ih)
return ih
}

type itemsHeap struct {
items []*item
}

func (ih *itemsHeap) peek() *item {
if ih.Len() == 0 {
return nil
}
return ih.items[0]
}

func (ih *itemsHeap) update(item *item) {
heap.Fix(ih, item.index)
}

func (ih *itemsHeap) push(item *item) {
heap.Push(ih, item)
}

func (ih *itemsHeap) pop() {
heap.Pop(ih)
}

func (ih *itemsHeap) remove(item *item) {
heap.Remove(ih, item.index)
}

func (ih itemsHeap) Len() int {
return len(ih.items)
}

func (ih itemsHeap) Less(i, j int) bool {
return ih.items[i].expireAt.Before(ih.items[j].expireAt)
}

func (ih itemsHeap) Swap(i, j int) {
ih.items[i], ih.items[j] = ih.items[j], ih.items[i]
ih.items[i].index = i
ih.items[j].index = j
}

func (ih *itemsHeap) Push(x interface{}) {
item := x.(*item)
item.index = len(ih.items)
ih.items = append(ih.items, item)
}

func (ih *itemsHeap) Pop() interface{} {
old := ih.items
n := len(old)
item := old[n-1]
item.index = -1
old[n-1] = nil
ih.items = old[0 : n-1]
return item
}
179 changes: 179 additions & 0 deletions niftycache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package niftycache

import (
"sync"
"time"

"github.com/eapache/queue"
)

type Cache struct {
ttl time.Duration
removeCB callback
setCB callback
updateCB callback
expireCB callback
extendTTL bool
items map[string]*item
ih *itemsHeap
m *sync.Mutex
maxExpires int
maxCallbacks int
cbLimiter chan struct{}
callbacks *queue.Queue
}

type Option func(*Cache)

type Callback func(string, interface{})

type callback func(string, interface{}) func()

func createCBClosure(f Callback) callback {
return func(k string, v interface{}) func() {
return func() {
f(k, v)
}
}
}

func RemoveCallback(f Callback) Option {
return func(nc *Cache) {
nc.removeCB = createCBClosure(f)
}
}

func UpdateCallback(f Callback) Option {
return func(nc *Cache) {
nc.updateCB = createCBClosure(f)
}
}

func SetCallback(f Callback) Option {
return func(nc *Cache) {
nc.setCB = createCBClosure(f)
}
}

func ExpireCallback(f Callback) Option {
return func(nc *Cache) {
nc.expireCB = createCBClosure(f)
}
}

func ExtendTTLOnHit() Option {
return func(nc *Cache) {
nc.extendTTL = true
}
}

func MaxExpires(maxExpires int) Option {
return func(nc *Cache) {
nc.maxExpires = maxExpires
}
}

func MaxCallbacks(maxCallbacks int) Option {
return func(nc *Cache) {
nc.maxCallbacks = maxCallbacks
}
}

func New(ttl time.Duration, options ...Option) *Cache {
nc := &Cache{
ttl: ttl,
items: make(map[string]*item),
ih: newItemsHeap(),
m: new(sync.Mutex),
maxExpires: 10000,
maxCallbacks: 1000,
callbacks: queue.New(),
}
for _, o := range options {
o(nc)
}
nc.cbLimiter = make(chan struct{}, nc.maxCallbacks)
go nc.handleExpirations()
go nc.handleCallbacks()
return nc
}

func (nc *Cache) handleCallbacks() {
for {
nc.m.Lock()
if nc.callbacks.Length() == 0 {
nc.m.Unlock()
time.Sleep(time.Second)
continue
}
out := nc.callbacks.Remove()
nc.m.Unlock()
cb := out.(func())
nc.cbLimiter <- struct{}{}
go func() {
cb()
<-nc.cbLimiter
}()
}
}

func (nc *Cache) handleExpirations() {
for range time.Tick(time.Second) {
nc.m.Lock()
for item, j := nc.ih.peek(), 0; j < nc.maxExpires && item != nil && item.expired(); item, j = nc.ih.peek(), j+1 {
delete(nc.items, item.key)
nc.ih.pop()
if nc.expireCB != nil {
nc.callbacks.Add(nc.expireCB(item.key, item.value))
}
}
nc.m.Unlock()
}
}

func (nc *Cache) Remove(key string) {
nc.m.Lock()
defer nc.m.Unlock()
item, ok := nc.items[key]
if !ok {
return
}
delete(nc.items, key)
nc.ih.remove(item)
if nc.removeCB != nil {
nc.callbacks.Add(nc.removeCB(item.key, item.value))
}
}

func (nc *Cache) Get(key string) (interface{}, bool) {
nc.m.Lock()
defer nc.m.Unlock()
item, ok := nc.items[key]
if !ok {
return nil, false
}
if nc.extendTTL {
item.touch()
}
return item.value, true
}

func (nc *Cache) Set(key string, value interface{}) {
nc.m.Lock()
item, ok := nc.items[key]
if !ok {
item = newItem(key, value, nc.ttl)
nc.items[key] = item
nc.ih.push(item)
if nc.setCB != nil {
nc.callbacks.Add(nc.setCB(item.key, item.value))
}
} else {
item.update(value)
nc.ih.update(item)
if nc.updateCB != nil {
nc.callbacks.Add(nc.updateCB(item.key, item.value))
}
}
nc.m.Unlock()
}

0 comments on commit 0091cfa

Please sign in to comment.