Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 50 additions & 49 deletions cmd/stellar-rpc/internal/fullhistory/storage/rocksdb/rocksdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,31 +586,55 @@ func (s *Store) constructAndOpen() error {
return nil
}

// applyTuning splits configuration into wrapper-pinned values
// (every CF, unconditional), per-facade Tuning fields (applied
// only when non-zero), and per-CF overrides (applied last so they
// win over the pinned defaults). BloomFilterBitsPerKey == 0 is
// the documented "no bloom filter" sentinel.
// applyTuning splits configuration into wrapper-pinned values (every CF,
// unconditional), per-CF overrides (applied after the pinned defaults so
// they win), and DB-wide Tuning fields (applied to the shared Options).
// BloomFilterBitsPerKey == 0 is the documented "no bloom filter" sentinel.
func (s *Store) applyTuning(opts *grocksdb.Options, cfNames []string, cfOpts []*grocksdb.Options) {
t := s.cfg.Tuning
for i, o := range cfOpts {
applyPinnedCFOptions(o)
applyCFTuning(o, t)
applyCFOverride(o, s.cfg.PerCFOptions[cfNames[i]])
}
applyDBTuning(opts, t)
s.applySharedTableOptions(cfNames, cfOpts, t)
}

// applyCFOverride applies the per-CF Compression override. BlockSize
// is applied inside applySharedTableOptions when the BBTO is built.
func applyCFOverride(o *grocksdb.Options, override CFOptions) {
// applyCFOverride applies the per-CF memtable/compaction/compression knobs.
// Each is applied only when non-zero, leaving the pinned default otherwise.
// BlockSize and BloomFilterBitsPerKey are applied inside
// applySharedTableOptions when the BBTO is built.
func applyCFOverride(o *grocksdb.Options, c CFOptions) {
// CompressionType is an int alias; NoCompression (the pinned
// default) is 0. A zero-value override is therefore a no-op and
// leaves the pinned NoCompression in place. Non-zero values
// (Snappy, ZSTD, ...) replace it.
if override.Compression != grocksdb.NoCompression {
o.SetCompression(override.Compression)
if c.Compression != grocksdb.NoCompression {
o.SetCompression(c.Compression)
}
if c.WriteBufferMB > 0 {
o.SetWriteBufferSize(uint64(c.WriteBufferMB) << 20)
}
if c.MaxWriteBufferNumber > 0 {
o.SetMaxWriteBufferNumber(c.MaxWriteBufferNumber)
}
if c.Level0FileNumCompactionTrigger > 0 {
o.SetLevel0FileNumCompactionTrigger(c.Level0FileNumCompactionTrigger)
}
if c.Level0SlowdownWritesTrigger > 0 {
o.SetLevel0SlowdownWritesTrigger(c.Level0SlowdownWritesTrigger)
}
if c.Level0StopWritesTrigger > 0 {
o.SetLevel0StopWritesTrigger(c.Level0StopWritesTrigger)
}
if c.DisableAutoCompactions {
o.SetDisableAutoCompactions(true)
}
if c.TargetFileSizeMB > 0 {
o.SetTargetFileSizeBase(uint64(c.TargetFileSizeMB) << 20)
}
if c.MaxBytesForLevelBaseMB > 0 {
o.SetMaxBytesForLevelBase(uint64(c.MaxBytesForLevelBaseMB) << 20)
}
}

Expand All @@ -625,33 +649,6 @@ func applyPinnedCFOptions(o *grocksdb.Options) {
o.SetCompression(grocksdb.NoCompression)
}

func applyCFTuning(o *grocksdb.Options, t Tuning) {
if t.WriteBufferMB > 0 {
o.SetWriteBufferSize(uint64(t.WriteBufferMB) << 20)
}
if t.MaxWriteBufferNumber > 0 {
o.SetMaxWriteBufferNumber(t.MaxWriteBufferNumber)
}
if t.Level0FileNumCompactionTrigger > 0 {
o.SetLevel0FileNumCompactionTrigger(t.Level0FileNumCompactionTrigger)
}
if t.Level0SlowdownWritesTrigger > 0 {
o.SetLevel0SlowdownWritesTrigger(t.Level0SlowdownWritesTrigger)
}
if t.Level0StopWritesTrigger > 0 {
o.SetLevel0StopWritesTrigger(t.Level0StopWritesTrigger)
}
if t.DisableAutoCompactions {
o.SetDisableAutoCompactions(true)
}
if t.TargetFileSizeMB > 0 {
o.SetTargetFileSizeBase(uint64(t.TargetFileSizeMB) << 20)
}
if t.MaxBytesForLevelBaseMB > 0 {
o.SetMaxBytesForLevelBase(uint64(t.MaxBytesForLevelBaseMB) << 20)
}
}

func applyDBTuning(opts *grocksdb.Options, t Tuning) {
if t.MaxBackgroundJobs > 0 {
opts.SetMaxBackgroundJobs(t.MaxBackgroundJobs)
Expand All @@ -665,15 +662,19 @@ func applyDBTuning(opts *grocksdb.Options, t Tuning) {
}

// applySharedTableOptions builds one BBTO per CF, referencing the
// shared block cache (when set), a fresh per-CF bloom filter (when
// set), and the per-CF BlockSize override (when set). The Store
// retains every BBTO and the cache; Close destroys them after
// opts/cfOpts.
// DB-wide block cache (when set), a fresh per-CF bloom filter (when
// that CF sets BloomFilterBitsPerKey), and the per-CF BlockSize
// override (when set). The Store retains every BBTO and the cache;
// Close destroys them after opts/cfOpts.
//
// The block cache is DB-wide (one LRU shared across every CF from
// Tuning.BlockCacheMB); the bloom filter and block size are per-CF
// (from PerCFOptions), so only a CF that opts in pays for them.
//
// A BBTO is installed on a CF iff any of the cache, a bloom filter,
// or that CF's BlockSize override is configured — preserving the
// previous behavior of leaving RocksDB's default BBTO untouched when
// no table-level knob is set.
// A BBTO is installed on a CF iff the shared cache, that CF's bloom
// filter, or that CF's BlockSize override is configured — preserving
// the previous behavior of leaving RocksDB's default BBTO untouched
// when no table-level knob is set.
//
// The bloom filter is built per CF because SetFilterPolicy MOVES the
// policy into the BBTO (it nils the source pointer), so a single
Expand All @@ -685,15 +686,15 @@ func (s *Store) applySharedTableOptions(cfNames []string, cfOpts []*grocksdb.Opt
}
for i, o := range cfOpts {
override := s.cfg.PerCFOptions[cfNames[i]]
if s.cache == nil && t.BloomFilterBitsPerKey == 0 && override.BlockSize == 0 {
if s.cache == nil && override.BloomFilterBitsPerKey == 0 && override.BlockSize == 0 {
continue
}
bbto := grocksdb.NewDefaultBlockBasedTableOptions()
if s.cache != nil {
bbto.SetBlockCache(s.cache)
}
if t.BloomFilterBitsPerKey > 0 {
bbto.SetFilterPolicy(grocksdb.NewBloomFilter(float64(t.BloomFilterBitsPerKey)))
if override.BloomFilterBitsPerKey > 0 {
bbto.SetFilterPolicy(grocksdb.NewBloomFilter(float64(override.BloomFilterBitsPerKey)))
}
if override.BlockSize > 0 {
bbto.SetBlockSize(override.BlockSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,14 @@ func TestNew_TwoStoresSamePathCollide(t *testing.T) {

func TestStore_ConstructAndOpenFailureFreesCacheAndBBTOs(t *testing.T) {
dir := t.TempDir()
tuning := Tuning{BlockCacheMB: 4, BloomFilterBitsPerKey: 10}
tuning := Tuning{BlockCacheMB: 4}
perCF := map[string]CFOptions{defaultCFName: {BloomFilterBitsPerKey: 10}}

holder, err := New(Config{Path: dir, Logger: silentLogger(), Tuning: tuning})
holder, err := New(Config{Path: dir, Logger: silentLogger(), Tuning: tuning, PerCFOptions: perCF})
require.NoError(t, err)
t.Cleanup(func() { _ = holder.Close() })

collider := &Store{cfg: Config{Path: dir, Logger: silentLogger(), Tuning: tuning}}
collider := &Store{cfg: Config{Path: dir, Logger: silentLogger(), Tuning: tuning, PerCFOptions: perCF}}
require.Error(t, collider.constructAndOpen())

assert.Nil(t, collider.cache)
Expand Down Expand Up @@ -665,18 +666,22 @@ func TestStore_TuningRoundTrip(t *testing.T) {
Path: t.TempDir(),
Logger: newTestLogger(&buf),
Tuning: Tuning{
WriteBufferMB: 8,
MaxWriteBufferNumber: 2,
Level0FileNumCompactionTrigger: 4,
Level0SlowdownWritesTrigger: 20,
Level0StopWritesTrigger: 36,
TargetFileSizeMB: 16,
MaxBytesForLevelBaseMB: 64,
MaxBackgroundJobs: 2,
MaxOpenFiles: 500,
BlockCacheMB: 4,
BloomFilterBitsPerKey: 10,
MaxTotalWalSizeMB: 16,
MaxBackgroundJobs: 2,
MaxOpenFiles: 500,
BlockCacheMB: 4,
MaxTotalWalSizeMB: 16,
},
PerCFOptions: map[string]CFOptions{
defaultCFName: {
WriteBufferMB: 8,
MaxWriteBufferNumber: 2,
Level0FileNumCompactionTrigger: 4,
Level0SlowdownWritesTrigger: 20,
Level0StopWritesTrigger: 36,
TargetFileSizeMB: 16,
MaxBytesForLevelBaseMB: 64,
BloomFilterBitsPerKey: 10,
},
},
})
require.NoError(t, err)
Expand Down
77 changes: 42 additions & 35 deletions cmd/stellar-rpc/internal/fullhistory/storage/rocksdb/tuning.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package rocksdb

import "github.com/linxGnu/grocksdb"

// CFOptions — per-CF overrides applied after the shared pinned
// defaults and global Tuning. Zero means "inherit the pinned
// default" (NoCompression for Compression, RocksDB's BBTO default
// block size for BlockSize). Use to opt specific CFs into
// compression or non-default block sizes while leaving the rest at
// the wrapper-pinned defaults.
// CFOptions — per-CF overrides applied after the shared pinned defaults.
// Zero means "inherit the pinned default" (NoCompression for Compression,
// RocksDB's BBTO default block size for BlockSize, no bloom filter, and
// grocksdb's memtable/compaction defaults for the rest). Each field a CF
// leaves zero rides on the wrapper-pinned defaults; a facade opts a specific
// CF into a knob by setting it here. The knobs are per-CF because each hot
// data type's workload differs (txhash is write-once/point-lookup; ledgers is
// never probed for missing keys; events holds compressible XDR) and one CF's
// tuning must not leak onto the others.
type CFOptions struct {
// Compression overrides the CF's compression type. The wrapper's
// pinned default is NoCompression; CFs that hold compressible
Expand All @@ -25,65 +28,69 @@ type CFOptions struct {
// block holding the target key; larger blocks waste I/O per
// cache miss.
BlockSize int
}

// Tuning — per-store RocksDB knobs. Zero means "leave grocksdb's
// default alone" (wrapper skips the setter). BloomFilterBitsPerKey == 0
// is the documented "install no bloom filter" sentinel.
//
// Wrapper-pinned values not exposed here (applied to every facade):
// MinWriteBufferNumberToMerge=1, CompactionStyle=Level,
// TargetFileSizeMultiplier=1, MaxBytesForLevelMultiplier=10,
// Compression=None, WAL=on, per-write Sync=on.
// Per-CF overrides for Compression and BlockSize live in
// Config.PerCFOptions.
type Tuning struct {
// WriteBufferMB sizes the active memtable per CF, in MB.
// WriteBufferMB sizes the active memtable for this CF, in MB. Zero
// leaves grocksdb's default.
WriteBufferMB int

// MaxWriteBufferNumber caps the active + immutable memtable count
// per CF before writes back-pressure.
// for this CF before writes back-pressure. Zero leaves the default.
MaxWriteBufferNumber int

// Level0FileNumCompactionTrigger — L0 file count that starts
// an L0→L1 compaction.
// an L0→L1 compaction. Zero leaves the default.
Level0FileNumCompactionTrigger int

// Level0SlowdownWritesTrigger — L0 file count that slows writes.
// Zero leaves the default.
Level0SlowdownWritesTrigger int

// Level0StopWritesTrigger — L0 file count that stalls writes
// entirely.
// entirely. Zero leaves the default.
Level0StopWritesTrigger int

// DisableAutoCompactions turns automatic compaction off per CF —
// for write-once, point-lookup stores where compaction would
// rewrite the same data with no reordering benefit.
// DisableAutoCompactions turns automatic compaction off for this CF —
// for write-once, point-lookup CFs where compaction would rewrite the
// same data with no reordering benefit.
DisableAutoCompactions bool

// TargetFileSizeMB — size at which compaction produces new SSTs.
// Zero leaves the default.
TargetFileSizeMB int

// MaxBytesForLevelBaseMB — byte budget for level 1; each later
// level is this × MaxBytesForLevelMultiplier (10, pinned).
// level is this × MaxBytesForLevelMultiplier (10, pinned). Zero
// leaves the default.
MaxBytesForLevelBaseMB int

// BloomFilterBitsPerKey installs this CF's bloom filter. 0 = no
// filter (the default) — right for a CF that is never probed for
// keys it may not hold. Positive values install one with that many
// bits; typical: 10 (~1% false positive), 12 (~0.4%).
BloomFilterBitsPerKey int
}

// Tuning — DB-wide RocksDB knobs shared across every CF of one store. Zero
// means "leave grocksdb's default alone" (wrapper skips the setter). Per-CF
// knobs (memtables, compaction, bloom, block size, compression) live in
// Config.PerCFOptions, not here.
//
// Wrapper-pinned per-CF values not exposed anywhere (applied to every CF):
// MinWriteBufferNumberToMerge=1, CompactionStyle=Level,
// TargetFileSizeMultiplier=1, MaxBytesForLevelMultiplier=10,
// Compression=None, WAL=on, per-write Sync=on.
type Tuning struct {
// MaxBackgroundJobs caps background threads for compactions
// and flushes combined. Orthogonal to DisableAutoCompactions:
// this rate-limits work, that turns compaction off entirely.
// and flushes combined, across the whole DB.
MaxBackgroundJobs int

// MaxOpenFiles caps concurrent open SST files.
// MaxOpenFiles caps concurrent open SST files, across the whole DB.
MaxOpenFiles int

// BlockCacheMB sizes the shared LRU block cache, per store.
// BlockCacheMB sizes the LRU block cache shared across every CF in
// the store.
BlockCacheMB int

// BloomFilterBitsPerKey — per-CF bloom filter. 0 = no filter
// installed; positive values install one with that many bits.
// Typical: 10 (~1% false positive), 12 (~0.4%).
BloomFilterBitsPerKey int

// MaxTotalWalSizeMB caps total live WAL size. Crash-recovery
// replay scales with this cap; graceful Close drains the
// memtable so this only bounds ungraceful shutdowns.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"context"
"fmt"
"iter"
"maps"
"slices"
"time"

Expand Down Expand Up @@ -54,18 +55,22 @@ func ColumnFamilies() []string {
return slices.Concat(ledger.CFNames(), eventstore.CFNames(), txhash.CFNames())
}

// config builds the shared store's rocksdb.Config: events' per-CF options (ZSTD
// on DataCF, tuned block sizes) plus the txhash workload's Tuning. Tuning's
// per-CF fields apply to every CF — a benign over-application (ledger/events CFs
// just gain a bloom + larger write buffer); the per-CF overrides keep events
// distinct.
// config builds the shared store's rocksdb.Config: the DB-wide Tuning
// (block cache, WAL cap, background jobs) from txhash — the only facade
// that historically set them — plus the per-CF options merged from every
// facade. txhash's per-CF knobs (bloom, write buffers, compaction) land on
// the txhash CF alone; events' overrides (ZSTD, block sizes) on its three
// CFs; the ledgers CF rides on RocksDB defaults with no bloom, matching each
// store's pre-unification standalone tuning.
func config(path string, logger *supportlog.Entry, readOnly, mustExist bool) rocksdb.Config {
perCF := eventstore.CFOptions()
maps.Copy(perCF, txhash.CFOptions())
return rocksdb.Config{
Path: path,
ColumnFamilies: ColumnFamilies(),
Logger: logger,
Tuning: txhash.Tuning(),
PerCFOptions: eventstore.CFOptions(),
PerCFOptions: perCF,
ReadOnly: readOnly,
MustExist: mustExist,
}
Expand Down
Loading
Loading