Skip to content

Commit

Permalink
Rework cache and its API
Browse files Browse the repository at this point in the history
  • Loading branch information
swithek committed Feb 5, 2022
1 parent 3642621 commit 2c1aa16
Show file tree
Hide file tree
Showing 9 changed files with 1,290 additions and 1,636 deletions.
874 changes: 383 additions & 491 deletions cache.go

Large diffs are not rendered by default.

1,560 changes: 611 additions & 949 deletions cache_test.go

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions expiration_queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package ttlcache

import (
"container/heap"
"container/list"
)

// expirationQueue stores items that are ordered by their expiration
// timestamps. The 0th item is closest to its expiration.
type expirationQueue[K comparable, V any] []*list.Element

// newExpirationQueue creates and initializes a new expiration queue.
func newExpirationQueue[K comparable, V any]() expirationQueue[K, V] {
q := make(expirationQueue[K, V], 0)
heap.Init(&q)
return q
}

// isEmpty checks if the queue is empty.
func (q expirationQueue[K, V]) isEmpty() bool {
return q.Len() == 0
}

// update updates an existing item's value and position in the queue.
func (q *expirationQueue[K, V]) update(elem *list.Element) {
heap.Fix(q, elem.Value.(*Item[K, V]).queueIndex)
}

// push pushes a new item into the queue and updates the order of its
// elements.
func (q *expirationQueue[K, V]) push(elem *list.Element) {
heap.Push(q, elem)
}

// remove removes an item from the queue and updates the order of its
// elements.
func (q *expirationQueue[K, V]) remove(elem *list.Element) {
heap.Remove(q, elem.Value.(*Item[K, V]).queueIndex)
}

// Len returns the total number of items in the queue.
func (q expirationQueue[K, V]) Len() int {
return len(q)
}

// Less checks if the item at the i position expires sooner than
// the one at the j position.
func (q expirationQueue[K, V]) Less(i, j int) bool {
item1, item2 := q[i].Value.(*Item[K, V]), q[j].Value.(*Item[K, V])
if item1.expiresAt.IsZero() {
return false
}

if item2.expiresAt.IsZero() {
return true
}

return item1.expiresAt.Before(item2.expiresAt)
}

// Swap switches the places of two queue items.
func (q expirationQueue[K, V]) Swap(i, j int) {
q[i], q[j] = q[j], q[i]
q[i].Value.(*Item[K, V]).queueIndex = i
q[j].Value.(*Item[K, V]).queueIndex = j
}

// Push appends a new item to the item slice.
func (q *expirationQueue[K, V]) Push(x interface{}) {
elem := x.(*list.Element)
elem.Value.(*Item[K, V]).queueIndex = len(*q)
*q = append(*q, elem)
}

// Pop removes and returns the last item.
func (q *expirationQueue[K, V]) Pop() interface{} {
old := *q
i := len(old) - 1
elem := old[i]
elem.Value.(*Item[K, V]).queueIndex = -1
old[i] = nil // avoid memory leak
*q = old[:i]

return elem
}
194 changes: 194 additions & 0 deletions expiration_queue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package ttlcache

