Skip to content

Commit ca2b210

Browse files
committed
db: strictly enforce prefix in [flushable]BatchIter.SeekPrefixGE
Every key we avoid propagating up the iterator stack avoids a key comparison during initialization of the merging iterator heap. Informs #3794.
1 parent 44e5fb4 commit ca2b210

File tree

4 files changed

+116
-4
lines changed

4 files changed

+116
-4
lines changed

batch.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package pebble
66

77
import (
8+
"bytes"
89
"context"
910
"encoding/binary"
1011
"fmt"
@@ -1686,7 +1687,16 @@ func (i *batchIter) SeekGE(key []byte, flags base.SeekGEFlags) *base.InternalKV
16861687
}
16871688

16881689
func (i *batchIter) SeekPrefixGE(prefix, key []byte, flags base.SeekGEFlags) *base.InternalKV {
1689-
return i.SeekGE(key, flags)
1690+
kv := i.SeekGE(key, flags)
1691+
if kv == nil {
1692+
return nil
1693+
}
1694+
// If the key doesn't have the sought prefix, return nil.
1695+
if !bytes.Equal(i.batch.comparer.Split.Prefix(kv.K.UserKey), prefix) {
1696+
i.kv = base.InternalKV{}
1697+
return nil
1698+
}
1699+
return kv
16901700
}
16911701

16921702
func (i *batchIter) SeekLT(key []byte, flags base.SeekLTFlags) *base.InternalKV {
@@ -2160,7 +2170,15 @@ func (i *flushableBatchIter) SeekGE(key []byte, flags base.SeekGEFlags) *base.In
21602170
func (i *flushableBatchIter) SeekPrefixGE(
21612171
prefix, key []byte, flags base.SeekGEFlags,
21622172
) *base.InternalKV {
2163-
return i.SeekGE(key, flags)
2173+
kv := i.SeekGE(key, flags)
2174+
if kv == nil {
2175+
return nil
2176+
}
2177+
// If the key doesn't have the sought prefix, return nil.
2178+
if !bytes.Equal(i.batch.comparer.Split.Prefix(kv.K.UserKey), prefix) {
2179+
return nil
2180+
}
2181+
return kv
21642182
}
21652183

21662184
// SeekLT implements internalIterator.SeekLT, as documented in the pebble

data_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ func printIterState(
339339
hasPoint, hasRange := iter.HasPointAndRange()
340340
fmt.Fprintf(b, "%s:%s (", iter.Key(), validityStateStr)
341341
if hasPoint {
342-
fmt.Fprintf(b, "%s, ", iter.Value())
342+
fmt.Fprintf(b, "%s, ", formatASCIIValue(iter.Value()))
343343
} else {
344344
fmt.Fprint(b, "., ")
345345
}
@@ -388,13 +388,24 @@ func formatASCIIKey(b []byte) string {
388388
return string(b)
389389
}
390390

391+
func formatASCIIValue(b []byte) string {
392+
if len(b) > 1<<10 {
393+
return fmt.Sprintf("[LARGE VALUE len=%d]", len(b))
394+
}
395+
if bytes.IndexFunc(b, func(r rune) bool { return r < '!' || r > 'z' }) != -1 {
396+
// This key is not just legible ASCII characters. Quote it.
397+
return fmt.Sprintf("%q", b)
398+
}
399+
return string(b)
400+
}
401+
391402
func writeRangeKeys(b io.Writer, iter *Iterator) {
392403
rangeKeys := iter.RangeKeys()
393404
for j := 0; j < len(rangeKeys); j++ {
394405
if j > 0 {
395406
fmt.Fprint(b, ",")
396407
}
397-
fmt.Fprintf(b, " %s=%s", rangeKeys[j].Suffix, rangeKeys[j].Value)
408+
fmt.Fprintf(b, " %s=%s", rangeKeys[j].Suffix, formatASCIIValue(rangeKeys[j].Value))
398409
}
399410
}
400411

iterator_histories_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ func TestIterHistories(t *testing.T) {
192192
return err.Error()
193193
}
194194
return ""
195+
case "disable-flushes":
196+
d.mu.Lock()
197+
d.mu.compact.flushing = true
198+
d.mu.Unlock()
199+
return ""
200+
case "enable-flushes":
201+
d.mu.Lock()
202+
d.mu.compact.flushing = false
203+
d.mu.Unlock()
204+
return ""
195205
case "get":
196206
var reader Reader = d
197207
if arg, ok := td.Arg("reader"); ok {

testdata/iter_histories/prefix_iteration

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,76 @@ stats
382382
----
383383
.
384384
stats: seeked 1 times (1 internal); stepped 0 times (0 internal)
385+
386+
# Test that a prefix seek through a batch iterator that enforces the prefix
387+
# strictly.
388+
389+
reset
390+
----
391+
392+
batch name=foo
393+
set b@1 b@1
394+
set d@9 d@9
395+
set g@4 g@4
396+
set e@2 e@2
397+
----
398+
wrote 4 keys to batch "foo"
399+
400+
# The stats should indicate only 3 KVs were ever surfaced to the merging iterator.
401+
402+
combined-iter reader=foo name=fooiter
403+
seek-prefix-ge b@10
404+
seek-prefix-ge c@10
405+
seek-prefix-ge d@10
406+
seek-prefix-ge g@2
407+
seek-prefix-ge e@2
408+
stats
409+
----
410+
b@1: (b@1, .)
411+
.
412+
d@9: (d@9, .)
413+
.
414+
e@2: (e@2, .)
415+
stats: seeked 5 times (5 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 3 (9B keys, 9B values)
416+
417+
# Test the above case but with a large committed batch (which should be a
418+
# flushableBatchIter).
419+
420+
define memtable-size=65536
421+
----
422+
423+
# We diable flushes to avoid scheduling a flush that might race with our
424+
# iterator. If the iterator observed the state after the large batch has been
425+
# flushed to sstables, we would see nonzero block bytes appear in the iterator
426+
# stats.
427+
disable-flushes
428+
----
429+
430+
batch commit
431+
set b@1 <rand-bytes=10000>
432+
set d@9 <rand-bytes=10000>
433+
set g@4 <rand-bytes=10000>
434+
set e@2 <rand-bytes=10000>
435+
----
436+
committed 4 keys
437+
438+
lsm
439+
----
440+
441+
combined-iter
442+
seek-prefix-ge b@10
443+
seek-prefix-ge c@10
444+
seek-prefix-ge d@10
445+
seek-prefix-ge g@2
446+
seek-prefix-ge e@2
447+
stats
448+
----
449+
b@1: ([LARGE VALUE len=10000], .)
450+
.
451+
d@9: ([LARGE VALUE len=10000], .)
452+
.
453+
e@2: ([LARGE VALUE len=10000], .)
454+
stats: seeked 5 times (5 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 3 (9B keys, 29KB values)
455+
456+
enable-flushes
457+
----

0 commit comments

Comments
 (0)