github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/sstable/writer_test.go (about) 1 // Copyright 2018 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package sstable 6 7 import ( 8 "bytes" 9 "encoding/binary" 10 "fmt" 11 "math/rand" 12 "strconv" 13 "strings" 14 "sync" 15 "testing" 16 17 "github.com/cockroachdb/errors" 18 "github.com/stretchr/testify/require" 19 "github.com/zuoyebang/bitalostable/bloom" 20 "github.com/zuoyebang/bitalostable/internal/base" 21 "github.com/zuoyebang/bitalostable/internal/cache" 22 "github.com/zuoyebang/bitalostable/internal/datadriven" 23 "github.com/zuoyebang/bitalostable/internal/humanize" 24 "github.com/zuoyebang/bitalostable/internal/testkeys" 25 "github.com/zuoyebang/bitalostable/vfs" 26 ) 27 28 func TestWriter(t *testing.T) { 29 runDataDriven(t, "testdata/writer", false) 30 } 31 32 func TestRewriter(t *testing.T) { 33 runDataDriven(t, "testdata/rewriter", false) 34 } 35 36 func TestWriterParallel(t *testing.T) { 37 runDataDriven(t, "testdata/writer", true) 38 } 39 40 func TestRewriterParallel(t *testing.T) { 41 runDataDriven(t, "testdata/rewriter", true) 42 } 43 44 func runDataDriven(t *testing.T, file string, parallelism bool) { 45 var r *Reader 46 defer func() { 47 if r != nil { 48 require.NoError(t, r.Close()) 49 } 50 }() 51 formatVersion := TableFormatMax 52 53 format := func(m *WriterMetadata) string { 54 var b bytes.Buffer 55 if m.HasPointKeys { 56 fmt.Fprintf(&b, "point: [%s-%s]\n", m.SmallestPoint, m.LargestPoint) 57 } 58 if m.HasRangeDelKeys { 59 fmt.Fprintf(&b, "rangedel: [%s-%s]\n", m.SmallestRangeDel, m.LargestRangeDel) 60 } 61 if m.HasRangeKeys { 62 fmt.Fprintf(&b, "rangekey: [%s-%s]\n", m.SmallestRangeKey, m.LargestRangeKey) 63 } 64 fmt.Fprintf(&b, "seqnums: [%d-%d]\n", m.SmallestSeqNum, m.LargestSeqNum) 65 return b.String() 66 } 67 68 datadriven.RunTest(t, file, func(td *datadriven.TestData) string { 69 switch td.Cmd { 70 case "build": 71 if r != nil { 72 _ = r.Close() 73 r = nil 74 } 75 var meta *WriterMetadata 76 var err error 77 meta, r, err = runBuildCmd(td, &WriterOptions{ 78 TableFormat: formatVersion, 79 Parallelism: parallelism, 80 }, 0) 81 if err != nil { 82 return err.Error() 83 } 84 return format(meta) 85 86 case "build-raw": 87 if r != nil { 88 _ = r.Close() 89 r = nil 90 } 91 var meta *WriterMetadata 92 var err error 93 meta, r, err = runBuildRawCmd(td, &WriterOptions{ 94 TableFormat: formatVersion, 95 }) 96 if err != nil { 97 return err.Error() 98 } 99 return format(meta) 100 101 case "scan": 102 origIter, err := r.NewIter(nil /* lower */, nil /* upper */) 103 if err != nil { 104 return err.Error() 105 } 106 iter := newIterAdapter(origIter) 107 defer iter.Close() 108 109 var buf bytes.Buffer 110 for valid := iter.First(); valid; valid = iter.Next() { 111 fmt.Fprintf(&buf, "%s:%s\n", iter.Key(), iter.Value()) 112 } 113 return buf.String() 114 115 case "get": 116 var buf bytes.Buffer 117 for _, k := range strings.Split(td.Input, "\n") { 118 value, err := r.get([]byte(k)) 119 if err != nil { 120 fmt.Fprintf(&buf, "get %s: %s\n", k, err.Error()) 121 } else { 122 fmt.Fprintf(&buf, "%s\n", value) 123 } 124 } 125 return buf.String() 126 127 case "scan-range-del": 128 iter, err := r.NewRawRangeDelIter() 129 if err != nil { 130 return err.Error() 131 } 132 if iter == nil { 133 return "" 134 } 135 defer iter.Close() 136 137 var buf bytes.Buffer 138 for s := iter.First(); s != nil; s = iter.Next() { 139 fmt.Fprintf(&buf, "%s\n", s) 140 } 141 return buf.String() 142 143 case "scan-range-key": 144 iter, err := r.NewRawRangeKeyIter() 145 if err != nil { 146 return err.Error() 147 } 148 if iter == nil { 149 return "" 150 } 151 defer iter.Close() 152 153 var buf bytes.Buffer 154 for s := iter.First(); s != nil; s = iter.Next() { 155 fmt.Fprintf(&buf, "%s\n", s) 156 } 157 return buf.String() 158 159 case "layout": 160 l, err := r.Layout() 161 if err != nil { 162 return err.Error() 163 } 164 verbose := false 165 if len(td.CmdArgs) > 0 { 166 if td.CmdArgs[0].Key == "verbose" { 167 verbose = true 168 } else { 169 return "unknown arg" 170 } 171 } 172 var buf bytes.Buffer 173 l.Describe(&buf, verbose, r, nil) 174 return buf.String() 175 176 case "rewrite": 177 var meta *WriterMetadata 178 var err error 179 meta, r, err = runRewriteCmd(td, r, WriterOptions{ 180 TableFormat: formatVersion, 181 }) 182 if err != nil { 183 return err.Error() 184 } 185 if err != nil { 186 return err.Error() 187 } 188 return format(meta) 189 190 default: 191 return fmt.Sprintf("unknown command: %s", td.Cmd) 192 } 193 }) 194 } 195 196 func testBlockBufClear(t *testing.T, b1, b2 *blockBuf) { 197 require.Equal(t, b1.tmp, b2.tmp) 198 } 199 200 func TestBlockBufClear(t *testing.T) { 201 b1 := &blockBuf{} 202 b1.tmp[0] = 1 203 b1.compressedBuf = make([]byte, 1) 204 b1.clear() 205 testBlockBufClear(t, b1, &blockBuf{}) 206 } 207 208 func TestClearDataBlockBuf(t *testing.T) { 209 d := newDataBlockBuf(1, ChecksumTypeCRC32c) 210 d.blockBuf.compressedBuf = make([]byte, 1) 211 d.dataBlock.add(ikey("apple"), nil) 212 d.dataBlock.add(ikey("banana"), nil) 213 214 d.clear() 215 testBlockCleared(t, &d.dataBlock, &blockWriter{}) 216 testBlockBufClear(t, &d.blockBuf, &blockBuf{}) 217 218 dataBlockBufPool.Put(d) 219 } 220 221 func TestClearIndexBlockBuf(t *testing.T) { 222 i := newIndexBlockBuf(false) 223 i.block.add(ikey("apple"), nil) 224 i.block.add(ikey("banana"), nil) 225 i.clear() 226 227 testBlockCleared(t, &i.block, &blockWriter{}) 228 require.Equal( 229 t, i.size.estimate, sizeEstimate{emptySize: i.size.estimate.emptySize}, 230 ) 231 indexBlockBufPool.Put(i) 232 } 233 234 func TestClearWriteTask(t *testing.T) { 235 w := writeTaskPool.Get().(*writeTask) 236 ch := make(chan bool, 1) 237 w.compressionDone = ch 238 w.buf = &dataBlockBuf{} 239 w.flushableIndexBlock = &indexBlockBuf{} 240 w.currIndexBlock = &indexBlockBuf{} 241 w.indexEntrySep = ikey("apple") 242 w.inflightSize = 1 243 w.indexInflightSize = 1 244 w.finishedIndexProps = []byte{'a', 'v'} 245 246 w.clear() 247 248 var nilDataBlockBuf *dataBlockBuf 249 var nilIndexBlockBuf *indexBlockBuf 250 // Channels should be the same(no new channel should be allocated) 251 require.Equal(t, w.compressionDone, ch) 252 require.Equal(t, w.buf, nilDataBlockBuf) 253 require.Equal(t, w.flushableIndexBlock, nilIndexBlockBuf) 254 require.Equal(t, w.currIndexBlock, nilIndexBlockBuf) 255 require.Equal(t, w.indexEntrySep, base.InvalidInternalKey) 256 require.Equal(t, w.inflightSize, 0) 257 require.Equal(t, w.indexInflightSize, 0) 258 require.Equal(t, w.finishedIndexProps, []byte(nil)) 259 260 writeTaskPool.Put(w) 261 } 262 263 func TestDoubleClose(t *testing.T) { 264 // There is code in Cockroach land which relies on Writer.Close being 265 // idempotent. We should test this in Pebble, so that we don't cause 266 // Cockroach test failures. 267 f := &discardFile{} 268 w := NewWriter(f, WriterOptions{ 269 BlockSize: 1, 270 TableFormat: TableFormatPebblev1, 271 }) 272 w.Set(ikey("a").UserKey, nil) 273 w.Set(ikey("b").UserKey, nil) 274 err := w.Close() 275 require.NoError(t, err) 276 err = w.Close() 277 require.Equal(t, err, errWriterClosed) 278 } 279 280 func TestParallelWriterErrorProp(t *testing.T) { 281 fs := vfs.NewMem() 282 f, err := fs.Create("test") 283 require.NoError(t, err) 284 opts := WriterOptions{ 285 TableFormat: TableFormatPebblev1, BlockSize: 1, Parallelism: true, 286 } 287 288 w := NewWriter(f, opts) 289 // Directly testing this, because it's difficult to get the Writer to 290 // encounter an error, precisely when the writeQueue is doing block writes. 291 w.coordination.writeQueue.err = errors.New("write queue write error") 292 w.Set(ikey("a").UserKey, nil) 293 w.Set(ikey("b").UserKey, nil) 294 err = w.Close() 295 require.Equal(t, err.Error(), "write queue write error") 296 } 297 298 func TestSizeEstimate(t *testing.T) { 299 var sizeEstimate sizeEstimate 300 datadriven.RunTest(t, "testdata/size_estimate", 301 func(td *datadriven.TestData) string { 302 switch td.Cmd { 303 case "init": 304 if len(td.CmdArgs) != 1 { 305 return "init <empty size>" 306 } 307 emptySize, err := strconv.Atoi(td.CmdArgs[0].String()) 308 if err != nil { 309 return "invalid empty size" 310 } 311 sizeEstimate.init(uint64(emptySize)) 312 return "success" 313 case "clear": 314 sizeEstimate.clear() 315 return fmt.Sprintf("%d", sizeEstimate.size()) 316 case "size": 317 return fmt.Sprintf("%d", sizeEstimate.size()) 318 case "add_inflight": 319 if len(td.CmdArgs) != 1 { 320 return "add_inflight <inflight size estimate>" 321 } 322 inflightSize, err := strconv.Atoi(td.CmdArgs[0].String()) 323 if err != nil { 324 return "invalid inflight size" 325 } 326 sizeEstimate.addInflight(inflightSize) 327 return fmt.Sprintf("%d", sizeEstimate.size()) 328 case "entry_written": 329 if len(td.CmdArgs) != 3 { 330 return "entry_written <new_size> <prev_inflight_size> <entry_size>" 331 } 332 newSize, err := strconv.Atoi(td.CmdArgs[0].String()) 333 if err != nil { 334 return "invalid inflight size" 335 } 336 inflightSize, err := strconv.Atoi(td.CmdArgs[1].String()) 337 if err != nil { 338 return "invalid inflight size" 339 } 340 entrySize, err := strconv.Atoi(td.CmdArgs[2].String()) 341 if err != nil { 342 return "invalid inflight size" 343 } 344 sizeEstimate.written(uint64(newSize), inflightSize, entrySize) 345 return fmt.Sprintf("%d", sizeEstimate.size()) 346 case "num_written_entries": 347 return fmt.Sprintf("%d", sizeEstimate.numWrittenEntries) 348 case "num_inflight_entries": 349 return fmt.Sprintf("%d", sizeEstimate.numInflightEntries) 350 case "num_entries": 351 return fmt.Sprintf("%d", sizeEstimate.numWrittenEntries+sizeEstimate.numInflightEntries) 352 default: 353 return fmt.Sprintf("unknown command: %s", td.Cmd) 354 } 355 }) 356 } 357 func TestWriterClearCache(t *testing.T) { 358 // Verify that Writer clears the cache of blocks that it writes. 359 mem := vfs.NewMem() 360 opts := ReaderOptions{Cache: cache.New(64 << 20)} 361 defer opts.Cache.Unref() 362 363 writerOpts := WriterOptions{Cache: opts.Cache} 364 cacheOpts := &cacheOpts{cacheID: 1, fileNum: 1} 365 invalidData := func() *cache.Value { 366 invalid := []byte("invalid data") 367 v := opts.Cache.Alloc(len(invalid)) 368 copy(v.Buf(), invalid) 369 return v 370 } 371 372 build := func(name string) { 373 f, err := mem.Create(name) 374 require.NoError(t, err) 375 376 w := NewWriter(f, writerOpts, cacheOpts) 377 require.NoError(t, w.Set([]byte("hello"), []byte("world"))) 378 require.NoError(t, w.Close()) 379 } 380 381 // Build the sstable a first time so that we can determine the locations of 382 // all of the blocks. 383 build("test") 384 385 f, err := mem.Open("test") 386 require.NoError(t, err) 387 388 r, err := NewReader(f, opts) 389 require.NoError(t, err) 390 391 layout, err := r.Layout() 392 require.NoError(t, err) 393 394 foreachBH := func(layout *Layout, f func(bh BlockHandle)) { 395 for _, bh := range layout.Data { 396 f(bh.BlockHandle) 397 } 398 for _, bh := range layout.Index { 399 f(bh) 400 } 401 f(layout.TopIndex) 402 f(layout.Filter) 403 f(layout.RangeDel) 404 f(layout.Properties) 405 f(layout.MetaIndex) 406 } 407 408 // Poison the cache for each of the blocks. 409 poison := func(bh BlockHandle) { 410 opts.Cache.Set(cacheOpts.cacheID, cacheOpts.fileNum, bh.Offset, invalidData()).Release() 411 } 412 foreachBH(layout, poison) 413 414 // Build the table a second time. This should clear the cache for the blocks 415 // that are written. 416 build("test") 417 418 // Verify that the written blocks have been cleared from the cache. 419 check := func(bh BlockHandle) { 420 h := opts.Cache.Get(cacheOpts.cacheID, cacheOpts.fileNum, bh.Offset) 421 if h.Get() != nil { 422 t.Fatalf("%d: expected cache to be cleared, but found %q", bh.Offset, h.Get()) 423 } 424 } 425 foreachBH(layout, check) 426 427 require.NoError(t, r.Close()) 428 } 429 430 type discardFile struct{ wrote int64 } 431 432 func (f discardFile) Close() error { 433 return nil 434 } 435 436 func (f *discardFile) Write(p []byte) (int, error) { 437 f.wrote += int64(len(p)) 438 return len(p), nil 439 } 440 441 func (f discardFile) Sync() error { 442 return nil 443 } 444 445 type blockPropErrSite uint 446 447 const ( 448 errSiteAdd blockPropErrSite = iota 449 errSiteFinishBlock 450 errSiteFinishIndex 451 errSiteFinishTable 452 errSiteNone 453 ) 454 455 type testBlockPropCollector struct { 456 errSite blockPropErrSite 457 err error 458 } 459 460 func (c *testBlockPropCollector) Name() string { return "testBlockPropCollector" } 461 462 func (c *testBlockPropCollector) Add(_ InternalKey, _ []byte) error { 463 if c.errSite == errSiteAdd { 464 return c.err 465 } 466 return nil 467 } 468 469 func (c *testBlockPropCollector) FinishDataBlock(_ []byte) ([]byte, error) { 470 if c.errSite == errSiteFinishBlock { 471 return nil, c.err 472 } 473 return nil, nil 474 } 475 476 func (c *testBlockPropCollector) AddPrevDataBlockToIndexBlock() {} 477 478 func (c *testBlockPropCollector) FinishIndexBlock(_ []byte) ([]byte, error) { 479 if c.errSite == errSiteFinishIndex { 480 return nil, c.err 481 } 482 return nil, nil 483 } 484 485 func (c *testBlockPropCollector) FinishTable(_ []byte) ([]byte, error) { 486 if c.errSite == errSiteFinishTable { 487 return nil, c.err 488 } 489 return nil, nil 490 } 491 492 func TestWriterBlockPropertiesErrors(t *testing.T) { 493 blockPropErr := errors.Newf("block property collector failed") 494 testCases := []blockPropErrSite{ 495 errSiteAdd, 496 errSiteFinishBlock, 497 errSiteFinishIndex, 498 errSiteFinishTable, 499 errSiteNone, 500 } 501 502 var ( 503 k1 = base.MakeInternalKey([]byte("a"), 0, base.InternalKeyKindSet) 504 v1 = []byte("apples") 505 k2 = base.MakeInternalKey([]byte("b"), 0, base.InternalKeyKindSet) 506 v2 = []byte("bananas") 507 k3 = base.MakeInternalKey([]byte("c"), 0, base.InternalKeyKindSet) 508 v3 = []byte("carrots") 509 ) 510 511 for _, tc := range testCases { 512 t.Run("", func(t *testing.T) { 513 fs := vfs.NewMem() 514 f, err := fs.Create("test") 515 require.NoError(t, err) 516 517 w := NewWriter(f, WriterOptions{ 518 BlockSize: 1, 519 BlockPropertyCollectors: []func() BlockPropertyCollector{ 520 func() BlockPropertyCollector { 521 return &testBlockPropCollector{ 522 errSite: tc, 523 err: blockPropErr, 524 } 525 }, 526 }, 527 TableFormat: TableFormatPebblev1, 528 }) 529 530 err = w.Add(k1, v1) 531 switch tc { 532 case errSiteAdd: 533 require.Error(t, err) 534 require.Equal(t, blockPropErr, err) 535 return 536 case errSiteFinishBlock: 537 require.NoError(t, err) 538 // Addition of a second key completes the first block. 539 err = w.Add(k2, v2) 540 require.Error(t, err) 541 require.Equal(t, blockPropErr, err) 542 return 543 case errSiteFinishIndex: 544 require.NoError(t, err) 545 // Addition of a second key completes the first block. 546 err = w.Add(k2, v2) 547 require.NoError(t, err) 548 // The index entry for the first block is added after the completion of 549 // the second block, which is triggered by adding a third key. 550 err = w.Add(k3, v3) 551 require.Error(t, err) 552 require.Equal(t, blockPropErr, err) 553 return 554 } 555 556 err = w.Close() 557 if tc == errSiteFinishTable { 558 require.Error(t, err) 559 require.Equal(t, blockPropErr, err) 560 } else { 561 require.NoError(t, err) 562 } 563 }) 564 } 565 } 566 567 func TestWriter_TableFormatCompatibility(t *testing.T) { 568 testCases := []struct { 569 name string 570 minFormat TableFormat 571 configureFn func(opts *WriterOptions) 572 writeFn func(w *Writer) error 573 }{ 574 { 575 name: "block properties", 576 minFormat: TableFormatPebblev1, 577 configureFn: func(opts *WriterOptions) { 578 opts.BlockPropertyCollectors = []func() BlockPropertyCollector{ 579 func() BlockPropertyCollector { 580 return NewBlockIntervalCollector( 581 "collector", &valueCharBlockIntervalCollector{charIdx: 0}, nil, 582 ) 583 }, 584 } 585 }, 586 }, 587 { 588 name: "range keys", 589 minFormat: TableFormatPebblev2, 590 writeFn: func(w *Writer) error { 591 return w.RangeKeyDelete([]byte("a"), []byte("b")) 592 }, 593 }, 594 } 595 596 for _, tc := range testCases { 597 t.Run(tc.name, func(t *testing.T) { 598 for tf := TableFormatLevelDB; tf <= TableFormatMax; tf++ { 599 t.Run(tf.String(), func(t *testing.T) { 600 fs := vfs.NewMem() 601 f, err := fs.Create("sst") 602 require.NoError(t, err) 603 604 opts := WriterOptions{TableFormat: tf} 605 if tc.configureFn != nil { 606 tc.configureFn(&opts) 607 } 608 609 w := NewWriter(f, opts) 610 if tc.writeFn != nil { 611 err = tc.writeFn(w) 612 require.NoError(t, err) 613 } 614 615 err = w.Close() 616 if tf < tc.minFormat { 617 require.Error(t, err) 618 } else { 619 require.NoError(t, err) 620 } 621 }) 622 } 623 }) 624 } 625 } 626 627 // Tests for races, such as https://github.com/cockroachdb/cockroach/issues/77194, 628 // in the Writer. 629 func TestWriterRace(t *testing.T) { 630 ks := testkeys.Alpha(5) 631 ks = ks.EveryN(ks.Count() / 1_000) 632 keys := make([][]byte, ks.Count()) 633 for ki := 0; ki < len(keys); ki++ { 634 keys[ki] = testkeys.Key(ks, ki) 635 } 636 readerOpts := ReaderOptions{ 637 Comparer: testkeys.Comparer, 638 Filters: map[string]base.FilterPolicy{}, 639 } 640 641 var wg sync.WaitGroup 642 for i := 0; i < 16; i++ { 643 wg.Add(1) 644 go func() { 645 val := make([]byte, rand.Intn(1000)) 646 opts := WriterOptions{ 647 Comparer: testkeys.Comparer, 648 BlockSize: rand.Intn(1 << 10), 649 Compression: NoCompression, 650 } 651 defer wg.Done() 652 f := &memFile{} 653 w := NewWriter(f, opts) 654 for ki := 0; ki < len(keys); ki++ { 655 require.NoError( 656 t, 657 w.Add(base.MakeInternalKey(keys[ki], uint64(ki), InternalKeyKindSet), val), 658 ) 659 require.Equal( 660 t, base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey).UserKey, keys[ki], 661 ) 662 } 663 require.NoError(t, w.Close()) 664 require.Equal(t, w.meta.LargestPoint.UserKey, keys[len(keys)-1]) 665 r, err := NewMemReader(f.Bytes(), readerOpts) 666 require.NoError(t, err) 667 defer r.Close() 668 it, err := r.NewIter(nil, nil) 669 require.NoError(t, err) 670 defer it.Close() 671 ki := 0 672 for k, v := it.First(); k != nil; k, v = it.Next() { 673 require.Equal(t, k.UserKey, keys[ki]) 674 require.Equal(t, v, val) 675 ki++ 676 } 677 }() 678 } 679 wg.Wait() 680 } 681 682 func BenchmarkWriter(b *testing.B) { 683 keys := make([][]byte, 1e6) 684 const keyLen = 24 685 keySlab := make([]byte, keyLen*len(keys)) 686 for i := range keys { 687 key := keySlab[i*keyLen : i*keyLen+keyLen] 688 binary.BigEndian.PutUint64(key[:8], 123) // 16-byte shared prefix 689 binary.BigEndian.PutUint64(key[8:16], 456) 690 binary.BigEndian.PutUint64(key[16:], uint64(i)) 691 keys[i] = key 692 } 693 694 b.ResetTimer() 695 696 for _, bs := range []int{base.DefaultBlockSize, 32 << 10} { 697 b.Run(fmt.Sprintf("block=%s", humanize.IEC.Int64(int64(bs))), func(b *testing.B) { 698 for _, filter := range []bool{true, false} { 699 b.Run(fmt.Sprintf("filter=%t", filter), func(b *testing.B) { 700 for _, comp := range []Compression{NoCompression, SnappyCompression, ZstdCompression} { 701 b.Run(fmt.Sprintf("compression=%s", comp), func(b *testing.B) { 702 opts := WriterOptions{ 703 BlockRestartInterval: 16, 704 BlockSize: bs, 705 Compression: comp, 706 } 707 if filter { 708 opts.FilterPolicy = bloom.FilterPolicy(10) 709 } 710 f := &discardFile{} 711 for i := 0; i < b.N; i++ { 712 f.wrote = 0 713 w := NewWriter(f, opts) 714 715 for j := range keys { 716 if err := w.Set(keys[j], keys[j]); err != nil { 717 b.Fatal(err) 718 } 719 } 720 if err := w.Close(); err != nil { 721 b.Fatal(err) 722 } 723 b.SetBytes(int64(f.wrote)) 724 } 725 }) 726 } 727 }) 728 } 729 }) 730 } 731 } 732 733 var test4bSuffixComparer = &base.Comparer{ 734 Compare: base.DefaultComparer.Compare, 735 Equal: base.DefaultComparer.Equal, 736 Separator: base.DefaultComparer.Separator, 737 Successor: base.DefaultComparer.Successor, 738 Split: func(key []byte) int { 739 if len(key) > 4 { 740 return len(key) - 4 741 } 742 return len(key) 743 }, 744 Name: "comparer-split-4b-suffix", 745 }