import (
"container/list"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_newExpirationQueue(t *testing.T) {
assert.NotNil(t, newExpirationQueue[string, string]())
}

func Test_expirationQueue_isEmpty(t *testing.T) {
assert.True(t, (expirationQueue[string, string]{}).isEmpty())
assert.False(t, (expirationQueue[string, string]{{}}).isEmpty())
}

func Test_expirationQueue_update(t *testing.T) {
q := expirationQueue[string, string]{
{
Value: &Item[string, string]{
value: "test1",
queueIndex: 0,
expiresAt: time.Now().Add(time.Hour),
},
},
{
Value: &Item[string, string]{
value: "test2",
queueIndex: 1,
expiresAt: time.Now().Add(time.Minute),
},
},
}

q.update(q[1])
require.Len(t, q, 2)
assert.Equal(t, "test2", q[0].Value.(*Item[string, string]).value)
}

func Test_expirationQueue_push(t *testing.T) {
q := expirationQueue[string, string]{
{
Value: &Item[string, string]{
value: "test1",
queueIndex: 0,
expiresAt: time.Now().Add(time.Hour),
},
},
}
elem := &list.Element{
Value: &Item[string, string]{
value: "test2",
queueIndex: 1,
expiresAt: time.Now().Add(time.Minute),
},
}

q.push(elem)
require.Len(t, q, 2)
assert.Equal(t, "test2", q[0].Value.(*Item[string, string]).value)
}

func Test_expirationQueue_remove(t *testing.T) {
q := expirationQueue[string, string]{
{
Value: &Item[string, string]{
value: "test1",
queueIndex: 0,
expiresAt: time.Now().Add(time.Hour),
},
},
{
Value: &Item[string, string]{
value: "test2",
queueIndex: 1,
expiresAt: time.Now().Add(time.Minute),
},
},
}

q.remove(q[1])
require.Len(t, q, 1)
assert.Equal(t, "test1", q[0].Value.(*Item[string, string]).value)
}

func Test_expirationQueue_Len(t *testing.T) {
assert.Equal(t, 1, (expirationQueue[string, string]{{}}).Len())
}

func Test_expirationQueue_Less(t *testing.T) {
q := expirationQueue[string, string]{
{
Value: &Item[string, string]{
value: "test1",
queueIndex: 0,
expiresAt: time.Now().Add(time.Hour),
},
},
{
Value: &Item[string, string]{
value: "test2",
queueIndex: 1,
expiresAt: time.Now().Add(time.Minute),
},
},
{
Value: &Item[string, string]{
value: "test3",
queueIndex: 2,
},
},
}

assert.False(t, q.Less(2, 1))
assert.True(t, q.Less(1, 2))
assert.True(t, q.Less(1, 0))
assert.False(t, q.Less(0, 1))
}

func Test_expirationQueue_Swap(t *testing.T) {
q := expirationQueue[string, string]{
{
Value: &Item[string, string]{
value: "test1",
queueIndex: 0,
expiresAt: time.Now().Add(time.Hour),
},
},
{
Value: &Item[string, string]{
value: "test2",
queueIndex: 1,
expiresAt: time.Now().Add(time.Minute),
},
},
}

q.Swap(0, 1)
assert.Equal(t, "test2", q[0].Value.(*Item[string, string]).value)
assert.Equal(t, "test1", q[1].Value.(*Item[string, string]).value)
}

func Test_expirationQueue_Push(t *testing.T) {
q := expirationQueue[string, string]{
{
Value: &Item[string, string]{
value: "test1",
queueIndex: 0,
expiresAt: time.Now().Add(time.Hour),
},
},
}

elem := &list.Element{
Value: &Item[string, string]{
value: "test2",
queueIndex: 1,
expiresAt: time.Now().Add(time.Minute),
},
}

q.Push(elem)
require.Len(t, q, 2)
assert.Equal(t, "test2", q[1].Value.(*Item[string, string]).value)
}

func Test_expirationQueue_Pop(t *testing.T) {
q := expirationQueue[string, string]{
{
Value: &Item[string, string]{
value: "test1",
queueIndex: 0,
expiresAt: time.Now().Add(time.Hour),
},
},
{
Value: &Item[string, string]{
value: "test2",
queueIndex: 1,
expiresAt: time.Now().Add(time.Minute),
},
},
}

v := q.Pop()
require.NotNil(t, v)
assert.Equal(t, "test2", v.(*list.Element).Value.(*Item[string, string]).value)
require.Len(t, q, 1)
assert.Equal(t, "test1", q[0].Value.(*Item[string, string]).value)
}
26 changes: 13 additions & 13 deletions item.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,41 @@ import (
)

const (
// NoExpiration indicates that an item should never expire.
NoExpiration time.Duration = -1
// NoTTL indicates that an item should never expire.
NoTTL time.Duration = -1

// DefaultExpiration indicates that the default expiration
// DefaultTTL indicates that the default TTL
// value should be used.
DefaultExpiration time.Duration = 0
DefaultTTL time.Duration = 0
)

// item holds all the information that is associated with a single
// cache value.
type item[K comparable, V any] struct {
// Item holds all the information that is associated with a single
// cache value.
type Item[K comparable, V any] struct {
key K
value V
ttl time.Duration
expiresAt time.Time
expiresAt time.Time
queueIndex int
}

// newItem creates a new cache item.
func newItem[K comparable, V any](key K, value V, ttl time.Duration) *item[K, V] {
item := &item[K, V]{
func newItem[K comparable, V any](key K, value V, ttl time.Duration) *Item[K, V] {
item := &Item[K, V]{
key: key,
value: value,
ttl: ttl,
}

// since nobody is aware yet of this item, it's safe to touch
// since nobody is aware of this item yet, it's safe to touch
// without lock here
item.touch()

return item
}

// touch updates the item's expiration timestamp.
func (item *item[K, V]) touch() {
func (item *Item[K, V]) touch() {
if item.ttl <= 0 {
return
}
Expand All @@ -49,7 +49,7 @@ func (item *item[K, V]) touch() {

// isExpired returns a bool value that indicates whether the
// the item is expired or not.
func (item *item[K, V]) isExpired() bool {
func (item *Item[K, V]) isExpired() bool {
if item.ttl <= 0 {
return false
}
Expand Down
8 changes: 4 additions & 4 deletions item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func Test_newItem(t *testing.T) {
assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute)
}

func Test_item_touch(t *testing.T) {
var item item[string, string]
func Test_Item_touch(t *testing.T) {
var item Item[string, string]
item.touch()
assert.Zero(t, item.expiresAt)

Expand All @@ -27,9 +27,9 @@ func Test_item_touch(t *testing.T) {
assert.WithinDuration(t, time.Now().Add(time.Hour), item.expiresAt, time.Minute)
}

func Test_item_isExpired(t *testing.T) {
func Test_Item_isExpired(t *testing.T) {
// no ttl
item := item[string, string]{
item := Item[string, string]{
expiresAt: time.Now().Add(-time.Hour),
}

Expand Down
3 changes: 0 additions & 3 deletions metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ type Metrics struct {
// Inserts specifies how many items were inserted.
Inserts uint64

// Retrievals specifies how many items were retrieved.
Retrievals uint64

// Hits specifies how many items were successfully retrieved
// from the cache.
// Retrievals made with a loader function are not tracked.
Expand Down
Loading

0 comments on commit 2c1aa16

Please sign in to comment.