From 00095e515cbdca25b7cdf633274c9a801f348bc2 Mon Sep 17 00:00:00 2001 From: maypok86 Date: Fri, 30 Aug 2024 22:06:08 +0300 Subject: [PATCH] [Chore] Use Clock instead of unixtime This should make the expiration policy much more accurate. --- cache.go | 56 +++++------ cmd/generator/main.go | 36 ++++--- entry.go | 8 +- entry_test.go | 4 +- extension.go | 5 +- internal/clock/clock.go | 35 +++++++ .../clock_bench_test.go} | 38 +++++--- .../unixtime_test.go => clock/clock_test.go} | 20 ++-- internal/expiry/disabled.go | 2 +- internal/expiry/fixed.go | 4 +- internal/expiry/variable.go | 61 +++++++----- internal/expiry/variable_test.go | 44 ++++----- internal/generated/node/b.go | 6 +- internal/generated/node/be.go | 12 +-- internal/generated/node/bew.go | 12 +-- internal/generated/node/bw.go | 6 +- internal/generated/node/manager.go | 8 +- internal/s3fifo/main.go | 4 +- internal/s3fifo/policy.go | 10 +- internal/s3fifo/policy_test.go | 16 ++-- internal/s3fifo/small.go | 6 +- internal/unixtime/unixtime.go | 93 ------------------- 22 files changed, 220 insertions(+), 266 deletions(-) create mode 100644 internal/clock/clock.go rename internal/{unixtime/unixtime_bench_test.go => clock/clock_bench_test.go} (59%) rename internal/{unixtime/unixtime_test.go => clock/clock_test.go} (77%) delete mode 100644 internal/unixtime/unixtime.go diff --git a/cache.go b/cache.go index 8a58f60..1c0b629 100644 --- a/cache.go +++ b/cache.go @@ -18,13 +18,13 @@ import ( "sync" "time" + "github.com/maypok86/otter/v2/internal/clock" "github.com/maypok86/otter/v2/internal/expiry" "github.com/maypok86/otter/v2/internal/generated/node" "github.com/maypok86/otter/v2/internal/hashtable" "github.com/maypok86/otter/v2/internal/lossy" "github.com/maypok86/otter/v2/internal/queue" "github.com/maypok86/otter/v2/internal/s3fifo" - "github.com/maypok86/otter/v2/internal/unixtime" "github.com/maypok86/otter/v2/internal/xmath" "github.com/maypok86/otter/v2/internal/xruntime" ) @@ -75,19 +75,10 @@ func init() { maxStripedBufferSize = 4 * roundedParallelism } -func getTTL(ttl time.Duration) uint32 { - //nolint:gosec // there will never be an overflow - return uint32((ttl + time.Second - 1) / time.Second) -} - -func getExpiration(ttl time.Duration) uint32 { - return unixtime.Now() + getTTL(ttl) -} - type expiryPolicy[K comparable, V any] interface { Add(n node.Node[K, V]) Delete(n node.Node[K, V]) - DeleteExpired() + DeleteExpired(nowNanos int64) Clear() } @@ -100,6 +91,7 @@ type Cache[K comparable, V any] struct { expiryPolicy expiryPolicy[K, V] stats statsCollector logger Logger + clock *clock.Clock stripedBuffer []*lossy.Buffer[K, V] writeBuffer *queue.Growable[task[K, V]] evictionMutex sync.Mutex @@ -110,7 +102,7 @@ type Cache[K comparable, V any] struct { deletionListener func(key K, value V, cause DeletionCause) capacity int mask uint32 - ttl uint32 + ttl time.Duration withExpiration bool } @@ -138,6 +130,7 @@ func newCache[K comparable, V any](b *Builder[K, V]) *Cache[K, V] { hashmap: hashmap, stats: newStatsCollector(b.statsCollector), logger: b.logger, + clock: clock.New(), stripedBuffer: stripedBuffer, writeBuffer: queue.NewGrowable[task[K, V]](minWriteBufferSize, maxWriteBufferSize), doneClear: make(chan struct{}), @@ -161,13 +154,12 @@ func newCache[K comparable, V any](b *Builder[K, V]) *Cache[K, V] { } if b.ttl != nil { - cache.ttl = getTTL(*b.ttl) + cache.ttl = *b.ttl } cache.withExpiration = b.ttl != nil || b.withVariableTTL if cache.withExpiration { - unixtime.Start() go cache.cleanup() } @@ -180,6 +172,10 @@ func (c *Cache[K, V]) getReadBufferIdx() int { return int(xruntime.Fastrand() & c.mask) } +func (c *Cache[K, V]) getExpiration(duration time.Duration) int64 { + return c.clock.Offset() + duration.Nanoseconds() +} + // Has checks if there is an item with the given key in the cache. func (c *Cache[K, V]) Has(key K) bool { _, ok := c.Get(key) @@ -204,7 +200,7 @@ func (c *Cache[K, V]) GetNode(key K) (node.Node[K, V], bool) { return nil, false } - if n.HasExpired() { + if n.HasExpired(c.clock.Offset()) { // avoid duplicate push deleted := c.hashmap.DeleteNode(n) if deleted != nil { @@ -227,7 +223,7 @@ func (c *Cache[K, V]) GetNode(key K) (node.Node[K, V], bool) { // such as updating statistics or the eviction policy. func (c *Cache[K, V]) GetNodeQuietly(key K) (node.Node[K, V], bool) { n, ok := c.hashmap.Get(key) - if !ok || !n.IsAlive() || n.HasExpired() { + if !ok || !n.IsAlive() || n.HasExpired(c.clock.Offset()) { return nil, false } @@ -253,19 +249,19 @@ func (c *Cache[K, V]) Set(key K, value V) bool { return c.set(key, value, c.defaultExpiration(), false) } -func (c *Cache[K, V]) defaultExpiration() uint32 { +func (c *Cache[K, V]) defaultExpiration() int64 { if c.ttl == 0 { return 0 } - return unixtime.Now() + c.ttl + return c.getExpiration(c.ttl) } // SetWithTTL associates the value with the key in this cache and sets the custom ttl for this key-value item. // // If it returns false, then the key-value item had too much weight and the SetWithTTL was dropped. func (c *Cache[K, V]) SetWithTTL(key K, value V, ttl time.Duration) bool { - return c.set(key, value, getExpiration(ttl), false) + return c.set(key, value, c.getExpiration(ttl), false) } // SetIfAbsent if the specified key is not already associated with a value associates it with the given value. @@ -284,10 +280,10 @@ func (c *Cache[K, V]) SetIfAbsent(key K, value V) bool { // // Also, it returns false if the key-value item had too much weight and the SetIfAbsent was dropped. func (c *Cache[K, V]) SetIfAbsentWithTTL(key K, value V, ttl time.Duration) bool { - return c.set(key, value, getExpiration(ttl), true) + return c.set(key, value, c.getExpiration(ttl), true) } -func (c *Cache[K, V]) set(key K, value V, expiration uint32, onlyIfAbsent bool) bool { +func (c *Cache[K, V]) set(key K, value V, expiration int64, onlyIfAbsent bool) bool { weight := c.weigher(key, value) if int(weight) > c.policy.MaxAvailableWeight() { c.stats.CollectRejectedSets(1) @@ -337,7 +333,7 @@ func (c *Cache[K, V]) afterDelete(deleted node.Node[K, V]) { // DeleteByFunc deletes the association for this key from the cache when the given function returns true. func (c *Cache[K, V]) DeleteByFunc(f func(key K, value V) bool) { c.hashmap.Range(func(n node.Node[K, V]) bool { - if !n.IsAlive() || n.HasExpired() { + if !n.IsAlive() || n.HasExpired(c.clock.Offset()) { return true } @@ -376,7 +372,7 @@ func (c *Cache[K, V]) cleanup() { return case <-ticker.C: c.evictionMutex.Lock() - c.expiryPolicy.DeleteExpired() + c.expiryPolicy.DeleteExpired(c.clock.Offset()) c.evictionMutex.Unlock() } } @@ -411,7 +407,7 @@ func (c *Cache[K, V]) onWrite(t task[K, V]) { case t.isAdd(): if n.IsAlive() { c.expiryPolicy.Add(n) - c.policy.Add(n) + c.policy.Add(n, c.clock.Offset()) } case t.isUpdate(): oldNode := t.oldNode() @@ -419,7 +415,7 @@ func (c *Cache[K, V]) onWrite(t task[K, V]) { c.policy.Delete(oldNode) if n.IsAlive() { c.expiryPolicy.Add(n) - c.policy.Add(n) + c.policy.Add(n, c.clock.Offset()) } c.notifyDeletion(oldNode.Key(), oldNode.Value(), Replaced) case t.isDelete(): @@ -452,7 +448,7 @@ func (c *Cache[K, V]) process() { // Iteration stops early when the given function returns false. func (c *Cache[K, V]) Range(f func(key K, value V) bool) { c.hashmap.Range(func(n node.Node[K, V]) bool { - if !n.IsAlive() || n.HasExpired() { + if !n.IsAlive() || n.HasExpired(c.clock.Offset()) { return true } @@ -483,9 +479,6 @@ func (c *Cache[K, V]) clear(t task[K, V]) { func (c *Cache[K, V]) Close() { c.closeOnce.Do(func() { c.clear(newCloseTask[K, V]()) - if c.withExpiration { - unixtime.Stop() - } }) } @@ -505,8 +498,3 @@ func (c *Cache[K, V]) Capacity() int { func (c *Cache[K, V]) Extension() Extension[K, V] { return newExtension(c) } - -// WithExpiration returns true if the cache was configured with the expiration policy enabled. -func (c *Cache[K, V]) WithExpiration() bool { - return c.withExpiration -} diff --git a/cmd/generator/main.go b/cmd/generator/main.go index 86d0b6a..7b9b136 100644 --- a/cmd/generator/main.go +++ b/cmd/generator/main.go @@ -1,3 +1,17 @@ +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package main import ( @@ -136,10 +150,6 @@ func (g *generator) printImports() { g.in() g.p("\"sync/atomic\"") g.p("\"unsafe\"") - if g.features[expiration] { - g.p("") - g.p("\"github.com/maypok86/otter/v2/internal/unixtime\"") - } g.out() g.p(")") g.p("") @@ -175,7 +185,7 @@ func (g *generator) printStruct() { if g.features[expiration] { g.p("prevExp *%s[K, V]", g.structName) g.p("nextExp *%s[K, V]", g.structName) - g.p("expiration uint32") + g.p("expiration int64") } if g.features[weight] { g.p("weight uint32") @@ -191,7 +201,7 @@ func (g *generator) printStruct() { func (g *generator) printConstructors() { g.p("// New%s creates a new %s.", g.structName, g.structName) - g.p("func New%s[K comparable, V any](key K, value V, expiration, weight uint32) Node[K, V] {", g.structName) + g.p("func New%s[K comparable, V any](key K, value V, expiration int64, weight uint32) Node[K, V] {", g.structName) g.in() g.p("return &%s[K, V]{", g.structName) g.in() @@ -337,10 +347,10 @@ func (g *generator) printFunctions() { g.p("}") g.p("") - g.p("func (n *%s[K, V]) HasExpired() bool {", g.structName) + g.p("func (n *%s[K, V]) HasExpired(now int64) bool {", g.structName) g.in() if g.features[expiration] { - g.p("return n.expiration <= unixtime.Now()") + g.p("return n.expiration <= now") } else { g.p("return false") } @@ -348,7 +358,7 @@ func (g *generator) printFunctions() { g.p("}") g.p("") - g.p("func (n *%s[K, V]) Expiration() uint32 {", g.structName) + g.p("func (n *%s[K, V]) Expiration() int64 {", g.structName) g.in() if g.features[expiration] { g.p("return n.expiration") @@ -503,9 +513,9 @@ type Node[K comparable, V any] interface { // SetNextExp sets the next node in the expiration policy. SetNextExp(v Node[K, V]) // HasExpired returns true if node has expired. - HasExpired() bool + HasExpired(now int64) bool // Expiration returns the expiration time. - Expiration() uint32 + Expiration() int64 // Weight returns the weight of the node. Weight() uint32 // IsAlive returns true if the entry is available in the hash-table. @@ -548,7 +558,7 @@ type Config struct { } type Manager[K comparable, V any] struct { - create func(key K, value V, expiration, weight uint32) Node[K, V] + create func(key K, value V, expiration int64, weight uint32) Node[K, V] fromPointer func(ptr unsafe.Pointer) Node[K, V] } @@ -568,7 +578,7 @@ func NewManager[K comparable, V any](c Config) *Manager[K, V] { const nodeFooter = `return m } -func (m *Manager[K, V]) Create(key K, value V, expiration, weight uint32) Node[K, V] { +func (m *Manager[K, V]) Create(key K, value V, expiration int64, weight uint32) Node[K, V] { return m.create(key, value, expiration, weight) } diff --git a/entry.go b/entry.go index fd1d617..d131977 100644 --- a/entry.go +++ b/entry.go @@ -38,7 +38,7 @@ func (e Entry[K, V]) Value() V { } // Expiration returns the entry's expiration time as a unix time, -// the number of seconds elapsed since January 1, 1970 UTC. +// the number of nanoseconds elapsed since January 1, 1970 UTC. // // If the cache was not configured with an expiration policy then this value is always 0. func (e Entry[K, V]) Expiration() int64 { @@ -56,12 +56,12 @@ func (e Entry[K, V]) TTL() time.Duration { return -1 } - now := time.Now().Unix() + now := time.Now().UnixNano() if expiration <= now { return 0 } - return time.Duration(expiration-now) * time.Second + return time.Duration(expiration - now) } // HasExpired returns true if the entry has expired. @@ -71,7 +71,7 @@ func (e Entry[K, V]) HasExpired() bool { return false } - return expiration <= time.Now().Unix() + return expiration <= time.Now().UnixNano() } // Weight returns the entry's weight. diff --git a/entry_test.go b/entry_test.go index 4e6bcf2..86874fa 100644 --- a/entry_test.go +++ b/entry_test.go @@ -51,7 +51,7 @@ func TestEntry(t *testing.T) { } newTTL := int64(10) - e.expiration = time.Now().Unix() + newTTL + e.expiration = time.Now().UnixNano() + (time.Duration(newTTL) * time.Second).Nanoseconds() if ttl := e.TTL(); ttl <= 0 || ttl > time.Duration(newTTL)*time.Second { t.Fatalf("ttl should be in the range (0, %d] seconds, but got %d seconds", newTTL, ttl/time.Second) } @@ -59,7 +59,7 @@ func TestEntry(t *testing.T) { t.Fatal("entry should not be expire") } - e.expiration -= 2 * newTTL + e.expiration -= 2 * (time.Duration(newTTL) * time.Second).Nanoseconds() if ttl := e.TTL(); ttl != 0 { t.Fatalf("ttl should be 0 seconds, but got %d seconds", ttl/time.Second) } diff --git a/extension.go b/extension.go index b7ed507..a4a45e4 100644 --- a/extension.go +++ b/extension.go @@ -16,7 +16,6 @@ package otter import ( "github.com/maypok86/otter/v2/internal/generated/node" - "github.com/maypok86/otter/v2/internal/unixtime" ) func zeroValue[V any]() V { @@ -39,8 +38,8 @@ func newExtension[K comparable, V any](cache *Cache[K, V]) Extension[K, V] { func (e Extension[K, V]) createEntry(n node.Node[K, V]) Entry[K, V] { var expiration int64 - if e.cache.WithExpiration() { - expiration = unixtime.StartTime() + int64(n.Expiration()) + if e.cache.withExpiration { + expiration = e.cache.clock.Time(n.Expiration()).UnixNano() } return Entry[K, V]{ diff --git a/internal/clock/clock.go b/internal/clock/clock.go new file mode 100644 index 0000000..aa743cd --- /dev/null +++ b/internal/clock/clock.go @@ -0,0 +1,35 @@ +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package clock + +import "time" + +type Clock struct { + start time.Time +} + +func New() *Clock { + return &Clock{ + start: time.Now(), + } +} + +func (c *Clock) Offset() int64 { + return time.Since(c.start).Nanoseconds() +} + +func (c *Clock) Time(offset int64) time.Time { + return c.start.Add(time.Duration(offset)) +} diff --git a/internal/unixtime/unixtime_bench_test.go b/internal/clock/clock_bench_test.go similarity index 59% rename from internal/unixtime/unixtime_bench_test.go rename to internal/clock/clock_bench_test.go index b818b5d..3d67411 100644 --- a/internal/unixtime/unixtime_bench_test.go +++ b/internal/clock/clock_bench_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Alexey Mayshev. All rights reserved. +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,39 +12,51 @@ // See the License for the specific language governing permissions and // limitations under the License. -package unixtime +package clock import ( "sync/atomic" "testing" "time" + _ "unsafe" ) -func BenchmarkNow(b *testing.B) { - Start() +//go:linkname nanotime runtime.nanotime +func nanotime() int64 +func BenchmarkTimeNow(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { - var ts uint32 + var ts int64 for pb.Next() { - ts += Now() + ts += time.Now().UnixNano() } - atomic.StoreUint32(&sink, ts) + atomic.StoreInt64(&sink, ts) }) +} - Stop() +func BenchmarkNanotime(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + var ts int64 + for pb.Next() { + ts += nanotime() + } + atomic.StoreInt64(&sink, ts) + }) } -func BenchmarkTimeNowUnix(b *testing.B) { +func BenchmarkClock(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { - var ts uint32 + var ts int64 + c := New() for pb.Next() { - ts += uint32(time.Now().Unix()) + ts += c.Offset() } - atomic.StoreUint32(&sink, ts) + atomic.StoreInt64(&sink, ts) }) } // sink should prevent from code elimination by optimizing compiler. -var sink uint32 +var sink int64 diff --git a/internal/unixtime/unixtime_test.go b/internal/clock/clock_test.go similarity index 77% rename from internal/unixtime/unixtime_test.go rename to internal/clock/clock_test.go index 0ca227f..4b0164a 100644 --- a/internal/unixtime/unixtime_test.go +++ b/internal/clock/clock_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Alexey Mayshev. All rights reserved. +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package unixtime +package clock import ( "testing" @@ -20,25 +20,17 @@ import ( ) func TestNow(t *testing.T) { - Start() + c := New() - got := Now() + got := c.Offset() / 1e9 if got != 0 { t.Fatalf("unexpected time since program start; got %d; want %d", got, 0) } time.Sleep(3 * time.Second) - got = Now() - if got != 2 && got != 3 { + got = c.Offset() / 1e9 + if got != 3 { t.Fatalf("unexpected time since program start; got %d; want %d", got, 3) } - - Stop() - - time.Sleep(3 * time.Second) - - if Now()-got > 1 { - t.Fatal("timer should have stopped") - } } diff --git a/internal/expiry/disabled.go b/internal/expiry/disabled.go index c956a24..c26dcfe 100644 --- a/internal/expiry/disabled.go +++ b/internal/expiry/disabled.go @@ -28,7 +28,7 @@ func (d *Disabled[K, V]) Add(n node.Node[K, V]) { func (d *Disabled[K, V]) Delete(n node.Node[K, V]) { } -func (d *Disabled[K, V]) DeleteExpired() { +func (d *Disabled[K, V]) DeleteExpired(nowNanos int64) { } func (d *Disabled[K, V]) Clear() { diff --git a/internal/expiry/fixed.go b/internal/expiry/fixed.go index c74cf66..cd56890 100644 --- a/internal/expiry/fixed.go +++ b/internal/expiry/fixed.go @@ -36,8 +36,8 @@ func (f *Fixed[K, V]) Delete(n node.Node[K, V]) { f.q.delete(n) } -func (f *Fixed[K, V]) DeleteExpired() { - for !f.q.isEmpty() && f.q.head.HasExpired() { +func (f *Fixed[K, V]) DeleteExpired(nowNanos int64) { + for !f.q.isEmpty() && f.q.head.HasExpired(nowNanos) { f.deleteNode(f.q.pop()) } } diff --git a/internal/expiry/variable.go b/internal/expiry/variable.go index a67cd26..5a9165f 100644 --- a/internal/expiry/variable.go +++ b/internal/expiry/variable.go @@ -20,32 +20,30 @@ import ( "time" "github.com/maypok86/otter/v2/internal/generated/node" - "github.com/maypok86/otter/v2/internal/unixtime" - "github.com/maypok86/otter/v2/internal/xmath" ) var ( - buckets = []uint32{64, 64, 32, 4, 1} - spans = []uint32{ - xmath.RoundUpPowerOf2(uint32((1 * time.Second).Seconds())), // 1s - xmath.RoundUpPowerOf2(uint32((1 * time.Minute).Seconds())), // 1.07m - xmath.RoundUpPowerOf2(uint32((1 * time.Hour).Seconds())), // 1.13h - xmath.RoundUpPowerOf2(uint32((24 * time.Hour).Seconds())), // 1.52d - buckets[3] * xmath.RoundUpPowerOf2(uint32((24 * time.Hour).Seconds())), // 6.07d - buckets[3] * xmath.RoundUpPowerOf2(uint32((24 * time.Hour).Seconds())), // 6.07d + buckets = []uint64{64, 64, 32, 4, 1} + spans = []uint64{ + roundUpPowerOf2(uint64((1 * time.Second).Nanoseconds())), // 1.07s + roundUpPowerOf2(uint64((1 * time.Minute).Nanoseconds())), // 1.14m + roundUpPowerOf2(uint64((1 * time.Hour).Nanoseconds())), // 1.22h + roundUpPowerOf2(uint64((24 * time.Hour).Nanoseconds())), // 1.63d + buckets[3] * roundUpPowerOf2(uint64((24 * time.Hour).Nanoseconds())), // 6.5d + buckets[3] * roundUpPowerOf2(uint64((24 * time.Hour).Nanoseconds())), // 6.5d } - shift = []uint32{ - uint32(bits.TrailingZeros32(spans[0])), - uint32(bits.TrailingZeros32(spans[1])), - uint32(bits.TrailingZeros32(spans[2])), - uint32(bits.TrailingZeros32(spans[3])), - uint32(bits.TrailingZeros32(spans[4])), + shift = []uint64{ + uint64(bits.TrailingZeros64(spans[0])), + uint64(bits.TrailingZeros64(spans[1])), + uint64(bits.TrailingZeros64(spans[2])), + uint64(bits.TrailingZeros64(spans[3])), + uint64(bits.TrailingZeros64(spans[4])), } ) type Variable[K comparable, V any] struct { wheel [][]node.Node[K, V] - time uint32 + time uint64 deleteNode func(node.Node[K, V]) } @@ -69,7 +67,7 @@ func NewVariable[K comparable, V any](nodeManager *node.Manager[K, V], deleteNod } // findBucket determines the bucket that the timer event should be added to. -func (v *Variable[K, V]) findBucket(expiration uint32) node.Node[K, V] { +func (v *Variable[K, V]) findBucket(expiration uint64) node.Node[K, V] { duration := expiration - v.time length := len(v.wheel) - 1 for i := 0; i < length; i++ { @@ -84,7 +82,7 @@ func (v *Variable[K, V]) findBucket(expiration uint32) node.Node[K, V] { // Add schedules a timer event for the node. func (v *Variable[K, V]) Add(n node.Node[K, V]) { - root := v.findBucket(n.Expiration()) + root := v.findBucket(uint64(n.Expiration())) link(root, n) } @@ -95,8 +93,8 @@ func (v *Variable[K, V]) Delete(n node.Node[K, V]) { n.SetPrevExp(nil) } -func (v *Variable[K, V]) DeleteExpired() { - currentTime := unixtime.Now() +func (v *Variable[K, V]) DeleteExpired(nowNanos int64) { + currentTime := uint64(nowNanos) prevTime := v.time v.time = currentTime @@ -112,7 +110,7 @@ func (v *Variable[K, V]) DeleteExpired() { } } -func (v *Variable[K, V]) deleteExpiredFromBucket(index int, prevTicks, delta uint32) { +func (v *Variable[K, V]) deleteExpiredFromBucket(index int, prevTicks, delta uint64) { mask := buckets[index] - 1 steps := buckets[index] if delta < steps { @@ -132,7 +130,7 @@ func (v *Variable[K, V]) deleteExpiredFromBucket(index int, prevTicks, delta uin n.SetPrevExp(nil) n.SetNextExp(nil) - if n.Expiration() <= v.time { + if uint64(n.Expiration()) <= v.time { v.deleteNode(n) } else { v.Add(n) @@ -158,7 +156,6 @@ func (v *Variable[K, V]) Clear() { } } } - v.time = unixtime.Now() } // link adds the entry at the tail of the bucket's list. @@ -179,3 +176,19 @@ func unlink[K comparable, V any](n node.Node[K, V]) { prev.SetNextExp(next) } } + +// roundUpPowerOf2 is based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2. +func roundUpPowerOf2(v uint64) uint64 { + if v == 0 { + return 1 + } + v-- + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v |= v >> 32 + v++ + return v +} diff --git a/internal/expiry/variable_test.go b/internal/expiry/variable_test.go index 959e06d..ded12d2 100644 --- a/internal/expiry/variable_test.go +++ b/internal/expiry/variable_test.go @@ -16,11 +16,15 @@ package expiry import ( "testing" + "time" "github.com/maypok86/otter/v2/internal/generated/node" - "github.com/maypok86/otter/v2/internal/unixtime" ) +func getTestExp(sec int64) int64 { + return (time.Duration(sec) * time.Second).Nanoseconds() +} + func contains[K comparable, V any](root, f node.Node[K, V]) bool { n := root.NextExp() for !node.Equals(n, root) { @@ -52,9 +56,9 @@ func TestVariable_Add(t *testing.T) { WithExpiration: true, }) nodes := []node.Node[string, string]{ - nm.Create("k1", "", 1, 1), - nm.Create("k2", "", 69, 1), - nm.Create("k3", "", 4399, 1), + nm.Create("k1", "", getTestExp(1), 1), + nm.Create("k2", "", getTestExp(69), 1), + nm.Create("k3", "", getTestExp(4399), 1), } v := NewVariable[string, string](nm, func(n node.Node[string, string]) { }) @@ -67,6 +71,7 @@ func TestVariable_Add(t *testing.T) { for _, root := range v.wheel[0] { if contains(root, nodes[0]) { found = true + break } } if !found { @@ -77,6 +82,7 @@ func TestVariable_Add(t *testing.T) { for _, root := range v.wheel[1] { if contains(root, nodes[1]) { found = true + break } } if !found { @@ -87,6 +93,7 @@ func TestVariable_Add(t *testing.T) { for _, root := range v.wheel[2] { if contains(root, nodes[2]) { found = true + break } } if !found { @@ -99,13 +106,13 @@ func TestVariable_DeleteExpired(t *testing.T) { WithExpiration: true, }) nodes := []node.Node[string, string]{ - nm.Create("k1", "", 1, 1), - nm.Create("k2", "", 10, 1), - nm.Create("k3", "", 30, 1), - nm.Create("k4", "", 120, 1), - nm.Create("k5", "", 6500, 1), - nm.Create("k6", "", 142000, 1), - nm.Create("k7", "", 1420000, 1), + nm.Create("k1", "", getTestExp(1), 1), + nm.Create("k2", "", getTestExp(10), 1), + nm.Create("k3", "", getTestExp(30), 1), + nm.Create("k4", "", getTestExp(120), 1), + nm.Create("k5", "", getTestExp(6500), 1), + nm.Create("k6", "", getTestExp(142000), 1), + nm.Create("k7", "", getTestExp(1420000), 1), } var expired []node.Node[string, string] v := NewVariable[string, string](nm, func(n node.Node[string, string]) { @@ -117,28 +124,23 @@ func TestVariable_DeleteExpired(t *testing.T) { } var keys []string - unixtime.SetNow(64) - v.DeleteExpired() + v.DeleteExpired(getTestExp(64)) keys = append(keys, "k1", "k2", "k3") match(t, expired, keys) - unixtime.SetNow(200) - v.DeleteExpired() + v.DeleteExpired(getTestExp(200)) keys = append(keys, "k4") match(t, expired, keys) - unixtime.SetNow(12000) - v.DeleteExpired() + v.DeleteExpired(getTestExp(12000)) keys = append(keys, "k5") match(t, expired, keys) - unixtime.SetNow(350000) - v.DeleteExpired() + v.DeleteExpired(getTestExp(350000)) keys = append(keys, "k6") match(t, expired, keys) - unixtime.SetNow(1520000) - v.DeleteExpired() + v.DeleteExpired(getTestExp(1520000)) keys = append(keys, "k7") match(t, expired, keys) } diff --git a/internal/generated/node/b.go b/internal/generated/node/b.go index a4b2130..c1d528d 100644 --- a/internal/generated/node/b.go +++ b/internal/generated/node/b.go @@ -22,7 +22,7 @@ type B[K comparable, V any] struct { } // NewB creates a new B. -func NewB[K comparable, V any](key K, value V, expiration, weight uint32) Node[K, V] { +func NewB[K comparable, V any](key K, value V, expiration int64, weight uint32) Node[K, V] { return &B[K, V]{ key: key, value: value, @@ -87,11 +87,11 @@ func (n *B[K, V]) SetNextExp(v Node[K, V]) { panic("not implemented") } -func (n *B[K, V]) HasExpired() bool { +func (n *B[K, V]) HasExpired(now int64) bool { return false } -func (n *B[K, V]) Expiration() uint32 { +func (n *B[K, V]) Expiration() int64 { panic("not implemented") } diff --git a/internal/generated/node/be.go b/internal/generated/node/be.go index dd5dd7f..d9b4c9a 100644 --- a/internal/generated/node/be.go +++ b/internal/generated/node/be.go @@ -6,8 +6,6 @@ package node import ( "sync/atomic" "unsafe" - - "github.com/maypok86/otter/v2/internal/unixtime" ) // BE is a cache entry that provide the following features: @@ -22,14 +20,14 @@ type BE[K comparable, V any] struct { next *BE[K, V] prevExp *BE[K, V] nextExp *BE[K, V] - expiration uint32 + expiration int64 state uint32 frequency uint8 queueType uint8 } // NewBE creates a new BE. -func NewBE[K comparable, V any](key K, value V, expiration, weight uint32) Node[K, V] { +func NewBE[K comparable, V any](key K, value V, expiration int64, weight uint32) Node[K, V] { return &BE[K, V]{ key: key, value: value, @@ -103,11 +101,11 @@ func (n *BE[K, V]) SetNextExp(v Node[K, V]) { n.nextExp = (*BE[K, V])(v.AsPointer()) } -func (n *BE[K, V]) HasExpired() bool { - return n.expiration <= unixtime.Now() +func (n *BE[K, V]) HasExpired(now int64) bool { + return n.expiration <= now } -func (n *BE[K, V]) Expiration() uint32 { +func (n *BE[K, V]) Expiration() int64 { return n.expiration } diff --git a/internal/generated/node/bew.go b/internal/generated/node/bew.go index 7ad3345..2a84ab2 100644 --- a/internal/generated/node/bew.go +++ b/internal/generated/node/bew.go @@ -6,8 +6,6 @@ package node import ( "sync/atomic" "unsafe" - - "github.com/maypok86/otter/v2/internal/unixtime" ) // BEW is a cache entry that provide the following features: @@ -24,7 +22,7 @@ type BEW[K comparable, V any] struct { next *BEW[K, V] prevExp *BEW[K, V] nextExp *BEW[K, V] - expiration uint32 + expiration int64 weight uint32 state uint32 frequency uint8 @@ -32,7 +30,7 @@ type BEW[K comparable, V any] struct { } // NewBEW creates a new BEW. -func NewBEW[K comparable, V any](key K, value V, expiration, weight uint32) Node[K, V] { +func NewBEW[K comparable, V any](key K, value V, expiration int64, weight uint32) Node[K, V] { return &BEW[K, V]{ key: key, value: value, @@ -107,11 +105,11 @@ func (n *BEW[K, V]) SetNextExp(v Node[K, V]) { n.nextExp = (*BEW[K, V])(v.AsPointer()) } -func (n *BEW[K, V]) HasExpired() bool { - return n.expiration <= unixtime.Now() +func (n *BEW[K, V]) HasExpired(now int64) bool { + return n.expiration <= now } -func (n *BEW[K, V]) Expiration() uint32 { +func (n *BEW[K, V]) Expiration() int64 { return n.expiration } diff --git a/internal/generated/node/bw.go b/internal/generated/node/bw.go index 7c40b43..24a196b 100644 --- a/internal/generated/node/bw.go +++ b/internal/generated/node/bw.go @@ -25,7 +25,7 @@ type BW[K comparable, V any] struct { } // NewBW creates a new BW. -func NewBW[K comparable, V any](key K, value V, expiration, weight uint32) Node[K, V] { +func NewBW[K comparable, V any](key K, value V, expiration int64, weight uint32) Node[K, V] { return &BW[K, V]{ key: key, value: value, @@ -91,11 +91,11 @@ func (n *BW[K, V]) SetNextExp(v Node[K, V]) { panic("not implemented") } -func (n *BW[K, V]) HasExpired() bool { +func (n *BW[K, V]) HasExpired(now int64) bool { return false } -func (n *BW[K, V]) Expiration() uint32 { +func (n *BW[K, V]) Expiration() int64 { panic("not implemented") } diff --git a/internal/generated/node/manager.go b/internal/generated/node/manager.go index 7ed6c3d..0e3e403 100644 --- a/internal/generated/node/manager.go +++ b/internal/generated/node/manager.go @@ -46,9 +46,9 @@ type Node[K comparable, V any] interface { // SetNextExp sets the next node in the expiration policy. SetNextExp(v Node[K, V]) // HasExpired returns true if node has expired. - HasExpired() bool + HasExpired(now int64) bool // Expiration returns the expiration time. - Expiration() uint32 + Expiration() int64 // Weight returns the weight of the node. Weight() uint32 // IsAlive returns true if the entry is available in the hash-table. @@ -91,7 +91,7 @@ type Config struct { } type Manager[K comparable, V any] struct { - create func(key K, value V, expiration, weight uint32) Node[K, V] + create func(key K, value V, expiration int64, weight uint32) Node[K, V] fromPointer func(ptr unsafe.Pointer) Node[K, V] } @@ -126,7 +126,7 @@ func NewManager[K comparable, V any](c Config) *Manager[K, V] { return m } -func (m *Manager[K, V]) Create(key K, value V, expiration, weight uint32) Node[K, V] { +func (m *Manager[K, V]) Create(key K, value V, expiration int64, weight uint32) Node[K, V] { return m.create(key, value, expiration, weight) } diff --git a/internal/s3fifo/main.go b/internal/s3fifo/main.go index fb03aee..0be0d74 100644 --- a/internal/s3fifo/main.go +++ b/internal/s3fifo/main.go @@ -41,12 +41,12 @@ func (m *main[K, V]) insert(n node.Node[K, V]) { m.weight += int(n.Weight()) } -func (m *main[K, V]) evict() { +func (m *main[K, V]) evict(nowNanos int64) { reinsertions := 0 for m.weight > 0 { n := m.q.pop() - if !n.IsAlive() || n.HasExpired() || n.Frequency() == 0 { + if !n.IsAlive() || n.HasExpired(nowNanos) || n.Frequency() == 0 { n.Unmark() m.weight -= int(n.Weight()) m.evictNode(n) diff --git a/internal/s3fifo/policy.go b/internal/s3fifo/policy.go index 9708eb0..75694e1 100644 --- a/internal/s3fifo/policy.go +++ b/internal/s3fifo/policy.go @@ -55,7 +55,7 @@ func (p *Policy[K, V]) Read(nodes []node.Node[K, V]) { } // Add adds node to the eviction policy. -func (p *Policy[K, V]) Add(n node.Node[K, V]) { +func (p *Policy[K, V]) Add(n node.Node[K, V], nowNanos int64) { if p.ghost.isGhost(n) { p.main.insert(n) n.ResetFrequency() @@ -64,17 +64,17 @@ func (p *Policy[K, V]) Add(n node.Node[K, V]) { } for p.isFull() { - p.evict() + p.evict(nowNanos) } } -func (p *Policy[K, V]) evict() { +func (p *Policy[K, V]) evict(nowNanos int64) { if p.small.weight >= p.maxWeight/10 { - p.small.evict() + p.small.evict(nowNanos) return } - p.main.evict() + p.main.evict(nowNanos) } func (p *Policy[K, V]) isFull() bool { diff --git a/internal/s3fifo/policy_test.go b/internal/s3fifo/policy_test.go index 673ee22..8912161 100644 --- a/internal/s3fifo/policy_test.go +++ b/internal/s3fifo/policy_test.go @@ -30,7 +30,7 @@ func TestPolicy_ReadAndWrite(t *testing.T) { n := newNode(2) p := NewPolicy[int, int](10, func(n node.Node[int, int]) { }) - p.Add(n) + p.Add(n, 1) if !n.IsSmall() { t.Fatalf("not valid node state: %+v", n) } @@ -51,11 +51,11 @@ func TestPolicy_OneHitWonders(t *testing.T) { } for _, n := range oneHitWonders { - p.Add(n) + p.Add(n, 1) } for _, n := range popular { - p.Add(n) + p.Add(n, 1) } p.Read(oneHitWonders) @@ -69,7 +69,7 @@ func TestPolicy_OneHitWonders(t *testing.T) { } for _, n := range newNodes { - p.Add(n) + p.Add(n, 1) } for _, n := range oneHitWonders { @@ -113,15 +113,15 @@ func TestPolicy_Update(t *testing.T) { m := node.NewManager[int, int](node.Config{WithWeight: true}) n1 := m.Create(1, 1, 0, n.Weight()+8) - p.Add(n) + p.Add(n, 1) p.Delete(n) - p.Add(n1) + p.Add(n1, 1) p.Read([]node.Node[int, int]{n1, n1}) n2 := m.Create(2, 1, 0, 92) collect = true - p.Add(n2) + p.Add(n2, 1) if !n1.IsMain() { t.Fatalf("updated node should be in main queue: %+v", n1) @@ -133,7 +133,7 @@ func TestPolicy_Update(t *testing.T) { n3 := m.Create(1, 1, 0, 109) p.Delete(n1) - p.Add(n3) + p.Add(n3, 1) if n3.IsSmall() || n3.IsMain() || len(deleted) != 1 || deleted[0] != n3 { t.Fatalf("updated node should be evicted: %+v", n3) } diff --git a/internal/s3fifo/small.go b/internal/s3fifo/small.go index 2aaa761..abf827b 100644 --- a/internal/s3fifo/small.go +++ b/internal/s3fifo/small.go @@ -48,7 +48,7 @@ func (s *small[K, V]) insert(n node.Node[K, V]) { s.weight += int(n.Weight()) } -func (s *small[K, V]) evict() { +func (s *small[K, V]) evict(nowNanos int64) { if s.weight == 0 { return } @@ -56,7 +56,7 @@ func (s *small[K, V]) evict() { n := s.q.pop() s.weight -= int(n.Weight()) n.Unmark() - if !n.IsAlive() || n.HasExpired() { + if !n.IsAlive() || n.HasExpired(nowNanos) { s.evictNode(n) return } @@ -64,7 +64,7 @@ func (s *small[K, V]) evict() { if n.Frequency() > 1 { s.main.insert(n) for s.main.isFull() { - s.main.evict() + s.main.evict(nowNanos) } n.ResetFrequency() return diff --git a/internal/unixtime/unixtime.go b/internal/unixtime/unixtime.go deleted file mode 100644 index 38f7b19..0000000 --- a/internal/unixtime/unixtime.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2023 Alexey Mayshev. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package unixtime - -import ( - "sync" - "sync/atomic" - "time" -) - -var ( - // We need this package because time.Now() is slower, allocates memory, - // and we don't need a more precise time for the expiry time (and most other operations). - now uint32 - startTime int64 - - mutex sync.Mutex - countInstance int - done chan struct{} -) - -func startTimer() { - done = make(chan struct{}) - atomic.StoreInt64(&startTime, time.Now().Unix()) - atomic.StoreUint32(&now, uint32(0)) - - go func() { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - for { - select { - case t := <-ticker.C: - //nolint:gosec // there will never be an overflow - atomic.StoreUint32(&now, uint32(t.Unix()-StartTime())) - case <-done: - return - } - } - }() -} - -// Start should be called when the cache instance is created to initialize the timer. -func Start() { - mutex.Lock() - defer mutex.Unlock() - - if countInstance == 0 { - startTimer() - } - - countInstance++ -} - -// Stop should be called when closing and stopping the cache instance to stop the timer. -func Stop() { - mutex.Lock() - defer mutex.Unlock() - - countInstance-- - if countInstance == 0 { - done <- struct{}{} - close(done) - } -} - -// Now returns time as a Unix time, the number of seconds elapsed since program start. -func Now() uint32 { - return atomic.LoadUint32(&now) -} - -// SetNow sets the current time. -// -// NOTE: use only for testing and debugging. -func SetNow(t uint32) { - atomic.StoreUint32(&now, t) -} - -// StartTime returns the start time of the program. -func StartTime() int64 { - return atomic.LoadInt64(&startTime) -}