From 542e8afd7e17cfd2e1c73b9b31c8332ac631ef9d Mon Sep 17 00:00:00 2001 From: maypok86 Date: Fri, 20 Sep 2024 00:39:16 +0300 Subject: [PATCH] chore: refactor linked queues --- cache.go | 2 +- internal/deque/linked.go | 157 ++++++++++++++++++ internal/deque/linked_test.go | 150 +++++++++++++++++ internal/{ => deque}/queue/growable.go | 0 internal/{ => deque}/queue/growable_test.go | 0 .../{ => deque}/queue/queue_bench_test.go | 0 internal/eviction/s3fifo/ghost.go | 2 +- internal/eviction/s3fifo/main.go | 19 ++- internal/eviction/s3fifo/policy.go | 2 + internal/eviction/s3fifo/queue.go | 75 --------- internal/eviction/s3fifo/queue_test.go | 150 ----------------- internal/eviction/s3fifo/small.go | 17 +- internal/expiry/fixed.go | 21 ++- internal/expiry/queue.go | 89 ---------- internal/expiry/queue_test.go | 150 ----------------- 15 files changed, 343 insertions(+), 491 deletions(-) create mode 100644 internal/deque/linked.go create mode 100644 internal/deque/linked_test.go rename internal/{ => deque}/queue/growable.go (100%) rename internal/{ => deque}/queue/growable_test.go (100%) rename internal/{ => deque}/queue/queue_bench_test.go (100%) delete mode 100644 internal/eviction/s3fifo/queue.go delete mode 100644 internal/eviction/s3fifo/queue_test.go delete mode 100644 internal/expiry/queue.go delete mode 100644 internal/expiry/queue_test.go diff --git a/cache.go b/cache.go index e3e35d5..4e13f2a 100644 --- a/cache.go +++ b/cache.go @@ -19,13 +19,13 @@ import ( "time" "github.com/maypok86/otter/v2/internal/clock" + "github.com/maypok86/otter/v2/internal/deque/queue" "github.com/maypok86/otter/v2/internal/eviction" "github.com/maypok86/otter/v2/internal/eviction/s3fifo" "github.com/maypok86/otter/v2/internal/expiry" "github.com/maypok86/otter/v2/internal/generated/node" "github.com/maypok86/otter/v2/internal/hashmap" "github.com/maypok86/otter/v2/internal/lossy" - "github.com/maypok86/otter/v2/internal/queue" "github.com/maypok86/otter/v2/internal/xmath" "github.com/maypok86/otter/v2/internal/xruntime" ) diff --git a/internal/deque/linked.go b/internal/deque/linked.go new file mode 100644 index 0000000..ac6890f --- /dev/null +++ b/internal/deque/linked.go @@ -0,0 +1,157 @@ +// 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 deque + +import ( + "github.com/maypok86/otter/v2/internal/generated/node" +) + +type Linked[K comparable, V any] struct { + head node.Node[K, V] + tail node.Node[K, V] + len int + isExp bool +} + +func NewLinked[K comparable, V any](isExp bool) *Linked[K, V] { + return &Linked[K, V]{ + isExp: isExp, + } +} + +func (d *Linked[K, V]) PushBack(n node.Node[K, V]) { + if d.IsEmpty() { + d.head = n + d.tail = n + } else { + d.setPrev(n, d.tail) + d.setNext(d.tail, n) + d.tail = n + } + + d.len++ +} + +func (d *Linked[K, V]) PushFront(n node.Node[K, V]) { + if d.IsEmpty() { + d.head = n + d.tail = n + } else { + d.setNext(n, d.head) + d.setPrev(d.head, n) + d.head = n + } + + d.len++ +} + +func (d *Linked[K, V]) PopFront() node.Node[K, V] { + if d.IsEmpty() { + return nil + } + + result := d.head + d.Delete(result) + return result +} + +func (d *Linked[K, V]) PopBack() node.Node[K, V] { + if d.IsEmpty() { + return nil + } + + result := d.tail + d.Delete(result) + return result +} + +func (d *Linked[K, V]) Delete(n node.Node[K, V]) { + next := d.getNext(n) + prev := d.getPrev(n) + + if node.Equals(prev, nil) { + if node.Equals(next, nil) && !node.Equals(d.head, n) { + return + } + + d.head = next + } else { + d.setNext(prev, next) + d.setPrev(n, nil) + } + + if node.Equals(next, nil) { + d.tail = prev + } else { + d.setPrev(next, prev) + d.setNext(n, nil) + } + + d.len-- +} + +func (d *Linked[K, V]) Clear() { + for !d.IsEmpty() { + d.PopFront() + } +} + +func (d *Linked[K, V]) Len() int { + return d.len +} + +func (d *Linked[K, V]) IsEmpty() bool { + return d.Len() == 0 +} + +func (d *Linked[K, V]) Head() node.Node[K, V] { + return d.head +} + +func (d *Linked[K, V]) Tail() node.Node[K, V] { + return d.tail +} + +func (d *Linked[K, V]) setPrev(to, n node.Node[K, V]) { + if d.isExp { + to.SetPrevExp(n) + } else { + to.SetPrev(n) + } +} + +func (d *Linked[K, V]) setNext(to, n node.Node[K, V]) { + if d.isExp { + to.SetNextExp(n) + } else { + to.SetNext(n) + } +} + +func (d *Linked[K, V]) getNext(n node.Node[K, V]) node.Node[K, V] { + if d.isExp { + return n.NextExp() + } else { + return n.Next() + } +} + +func (d *Linked[K, V]) getPrev(n node.Node[K, V]) node.Node[K, V] { + if d.isExp { + return n.PrevExp() + } else { + return n.Prev() + } +} diff --git a/internal/deque/linked_test.go b/internal/deque/linked_test.go new file mode 100644 index 0000000..6573592 --- /dev/null +++ b/internal/deque/linked_test.go @@ -0,0 +1,150 @@ +// Copyright (c) 2024 Alexey Mayshev. All rights reserved. +// Copyright 2009 The Go Authors. All rights reserved. +// +// Copyright notice. Initial version of the following tests was based on +// the following file from the Go Programming Language core repo: +// https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/container/list/list_test.go +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// That can be found at https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:LICENSE + +package deque + +import ( + "strconv" + "testing" + + "github.com/maypok86/otter/v2/internal/generated/node" +) + +func checkLinkedLen[K comparable, V any](t *testing.T, d *Linked[K, V], length int) bool { + t.Helper() + + if n := d.Len(); n != length { + t.Errorf("d.Len() = %d, want %d", n, length) + return false + } + return true +} + +func checkLinkedPtrs[K comparable, V any](t *testing.T, d *Linked[K, V], nodes []node.Node[K, V]) { + t.Helper() + + if !checkLinkedLen(t, d, len(nodes)) { + return + } + + // zero length queues must be the zero value + if len(nodes) == 0 { + if !(node.Equals(d.head, nil) && node.Equals(d.tail, nil)) { + t.Errorf("d.head = %p, d.tail = %p; both should be nil", d.head, d.tail) + } + return + } + + // check internal and external prev/next connections + for i, n := range nodes { + var prev node.Node[K, V] + if i > 0 { + prev = nodes[i-1] + } + if p := d.getPrev(n); !node.Equals(p, prev) { + t.Errorf("elt[%d](%p).prev = %p, want %p", i, n, p, prev) + } + + var next node.Node[K, V] + if i < len(nodes)-1 { + next = nodes[i+1] + } + if nn := d.getNext(n); !node.Equals(nn, next) { + t.Errorf("nodes[%d](%p).next = %p, want %p", i, n, nn, next) + } + } +} + +func newNode[K comparable](e K) node.Node[K, K] { + m := node.NewManager[K, K](node.Config{WithWeight: true, WithExpiration: true}) + return m.Create(e, e, 0, 0) +} + +func TestLinked(t *testing.T) { + d := NewLinked[string, string](false) + checkLinkedPtrs(t, d, []node.Node[string, string]{}) + + // Single element Linked + e := newNode("a") + d.PushBack(e) + checkLinkedPtrs(t, d, []node.Node[string, string]{e}) + d.Delete(e) + d.PushBack(e) + checkLinkedPtrs(t, d, []node.Node[string, string]{e}) + d.Delete(e) + checkLinkedPtrs(t, d, []node.Node[string, string]{}) + + // Bigger Linked + e2 := newNode("2") + e1 := newNode("1") + e3 := newNode("3") + e4 := newNode("4") + d.PushBack(e1) + d.PushBack(e2) + d.PushBack(e3) + d.PushBack(e4) + checkLinkedPtrs(t, d, []node.Node[string, string]{e1, e2, e3, e4}) + + d.Delete(e2) + checkLinkedPtrs(t, d, []node.Node[string, string]{e1, e3, e4}) + + // move from middle + d.Delete(e3) + d.PushBack(e3) + checkLinkedPtrs(t, d, []node.Node[string, string]{e1, e4, e3}) + + d.Clear() + d.PushBack(e3) + d.PushBack(e1) + d.PushBack(e4) + checkLinkedPtrs(t, d, []node.Node[string, string]{e3, e1, e4}) + + // should be no-op + d.Delete(e3) + d.PushBack(e3) + checkLinkedPtrs(t, d, []node.Node[string, string]{e1, e4, e3}) + + // Check standard iteration. + sum := 0 + for e := d.head; !node.Equals(e, nil); e = d.getNext(e) { + i, err := strconv.Atoi(e.Value()) + if err != nil { + continue + } + sum += i + } + if sum != 8 { + t.Errorf("sum over l = %d, want 8", sum) + } + + // Clear all elements by iterating + var next node.Node[string, string] + for e := d.head; !node.Equals(e, nil); e = next { + next = d.getNext(e) + d.Delete(e) + } + checkLinkedPtrs(t, d, []node.Node[string, string]{}) +} + +func TestLinked_Delete(t *testing.T) { + d := NewLinked[int, int](true) + + e1 := newNode(1) + e2 := newNode(2) + d.PushBack(e1) + d.PushBack(e2) + checkLinkedPtrs(t, d, []node.Node[int, int]{e1, e2}) + e := d.head + d.Delete(e) + checkLinkedPtrs(t, d, []node.Node[int, int]{e2}) + d.Delete(e) + checkLinkedPtrs(t, d, []node.Node[int, int]{e2}) +} diff --git a/internal/queue/growable.go b/internal/deque/queue/growable.go similarity index 100% rename from internal/queue/growable.go rename to internal/deque/queue/growable.go diff --git a/internal/queue/growable_test.go b/internal/deque/queue/growable_test.go similarity index 100% rename from internal/queue/growable_test.go rename to internal/deque/queue/growable_test.go diff --git a/internal/queue/queue_bench_test.go b/internal/deque/queue/queue_bench_test.go similarity index 100% rename from internal/queue/queue_bench_test.go rename to internal/deque/queue/queue_bench_test.go diff --git a/internal/eviction/s3fifo/ghost.go b/internal/eviction/s3fifo/ghost.go index 088f9b2..7d10d46 100644 --- a/internal/eviction/s3fifo/ghost.go +++ b/internal/eviction/s3fifo/ghost.go @@ -51,7 +51,7 @@ func (g *ghost[K, V]) insert(n node.Node[K, V]) { return } - maxLength := g.small.length() + g.main.length() + maxLength := g.small.len() + g.main.len() if maxLength == 0 { return } diff --git a/internal/eviction/s3fifo/main.go b/internal/eviction/s3fifo/main.go index 1dcc66b..dfc03a6 100644 --- a/internal/eviction/s3fifo/main.go +++ b/internal/eviction/s3fifo/main.go @@ -15,13 +15,14 @@ package s3fifo import ( + "github.com/maypok86/otter/v2/internal/deque" "github.com/maypok86/otter/v2/internal/generated/node" ) const maxReinsertions = 20 type main[K comparable, V any] struct { - q *queue[K, V] + d *deque.Linked[K, V] weight uint64 maxWeight uint64 evictNode func(n node.Node[K, V], nowNanos int64) @@ -29,14 +30,14 @@ type main[K comparable, V any] struct { func newMain[K comparable, V any](maxWeight uint64, evictNode func(n node.Node[K, V], nowNanos int64)) *main[K, V] { return &main[K, V]{ - q: newQueue[K, V](), + d: deque.NewLinked[K, V](isExp), maxWeight: maxWeight, evictNode: evictNode, } } func (m *main[K, V]) insert(n node.Node[K, V]) { - m.q.push(n) + m.d.PushBack(n) n.MarkMain() m.weight += uint64(n.Weight()) } @@ -44,7 +45,7 @@ func (m *main[K, V]) insert(n node.Node[K, V]) { func (m *main[K, V]) evict(nowNanos int64) { reinsertions := 0 for m.weight > 0 { - n := m.q.pop() + n := m.d.PopFront() if !n.IsAlive() || n.HasExpired(nowNanos) || n.Frequency() == 0 { n.Unmark() @@ -62,7 +63,7 @@ func (m *main[K, V]) evict(nowNanos int64) { return } - m.q.push(n) + m.d.PushBack(n) n.DecrementFrequency() } } @@ -70,15 +71,15 @@ func (m *main[K, V]) evict(nowNanos int64) { func (m *main[K, V]) delete(n node.Node[K, V]) { m.weight -= uint64(n.Weight()) n.Unmark() - m.q.delete(n) + m.d.Delete(n) } -func (m *main[K, V]) length() int { - return m.q.length() +func (m *main[K, V]) len() int { + return m.d.Len() } func (m *main[K, V]) clear() { - m.q.clear() + m.d.Clear() m.weight = 0 } diff --git a/internal/eviction/s3fifo/policy.go b/internal/eviction/s3fifo/policy.go index 6c75446..e434d89 100644 --- a/internal/eviction/s3fifo/policy.go +++ b/internal/eviction/s3fifo/policy.go @@ -18,6 +18,8 @@ import ( "github.com/maypok86/otter/v2/internal/generated/node" ) +const isExp = false + // Policy is an eviction policy based on S3-FIFO eviction algorithm // from the following paper: https://dl.acm.org/doi/10.1145/3600006.3613147. type Policy[K comparable, V any] struct { diff --git a/internal/eviction/s3fifo/queue.go b/internal/eviction/s3fifo/queue.go deleted file mode 100644 index ab14359..0000000 --- a/internal/eviction/s3fifo/queue.go +++ /dev/null @@ -1,75 +0,0 @@ -package s3fifo - -import "github.com/maypok86/otter/v2/internal/generated/node" - -type queue[K comparable, V any] struct { - head node.Node[K, V] - tail node.Node[K, V] - len int -} - -func newQueue[K comparable, V any]() *queue[K, V] { - return &queue[K, V]{} -} - -func (q *queue[K, V]) length() int { - return q.len -} - -func (q *queue[K, V]) isEmpty() bool { - return q.length() == 0 -} - -func (q *queue[K, V]) push(n node.Node[K, V]) { - if q.isEmpty() { - q.head = n - q.tail = n - } else { - n.SetPrev(q.tail) - q.tail.SetNext(n) - q.tail = n - } - - q.len++ -} - -func (q *queue[K, V]) pop() node.Node[K, V] { - if q.isEmpty() { - return nil - } - - result := q.head - q.delete(result) - return result -} - -func (q *queue[K, V]) delete(n node.Node[K, V]) { - next := n.Next() - prev := n.Prev() - - if node.Equals(prev, nil) { - if node.Equals(next, nil) && !node.Equals(q.head, n) { - return - } - - q.head = next - } else { - prev.SetNext(next) - n.SetPrev(nil) - } - - if node.Equals(next, nil) { - q.tail = prev - } else { - next.SetPrev(prev) - n.SetNext(nil) - } - - q.len-- -} - -func (q *queue[K, V]) clear() { - for !q.isEmpty() { - q.pop() - } -} diff --git a/internal/eviction/s3fifo/queue_test.go b/internal/eviction/s3fifo/queue_test.go deleted file mode 100644 index a51c95a..0000000 --- a/internal/eviction/s3fifo/queue_test.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) 2023 Alexey Mayshev. All rights reserved. -// Copyright 2009 The Go Authors. All rights reserved. -// -// Copyright notice. Initial version of the following tests was based on -// the following file from the Go Programming Language core repo: -// https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/container/list/list_test.go -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// That can be found at https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:LICENSE - -package s3fifo - -import ( - "strconv" - "testing" - - "github.com/maypok86/otter/v2/internal/generated/node" -) - -func checkQueueLen[K comparable, V any](t *testing.T, q *queue[K, V], length int) bool { - t.Helper() - - if n := q.length(); n != length { - t.Errorf("q.length() = %d, want %d", n, length) - return false - } - return true -} - -func checkQueuePointers[K comparable, V any](t *testing.T, q *queue[K, V], nodes []node.Node[K, V]) { - t.Helper() - - if !checkQueueLen(t, q, len(nodes)) { - return - } - - // zero length queues must be the zero value - if len(nodes) == 0 { - if !(node.Equals(q.head, nil) && node.Equals(q.tail, nil)) { - t.Errorf("q.head = %p, q.tail = %p; both should be nil", q.head, q.tail) - } - return - } - - // check internal and external prev/next connections - for i, n := range nodes { - var prev node.Node[K, V] - if i > 0 { - prev = nodes[i-1] - } - if p := n.Prev(); !node.Equals(p, prev) { - t.Errorf("elt[%d](%p).prev = %p, want %p", i, n, p, prev) - } - - var next node.Node[K, V] - if i < len(nodes)-1 { - next = nodes[i+1] - } - if nn := n.Next(); !node.Equals(nn, next) { - t.Errorf("nodes[%d](%p).next = %p, want %p", i, n, nn, next) - } - } -} - -func newFakeNode[K comparable](e K) node.Node[K, K] { - m := node.NewManager[K, K](node.Config{WithWeight: true}) - return m.Create(e, e, 0, 0) -} - -func TestQueue(t *testing.T) { - q := newQueue[string, string]() - checkQueuePointers(t, q, []node.Node[string, string]{}) - - // Single element queue - e := newFakeNode("a") - q.push(e) - checkQueuePointers(t, q, []node.Node[string, string]{e}) - q.delete(e) - q.push(e) - checkQueuePointers(t, q, []node.Node[string, string]{e}) - q.delete(e) - checkQueuePointers(t, q, []node.Node[string, string]{}) - - // Bigger queue - e2 := newFakeNode("2") - e1 := newFakeNode("1") - e3 := newFakeNode("3") - e4 := newFakeNode("4") - q.push(e1) - q.push(e2) - q.push(e3) - q.push(e4) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e2, e3, e4}) - - q.delete(e2) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e3, e4}) - - // move from middle - q.delete(e3) - q.push(e3) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) - - q.clear() - q.push(e3) - q.push(e1) - q.push(e4) - checkQueuePointers(t, q, []node.Node[string, string]{e3, e1, e4}) - - // should be no-op - q.delete(e3) - q.push(e3) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) - - // Check standard iteration. - sum := 0 - for e := q.head; !node.Equals(e, nil); e = e.Next() { - i, err := strconv.Atoi(e.Value()) - if err != nil { - continue - } - sum += i - } - if sum != 8 { - t.Errorf("sum over l = %d, want 8", sum) - } - - // Clear all elements by iterating - var next node.Node[string, string] - for e := q.head; !node.Equals(e, nil); e = next { - next = e.Next() - q.delete(e) - } - checkQueuePointers(t, q, []node.Node[string, string]{}) -} - -func TestQueue_Remove(t *testing.T) { - q := newQueue[int, int]() - - e1 := newFakeNode(1) - e2 := newFakeNode(2) - q.push(e1) - q.push(e2) - checkQueuePointers(t, q, []node.Node[int, int]{e1, e2}) - e := q.head - q.delete(e) - checkQueuePointers(t, q, []node.Node[int, int]{e2}) - q.delete(e) - checkQueuePointers(t, q, []node.Node[int, int]{e2}) -} diff --git a/internal/eviction/s3fifo/small.go b/internal/eviction/s3fifo/small.go index e17cb18..419bf6b 100644 --- a/internal/eviction/s3fifo/small.go +++ b/internal/eviction/s3fifo/small.go @@ -15,11 +15,12 @@ package s3fifo import ( + "github.com/maypok86/otter/v2/internal/deque" "github.com/maypok86/otter/v2/internal/generated/node" ) type small[K comparable, V any] struct { - q *queue[K, V] + d *deque.Linked[K, V] main *main[K, V] ghost *ghost[K, V] weight uint64 @@ -34,7 +35,7 @@ func newSmall[K comparable, V any]( evictNode func(n node.Node[K, V], nowNanos int64), ) *small[K, V] { return &small[K, V]{ - q: newQueue[K, V](), + d: deque.NewLinked[K, V](isExp), main: main, ghost: ghost, maxWeight: maxWeight, @@ -43,7 +44,7 @@ func newSmall[K comparable, V any]( } func (s *small[K, V]) insert(n node.Node[K, V]) { - s.q.push(n) + s.d.PushBack(n) n.MarkSmall() s.weight += uint64(n.Weight()) } @@ -53,7 +54,7 @@ func (s *small[K, V]) evict(nowNanos int64) { return } - n := s.q.pop() + n := s.d.PopFront() s.weight -= uint64(n.Weight()) n.Unmark() if !n.IsAlive() || n.HasExpired(nowNanos) { @@ -77,14 +78,14 @@ func (s *small[K, V]) evict(nowNanos int64) { func (s *small[K, V]) delete(n node.Node[K, V]) { s.weight -= uint64(n.Weight()) n.Unmark() - s.q.delete(n) + s.d.Delete(n) } -func (s *small[K, V]) length() int { - return s.q.length() +func (s *small[K, V]) len() int { + return s.d.Len() } func (s *small[K, V]) clear() { - s.q.clear() + s.d.Clear() s.weight = 0 } diff --git a/internal/expiry/fixed.go b/internal/expiry/fixed.go index 315856d..349a1f9 100644 --- a/internal/expiry/fixed.go +++ b/internal/expiry/fixed.go @@ -14,34 +14,39 @@ package expiry -import "github.com/maypok86/otter/v2/internal/generated/node" +import ( + "github.com/maypok86/otter/v2/internal/deque" + "github.com/maypok86/otter/v2/internal/generated/node" +) + +const isExp = true type Fixed[K comparable, V any] struct { - q *queue[K, V] + d *deque.Linked[K, V] expireNode func(n node.Node[K, V], nowNanos int64) } func NewFixed[K comparable, V any](expireNode func(n node.Node[K, V], nowNanos int64)) *Fixed[K, V] { return &Fixed[K, V]{ - q: newQueue[K, V](), + d: deque.NewLinked[K, V](isExp), expireNode: expireNode, } } func (f *Fixed[K, V]) Add(n node.Node[K, V]) { - f.q.push(n) + f.d.PushBack(n) } func (f *Fixed[K, V]) Delete(n node.Node[K, V]) { - f.q.delete(n) + f.d.Delete(n) } func (f *Fixed[K, V]) DeleteExpired(nowNanos int64) { - for !f.q.isEmpty() && f.q.head.HasExpired(nowNanos) { - f.expireNode(f.q.pop(), nowNanos) + for !f.d.IsEmpty() && f.d.Head().HasExpired(nowNanos) { + f.expireNode(f.d.PopFront(), nowNanos) } } func (f *Fixed[K, V]) Clear() { - f.q.clear() + f.d.Clear() } diff --git a/internal/expiry/queue.go b/internal/expiry/queue.go deleted file mode 100644 index e4154f7..0000000 --- a/internal/expiry/queue.go +++ /dev/null @@ -1,89 +0,0 @@ -// 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 expiry - -import "github.com/maypok86/otter/v2/internal/generated/node" - -type queue[K comparable, V any] struct { - head node.Node[K, V] - tail node.Node[K, V] - len int -} - -func newQueue[K comparable, V any]() *queue[K, V] { - return &queue[K, V]{} -} - -func (q *queue[K, V]) length() int { - return q.len -} - -func (q *queue[K, V]) isEmpty() bool { - return q.length() == 0 -} - -func (q *queue[K, V]) push(n node.Node[K, V]) { - if q.isEmpty() { - q.head = n - q.tail = n - } else { - n.SetPrevExp(q.tail) - q.tail.SetNextExp(n) - q.tail = n - } - - q.len++ -} - -func (q *queue[K, V]) pop() node.Node[K, V] { - if q.isEmpty() { - return nil - } - - result := q.head - q.delete(result) - return result -} - -func (q *queue[K, V]) delete(n node.Node[K, V]) { - next := n.NextExp() - prev := n.PrevExp() - - if node.Equals(prev, nil) { - if node.Equals(next, nil) && !node.Equals(q.head, n) { - return - } - - q.head = next - } else { - prev.SetNextExp(next) - n.SetPrevExp(nil) - } - - if node.Equals(next, nil) { - q.tail = prev - } else { - next.SetPrevExp(prev) - n.SetNextExp(nil) - } - - q.len-- -} - -func (q *queue[K, V]) clear() { - for !q.isEmpty() { - q.pop() - } -} diff --git a/internal/expiry/queue_test.go b/internal/expiry/queue_test.go deleted file mode 100644 index 7863cb1..0000000 --- a/internal/expiry/queue_test.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) 2024 Alexey Mayshev. All rights reserved. -// Copyright 2009 The Go Authors. All rights reserved. -// -// Copyright notice. Initial version of the following tests was based on -// the following file from the Go Programming Language core repo: -// https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/container/list/list_test.go -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// That can be found at https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:LICENSE - -package expiry - -import ( - "strconv" - "testing" - - "github.com/maypok86/otter/v2/internal/generated/node" -) - -func checkQueueLen[K comparable, V any](t *testing.T, q *queue[K, V], length int) bool { - t.Helper() - - if n := q.length(); n != length { - t.Errorf("q.length() = %d, want %d", n, length) - return false - } - return true -} - -func checkQueuePointers[K comparable, V any](t *testing.T, q *queue[K, V], nodes []node.Node[K, V]) { - t.Helper() - - if !checkQueueLen(t, q, len(nodes)) { - return - } - - // zero length queues must be the zero value - if len(nodes) == 0 { - if !(node.Equals(q.head, nil) && node.Equals(q.tail, nil)) { - t.Errorf("q.head = %p, q.tail = %p; both should be nil", q.head, q.tail) - } - return - } - - // check internal and external prev/next connections - for i, n := range nodes { - var prev node.Node[K, V] - if i > 0 { - prev = nodes[i-1] - } - if p := n.PrevExp(); !node.Equals(p, prev) { - t.Errorf("elt[%d](%p).prev = %p, want %p", i, n, p, prev) - } - - var next node.Node[K, V] - if i < len(nodes)-1 { - next = nodes[i+1] - } - if nn := n.NextExp(); !node.Equals(nn, next) { - t.Errorf("nodes[%d](%p).next = %p, want %p", i, n, nn, next) - } - } -} - -func newNode[K comparable](e K) node.Node[K, K] { - m := node.NewManager[K, K](node.Config{WithWeight: true, WithExpiration: true}) - return m.Create(e, e, 0, 0) -} - -func TestQueue(t *testing.T) { - q := newQueue[string, string]() - checkQueuePointers(t, q, []node.Node[string, string]{}) - - // Single element queue - e := newNode("a") - q.push(e) - checkQueuePointers(t, q, []node.Node[string, string]{e}) - q.delete(e) - q.push(e) - checkQueuePointers(t, q, []node.Node[string, string]{e}) - q.delete(e) - checkQueuePointers(t, q, []node.Node[string, string]{}) - - // Bigger queue - e2 := newNode("2") - e1 := newNode("1") - e3 := newNode("3") - e4 := newNode("4") - q.push(e1) - q.push(e2) - q.push(e3) - q.push(e4) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e2, e3, e4}) - - q.delete(e2) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e3, e4}) - - // move from middle - q.delete(e3) - q.push(e3) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) - - q.clear() - q.push(e3) - q.push(e1) - q.push(e4) - checkQueuePointers(t, q, []node.Node[string, string]{e3, e1, e4}) - - // should be no-op - q.delete(e3) - q.push(e3) - checkQueuePointers(t, q, []node.Node[string, string]{e1, e4, e3}) - - // Check standard iteration. - sum := 0 - for e := q.head; !node.Equals(e, nil); e = e.NextExp() { - i, err := strconv.Atoi(e.Value()) - if err != nil { - continue - } - sum += i - } - if sum != 8 { - t.Errorf("sum over l = %d, want 8", sum) - } - - // Clear all elements by iterating - var next node.Node[string, string] - for e := q.head; !node.Equals(e, nil); e = next { - next = e.NextExp() - q.delete(e) - } - checkQueuePointers(t, q, []node.Node[string, string]{}) -} - -func TestQueue_Remove(t *testing.T) { - q := newQueue[int, int]() - - e1 := newNode(1) - e2 := newNode(2) - q.push(e1) - q.push(e2) - checkQueuePointers(t, q, []node.Node[int, int]{e1, e2}) - e := q.head - q.delete(e) - checkQueuePointers(t, q, []node.Node[int, int]{e2}) - q.delete(e) - checkQueuePointers(t, q, []node.Node[int, int]{e2}) -}