Skip to content

Commit 2d7040f

Browse files
committed
manifest: improve API around TableStats
Using `TableMetadata.TableStats` is very fragile; any code that uses it must first check that the stats are valid. Code that doesn't do so relies on subtle higher level synchronization assumptions. We improve this with a simple change: we unexport the stats and add an getter which only returns the `*TableStats` if they have been populated, and a setter with can be called at most once. All access through these methods is safe (without assumptions about any external locks), as long as we populate the stats only once.
1 parent 7d7cc28 commit 2d7040f

13 files changed

+232
-190
lines changed

compaction.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2810,10 +2810,12 @@ func (d *DB) runCopyCompaction(
28102810
SmallestSeqNum: inputMeta.SmallestSeqNum,
28112811
LargestSeqNum: inputMeta.LargestSeqNum,
28122812
LargestSeqNumAbsolute: inputMeta.LargestSeqNumAbsolute,
2813-
Stats: inputMeta.Stats,
28142813
Virtual: inputMeta.Virtual,
28152814
SyntheticPrefixAndSuffix: inputMeta.SyntheticPrefixAndSuffix,
28162815
}
2816+
if inputStats, ok := inputMeta.Stats(); ok {
2817+
newMeta.PopulateStats(inputStats)
2818+
}
28172819
if inputMeta.HasPointKeys {
28182820
newMeta.ExtendPointKeyBounds(c.comparer.Compare,
28192821
inputMeta.PointKeyBounds.Smallest(),

compaction_picker.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,10 @@ func (c *candidateLevelInfo) shouldCompact() bool {
701701
}
702702

703703
func tableTombstoneCompensation(t *manifest.TableMetadata) uint64 {
704-
return t.Stats.PointDeletionsBytesEstimate + t.Stats.RangeDeletionsBytesEstimate
704+
if stats, ok := t.Stats(); ok {
705+
return stats.PointDeletionsBytesEstimate + stats.RangeDeletionsBytesEstimate
706+
}
707+
return 0
705708
}
706709

707710
// tableCompensatedSize returns t's size, including an estimate of the physical
@@ -1199,7 +1202,9 @@ func pickCompactionSeedFile(
11991202
// out of the overlapping bytes helps prioritize these compactions
12001203
// that are cheaper than their file sizes suggest.
12011204
if outputLevel == numLevels-1 && outputFile.LargestSeqNum < earliestSnapshotSeqNum {
1202-
overlappingBytes -= outputFile.Stats.RangeDeletionsBytesEstimate
1205+
if stats, ok := outputFile.Stats(); ok {
1206+
overlappingBytes -= stats.RangeDeletionsBytesEstimate
1207+
}
12031208
}
12041209

12051210
// If the file in the next level extends beyond f's largest key,
@@ -1550,7 +1555,8 @@ var elisionOnlyAnnotator = &manifest.Annotator[manifest.TableMetadata]{
15501555
if f.IsCompacting() {
15511556
return false, true
15521557
}
1553-
if !f.StatsValid() {
1558+
stats, statsValid := f.Stats()
1559+
if !statsValid {
15541560
return false, false
15551561
}
15561562
// Bottommost files are large and not worthwhile to compact just
@@ -1567,7 +1573,7 @@ var elisionOnlyAnnotator = &manifest.Annotator[manifest.TableMetadata]{
15671573
// `NumEntries` and `RangeDeletionsBytesEstimate` are both zero) are excluded
15681574
// from elision-only compactions.
15691575
// TODO(travers): Consider an alternative heuristic for elision of range-keys.
1570-
return f.Stats.RangeDeletionsBytesEstimate*10 >= f.Size || f.Stats.NumDeletions*10 > f.Stats.NumEntries, true
1576+
return stats.RangeDeletionsBytesEstimate*10 >= f.Size || stats.NumDeletions*10 > stats.NumEntries, true
15711577
},
15721578
Compare: func(f1 *manifest.TableMetadata, f2 *manifest.TableMetadata) bool {
15731579
return f1.LargestSeqNum < f2.LargestSeqNum
@@ -1733,6 +1739,7 @@ func (p *compactionPickerByScore) pickTombstoneDensityCompaction(
17331739
}
17341740

17351741
var candidate *manifest.TableMetadata
1742+
var candidateTombstoneDenseBlocksRatio float64
17361743
var level int
17371744
// If a candidate file has a very high overlapping ratio, point tombstones
17381745
// in it are likely sparse in keyspace even if the sstable itself is tombstone
@@ -1747,18 +1754,20 @@ func (p *compactionPickerByScore) pickTombstoneDensityCompaction(
17471754
for l := numLevels - 2; l >= 0; l-- {
17481755
iter := p.vers.Levels[l].Iter()
17491756
for f := iter.First(); f != nil; f = iter.Next() {
1750-
if f.IsCompacting() || !f.StatsValid() || f.Size == 0 {
1757+
if f.IsCompacting() || f.Size == 0 {
17511758
continue
17521759
}
1753-
if f.Stats.TombstoneDenseBlocksRatio < p.opts.Experimental.TombstoneDenseCompactionThreshold {
1760+
stats, statsValid := f.Stats()
1761+
if !statsValid || stats.TombstoneDenseBlocksRatio < p.opts.Experimental.TombstoneDenseCompactionThreshold {
17541762
continue
17551763
}
17561764
overlaps := p.vers.Overlaps(lastNonEmptyLevel, f.UserKeyBounds())
17571765
if float64(overlaps.AggregateSizeSum())/float64(f.Size) > maxOverlappingRatio {
17581766
continue
17591767
}
1760-
if candidate == nil || candidate.Stats.TombstoneDenseBlocksRatio < f.Stats.TombstoneDenseBlocksRatio {
1768+
if candidate == nil || candidateTombstoneDenseBlocksRatio < stats.TombstoneDenseBlocksRatio {
17611769
candidate = f
1770+
candidateTombstoneDenseBlocksRatio = stats.TombstoneDenseBlocksRatio
17621771
level = l
17631772
}
17641773
}

compaction_picker_test.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ func loadVersion(
7272
}
7373
}
7474

75-
var lastFile *manifest.TableMetadata
7675
for i := uint64(1); sizes[level] < int64(size); i++ {
7776
var key InternalKey
7877
if level == 0 {
@@ -81,19 +80,15 @@ func loadVersion(
8180
} else {
8281
key = base.MakeInternalKey([]byte(fmt.Sprintf("%04d", i)), base.SeqNum(i), InternalKeyKindSet)
8382
}
84-
m := (&manifest.TableMetadata{
83+
m := &manifest.TableMetadata{
8584
TableNum: base.TableNum(uint64(level)*100_000 + i),
8685
SmallestSeqNum: key.SeqNum(),
8786
LargestSeqNum: key.SeqNum(),
8887
LargestSeqNumAbsolute: key.SeqNum(),
8988
Size: 1,
90-
Stats: manifest.TableStats{
91-
RangeDeletionsBytesEstimate: 0,
92-
},
93-
}).ExtendPointKeyBounds(opts.Comparer.Compare, key, key)
89+
}
90+
m.ExtendPointKeyBounds(opts.Comparer.Compare, key, key)
9491
m.InitPhysicalBacking()
95-
m.StatsMarkValid()
96-
lastFile = m
9792
if size >= 100 {
9893
// If the requested size of the level is very large only add a single
9994
// file in order to avoid massive blow-up in the number of files in
@@ -108,13 +103,14 @@ func loadVersion(
108103
m.ExtendPointKeyBounds(opts.Comparer.Compare, key, endKey)
109104
}
110105
}
106+
if compensation > 0 {
107+
m.PopulateStats(&manifest.TableStats{
108+
RangeDeletionsBytesEstimate: compensation,
109+
})
110+
}
111111
files[level] = append(files[level], m)
112112
sizes[level] += int64(m.Size)
113113
}
114-
// Let all the compensation be due to the last file.
115-
if lastFile != nil && compensation > 0 {
116-
lastFile.Stats.RangeDeletionsBytesEstimate = compensation
117-
}
118114
}
119115
}
120116

@@ -139,10 +135,11 @@ func parseTableMeta(t *testing.T, s string, opts *Options) (*manifest.TableMetad
139135
if len(parts) != 2 {
140136
return nil, errors.Errorf("malformed table spec: %s. usage: <optional-file-num>:start.SET.1-end.SET.2", s)
141137
}
142-
m := (&manifest.TableMetadata{
138+
m := &manifest.TableMetadata{
143139
TableNum: tableNum,
144140
Size: 1028,
145-
}).ExtendPointKeyBounds(
141+
}
142+
m.ExtendPointKeyBounds(
146143
opts.Comparer.Compare,
147144
base.ParseInternalKey(strings.TrimSpace(parts[0])),
148145
base.ParseInternalKey(strings.TrimSpace(parts[1])),
@@ -160,9 +157,10 @@ func parseTableMeta(t *testing.T, s string, opts *Options) (*manifest.TableMetad
160157
if err != nil {
161158
return nil, err
162159
}
163-
m.Stats.RangeDeletionsBytesEstimate = uint64(v)
164-
m.Stats.NumDeletions = 1 // At least one range del responsible for the deletion bytes.
165-
m.StatsMarkValid()
160+
m.PopulateStats(&manifest.TableStats{
161+
RangeDeletionsBytesEstimate: uint64(v),
162+
NumDeletions: 1, // At least one range del responsible for the deletion bytes.
163+
})
166164
}
167165
}
168166
m.SmallestSeqNum = m.Smallest().SeqNum()
@@ -1306,8 +1304,10 @@ func TestCompactionPickerCompensatedSize(t *testing.T) {
13061304
t.Run("", func(t *testing.T) {
13071305
f := &manifest.TableMetadata{Size: tc.size}
13081306
f.InitPhysicalBacking()
1309-
f.Stats.PointDeletionsBytesEstimate = tc.pointDelEstimateBytes
1310-
f.Stats.RangeDeletionsBytesEstimate = tc.rangeDelEstimateBytes
1307+
f.PopulateStats(&manifest.TableStats{
1308+
PointDeletionsBytesEstimate: tc.pointDelEstimateBytes,
1309+
RangeDeletionsBytesEstimate: tc.rangeDelEstimateBytes,
1310+
})
13111311
gotBytes := tableCompensatedSize(f)
13121312
require.Equal(t, tc.wantBytes, gotBytes)
13131313
})

compaction_test.go

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3390,18 +3390,17 @@ func TestTombstoneDensityCompactionMoveOptimization(t *testing.T) {
33903390
meta := &manifest.TableMetadata{
33913391
TableNum: 1,
33923392
Size: 1024,
3393-
Stats: manifest.TableStats{
3394-
NumEntries: 10,
3395-
NumDeletions: 8,
3396-
TombstoneDenseBlocksRatio: 0.9, // Above threshold
3397-
},
33983393
}
3394+
meta.PopulateStats(&manifest.TableStats{
3395+
NumEntries: 10,
3396+
NumDeletions: 8,
3397+
TombstoneDenseBlocksRatio: 0.9, // Above threshold
3398+
})
33993399
meta.ExtendPointKeyBounds(opts.Comparer.Compare,
34003400
base.ParseInternalKey("a.SET.1"),
34013401
base.ParseInternalKey("z.SET.2"),
34023402
)
34033403
meta.InitPhysicalBacking()
3404-
meta.StatsMarkValid()
34053404

34063405
// Set up the version: L4 has the file, L5 and L6 are empty.
34073406
var files [numLevels][]*manifest.TableMetadata
@@ -3490,18 +3489,17 @@ func TestTombstoneDensityCompactionMoveOptimization_NoMoveWithOverlap(t *testing
34903489
metaL4 := &manifest.TableMetadata{
34913490
TableNum: 1,
34923491
Size: 1024,
3493-
Stats: manifest.TableStats{
3494-
NumEntries: 10,
3495-
NumDeletions: 8,
3496-
TombstoneDenseBlocksRatio: 0.9, // Above threshold
3497-
},
34983492
}
3493+
metaL4.PopulateStats(&manifest.TableStats{
3494+
NumEntries: 10,
3495+
NumDeletions: 8,
3496+
TombstoneDenseBlocksRatio: 0.9, // Above threshold
3497+
})
34993498
metaL4.ExtendPointKeyBounds(opts.Comparer.Compare,
35003499
base.ParseInternalKey("a.SET.1"),
35013500
base.ParseInternalKey("z.SET.2"),
35023501
)
35033502
metaL4.InitPhysicalBacking()
3504-
metaL4.StatsMarkValid()
35053503

35063504
// Create an overlapping file in L5.
35073505
metaL5 := &manifest.TableMetadata{
@@ -3513,7 +3511,7 @@ func TestTombstoneDensityCompactionMoveOptimization_NoMoveWithOverlap(t *testing
35133511
base.ParseInternalKey("z.SET.3"),
35143512
)
35153513
metaL5.InitPhysicalBacking()
3516-
metaL5.StatsMarkValid()
3514+
metaL5.PopulateStats(&manifest.TableStats{})
35173515

35183516
// Set up the version: L4 has metaL4, L5 has metaL5.
35193517
var files [numLevels][]*manifest.TableMetadata
@@ -3571,18 +3569,17 @@ func TestTombstoneDensityCompactionMoveOptimization_GrandparentOverlapTooLarge(t
35713569
metaL4 := &manifest.TableMetadata{
35723570
TableNum: 1,
35733571
Size: 1024,
3574-
Stats: manifest.TableStats{
3575-
NumEntries: 10,
3576-
NumDeletions: 8,
3577-
TombstoneDenseBlocksRatio: 0.9,
3578-
},
35793572
}
3573+
metaL4.PopulateStats(&manifest.TableStats{
3574+
NumEntries: 10,
3575+
NumDeletions: 8,
3576+
TombstoneDenseBlocksRatio: 0.9,
3577+
})
35803578
metaL4.ExtendPointKeyBounds(opts.Comparer.Compare,
35813579
base.ParseInternalKey("a.SET.1"),
35823580
base.ParseInternalKey("z.SET.2"),
35833581
)
35843582
metaL4.InitPhysicalBacking()
3585-
metaL4.StatsMarkValid()
35863583

35873584
// Large overlapping file in L6 (grandparent level).
35883585
metaL6 := &manifest.TableMetadata{
@@ -3594,7 +3591,7 @@ func TestTombstoneDensityCompactionMoveOptimization_GrandparentOverlapTooLarge(t
35943591
base.ParseInternalKey("z.SET.3"),
35953592
)
35963593
metaL6.InitPhysicalBacking()
3597-
metaL6.StatsMarkValid()
3594+
metaL6.PopulateStats(&manifest.TableStats{})
35983595

35993596
var files [numLevels][]*manifest.TableMetadata
36003597
files[inputLevel] = []*manifest.TableMetadata{metaL4}
@@ -3634,18 +3631,17 @@ func TestTombstoneDensityCompactionMoveOptimization_BelowDensityThreshold(t *tes
36343631
meta := &manifest.TableMetadata{
36353632
TableNum: 1,
36363633
Size: 1024,
3637-
Stats: manifest.TableStats{
3638-
NumEntries: 10,
3639-
NumDeletions: 5,
3640-
TombstoneDenseBlocksRatio: 0.5, // Below threshold
3641-
},
36423634
}
3635+
meta.PopulateStats(&manifest.TableStats{
3636+
NumEntries: 10,
3637+
NumDeletions: 5,
3638+
TombstoneDenseBlocksRatio: 0.5, // Below threshold
3639+
})
36433640
meta.ExtendPointKeyBounds(opts.Comparer.Compare,
36443641
base.ParseInternalKey("a.SET.1"),
36453642
base.ParseInternalKey("z.SET.2"),
36463643
)
36473644
meta.InitPhysicalBacking()
3648-
meta.StatsMarkValid()
36493645

36503646
var files [numLevels][]*manifest.TableMetadata
36513647
files[inputLevel] = []*manifest.TableMetadata{meta}

data_test.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,28 +1233,33 @@ func runTableStatsCmd(td *datadriven.TestData, d *DB) string {
12331233
continue
12341234
}
12351235

1236-
if !f.StatsValid() {
1236+
stats, statsValid := f.Stats()
1237+
if !statsValid {
12371238
d.waitTableStats()
1239+
stats, statsValid = f.Stats()
1240+
if !statsValid {
1241+
panic("stats not valid after waitTableStats")
1242+
}
12381243
}
12391244

12401245
var b bytes.Buffer
1241-
fmt.Fprintf(&b, "num-entries: %d\n", f.Stats.NumEntries)
1242-
fmt.Fprintf(&b, "num-deletions: %d\n", f.Stats.NumDeletions)
1243-
fmt.Fprintf(&b, "num-range-key-sets: %d\n", f.Stats.NumRangeKeySets)
1244-
fmt.Fprintf(&b, "point-deletions-bytes-estimate: %d\n", f.Stats.PointDeletionsBytesEstimate)
1245-
fmt.Fprintf(&b, "range-deletions-bytes-estimate: %d\n", f.Stats.RangeDeletionsBytesEstimate)
1246+
fmt.Fprintf(&b, "num-entries: %d\n", stats.NumEntries)
1247+
fmt.Fprintf(&b, "num-deletions: %d\n", stats.NumDeletions)
1248+
fmt.Fprintf(&b, "num-range-key-sets: %d\n", stats.NumRangeKeySets)
1249+
fmt.Fprintf(&b, "point-deletions-bytes-estimate: %d\n", stats.PointDeletionsBytesEstimate)
1250+
fmt.Fprintf(&b, "range-deletions-bytes-estimate: %d\n", stats.RangeDeletionsBytesEstimate)
12461251

12471252
// If requested, override the tombstone density ratio for testing
12481253
if forceTombstoneDensityRatio >= 0 {
1249-
f.Stats.TombstoneDenseBlocksRatio = forceTombstoneDensityRatio
1254+
stats.TombstoneDenseBlocksRatio = forceTombstoneDensityRatio
12501255
}
12511256
// Only include the tombstone-dense-blocks-ratio if it was forced or is non-zero
1252-
if forceTombstoneDensityRatio >= 0 || f.Stats.TombstoneDenseBlocksRatio > 0 {
1253-
fmt.Fprintf(&b, "tombstone-dense-blocks-ratio: %0.1f\n", f.Stats.TombstoneDenseBlocksRatio)
1257+
if forceTombstoneDensityRatio >= 0 || stats.TombstoneDenseBlocksRatio > 0 {
1258+
fmt.Fprintf(&b, "tombstone-dense-blocks-ratio: %0.1f\n", stats.TombstoneDenseBlocksRatio)
12541259
}
12551260

1256-
if !f.Stats.CompressionStats.IsEmpty() {
1257-
fmt.Fprintf(&b, "compression: %s\n", f.Stats.CompressionStats.String())
1261+
if !stats.CompressionStats.IsEmpty() {
1262+
fmt.Fprintf(&b, "compression: %s\n", stats.CompressionStats.String())
12581263
}
12591264

12601265
return b.String()

db.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2350,13 +2350,11 @@ func (d *DB) SSTables(opts ...SSTablesOption) ([][]SSTableInfo, error) {
23502350
continue
23512351
}
23522352
}
2353-
var tableStats manifest.TableStats
2354-
if m.StatsValid() {
2355-
tableStats = m.Stats
2356-
}
23572353
destTables[j] = SSTableInfo{
2358-
TableInfo: m.TableInfo(),
2359-
TableStats: tableStats,
2354+
TableInfo: m.TableInfo(),
2355+
}
2356+
if stats, ok := m.Stats(); ok {
2357+
destTables[j].TableStats = *stats
23602358
}
23612359
if opt.withProperties {
23622360
p, err := d.fileCache.getTableProperties(

file_cache_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ func TestVirtualReadsWiring(t *testing.T) {
358358
base.MakeInternalKey([]byte{'a'}, seqNumA, InternalKeyKindSet))
359359
v1.ExtendPointKeyBounds(DefaultComparer.Compare, v1.PointKeyBounds.Smallest(), v1.PointKeyBounds.Largest())
360360
v1.AttachVirtualBacking(parentFile.TableBacking)
361-
v1.Stats.NumEntries = 1
361+
v1.PopulateStats(&manifest.TableStats{NumEntries: 1})
362362

363363
v2 := &manifest.TableMetadata{
364364
TableNum: f2,
@@ -378,7 +378,7 @@ func TestVirtualReadsWiring(t *testing.T) {
378378
base.MakeInternalKey([]byte{'k'}, seqNumRangeUnset, InternalKeyKindRangeKeyUnset))
379379
v2.ExtendPointKeyBounds(DefaultComparer.Compare, v2.PointKeyBounds.Smallest(), v2.PointKeyBounds.Largest())
380380
v2.AttachVirtualBacking(parentFile.TableBacking)
381-
v2.Stats.NumEntries = 6
381+
v2.PopulateStats(&manifest.TableStats{NumEntries: 6})
382382

383383
v1.PointKeyBounds.SetInternalKeyBounds(v1.Smallest(), v1.Largest())
384384

0 commit comments

Comments
 (0)