You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

474 lines
14 KiB

//go:build !(js && wasm)
package database
import (
"bytes"
"fmt"
"io"
"time"
"github.com/dgraph-io/badger/v4"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/database/indexes"
"next.orly.dev/pkg/database/indexes/types"
)
// HealthReport contains the results of a database health check.
type HealthReport struct {
// Scan metadata
ScanStarted time.Time
ScanDuration time.Duration
// Event counts
CompactEvents int64 // Events stored in compact format (cmp)
LegacyEvents int64 // Events in legacy format (evt)
SmallEvents int64 // Small inline events (sev)
TotalEvents int64 // Total events
SerialIdCount int64 // Serial to EventID mappings (sei)
// Pubkey serial counts
PubkeySerials int64 // pks entries (pubkey hash -> serial)
SerialPubkeys int64 // spk entries (serial -> pubkey)
// Graph edge counts
EventPubkeyEdges int64 // epg entries
PubkeyEventEdges int64 // peg entries
EventEventEdges int64 // eeg entries
GraphEventEdges int64 // gee entries
// Index counts
KindIndexes int64 // kc- entries
PubkeyIndexes int64 // pc- entries
TagIndexes int64 // tc- entries
WordIndexes int64 // wrd entries
IdIndexes int64 // eid entries
// Issues found
MissingSerialEventIds int64 // cmp entries without corresponding sei
OrphanedSerialEventIds int64 // sei entries without corresponding cmp
PubkeySerialMismatches int64 // pks without matching spk or vice versa
OrphanedIndexes int64 // Index entries pointing to non-existent events
// Sample of missing sei serials (for debugging)
MissingSeiSamples []uint64
// Health score (0-100)
HealthScore int
}
// String returns a human-readable health report.
func (r *HealthReport) String() string {
var buf bytes.Buffer
fmt.Fprintln(&buf, "Database Health Report")
fmt.Fprintln(&buf, "======================")
fmt.Fprintf(&buf, "Scan duration: %v\n\n", r.ScanDuration)
fmt.Fprintln(&buf, "Event Storage:")
fmt.Fprintf(&buf, " Compact events (cmp): %d\n", r.CompactEvents)
fmt.Fprintf(&buf, " Legacy events (evt): %d\n", r.LegacyEvents)
fmt.Fprintf(&buf, " Small events (sev): %d\n", r.SmallEvents)
fmt.Fprintf(&buf, " Total events: %d\n", r.TotalEvents)
fmt.Fprintf(&buf, " Serial->ID maps (sei): %d\n\n", r.SerialIdCount)
fmt.Fprintln(&buf, "Pubkey Mappings:")
fmt.Fprintf(&buf, " Pubkey serials (pks): %d\n", r.PubkeySerials)
fmt.Fprintf(&buf, " Serial pubkeys (spk): %d\n\n", r.SerialPubkeys)
fmt.Fprintln(&buf, "Graph Edges:")
fmt.Fprintf(&buf, " Event->Pubkey (epg): %d\n", r.EventPubkeyEdges)
fmt.Fprintf(&buf, " Pubkey->Event (peg): %d\n", r.PubkeyEventEdges)
fmt.Fprintf(&buf, " Event->Event (eeg): %d\n", r.EventEventEdges)
fmt.Fprintf(&buf, " Event<-Event (gee): %d\n\n", r.GraphEventEdges)
fmt.Fprintln(&buf, "Search Indexes:")
fmt.Fprintf(&buf, " Kind indexes (kc-): %d\n", r.KindIndexes)
fmt.Fprintf(&buf, " Pubkey indexes (pc-): %d\n", r.PubkeyIndexes)
fmt.Fprintf(&buf, " Tag indexes (tc-): %d\n", r.TagIndexes)
fmt.Fprintf(&buf, " Word indexes (wrd): %d\n", r.WordIndexes)
fmt.Fprintf(&buf, " ID indexes (eid): %d\n\n", r.IdIndexes)
fmt.Fprintln(&buf, "Issues Found:")
fmt.Fprintf(&buf, " Missing sei mappings: %d", r.MissingSerialEventIds)
if r.MissingSerialEventIds > 0 {
fmt.Fprint(&buf, " (CRITICAL)")
}
fmt.Fprintln(&buf)
fmt.Fprintf(&buf, " Orphaned sei mappings: %d\n", r.OrphanedSerialEventIds)
fmt.Fprintf(&buf, " Pubkey serial mismatch: %d\n", r.PubkeySerialMismatches)
fmt.Fprintf(&buf, " Orphaned indexes: %d\n\n", r.OrphanedIndexes)
if len(r.MissingSeiSamples) > 0 {
fmt.Fprintln(&buf, "Sample missing sei serials:")
for i, s := range r.MissingSeiSamples {
if i >= 10 {
fmt.Fprintf(&buf, " ... and %d more\n", len(r.MissingSeiSamples)-10)
break
}
fmt.Fprintf(&buf, " - %d\n", s)
}
fmt.Fprintln(&buf)
}
fmt.Fprintf(&buf, "Health Score: %d/100\n", r.HealthScore)
if r.HealthScore < 50 {
fmt.Fprintln(&buf, "\n⚠ Database has critical issues. Run 'orly db repair' to fix.")
} else if r.HealthScore < 80 {
fmt.Fprintln(&buf, "\n⚠ Database has some issues. Consider running 'orly db repair'.")
} else {
fmt.Fprintln(&buf, "\n✓ Database is healthy.")
}
return buf.String()
}
// HealthCheck performs a comprehensive health check of the database.
// It scans all index prefixes and verifies referential integrity.
func (d *D) HealthCheck(progress io.Writer) (report *HealthReport, err error) {
report = &HealthReport{
ScanStarted: time.Now(),
MissingSeiSamples: make([]uint64, 0, 100),
}
if progress != nil {
fmt.Fprintln(progress, "Starting database health check...")
}
// Build prefix buffers for all index types
cmpPrf := buildPrefix(indexes.CompactEventEnc(nil))
seiPrf := buildPrefix(indexes.SerialEventIdEnc(nil))
evtPrf := buildPrefix(indexes.EventEnc(nil))
sevPrf := buildPrefix(indexes.SmallEventEnc(nil))
pksPrf := buildPrefix(indexes.PubkeySerialEnc(nil, nil))
spkPrf := buildPrefix(indexes.SerialPubkeyEnc(nil))
epgPrf := buildPrefix(indexes.EventPubkeyGraphEnc(nil, nil, nil, nil))
pegPrf := buildPrefix(indexes.PubkeyEventGraphEnc(nil, nil, nil, nil))
eegPrf := buildPrefix(indexes.EventEventGraphEnc(nil, nil, nil, nil))
geePrf := buildPrefix(indexes.GraphEventEventEnc(nil, nil, nil, nil))
kcPrf := buildPrefix(indexes.KindEnc(nil, nil, nil))
pcPrf := buildPrefix(indexes.PubkeyEnc(nil, nil, nil))
tcPrf := buildPrefix(indexes.TagEnc(nil, nil, nil, nil))
wrdPrf := buildPrefix(indexes.WordEnc(nil, nil))
eidPrf := buildPrefix(indexes.IdEnc(nil, nil))
// Phase 1: Count all entries with each prefix
if progress != nil {
fmt.Fprintln(progress, "Phase 1: Counting entries by prefix...")
}
err = d.View(func(txn *badger.Txn) error {
// Count compact events
report.CompactEvents = countPrefix(txn, cmpPrf)
if progress != nil {
fmt.Fprintf(progress, " Compact events (cmp): %d\n", report.CompactEvents)
}
// Count serial->eventID mappings
report.SerialIdCount = countPrefix(txn, seiPrf)
if progress != nil {
fmt.Fprintf(progress, " Serial->ID maps (sei): %d\n", report.SerialIdCount)
}
// Count legacy events
report.LegacyEvents = countPrefix(txn, evtPrf)
report.SmallEvents = countPrefix(txn, sevPrf)
report.TotalEvents = report.CompactEvents + report.LegacyEvents + report.SmallEvents
if progress != nil {
fmt.Fprintf(progress, " Legacy events (evt): %d\n", report.LegacyEvents)
fmt.Fprintf(progress, " Small events (sev): %d\n", report.SmallEvents)
}
// Count pubkey serial mappings
report.PubkeySerials = countPrefix(txn, pksPrf)
report.SerialPubkeys = countPrefix(txn, spkPrf)
if progress != nil {
fmt.Fprintf(progress, " Pubkey serials (pks): %d, (spk): %d\n", report.PubkeySerials, report.SerialPubkeys)
}
// Count graph edges
report.EventPubkeyEdges = countPrefix(txn, epgPrf)
report.PubkeyEventEdges = countPrefix(txn, pegPrf)
report.EventEventEdges = countPrefix(txn, eegPrf)
report.GraphEventEdges = countPrefix(txn, geePrf)
if progress != nil {
fmt.Fprintf(progress, " Graph edges: epg=%d, peg=%d, eeg=%d, gee=%d\n",
report.EventPubkeyEdges, report.PubkeyEventEdges, report.EventEventEdges, report.GraphEventEdges)
}
// Count search indexes
report.KindIndexes = countPrefix(txn, kcPrf)
report.PubkeyIndexes = countPrefix(txn, pcPrf)
report.TagIndexes = countPrefix(txn, tcPrf)
report.WordIndexes = countPrefix(txn, wrdPrf)
report.IdIndexes = countPrefix(txn, eidPrf)
if progress != nil {
fmt.Fprintf(progress, " Indexes: kc=%d, pc=%d, tc=%d, wrd=%d, eid=%d\n",
report.KindIndexes, report.PubkeyIndexes, report.TagIndexes, report.WordIndexes, report.IdIndexes)
}
return nil
})
if chk.E(err) {
return nil, err
}
// Phase 2: Check cmp->sei integrity (CRITICAL)
if progress != nil {
fmt.Fprintln(progress, "\nPhase 2: Checking compact event -> serial ID integrity...")
}
err = d.View(func(txn *badger.Txn) error {
it := txn.NewIterator(badger.IteratorOptions{Prefix: cmpPrf})
defer it.Close()
checked := int64(0)
for it.Rewind(); it.Valid(); it.Next() {
// Extract serial from cmp key: prefix (3 bytes) + serial (5 bytes)
key := it.Item().Key()
if len(key) < 8 {
continue
}
// Extract the serial
serial := extractSerial(key[3:8])
// Check if sei entry exists for this serial
seiKey := buildSeiKey(serial)
_, err := txn.Get(seiKey)
if err == badger.ErrKeyNotFound {
report.MissingSerialEventIds++
if len(report.MissingSeiSamples) < 100 {
report.MissingSeiSamples = append(report.MissingSeiSamples, serial)
}
} else if err != nil {
log.W.F("error checking sei for serial %d: %v", serial, err)
}
checked++
if progress != nil && checked%100000 == 0 {
fmt.Fprintf(progress, " Checked %d compact events, %d missing sei so far...\n",
checked, report.MissingSerialEventIds)
}
}
if progress != nil {
fmt.Fprintf(progress, " Checked %d compact events, found %d missing sei entries\n",
checked, report.MissingSerialEventIds)
}
return nil
})
if chk.E(err) {
return nil, err
}
// Phase 3: Check for orphaned sei entries (sei without cmp)
if progress != nil {
fmt.Fprintln(progress, "\nPhase 3: Checking for orphaned serial ID mappings...")
}
err = d.View(func(txn *badger.Txn) error {
it := txn.NewIterator(badger.IteratorOptions{Prefix: seiPrf})
defer it.Close()
checked := int64(0)
for it.Rewind(); it.Valid(); it.Next() {
key := it.Item().Key()
if len(key) < 8 {
continue
}
// Extract serial from sei key
serial := extractSerial(key[3:8])
// Check if cmp entry exists for this serial
cmpKey := buildCmpKey(serial)
_, err := txn.Get(cmpKey)
if err == badger.ErrKeyNotFound {
// Also check legacy evt format
evtKey := buildEvtKey(serial)
_, err2 := txn.Get(evtKey)
if err2 == badger.ErrKeyNotFound {
report.OrphanedSerialEventIds++
}
} else if err != nil {
log.W.F("error checking cmp for serial %d: %v", serial, err)
}
checked++
if progress != nil && checked%100000 == 0 {
fmt.Fprintf(progress, " Checked %d sei entries, %d orphaned so far...\n",
checked, report.OrphanedSerialEventIds)
}
}
if progress != nil {
fmt.Fprintf(progress, " Checked %d sei entries, found %d orphaned\n",
checked, report.OrphanedSerialEventIds)
}
return nil
})
if chk.E(err) {
return nil, err
}
// Phase 4: Check pubkey serial consistency
if progress != nil {
fmt.Fprintln(progress, "\nPhase 4: Checking pubkey serial consistency...")
}
err = d.View(func(txn *badger.Txn) error {
// Check that pks count roughly matches spk count
// A small difference is acceptable due to timing, but large differences indicate corruption
diff := report.PubkeySerials - report.SerialPubkeys
if diff < 0 {
diff = -diff
}
// Allow 1% difference
threshold := report.PubkeySerials / 100
if threshold < 10 {
threshold = 10
}
if diff > threshold {
report.PubkeySerialMismatches = diff
if progress != nil {
fmt.Fprintf(progress, " Found %d pubkey serial mismatches (pks=%d, spk=%d)\n",
diff, report.PubkeySerials, report.SerialPubkeys)
}
} else if progress != nil {
fmt.Fprintln(progress, " Pubkey serial counts are consistent")
}
return nil
})
if chk.E(err) {
return nil, err
}
// Calculate health score
report.ScanDuration = time.Since(report.ScanStarted)
report.HealthScore = calculateHealthScore(report)
if progress != nil {
fmt.Fprintf(progress, "\nHealth check complete. Score: %d/100\n", report.HealthScore)
}
return report, nil
}
// buildPrefix creates a prefix buffer from an encoder.
func buildPrefix(enc *indexes.T) []byte {
buf := new(bytes.Buffer)
if err := enc.MarshalWrite(buf); err != nil {
return nil
}
// Return only the prefix part (3 bytes)
b := buf.Bytes()
if len(b) >= 3 {
return b[:3]
}
return b
}
// countPrefix counts the number of entries with the given prefix.
func countPrefix(txn *badger.Txn, prefix []byte) int64 {
it := txn.NewIterator(badger.IteratorOptions{
Prefix: prefix,
PrefetchValues: false,
})
defer it.Close()
var count int64
for it.Rewind(); it.Valid(); it.Next() {
count++
}
return count
}
// extractSerial extracts a 40-bit serial from 5 bytes (big-endian).
func extractSerial(b []byte) uint64 {
if len(b) < 5 {
return 0
}
return (uint64(b[0]) << 32) |
(uint64(b[1]) << 24) |
(uint64(b[2]) << 16) |
(uint64(b[3]) << 8) |
uint64(b[4])
}
// buildSeiKey builds a sei (serial->eventID) key for the given serial.
func buildSeiKey(serial uint64) []byte {
ser := new(types.Uint40)
ser.Set(serial)
buf := new(bytes.Buffer)
indexes.SerialEventIdEnc(ser).MarshalWrite(buf)
return buf.Bytes()
}
// buildCmpKey builds a cmp (compact event) key for the given serial.
func buildCmpKey(serial uint64) []byte {
ser := new(types.Uint40)
ser.Set(serial)
buf := new(bytes.Buffer)
indexes.CompactEventEnc(ser).MarshalWrite(buf)
return buf.Bytes()
}
// buildEvtKey builds an evt (legacy event) key for the given serial.
func buildEvtKey(serial uint64) []byte {
ser := new(types.Uint40)
ser.Set(serial)
buf := new(bytes.Buffer)
indexes.EventEnc(ser).MarshalWrite(buf)
return buf.Bytes()
}
// calculateHealthScore calculates a health score from 0-100 based on the report.
func calculateHealthScore(r *HealthReport) int {
score := 100
// Missing sei is critical - each one costs 1 point, max 50 point penalty
if r.MissingSerialEventIds > 0 {
penalty := int(r.MissingSerialEventIds)
if penalty > 50 {
penalty = 50
}
score -= penalty
}
// Orphaned sei is less critical - each one costs 0.1 points, max 20 point penalty
if r.OrphanedSerialEventIds > 0 {
penalty := int(r.OrphanedSerialEventIds / 10)
if penalty > 20 {
penalty = 20
}
score -= penalty
}
// Pubkey mismatches cost 0.5 points each, max 20 point penalty
if r.PubkeySerialMismatches > 0 {
penalty := int(r.PubkeySerialMismatches / 2)
if penalty > 20 {
penalty = 20
}
score -= penalty
}
// Orphaned indexes cost 0.01 points each, max 10 point penalty
if r.OrphanedIndexes > 0 {
penalty := int(r.OrphanedIndexes / 100)
if penalty > 10 {
penalty = 10
}
score -= penalty
}
if score < 0 {
score = 0
}
return score
}