Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions cachert/rt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package cachert

import (
"time"

"github.com/emirpasic/gods/v2/trees/avltree"
)

type Key = string

type KeySearchRange struct {
KeyLower Key
KeyUpper Key
Time time.Time
}

type RT struct {
at *avltree.Tree[Key, *KeySearchRange]
ranges map[*KeySearchRange]struct{}
}

func NewRT() *RT {
return &RT{
at: avltree.New[Key, *KeySearchRange](),
ranges: make(map[*KeySearchRange]struct{}),
}
}

func (t *RT) GetRanges() []KeySearchRange {
ranges := make([]KeySearchRange, 0, len(t.ranges))
for k := range t.ranges {
if k.KeyLower > k.KeyUpper {
panic("lower key must be less than upper key")
}
ranges = append(ranges, KeySearchRange{
KeyLower: k.KeyLower,
KeyUpper: k.KeyUpper,
Time: k.Time,
})
}
return ranges
}

func (t *RT) InsertRange(lower, upper Key, expiration time.Time) {
if lower > upper {
panic("lower key must be less than upper key")
}
if len([]byte(lower)) != len([]byte(upper)) && len([]byte(lower)) != 32 {
panic("lower and upper keys must be 32 bytes")
}

r := &KeySearchRange{lower, upper, expiration}

// Find the range that starts the latest, but before the start of this range
f, ok := t.at.Floor(r.KeyLower)
// If there are no nodes that start before this one
type rr struct {
k Key
r *KeySearchRange
}

var rangesToReplace []rr

if !ok {
f = t.at.Left()
} else {
if f.Value.KeyUpper > r.KeyLower {
// Truncate the previous range to stop where this one starts
f.Value.KeyUpper = r.KeyLower
rangesToReplace = append(rangesToReplace, rr{k: f.Key, r: f.Value})
}
}
// Insert this one
t.at.Put(r.KeyLower, r)
t.ranges[r] = struct{}{}

// Continue through subsequent ranges seeing if any get clobbered by this one
for f := f.Next(); f != nil; f = f.Next() {
if f.Value == r {
// This is the same range, so we can skip it
continue
}
if f.Value.KeyLower < r.KeyUpper {
if f.Value.KeyUpper <= r.KeyUpper {
rangesToReplace = append(rangesToReplace, rr{k: f.Key, r: nil})
delete(t.ranges, f.Value)
} else {
f.Value.KeyUpper = r.KeyUpper
rangesToReplace = append(rangesToReplace, rr{k: f.Key, r: f.Value})
}
} else {
break
}
}

for _, rng := range rangesToReplace {
t.at.Remove(rng.k)
if rng.r == nil {
continue
}
if rng.r.KeyLower == rng.r.KeyUpper {
// If the range is empty, we don't want to keep it
delete(t.ranges, rng.r)
} else {
t.at.Put(rng.r.KeyLower, rng.r)
}
}
}

func (t *RT) RangeIsCovered(lower, upper Key) bool {
if lower > upper {
panic("lower key must be less than upper key")
}
if len([]byte(lower)) != len([]byte(upper)) && len([]byte(lower)) != 32 {
panic("lower and upper keys must be 32 bytes")
}

lowest := lower
f, ok := t.at.Floor(lowest)
if !ok {
return false
}

for ; f != nil; f = f.Next() {
if f.Value.KeyLower > lowest {
return false
}

if f.Value.KeyUpper >= upper {
return true
}

lowest = f.Value.KeyUpper
}
return false
}

func (t *RT) CollectGarbage(ti time.Time) {
for r, _ := range t.ranges {
if r.Time.Before(ti) {
t.at.Remove(r.KeyLower)
delete(t.ranges, r)
}
}
}
91 changes: 91 additions & 0 deletions cachert/rt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cachert

import (
"bytes"
"testing"
"time"
)

func TestRT_InsertRangeAndGetRanges(t *testing.T) {
rt := NewRT()

// Valid 32-byte keys
key1 := bytes.Repeat([]byte("a"), 32)
key2 := bytes.Repeat([]byte("b"), 32)
key3 := bytes.Repeat([]byte("c"), 32)
key4 := bytes.Repeat([]byte("d"), 32)

// Insert non-overlapping range
rt.InsertRange(string(key1), string(key2), time.Now().Add(1*time.Hour))
ranges := rt.GetRanges()
if len(ranges) != 1 {
t.Fatalf("expected 1 range, got %d", len(ranges))
}

// Insert overlapping range
rt.InsertRange(string(key2), string(key3), time.Now().Add(2*time.Hour))
ranges = rt.GetRanges()
if len(ranges) != 2 {
t.Fatalf("expected 2 ranges, got %d", len(ranges))
}

// Insert range that merges with existing ranges
rt.InsertRange(string(key1), string(key4), time.Now().Add(3*time.Hour))
ranges = rt.GetRanges()
if len(ranges) != 1 {
t.Fatalf("expected 1 merged range, got %d", len(ranges))
}
if ranges[0].KeyLower != string(key1) || ranges[0].KeyUpper != string(key4) {
t.Fatalf("merged range has incorrect bounds: %+v", ranges[0])
}
}

func TestRT_RangeIsCovered(t *testing.T) {
rt := NewRT()

// Valid 32-byte keys
key1 := bytes.Repeat([]byte("a"), 32)
key2 := bytes.Repeat([]byte("b"), 32)
key3 := bytes.Repeat([]byte("c"), 32)
key4 := bytes.Repeat([]byte("d"), 32)

// Insert ranges
rt.InsertRange(string(key1), string(key2), time.Now().Add(1*time.Hour))
rt.InsertRange(string(key2), string(key3), time.Now().Add(2*time.Hour))

// Check covered range
if !rt.RangeIsCovered(string(key1), string(key3)) {
t.Fatalf("expected range [%s, %s] to be covered", key1, key3)
}

// Check partially covered range
if rt.RangeIsCovered(string(key1), string(key4)) {
t.Fatalf("expected range [%s, %s] to not be fully covered", key1, key4)
}

// Check uncovered range
if rt.RangeIsCovered(string(key3), string(key4)) {
t.Fatalf("expected range [%s, %s] to not be covered", key3, key4)
}
}

func TestRT_CollectGarbage(t *testing.T) {
rt := NewRT()

// Valid 32-byte keys
key1 := bytes.Repeat([]byte("a"), 32)
key2 := bytes.Repeat([]byte("b"), 32)

// Insert range with expiration
expiredTime := time.Now().Add(-1 * time.Hour)
rt.InsertRange(string(key1), string(key2), expiredTime)

// Collect garbage
rt.CollectGarbage(time.Now())

// Validate range is removed
ranges := rt.GetRanges()
if len(ranges) != 0 {
t.Fatalf("expected 0 ranges after garbage collection, got %d", len(ranges))
}
}
Loading