zrepl/pruning/retentiongrid/retentiongrid_test.go
Christian Schwarz 428a60870a pruning: cleanup retention grid impl + tests + correct docs
package is now at 95% code coverage and the additional tests codify
all behavior specified in the docs

There is a slight change in behavior:
Intervals are now [duration) instead of (duration].
If the leftmost interval is not keep=all, the most recently created
snapshot will be destroyed if there are other snapshots within
that first interval.
Since we recommend keep=all all over the docs, and zrepl 0.3
will put holds on that snapshot if it is being replicated,
I feel like this is an acceptable change in behavior.

refs #292
fixup of 0bbe2befce
2020-09-02 22:45:44 +02:00

186 lines
5.0 KiB
Go

package retentiongrid
import (
"fmt"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type testInterval struct {
length time.Duration
keepCount int
}
func (i *testInterval) Length() time.Duration { return i.length }
func (i *testInterval) KeepCount() int { return i.keepCount }
func gridFromString(gs string) (g *Grid) {
sintervals := strings.Split(gs, "|")
intervals := make([]Interval, len(sintervals))
for idx, i := range sintervals {
comps := strings.SplitN(i, ",", 2)
var durationStr, numSnapsStr string
durationStr = comps[0]
if len(comps) == 1 {
numSnapsStr = "1"
} else {
numSnapsStr = comps[1]
}
var err error
var interval testInterval
if interval.keepCount, err = strconv.Atoi(numSnapsStr); err != nil {
panic(err)
}
if interval.length, err = time.ParseDuration(durationStr); err != nil {
panic(err)
}
intervals[idx] = &interval
}
return NewGrid(intervals)
}
type testSnap struct {
Name string
ShouldKeep bool
date time.Time
}
func (ds testSnap) Date() time.Time { return ds.date }
func validateRetentionGridFitEntries(t *testing.T, now time.Time, input, keep, remove []Entry) {
snapDescr := func(d testSnap) string {
return fmt.Sprintf("%s@%s", d.Name, d.date.Sub(now))
}
t.Logf("keep list:\n")
for k := range keep {
t.Logf("\t%s\n", snapDescr(keep[k].(testSnap)))
}
t.Logf("remove list:\n")
for k := range remove {
t.Logf("\t%s\n", snapDescr(remove[k].(testSnap)))
}
t.Logf("\n\n")
for _, s := range input {
d := s.(testSnap)
descr := snapDescr(d)
t.Logf("testing %s\n", descr)
if d.ShouldKeep {
assert.Contains(t, keep, d, "expecting %s to be kept", descr)
} else {
assert.Contains(t, remove, d, "expecting %s to be removed", descr)
}
}
t.Logf("resulting list:\n")
for k := range keep {
t.Logf("\t%s\n", snapDescr(keep[k].(testSnap)))
}
}
func TestEmptyInput(t *testing.T) {
g := gridFromString("10m|10m|10m|1h")
keep, remove := g.FitEntries([]Entry{})
assert.Empty(t, keep)
assert.Empty(t, remove)
}
func TestIntervalBoundariesAndAlignment(t *testing.T) {
g := gridFromString("10m|10m|10m")
t.Logf("%#v\n", g)
now := time.Unix(0, 0)
snaps := []Entry{
testSnap{"0", true, now.Add(1 * time.Minute)}, // before now => keep unconditionally
testSnap{"1", true, now}, // 1st interval left edge => inclusive
testSnap{"2", true, now.Add(-10 * time.Minute)}, // 2nd interval left edge => inclusive
testSnap{"3", true, now.Add(-20 * time.Minute)}, // 3rd interval left edge => inclusuive
testSnap{"4", false, now.Add(-30 * time.Minute)}, // 3rd interval right edge => excludive
testSnap{"5", false, now.Add(-40 * time.Minute)}, // after last interval => remove unconditionally
}
keep, remove := g.fitEntriesWithNow(now, snaps)
validateRetentionGridFitEntries(t, now, snaps, keep, remove)
}
func TestKeepsOldestSnapsInABucket(t *testing.T) {
g := gridFromString("1m,2")
relt := func(secs int64) time.Time { return time.Unix(secs, 0) }
snaps := []Entry{
testSnap{"1", true, relt(1)},
testSnap{"2", true, relt(2)},
testSnap{"3", false, relt(3)},
testSnap{"4", false, relt(4)},
testSnap{"5", false, relt(5)},
}
now := relt(6)
keep, remove := g.FitEntries(snaps)
validateRetentionGridFitEntries(t, now, snaps, keep, remove)
}
func TestRespectsKeepCountAll(t *testing.T) {
g := gridFromString("1m,-1|1m,1")
relt := func(secs int64) time.Time { return time.Unix(secs, 0) }
snaps := []Entry{
testSnap{"a", true, relt(0)},
testSnap{"b", true, relt(-1)},
testSnap{"c", true, relt(-2)},
testSnap{"d", false, relt(-60)},
testSnap{"e", true, relt(-61)},
}
keep, remove := g.FitEntries(snaps)
validateRetentionGridFitEntries(t, relt(61), snaps, keep, remove)
}
func TestComplex(t *testing.T) {
g := gridFromString("10m,-1|10m|10m,2|1h")
t.Logf("%#v\n", g)
now := time.Unix(0, 0)
snaps := []Entry{
// pre-now must always be kept
testSnap{"1", true, now.Add(3 * time.Minute)},
// 1st interval allows unlimited entries
testSnap{"b1", true, now.Add(-6 * time.Minute)},
testSnap{"b3", true, now.Add(-8 * time.Minute)},
testSnap{"b2", true, now.Add(-9 * time.Minute)},
// 2nd interval allows 1 entry
testSnap{"a", false, now.Add(-11 * time.Minute)},
testSnap{"c", true, now.Add(-19 * time.Minute)},
// 3rd interval allows 2 entries
testSnap{"foo", true, now.Add(-25 * time.Minute)},
testSnap{"bar", true, now.Add(-26 * time.Minute)},
// this is at the left edge of the 4th interval
testSnap{"border", false, now.Add(-30 * time.Minute)},
// right in the 4th interval
testSnap{"d", true, now.Add(-1*time.Hour - 15*time.Minute)},
// on the right edge of 4th interval => not in it => delete
testSnap{"q", false, now.Add(-1*time.Hour - 30*time.Minute)},
// older then 4th interval => always delete
testSnap{"e", false, now.Add(-1*time.Hour - 31*time.Minute)},
testSnap{"f", false, now.Add(-2 * time.Hour)},
}
keep, remove := g.fitEntriesWithNow(now, snaps)
validateRetentionGridFitEntries(t, now, snaps, keep, remove)
}