Files
nostrlib/eventstore/mmm/fix.go
T
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

205 lines
4.8 KiB
Go

package mmm
import (
"bytes"
"encoding/binary"
"slices"
"fiatjaf.com/nostr"
"github.com/PowerDNS/lmdb-go/lmdb"
)
func (b *MultiMmapManager) Rescan() error {
b.writeMutex.Lock()
defer b.writeMutex.Unlock()
return b.lmdbEnv.Update(func(mmmtxn *lmdb.Txn) error {
cursor, err := mmmtxn.OpenCursor(b.indexId)
if err != nil {
return err
}
defer cursor.Close()
var toPurge [][]byte // a list of idPrefix entries
for key, val, err := cursor.Get(nil, nil, lmdb.First); err == nil; key, val, err = cursor.Get(key, val, lmdb.Next) {
pos := positionFromBytes(val[0:12])
// for every event in this index
var borked bool
// we try to load it
var evt nostr.Event
if err := b.loadEvent(pos, &evt); err == nil && bytes.Equal(evt.ID[0:8], key) {
// all good
borked = false
} else {
// it's borked
borked = true
}
var layersToRemove []uint16
// then for every layer referenced in there we check
for s := 12; s < len(val); s += 2 {
layerId := binary.BigEndian.Uint16(val[s : s+2])
layer := b.layers.ByID(layerId)
if layer == nil {
continue
}
if err := layer.lmdbEnv.Update(func(txn *lmdb.Txn) error {
txn.RawRead = true
if borked {
// for borked events we have to do a bruteforce check
if layer.hasAtPosition(txn, pos) {
// expected -- delete anyway since it's borked
if err := layer.bruteDeleteIndexes(txn, pos); err != nil {
return err
}
} else {
// this stuff is doubly borked -- let's do nothing
return nil
}
} else {
// otherwise we do a more reasonable check
if layer.hasAtTimestampAndPosition(txn, evt.CreatedAt, pos) {
// expected, all good
} else {
// can't find it in this layer, so update source reference to remove this
// and clear it from this layer (if any traces remain)
if err := layer.deleteIndexes(txn, evt, val[0:12]); err != nil {
return err
}
// we'll remove references to this later
// (no need to do anything in the borked case as everything will be deleted)
layersToRemove = append(layersToRemove, layerId)
}
}
return nil
}); err != nil {
return err
}
}
if borked {
toPurge = append(toPurge, key)
} else if len(layersToRemove) > 0 {
for s := 12; s < len(val); {
if slices.Contains(layersToRemove, binary.BigEndian.Uint16(val[s:s+2])) {
// swap-delete
copy(val[s:s+2], val[len(val)-2:])
val = val[0 : len(val)-2]
} else {
s += 2
}
}
if len(val) > 12 {
if err := mmmtxn.Put(b.indexId, key, val, 0); err != nil {
return err
}
} else {
toPurge = append(toPurge, key)
}
}
}
for _, idPrefix := range toPurge {
// just delete from the ids index,
// no need to deal with the freeranges list as it will be recalculated afterwards.
// this also ensures any brokenly overlapping overwritten events don't have to be sacrificed.
if err := mmmtxn.Del(b.indexId, idPrefix, nil); err != nil {
return err
}
}
if err := b.gatherFreeRanges(mmmtxn); err != nil {
return err
}
return nil
})
}
func (il *IndexingLayer) hasAtTimestampAndPosition(iltxn *lmdb.Txn, ts nostr.Timestamp, pos position) (exists bool) {
cursor, err := iltxn.OpenCursor(il.indexCreatedAt)
if err != nil {
panic(err)
}
defer cursor.Close()
key := make([]byte, 4)
binary.BigEndian.PutUint32(key[0:4], uint32(ts))
if _, val, err := cursor.Get(key, nil, lmdb.SetKey); err == nil {
if positionFromBytes(val[0:12]) == pos {
exists = true
}
}
return exists
}
func (il *IndexingLayer) hasAtPosition(iltxn *lmdb.Txn, pos position) (exists bool) {
cursor, err := iltxn.OpenCursor(il.indexCreatedAt)
if err != nil {
panic(err)
}
defer cursor.Close()
for key, val, err := cursor.Get(nil, nil, lmdb.First); err == nil; key, val, err = cursor.Get(key, val, lmdb.Next) {
if positionFromBytes(val[0:12]) == pos {
exists = true
break
}
}
return exists
}
func (il *IndexingLayer) bruteDeleteIndexes(iltxn *lmdb.Txn, pos position) error {
type entry struct {
key []byte
val []byte
}
toDelete := make([]entry, 0, 8)
for _, index := range []lmdb.DBI{
il.indexCreatedAt,
il.indexKind,
il.indexPubkey,
il.indexPubkeyKind,
il.indexPTagKind,
il.indexTag,
il.indexTag32,
il.indexTagAddr,
} {
cursor, err := iltxn.OpenCursor(index)
if err != nil {
return err
}
for key, val, err := cursor.Get(nil, nil, lmdb.First); err == nil; key, val, err = cursor.Get(key, val, lmdb.Next) {
if positionFromBytes(val[0:12]) == pos {
toDelete = append(toDelete, entry{key, val})
}
}
cursor.Close()
for _, entry := range toDelete {
if err := iltxn.Del(index, entry.key, entry.val); err != nil {
return err
}
}
toDelete = toDelete[:0]
}
return nil
}