
This commit fixes an issue where Tile38 was using lots of extra memory to track objects that are marked to expire. This was creating problems with applications that set big TTLs. How it worked before: Every collection had a unique hashmap that stores expiration timestamps for every object in that collection. Along with the hashmaps, there's also one big server-wide list that gets appended every time a new SET+EX is performed. From a background routine, this list is looped over at least 10 times per second and is randomly searched for potential candidates that might need expiring. The routine then removes those entries from the list and tests if the objects matching the entries have actually expired. If so, these objects are deleted them from the database. When at least 25% of the 20 candidates are deleted the loop is immediately continued, otherwise the loop backs off with a 100ms pause. Why this was a problem. The list grows one entry for every SET+EX. When TTLs are long, like 24-hours or more, it would take at least that much time before the entry is removed. So for databased that have objects that use TTLs and are updated often this could lead to a very large list. How it was fixed. The list was removed and the hashmap is now search randomly. This required a new hashmap implementation, as the built-in Go map does not provide an operation for randomly geting entries. The chosen implementation is a robinhood-hash because it provides open-addressing, which makes for simple random bucket selections. Issue #502
325 lines
6.9 KiB
Go
325 lines
6.9 KiB
Go
// Copyright 2019 Joshua J Baker. All rights reserved.
|
|
// Use of this source code is governed by an ISC-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package rhh
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tidwall/lotsa"
|
|
)
|
|
|
|
type keyT = string
|
|
type valueT = interface{}
|
|
|
|
func k(key int) keyT {
|
|
return strconv.FormatInt(int64(key), 10)
|
|
}
|
|
|
|
func add(x keyT, delta int) int {
|
|
i, err := strconv.ParseInt(x, 10, 64)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return int(i + int64(delta))
|
|
}
|
|
|
|
///////////////////////////
|
|
func random(N int, perm bool) []keyT {
|
|
nums := make([]keyT, N)
|
|
if perm {
|
|
for i, x := range rand.Perm(N) {
|
|
nums[i] = k(x)
|
|
}
|
|
} else {
|
|
m := make(map[keyT]bool)
|
|
for len(m) < N {
|
|
m[k(int(rand.Uint64()))] = true
|
|
}
|
|
var i int
|
|
for k := range m {
|
|
nums[i] = k
|
|
i++
|
|
}
|
|
}
|
|
return nums
|
|
}
|
|
|
|
func shuffle(nums []keyT) {
|
|
for i := range nums {
|
|
j := rand.Intn(i + 1)
|
|
nums[i], nums[j] = nums[j], nums[i]
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
//var seed int64 = 1519776033517775607
|
|
seed := (time.Now().UnixNano())
|
|
println("seed:", seed)
|
|
rand.Seed(seed)
|
|
}
|
|
|
|
func TestRandomData(t *testing.T) {
|
|
N := 10000
|
|
start := time.Now()
|
|
for time.Since(start) < time.Second*2 {
|
|
nums := random(N, true)
|
|
var m *Map
|
|
switch rand.Int() % 5 {
|
|
default:
|
|
m = New(N / ((rand.Int() % 3) + 1))
|
|
case 1:
|
|
m = new(Map)
|
|
case 2:
|
|
m = New(0)
|
|
}
|
|
v, ok := m.Get(k(999))
|
|
if ok || v != nil {
|
|
t.Fatalf("expected %v, got %v", nil, v)
|
|
}
|
|
v, ok = m.Delete(k(999))
|
|
if ok || v != nil {
|
|
t.Fatalf("expected %v, got %v", nil, v)
|
|
}
|
|
if m.Len() != 0 {
|
|
t.Fatalf("expected %v, got %v", 0, m.Len())
|
|
}
|
|
// set a bunch of items
|
|
for i := 0; i < len(nums); i++ {
|
|
v, ok := m.Set(nums[i], nums[i])
|
|
if ok || v != nil {
|
|
t.Fatalf("expected %v, got %v", nil, v)
|
|
}
|
|
}
|
|
if m.Len() != N {
|
|
t.Fatalf("expected %v, got %v", N, m.Len())
|
|
}
|
|
// retrieve all the items
|
|
shuffle(nums)
|
|
for i := 0; i < len(nums); i++ {
|
|
v, ok := m.Get(nums[i])
|
|
if !ok || v == nil || v != nums[i] {
|
|
t.Fatalf("expected %v, got %v", nums[i], v)
|
|
}
|
|
}
|
|
// replace all the items
|
|
shuffle(nums)
|
|
for i := 0; i < len(nums); i++ {
|
|
v, ok := m.Set(nums[i], add(nums[i], 1))
|
|
if !ok || v != nums[i] {
|
|
t.Fatalf("expected %v, got %v", nums[i], v)
|
|
}
|
|
}
|
|
if m.Len() != N {
|
|
t.Fatalf("expected %v, got %v", N, m.Len())
|
|
}
|
|
// retrieve all the items
|
|
shuffle(nums)
|
|
for i := 0; i < len(nums); i++ {
|
|
v, ok := m.Get(nums[i])
|
|
if !ok || v != add(nums[i], 1) {
|
|
t.Fatalf("expected %v, got %v", add(nums[i], 1), v)
|
|
}
|
|
}
|
|
// remove half the items
|
|
shuffle(nums)
|
|
for i := 0; i < len(nums)/2; i++ {
|
|
v, ok := m.Delete(nums[i])
|
|
if !ok || v != add(nums[i], 1) {
|
|
t.Fatalf("expected %v, got %v", add(nums[i], 1), v)
|
|
}
|
|
}
|
|
if m.Len() != N/2 {
|
|
t.Fatalf("expected %v, got %v", N/2, m.Len())
|
|
}
|
|
// check to make sure that the items have been removed
|
|
for i := 0; i < len(nums)/2; i++ {
|
|
v, ok := m.Get(nums[i])
|
|
if ok || v != nil {
|
|
t.Fatalf("expected %v, got %v", nil, v)
|
|
}
|
|
}
|
|
// check the second half of the items
|
|
for i := len(nums) / 2; i < len(nums); i++ {
|
|
v, ok := m.Get(nums[i])
|
|
if !ok || v != add(nums[i], 1) {
|
|
t.Fatalf("expected %v, got %v", add(nums[i], 1), v)
|
|
}
|
|
}
|
|
// try to delete again, make sure they don't exist
|
|
for i := 0; i < len(nums)/2; i++ {
|
|
v, ok := m.Delete(nums[i])
|
|
if ok || v != nil {
|
|
t.Fatalf("expected %v, got %v", nil, v)
|
|
}
|
|
}
|
|
if m.Len() != N/2 {
|
|
t.Fatalf("expected %v, got %v", N/2, m.Len())
|
|
}
|
|
m.Range(func(key keyT, value valueT) bool {
|
|
if value != add(key, 1) {
|
|
t.Fatalf("expected %v, got %v", add(key, 1), value)
|
|
}
|
|
return true
|
|
})
|
|
var n int
|
|
m.Range(func(key keyT, value valueT) bool {
|
|
n++
|
|
return false
|
|
})
|
|
if n != 1 {
|
|
t.Fatalf("expected %v, got %v", 1, n)
|
|
}
|
|
for i := len(nums) / 2; i < len(nums); i++ {
|
|
v, ok := m.Delete(nums[i])
|
|
if !ok || v != add(nums[i], 1) {
|
|
t.Fatalf("expected %v, got %v", add(nums[i], 1), v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBench(t *testing.T) {
|
|
N, _ := strconv.ParseUint(os.Getenv("MAPBENCH"), 10, 64)
|
|
if N == 0 {
|
|
fmt.Printf("Enable benchmarks with MAPBENCH=1000000\n")
|
|
return
|
|
}
|
|
nums := random(int(N), false)
|
|
var pnums []valueT
|
|
for i := range nums {
|
|
pnums = append(pnums, valueT(&nums[i]))
|
|
}
|
|
fmt.Printf("\n## STRING KEYS\n\n")
|
|
t.Run("RobinHood", func(t *testing.T) {
|
|
testPerf(nums, pnums, "robinhood")
|
|
})
|
|
t.Run("Stdlib", func(t *testing.T) {
|
|
testPerf(nums, pnums, "stdlib")
|
|
})
|
|
}
|
|
|
|
func printItem(s string, size int, dir int) {
|
|
for len(s) < size {
|
|
if dir == -1 {
|
|
s += " "
|
|
} else {
|
|
s = " " + s
|
|
}
|
|
}
|
|
fmt.Printf("%s ", s)
|
|
}
|
|
|
|
func testPerf(nums []keyT, pnums []valueT, which keyT) {
|
|
var ms1, ms2 runtime.MemStats
|
|
initSize := 0 //len(nums) * 2
|
|
defer func() {
|
|
heapBytes := int(ms2.HeapAlloc - ms1.HeapAlloc)
|
|
fmt.Printf("memory %13s bytes %19s/entry \n",
|
|
commaize(heapBytes), commaize(heapBytes/len(nums)))
|
|
fmt.Printf("\n")
|
|
}()
|
|
runtime.GC()
|
|
time.Sleep(time.Millisecond * 100)
|
|
runtime.ReadMemStats(&ms1)
|
|
|
|
var setop, getop, delop func(int, int)
|
|
var scnop func()
|
|
switch which {
|
|
case "stdlib":
|
|
m := make(map[keyT]valueT, initSize)
|
|
setop = func(i, _ int) { m[nums[i]] = pnums[i] }
|
|
getop = func(i, _ int) { _ = m[nums[i]] }
|
|
delop = func(i, _ int) { delete(m, nums[i]) }
|
|
scnop = func() {
|
|
for range m {
|
|
}
|
|
}
|
|
case "robinhood":
|
|
m := New(initSize)
|
|
setop = func(i, _ int) { m.Set(nums[i], pnums[i]) }
|
|
getop = func(i, _ int) { m.Get(nums[i]) }
|
|
delop = func(i, _ int) { m.Delete(nums[i]) }
|
|
scnop = func() {
|
|
m.Range(func(key keyT, value valueT) bool {
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
fmt.Printf("-- %s --", which)
|
|
fmt.Printf("\n")
|
|
|
|
ops := []func(int, int){setop, getop, setop, nil, delop}
|
|
tags := []keyT{"set", "get", "reset", "scan", "delete"}
|
|
for i := range ops {
|
|
shuffle(nums)
|
|
var na bool
|
|
var n int
|
|
start := time.Now()
|
|
if tags[i] == "scan" {
|
|
op := scnop
|
|
if op == nil {
|
|
na = true
|
|
} else {
|
|
n = 20
|
|
lotsa.Ops(n, 1, func(_, _ int) { op() })
|
|
}
|
|
|
|
} else {
|
|
n = len(nums)
|
|
lotsa.Ops(n, 1, ops[i])
|
|
}
|
|
dur := time.Since(start)
|
|
if i == 0 {
|
|
runtime.GC()
|
|
time.Sleep(time.Millisecond * 100)
|
|
runtime.ReadMemStats(&ms2)
|
|
}
|
|
printItem(tags[i], 9, -1)
|
|
if na {
|
|
printItem("-- unavailable --", 14, 1)
|
|
} else {
|
|
if n == -1 {
|
|
printItem("unknown ops", 14, 1)
|
|
} else {
|
|
printItem(fmt.Sprintf("%s ops", commaize(n)), 14, 1)
|
|
}
|
|
printItem(fmt.Sprintf("%.0fms", dur.Seconds()*1000), 8, 1)
|
|
if n != -1 {
|
|
printItem(fmt.Sprintf("%s/sec", commaize(int(float64(n)/dur.Seconds()))), 18, 1)
|
|
}
|
|
}
|
|
fmt.Printf("\n")
|
|
}
|
|
}
|
|
|
|
func commaize(n int) string {
|
|
s1, s2 := fmt.Sprintf("%d", n), ""
|
|
for i, j := len(s1)-1, 0; i >= 0; i, j = i-1, j+1 {
|
|
if j%3 == 0 && j != 0 {
|
|
s2 = "," + s2
|
|
}
|
|
s2 = string(s1[i]) + s2
|
|
}
|
|
return s2
|
|
}
|
|
|
|
func TestHashDIB(t *testing.T) {
|
|
var e entry
|
|
e.setDIB(100)
|
|
e.setHash(90000)
|
|
if e.dib() != 100 {
|
|
t.Fatalf("expected %v, got %v", 100, e.dib())
|
|
}
|
|
if e.hash() != 90000 {
|
|
t.Fatalf("expected %v, got %v", 90000, e.hash())
|
|
}
|
|
}
|