Add BBolt database backend for HDD-optimized archival relays (v0.48.0)
- BBolt B+tree backend with sequential access patterns for spinning disks - Write batching (5000 events / 128MB / 30s flush) to reduce disk thrashing - Adjacency list storage for graph data (one key per vertex, not per edge) - Bloom filter for fast negative edge existence checks (~12MB for 10M edges) - No query cache (saves RAM, B+tree reads are fast enough on HDD) - Migration tool: orly migrate --from badger --to bbolt - Configuration: ORLY_BBOLT_* environment variables Files modified: - app/config/config.go: Added BBolt configuration options - main.go: Added migrate subcommand and BBolt config wiring - pkg/database/factory.go: Added BBolt factory registration - pkg/bbolt/*: New BBolt database backend implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
134
pkg/bbolt/fetch-event.go
Normal file
134
pkg/bbolt/fetch-event.go
Normal file
@@ -0,0 +1,134 @@
|
||||
//go:build !(js && wasm)
|
||||
|
||||
package bbolt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
"lol.mleku.dev/chk"
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/database/indexes/types"
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/filter"
|
||||
)
|
||||
|
||||
// FetchEventBySerial fetches an event by its serial number.
|
||||
func (b *B) FetchEventBySerial(ser *types.Uint40) (ev *event.E, err error) {
|
||||
if ser == nil {
|
||||
return nil, errors.New("bbolt: nil serial")
|
||||
}
|
||||
|
||||
serial := ser.Get()
|
||||
key := makeSerialKey(serial)
|
||||
|
||||
err = b.db.View(func(tx *bolt.Tx) error {
|
||||
// Get event ID first
|
||||
seiBucket := tx.Bucket(bucketSei)
|
||||
var eventId []byte
|
||||
if seiBucket != nil {
|
||||
eventId = seiBucket.Get(key)
|
||||
}
|
||||
|
||||
// Try compact event storage first
|
||||
cmpBucket := tx.Bucket(bucketCmp)
|
||||
if cmpBucket != nil {
|
||||
data := cmpBucket.Get(key)
|
||||
if data != nil && eventId != nil && len(eventId) == 32 {
|
||||
// Unmarshal compact event
|
||||
resolver := &bboltSerialResolver{b: b}
|
||||
ev, err = database.UnmarshalCompactEvent(data, eventId, resolver)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// Fall through to try legacy format
|
||||
}
|
||||
}
|
||||
|
||||
// Try legacy event storage
|
||||
evtBucket := tx.Bucket(bucketEvt)
|
||||
if evtBucket != nil {
|
||||
data := evtBucket.Get(key)
|
||||
if data != nil {
|
||||
ev = new(event.E)
|
||||
reader := bytes.NewReader(data)
|
||||
if err = ev.UnmarshalBinary(reader); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("bbolt: event not found")
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FetchEventsBySerials fetches multiple events by their serial numbers.
|
||||
func (b *B) FetchEventsBySerials(serials []*types.Uint40) (events map[uint64]*event.E, err error) {
|
||||
events = make(map[uint64]*event.E, len(serials))
|
||||
|
||||
err = b.db.View(func(tx *bolt.Tx) error {
|
||||
cmpBucket := tx.Bucket(bucketCmp)
|
||||
evtBucket := tx.Bucket(bucketEvt)
|
||||
seiBucket := tx.Bucket(bucketSei)
|
||||
resolver := &bboltSerialResolver{b: b}
|
||||
|
||||
for _, ser := range serials {
|
||||
if ser == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
serial := ser.Get()
|
||||
key := makeSerialKey(serial)
|
||||
|
||||
// Get event ID
|
||||
var eventId []byte
|
||||
if seiBucket != nil {
|
||||
eventId = seiBucket.Get(key)
|
||||
}
|
||||
|
||||
// Try compact event storage first
|
||||
if cmpBucket != nil {
|
||||
data := cmpBucket.Get(key)
|
||||
if data != nil && eventId != nil && len(eventId) == 32 {
|
||||
ev, e := database.UnmarshalCompactEvent(data, eventId, resolver)
|
||||
if e == nil {
|
||||
events[serial] = ev
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try legacy event storage
|
||||
if evtBucket != nil {
|
||||
data := evtBucket.Get(key)
|
||||
if data != nil {
|
||||
ev := new(event.E)
|
||||
reader := bytes.NewReader(data)
|
||||
if e := ev.UnmarshalBinary(reader); e == nil {
|
||||
events[serial] = ev
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CountEvents counts events matching a filter.
|
||||
func (b *B) CountEvents(c context.Context, f *filter.F) (count int, approximate bool, err error) {
|
||||
// Get serials matching filter
|
||||
var serials types.Uint40s
|
||||
if serials, err = b.GetSerialsFromFilter(f); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
count = len(serials)
|
||||
approximate = false
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user