Skip to content

Commit

Permalink
Making lazylru generic, deprecating the lazylru/generic package (#25)
Browse files Browse the repository at this point in the history
This was supposed to happen last September. There is no need to support Go <=1.17 at this point, so the generic interface is sufficient for everyone. This will cause deprecation warnings for everyone using the `github.com/TriggerMail/lazylru/genric`, but switching to the `github.com/TriggerMail/lazylru` package should be seamless.
  • Loading branch information
dangermike authored Jun 27, 2023
1 parent 307bd90 commit fa7c520
Show file tree
Hide file tree
Showing 42 changed files with 3,260 additions and 218 deletions.
26 changes: 12 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,39 +39,37 @@ If sharding makes sense for you, it should be pretty easy to make a list of Lazy

## Usage

### Go &lt;= 1.17

Like Go's [`heap`](https://golang.org/pkg/container/heap/) itself, Lazy LRU uses the `interface{}` type for its values. That means that casting is required on the way out. I promised that as soon as Go had [generics](https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md), I'd get right on it. See below!
The Go [`heap`](https://golang.org/pkg/container/heap/) has been copied and made to support generics. That allows the LRU to also support generics. To access that feature, import the `lazylru/generic` module. To maintain compatibility with pre-generics versions, the `New` factory method still uses `string` keys and `interface{}` values. However, this is just a wrapper over the `NewT[K,V]` factory method.

```go
// import "github.com/TriggerMail/lazylru"

lru := lazylru.New(10, 5 * time.minute)
lru := lazylru.NewT[string, string](10, 5 * time.minute)
defer lru.Close()

lru.Set("abloy", "medeco")

v, ok := lru.Get("abloy")
vstr, vok := v.(string)
vstr, ok := lru.Get("abloy")
```

### Go &gt;= 1.18
It is important to note that `LazyLRU` should be closed if the TTL is non-zero. Otherwise, the background reaper thread will be left running. To be fair, under most circumstances I can imagine, the cache lives as long as the host process. So do what you like.

The Go [`heap`](https://golang.org/pkg/container/heap/) has been copied and made to support generics. That allows the LRU to also support generics. To access that feature, import the `lazylru/generic` module. To maintain compatibility, the `New` factory method still uses `string` keys and `interface{}` values. However, this is just a wrapper over the `NewT[K,V]` factory method.
### Go &lt;= 1.17

Once Go 1.18 is baked-in and commonly used, the `lazylru/generic` module will be retired and only the `lazylru` module will remain. Because the `New` factory method is the same, the changes here are purely additive and are in the spirit of the [compatibility guarantee](https://go.dev/doc/go1compat). Because LazyLRU is not yet at 1.0, removing the `lazylru/generic` module isn't out-of-bounds and it will be removed within a few months. According to the [2020 Go Developer Survey](https://go.dev/blog/survey2020-results), ~90% of users who are consistent with upgrades upgrade Go versions within 6 months.
As of v0.4.0, LazyLRU takes advantage of Go [generics](https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md). If you want to use this library in Go 1.17 or lower, please use v0.3.x. [v0.3.3](https://github.com/TriggerMail/lazylru/releases/tag/v0.3.3) is the latest as of the time of this writing.

As such, the plan is to move the generic code up to the main package by September, 2022 and fully deprecate the `lazylru/generic` package by March, 2023.
Like Go's [`heap`](https://golang.org/pkg/container/heap/) itself, Lazy LRU uses the `interface{}` type for its values. That means that casting is required on the way out. I promised that as soon as Go had [generics](https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md), I'd get right on it. See below!

```go
// import "github.com/TriggerMail/lazylru/generic"
// import "github.com/TriggerMail/lazylru"

lru := lazylru.NewT[string, string](10, 5 * time.minute)
lru := lazylru.New(10, 5 * time.minute)
defer lru.Close()

lru.Set("abloy", "medeco")

vstr, ok := lru.Get("abloy")
v, ok := lru.Get("abloy")
vstr, vok := v.(string)
```

It is important to note that `LazyLRU` should be closed if the TTL is non-zero. Otherwise, the background reaper thread will be left running. To be fair, under most circumstances I can imagine, the cache lives as long as the host process. So do what you like.
###
1 change: 0 additions & 1 deletion bench/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ replace github.com/TriggerMail/lazylru/generic => ../generic

require (
github.com/TriggerMail/lazylru v0.3.3
github.com/TriggerMail/lazylru/generic v0.3.0
github.com/hashicorp/golang-lru v0.5.4
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.24.0
Expand Down
1 change: 1 addition & 0 deletions bench/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
Expand Down
24 changes: 2 additions & 22 deletions bench/lazylru_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"time"

"github.com/TriggerMail/lazylru"
lazylruT "github.com/TriggerMail/lazylru/generic"
)

const keycnt = 100000
Expand Down Expand Up @@ -39,26 +38,8 @@ func (bc benchconfig) Name() string {
return fmt.Sprintf("%dW/%dR_%s", 100-int(100*bc.readRate), int(100*bc.readRate), comment)
}

func (bc benchconfig) Interface(b *testing.B) {
lru := lazylru.New(bc.capacity, time.Minute)
defer lru.Close()
for i := 0; i < bc.keyCount; i++ {
lru.Set(keys[i], i)
}
runtime.GC()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ix := rand.Intn(bc.keyCount) //nolint:gosec
if rand.Float64() < bc.readRate { //nolint:gosec
lru.Get(keys[ix])
} else {
lru.Set(keys[ix], ix)
}
}
}

func (bc benchconfig) Generic(b *testing.B) {
lru := lazylruT.NewT[string, int](bc.capacity, time.Minute)
lru := lazylru.NewT[string, int](bc.capacity, time.Minute)
defer lru.Close()
for i := 0; i < bc.keyCount; i++ {
lru.Set(keys[i], i)
Expand All @@ -77,7 +58,7 @@ func (bc benchconfig) Generic(b *testing.B) {
}

func (bc benchconfig) GenInterface(b *testing.B) {
lru := lazylruT.New(bc.capacity, time.Minute) //nolint:staticcheck
lru := lazylru.New(bc.capacity, time.Minute) //nolint:staticcheck
defer lru.Close()
for i := 0; i < bc.keyCount; i++ {
lru.Set(keys[i], i)
Expand Down Expand Up @@ -114,7 +95,6 @@ func Benchmark(b *testing.B) {
{100, 100, 0.75},
{100, 100, 0.99},
} {
b.Run(bc.Name()+"/interface_based", bc.Interface)
b.Run(bc.Name()+"/gen[string,iface]", bc.GenInterface)
b.Run(bc.Name()+"/gen[string,int]", bc.Generic)
}
Expand Down
26 changes: 0 additions & 26 deletions bench/lazylru_typesafe.go

This file was deleted.

7 changes: 2 additions & 5 deletions bench/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"time"

"github.com/TriggerMail/lazylru"
lazylruT "github.com/TriggerMail/lazylru/generic"
"go.uber.org/zap"
)

Expand Down Expand Up @@ -76,10 +75,8 @@ func main() {
{func(size int) Cache { return NullCache }, "null"},
{func(size int) Cache { return NewMapCache[string, string](size, time.Hour) }, "mapcache.hour"},
{func(size int) Cache { return NewMapCache[string, string](size, time.Millisecond*50) }, "mapcache.50ms"},
{func(size int) Cache { return (*LazyLRUTypesafe[string])(lazylru.New(size, time.Hour)) }, "lazylru.hour"},
{func(size int) Cache { return (*LazyLRUTypesafe[string])(lazylru.New(size, time.Millisecond*50)) }, "lazylru.50ms"},
{func(size int) Cache { return lazylruT.NewT[string, string](size, time.Hour) }, "lazylruT.hour"},
{func(size int) Cache { return lazylruT.NewT[string, string](size, time.Millisecond*50) }, "lazylruT.50ms"},
{func(size int) Cache { return lazylru.NewT[string, string](size, time.Hour) }, "lazylru.hour"},
{func(size int) Cache { return lazylru.NewT[string, string](size, time.Millisecond*50) }, "lazylru.50ms"},
{func(size int) Cache { return NewHashicorpWrapper[string, string](size) }, "hashicorp.lru"},
{func(size int) Cache { return NewHashicorpWrapperExp[string, string](size, time.Hour) }, "hashicorp.exp_hour"},
{func(size int) Cache { return NewHashicorpWrapperExp[string, string](size, time.Millisecond*50) }, "hashicorp.exp_50ms"},
Expand Down
4 changes: 1 addition & 3 deletions bench/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/TriggerMail/lazylru"
bench "github.com/TriggerMail/lazylru/bench"
lazylruT "github.com/TriggerMail/lazylru/generic"
"github.com/stretchr/testify/require"
)

Expand All @@ -17,8 +16,7 @@ func TestWrapperFunctions(t *testing.T) {
name string
}{
{bench.NewMapCache[string, string](10, time.Hour), "mapcache"},
{(*bench.LazyLRUTypesafe[string])(lazylru.New(10, time.Hour)), "lazylru"},
{lazylruT.NewT[string, string](10, time.Hour), "lazylruT"},
{lazylru.NewT[string, string](10, time.Hour), "lazylru"},
{bench.NewHashicorpWrapper[string, string](10), "hashicorp.lru"},
{bench.NewHashicorpWrapperExp[string, string](10, time.Hour), "hashicorp.exp"},
{bench.NewHashicorpARCWrapper[string, string](10), "hashicorp.arc"},
Expand Down
5 changes: 5 additions & 0 deletions containers/heap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generic Heap

This is a LITERAL COPY from the Go standard library because there is not yet a genric version of the `containers/heap` module in the Go 1.18 beta 1 release. I am sure this will come out in Go 1.18 or in a subsequent release shortly thereafter. This library is meant to be a bridge until that is done and nothing more.

Please do not use this for anything outside the lazylru module because it _will_ be deleted as soon as a generic heap is available in the stardard library.
118 changes: 118 additions & 0 deletions containers/heap/heap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package heap provides heap operations for any type that implements
// heap.Interface. A heap is a tree with the property that each node is the
// minimum-valued node in its subtree.
//
// The minimum element in the tree is the root, at index 0.
//
// A heap is a common way to implement a priority queue. To build a priority
// queue, implement the Heap interface with the (negative) priority as the
// ordering for the Less method, so Push adds items while Pop removes the
// highest-priority item from the queue. The Examples include such an
// implementation; the file example_pq_test.go has the complete source.
package heap

import "sort"

// The Interface type describes the requirements
// for a type using the routines in this package.
// Any type that implements it may be used as a
// min-heap with the following invariants (established after
// Init has been called or if the data is empty or sorted):
//
// !h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()
//
// Note that Push and Pop in this interface are for package heap's
// implementation to call. To add and remove things from the heap,
// use heap.Push and heap.Pop.
type Interface[T any] interface {
sort.Interface
Push(x T) // add x as element Len()
Pop() T // remove and return element Len() - 1.
}

// Init establishes the heap invariants required by the other routines in this package.
// Init is idempotent with respect to the heap invariants
// and may be called whenever the heap invariants may have been invalidated.
// The complexity is O(n) where n = h.Len().
func Init[T any](h Interface[T]) {
// heapify
n := h.Len()
for i := n/2 - 1; i >= 0; i-- {
down(h, i, n)
}
}

// Push pushes the element x onto the heap.
// The complexity is O(log n) where n = h.Len().
func Push[T any](h Interface[T], x T) {
h.Push(x)
up(h, h.Len()-1)
}

// Pop removes and returns the minimum element (according to Less) from the heap.
// The complexity is O(log n) where n = h.Len().
// Pop is equivalent to Remove(h, 0).
func Pop[T any](h Interface[T]) T {
n := h.Len() - 1
h.Swap(0, n)
down(h, 0, n)
return h.Pop()
}

// Remove removes and returns the element at index i from the heap.
// The complexity is O(log n) where n = h.Len().
func Remove[T any](h Interface[T], i int) T {
n := h.Len() - 1
if n != i {
h.Swap(i, n)
if !down(h, i, n) {
up(h, i)
}
}
return h.Pop()
}

// Fix re-establishes the heap ordering after the element at index i has changed its value.
// Changing the value of the element at index i and then calling Fix is equivalent to,
// but less expensive than, calling Remove(h, i) followed by a Push of the new value.
// The complexity is O(log n) where n = h.Len().
func Fix[T any](h Interface[T], i int) {
if !down(h, i, h.Len()) {
up(h, i)
}
}

func up[T any](h Interface[T], j int) {
for {
i := (j - 1) / 2 // parent
if i == j || !h.Less(j, i) {
break
}
h.Swap(i, j)
j = i
}
}

func down[T any](h Interface[T], i0, n int) bool {
i := i0
for {
j1 := 2*i + 1
if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
break
}
j := j1 // left child
if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
j = j2 // = 2*i + 2 // right child
}
if !h.Less(j, i) {
break
}
h.Swap(i, j)
i = j
}
return i > i0
}
Loading

0 comments on commit fa7c520

Please sign in to comment.