Files
nostrlib/eventstore/mmm/freeranges_test.go
fiatjaf d445ba9919 mmm: free ranges tracking improved with b.freeRangesLarge and b.freeRangesAll
one is unsorted and fast and we only care about it with picking a new free range.
the other is sorted and used when merging a new freed range with existing free ranges.
both are computed from the events id index at beginning, then tracked manually on each addition or deletion.
this change uncovered some errors so we fixed them and added some more fuzz test invariant checking.
code is simplified a little bit.
there was another thing I forgot.
2026-02-17 18:33:59 -03:00

179 lines
4.1 KiB
Go

package mmm
import (
"math/rand/v2"
"os"
"strings"
"testing"
"fiatjaf.com/nostr"
"github.com/PowerDNS/lmdb-go/lmdb"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)
func FuzzFreeRanges(f *testing.F) {
f.Add(0)
f.Fuzz(func(t *testing.T, seed int) {
// create a temporary directory for the test
tmpDir, err := os.MkdirTemp("", "mmm-freeranges-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
logger := zerolog.Nop()
rnd := rand.New(rand.NewPCG(uint64(seed), 0))
chance := func(n uint) bool {
return rnd.UintN(100) < n
}
// initialize MMM
mmmm := &MultiMmapManager{
Dir: tmpDir,
Logger: &logger,
}
err = mmmm.Init()
require.NoError(t, err)
defer mmmm.Close()
// create a single layer
il, err := mmmm.EnsureLayer("a")
require.NoError(t, err)
defer il.Close()
sk := nostr.MustSecretKeyFromHex("945e01e37662430162121b804d3645a86d97df9d256917d86735d0eb219393eb")
total := 0
for {
freeBefore, spaceBefore := countUsableFreeRanges(t, mmmm)
hasAdded := false
for i := range rnd.IntN(40) {
hasAdded = true
content := "1" // ensure at least one event is as small as it can be
if i > 0 {
content = strings.Repeat("z", rnd.IntN(1000))
}
evt := nostr.Event{
CreatedAt: nostr.Timestamp(rnd.Uint32()),
Kind: 1,
Content: content,
Tags: nostr.Tags{},
}
evt.Sign(sk)
err := il.SaveEvent(evt)
require.NoError(t, err)
total++
}
freeAfter, spaceAfter := countUsableFreeRanges(t, mmmm)
if hasAdded && freeBefore > 0 {
require.Lessf(t, spaceAfter, spaceBefore, "must use some of the existing free ranges when inserting new events (before: %d, after: %d)", freeBefore, freeAfter)
}
// delete some events
if total > 0 {
for range rnd.IntN(total) {
for evt := range il.QueryEvents(nostr.Filter{}, 1) {
err := il.DeleteEvent(evt.ID)
require.NoError(t, err)
total--
}
}
}
verifyFreeRangesInvariants(t, mmmm)
// add more events
for i := range rnd.IntN(40) {
content := "1"
if i > 0 {
content = strings.Repeat("z", rnd.IntN(1000))
}
evt := nostr.Event{
CreatedAt: nostr.Timestamp(rnd.Uint32()),
Kind: 1,
Content: content,
Tags: nostr.Tags{},
}
evt.Sign(sk)
err := il.SaveEvent(evt)
require.NoError(t, err)
total++
}
verifyFreeRangesInvariants(t, mmmm)
mmmm.lmdbEnv.View(func(txn *lmdb.Txn) error {
before := mmmm.freeRangesAll
err := mmmm.gatherFreeRanges(txn)
require.NoError(t, err)
require.Equalf(t, mmmm.freeRangesAll, before, "expected %s, got %s", before, mmmm.freeRangesAll)
return nil
})
if chance(20) {
break
}
}
})
}
func countUsableFreeRanges(t *testing.T, mmmm *MultiMmapManager) (count int, space int) {
for _, fr := range mmmm.freeRangesAll {
if fr.size >= LARGE_FREERANGE {
count++
space += int(fr.size)
}
}
require.Equal(t, count, len(mmmm.freeRangesLarge))
return count, space
}
func verifyFreeRangesInvariants(t *testing.T, mmmm *MultiMmapManager) {
all := mmmm.freeRangesAll
large := mmmm.freeRangesLarge
for _, l := range large {
found := false
for _, a := range all {
if l.start == a.start && l.size == a.size {
found = true
break
}
}
require.True(t, found, "large range %v not found in all ranges", l)
}
for i := 1; i < len(all); i++ {
require.Greater(t, all[i].start, all[i-1].start, "all ranges should be sorted by start")
}
for i := range all {
for j := i + 1; j < len(all); j++ {
end1 := all[i].start + uint64(all[i].size)
end2 := all[j].start + uint64(all[j].size)
require.False(t, (all[i].start >= all[j].start && all[i].start < end2) ||
(all[j].start >= all[i].start && all[j].start < end1),
"ranges %v and %v overlap", all[i], all[j])
}
}
mmmm.lmdbEnv.View(func(txn *lmdb.Txn) error {
before := make(positions, len(mmmm.freeRangesAll))
copy(before, mmmm.freeRangesAll)
err := mmmm.gatherFreeRanges(txn)
require.NoError(t, err)
require.Equal(t, before, mmmm.freeRangesAll, "recomputing free ranges should yield the same result")
return nil
})
}