From 43ee66791377f47183c7c803117a30c46d97cf59 Mon Sep 17 00:00:00 2001 From: adityamaru Date: Fri, 28 Oct 2022 11:56:07 -0400 Subject: [PATCH] storage: add MVCCExportFingerprint method and fingerprintWriter This change introduces a fingerprintWriter that hashes every key/timestamp and value for point keys, and combines their hashes via a XOR into a running aggregate. Range keys are not fingerprinted but instead written to a pebble SST that is returned to the caller. This is because range keys do not have a stable, discrete identity and so it is up to the caller to define a deterministic fingerprinting scheme across all returned range keys. The fingerprintWriter is used by `MVCCExportFingerprint` that exports a fingerprint for point keys in the keyrange [StartKey, EndKey) over the interval (StartTS, EndTS]. The export logic used by `MVCCExportFingerprint` is the same that drives `MVCCExportToSST`. The former writes to a fingerprintWriter while the latter writes to an sstWriter. Currently, this method only support using an `fnv64` hasher to fingerprint each KV. This change does not wire `MVCCExportFingerprint` to ExportRequest command evaluation. This will be done as a followup. Informs: #89336 Release note: None --- pkg/storage/BUILD.bazel | 1 + pkg/storage/fingerprint_writer.go | 260 +++++++++++++++++++++++ pkg/storage/mvcc.go | 30 +++ pkg/storage/mvcc_test.go | 339 ++++++++++++++++++++++++++++++ 4 files changed, 630 insertions(+) create mode 100644 pkg/storage/fingerprint_writer.go diff --git a/pkg/storage/BUILD.bazel b/pkg/storage/BUILD.bazel index ffa749daf3d1..1aedc83ff7b6 100644 --- a/pkg/storage/BUILD.bazel +++ b/pkg/storage/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "doc.go", "engine.go", "engine_key.go", + "fingerprint_writer.go", "in_mem.go", "intent_interleaving_iter.go", "intent_reader_writer.go", diff --git a/pkg/storage/fingerprint_writer.go b/pkg/storage/fingerprint_writer.go new file mode 100644 index 000000000000..1377137c2d24 --- /dev/null +++ b/pkg/storage/fingerprint_writer.go @@ -0,0 +1,260 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package storage + +import ( + "context" + "hash" + "io" + + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/settings/cluster" + "github.com/cockroachdb/cockroach/pkg/util/uuid" + "github.com/cockroachdb/errors" +) + +// fingerprintWriter hashes every key/timestamp and value for point keys, and +// combines their hashes via a XOR into a running aggregate. +// +// Range keys are not fingerprinted but instead written to a pebble SST that is +// returned to the caller. This is because range keys do not have a stable, +// discrete identity and so it is up to the caller to define a deterministic +// fingerprinting scheme across all returned range keys. +// +// The caller must Finish() and Close() the fingerprintWriter to finalize the +// writes to the underlying pebble SST. +type fingerprintWriter struct { + hasher hash.Hash64 + + sstWriter *SSTWriter + xorAgg *uintXorAggregate +} + +// makeFingerprintWriter creates a new fingerprintWriter. +func makeFingerprintWriter( + ctx context.Context, hasher hash.Hash64, cs *cluster.Settings, f io.Writer, +) fingerprintWriter { + // TODO(adityamaru,dt): Once + // https://github.com/cockroachdb/cockroach/issues/90450 has been addressed we + // should write to a kvBuf instead of a Backup SST writer. + sstWriter := MakeBackupSSTWriter(ctx, cs, f) + return fingerprintWriter{ + sstWriter: &sstWriter, + hasher: hasher, + xorAgg: &uintXorAggregate{}, + } +} + +type uintXorAggregate struct { + sum uint64 +} + +// add inserts one value into the running xor. +func (a *uintXorAggregate) add(x uint64) { + a.sum = a.sum ^ x +} + +// result returns the xor. +func (a *uintXorAggregate) result() uint64 { + return a.sum +} + +// Finish finalizes the underlying SSTWriter, and returns the aggregated +// fingerprint for point keys. +func (f *fingerprintWriter) Finish() (uint64, error) { + if err := f.sstWriter.Finish(); err != nil { + return 0, err + } + return f.xorAgg.result(), nil +} + +// Close finishes and frees memory and other resources. Close is idempotent. +func (f *fingerprintWriter) Close() { + if f.sstWriter == nil { + return + } + f.sstWriter.Close() + f.hasher.Reset() + f.xorAgg = nil + f.sstWriter = nil +} + +var _ Writer = &fingerprintWriter{} + +// PutRawMVCCRangeKey implements the Writer interface. +func (f *fingerprintWriter) PutRawMVCCRangeKey(key MVCCRangeKey, bytes []byte) error { + // We do not fingerprint range keys, instead, we write them to a Pebble SST. + // This is because range keys do not have a stable, discrete identity and so + // it is up to the caller to define a deterministic fingerprinting scheme + // across all returned range keys.ler to decide how to fingerprint them. + return f.sstWriter.PutRawMVCCRangeKey(key, bytes) +} + +// PutRawMVCC implements the Writer interface. +func (f *fingerprintWriter) PutRawMVCC(key MVCCKey, value []byte) error { + defer f.hasher.Reset() + + // Hash the key/timestamp and value of the RawMVCC. + _, err := f.hasher.Write(key.Key) + if err != nil { + return errors.NewAssertionErrorWithWrappedErrf(err, + `"It never returns an error." -- https://golang.org/pkg/hash: %T`, f) + } + _, err = f.hasher.Write([]byte(key.Timestamp.String())) + if err != nil { + return errors.NewAssertionErrorWithWrappedErrf(err, + `"It never returns an error." -- https://golang.org/pkg/hash: %T`, f) + } + _, err = f.hasher.Write(value) + if err != nil { + return errors.NewAssertionErrorWithWrappedErrf(err, + `"It never returns an error." -- https://golang.org/pkg/hash: %T`, f) + } + + f.xorAgg.add(f.hasher.Sum64()) + return nil +} + +// PutUnversioned implements the Writer interface. +func (f *fingerprintWriter) PutUnversioned(key roachpb.Key, value []byte) error { + defer f.hasher.Reset() + + // Hash the key and value in the absence of a timestamp. + _, err := f.hasher.Write(key) + if err != nil { + return errors.NewAssertionErrorWithWrappedErrf(err, + `"It never returns an error." -- https://golang.org/pkg/hash: %T`, f) + } + _, err = f.hasher.Write(value) + if err != nil { + return errors.NewAssertionErrorWithWrappedErrf(err, + `"It never returns an error." -- https://golang.org/pkg/hash: %T`, f) + } + + f.xorAgg.add(f.hasher.Sum64()) + return nil +} + +// Unimplemented interface methods. + +// ApplyBatchRepr implements the Writer interface. +func (f *fingerprintWriter) ApplyBatchRepr(repr []byte, sync bool) error { + panic("unimplemented") +} + +// ClearMVCC implements the Writer interface. +func (f *fingerprintWriter) ClearMVCC(key MVCCKey) error { + panic("unimplemented") +} + +// ClearUnversioned implements the Writer interface. +func (f *fingerprintWriter) ClearUnversioned(key roachpb.Key) error { + panic("unimplemented") +} + +// ClearIntent implements the Writer interface. +func (f *fingerprintWriter) ClearIntent( + key roachpb.Key, txnDidNotUpdateMeta bool, txnUUID uuid.UUID, +) error { + panic("unimplemented") +} + +// ClearEngineKey implements the Writer interface. +func (f *fingerprintWriter) ClearEngineKey(key EngineKey) error { + panic("unimplemented") +} + +// ClearRawRange implements the Writer interface. +func (f *fingerprintWriter) ClearRawRange(start, end roachpb.Key, pointKeys, rangeKeys bool) error { + panic("unimplemented") +} + +// ClearMVCCRange implements the Writer interface. +func (f *fingerprintWriter) ClearMVCCRange( + start, end roachpb.Key, pointKeys, rangeKeys bool, +) error { + panic("unimplemented") +} + +// ClearMVCCVersions implements the Writer interface. +func (f *fingerprintWriter) ClearMVCCVersions(start, end MVCCKey) error { + panic("unimplemented") +} + +// ClearMVCCIteratorRange implements the Writer interface. +func (f *fingerprintWriter) ClearMVCCIteratorRange( + start, end roachpb.Key, pointKeys, rangeKeys bool, +) error { + panic("unimplemented") +} + +// ClearMVCCRangeKey implements the Writer interface. +func (f *fingerprintWriter) ClearMVCCRangeKey(rangeKey MVCCRangeKey) error { + panic("unimplemented") +} + +// PutMVCCRangeKey implements the Writer interface. +func (f *fingerprintWriter) PutMVCCRangeKey(key MVCCRangeKey, value MVCCValue) error { + panic("unimplemented") +} + +// PutEngineRangeKey implements the Writer interface. +func (f *fingerprintWriter) PutEngineRangeKey(start, end roachpb.Key, suffix, value []byte) error { + panic("unimplemented") +} + +// ClearEngineRangeKey implements the Writer interface. +func (f *fingerprintWriter) ClearEngineRangeKey(start, end roachpb.Key, suffix []byte) error { + panic("unimplemented") +} + +// Merge implements the Writer interface. +func (f *fingerprintWriter) Merge(key MVCCKey, value []byte) error { + panic("unimplemented") +} + +// PutMVCC implements the Writer interface. +func (f *fingerprintWriter) PutMVCC(key MVCCKey, value MVCCValue) error { + panic("unimplemented") +} + +// PutIntent implements the Writer interface. +func (f *fingerprintWriter) PutIntent( + ctx context.Context, key roachpb.Key, value []byte, txnUUID uuid.UUID, +) error { + panic("unimplemented") +} + +// PutEngineKey implements the Writer interface. +func (f *fingerprintWriter) PutEngineKey(key EngineKey, value []byte) error { + panic("unimplemented") +} + +// LogData implements the Writer interface. +func (f *fingerprintWriter) LogData(data []byte) error { + // No-op. + return nil +} + +// LogLogicalOp implements the Writer interface. +func (f *fingerprintWriter) LogLogicalOp(op MVCCLogicalOpType, details MVCCLogicalOpDetails) { + // No-op. +} + +// SingleClearEngineKey implements the Writer interface. +func (f *fingerprintWriter) SingleClearEngineKey(key EngineKey) error { + panic("unimplemented") +} + +// ShouldWriteLocalTimestamps implements the Writer interface. +func (f *fingerprintWriter) ShouldWriteLocalTimestamps(ctx context.Context) bool { + panic("unimplemented") +} diff --git a/pkg/storage/mvcc.go b/pkg/storage/mvcc.go index 47b7a22e3590..47d1f00337d1 100644 --- a/pkg/storage/mvcc.go +++ b/pkg/storage/mvcc.go @@ -14,6 +14,7 @@ import ( "bytes" "context" "fmt" + "hash/fnv" "io" "math" "runtime" @@ -5764,6 +5765,35 @@ func MVCCIsSpanEmpty( return !valid, nil } +// MVCCExportFingerprint exports a fingerprint for point keys in the keyrange +// [StartKey, EndKey) over the interval (StartTS, EndTS]. Each key/timestamp and +// value is hashed using a fnv64 hasher, and combined into a running aggregate +// via a XOR. On completion of the export this aggregate is returned as the +// fingerprint. +// +// Range keys are not fingerprinted but instead written to a pebble SST that is +// returned to the caller. This is because range keys do not have a stable, +// discrete identity and so it is up to the caller to define a deterministic +// fingerprinting scheme across all returned range keys. +func MVCCExportFingerprint( + ctx context.Context, cs *cluster.Settings, reader Reader, opts MVCCExportOptions, dest io.Writer, +) (roachpb.BulkOpSummary, MVCCKey, uint64, error) { + ctx, span := tracing.ChildSpan(ctx, "storage.MVCCExportToSST") + defer span.Finish() + + hasher := fnv.New64() + fingerprintWriter := makeFingerprintWriter(ctx, hasher, cs, dest) + defer fingerprintWriter.Close() + + summary, resumeKey, err := mvccExportToWriter(ctx, reader, opts, &fingerprintWriter) + if err != nil { + return roachpb.BulkOpSummary{}, MVCCKey{}, 0, err + } + + fingerprint, err := fingerprintWriter.Finish() + return summary, resumeKey, fingerprint, err +} + // MVCCExportToSST exports changes to the keyrange [StartKey, EndKey) over the // interval (StartTS, EndTS] as a Pebble SST. See mvccExportToWriter for more // details. diff --git a/pkg/storage/mvcc_test.go b/pkg/storage/mvcc_test.go index a148d1821fba..903c0d46acad 100644 --- a/pkg/storage/mvcc_test.go +++ b/pkg/storage/mvcc_test.go @@ -14,6 +14,7 @@ import ( "bytes" "context" "fmt" + "hash/fnv" "math" "math/rand" "reflect" @@ -6255,6 +6256,344 @@ func TestMVCCExportToSSTSErrorsOnLargeKV(t *testing.T) { require.ErrorAs(t, err, &expectedErr) } +// TestMVCCExportFingerprint verifies that MVCCExportFingerprint correctly +// fingerprints point keys in a given key and time interval, and returns the +// range keys in a pebble SST. +// +// This test uses a `fingerprintOracle` to verify that the fingerprint generated +// by `MVCCExportFingerprint` is what we would get if we iterated over an SST +// with all keys and computed our own fingerprint. +func TestMVCCExportFingerprint(t *testing.T) { + defer leaktest.AfterTest(t)() + + ctx := context.Background() + st := cluster.MakeTestingClusterSettings() + + fingerprint := func(opts MVCCExportOptions, engine Engine) (uint64, []byte, roachpb.BulkOpSummary, MVCCKey) { + dest := &MemFile{} + var err error + res, resumeKey, fingerprint, err := MVCCExportFingerprint( + ctx, st, engine, opts, dest) + require.NoError(t, err) + return fingerprint, dest.Data(), res, resumeKey + } + + // verifyFingerprintAgainstOracle uses the `fingerprintOracle` to compute a + // fingerprint over the same key and time interval, and ensure our fingerprint + // and range keys match up with that generated by the oracle. + verifyFingerprintAgainstOracle := func( + actualFingerprint uint64, + actualRangekeys []MVCCRangeKeyStack, + opts MVCCExportOptions, + engine Engine) { + oracle := makeFingerprintOracle(st, engine, opts) + expectedFingerprint, expectedRangeKeys := oracle.getFingerprintAndRangeKeys(ctx, t) + require.Equal(t, expectedFingerprint, actualFingerprint) + require.Equal(t, expectedRangeKeys, actualRangekeys) + } + + engine := createTestPebbleEngine() + defer engine.Close() + + kvSize := int64(16) + rangeKeySize := int64(10) + + // Insert some point keys. + // + // 2000 value3 value4 + // + // 1000 value1 value2 + // + // 1 2 3 + var testData = []testValue{ + value(key(1), "value1", ts(1000)), + value(key(2), "value2", ts(1000)), + value(key(2), "value3", ts(2000)), + value(key(3), "value4", ts(2000)), + } + require.NoError(t, fillInData(ctx, engine, testData)) + + // Insert range keys. + // + // 3000 [--- r2 ---) + // + // 2000 value3 value4 [--- r1 ---) + // + // 1000 value1 value2 + // + // 1 2 3 4 5 + require.NoError(t, engine.PutRawMVCCRangeKey(MVCCRangeKey{ + StartKey: key(4), + EndKey: key(5), + Timestamp: ts(2000), + }, []byte{})) + require.NoError(t, engine.PutRawMVCCRangeKey(MVCCRangeKey{ + StartKey: key(1), + EndKey: key(2), + Timestamp: ts(3000), + }, []byte{})) + + testutils.RunTrueAndFalse(t, "allRevisions", func(t *testing.T, allRevisions bool) { + t.Run("no-key-or-ts-bounds", func(t *testing.T) { + opts := MVCCExportOptions{ + StartKey: MVCCKey{Key: key(1)}, + EndKey: keys.MaxKey, + StartTS: hlc.Timestamp{}, + EndTS: hlc.Timestamp{WallTime: 9999}, + ExportAllRevisions: allRevisions, + } + fingerprint, rangeKeySST, summary, resumeKey := fingerprint(opts, engine) + require.Empty(t, resumeKey) + rangeKeys := getRangeKeys(t, rangeKeySST) + if allRevisions { + require.Equal(t, kvSize*4+rangeKeySize*2, summary.DataSize) + require.Equal(t, 2, len(rangeKeys)) + } else { + require.Equal(t, kvSize*2, summary.DataSize) + // StartTime is empty so we don't read rangekeys when not exporting all + // revisions. + require.Empty(t, rangeKeys) + } + verifyFingerprintAgainstOracle(fingerprint, rangeKeys, opts, engine) + }) + + t.Run("key-bounds", func(t *testing.T) { + opts := MVCCExportOptions{ + StartKey: MVCCKey{Key: key(1)}, + EndKey: key(2).Next(), + StartTS: hlc.Timestamp{}, + EndTS: hlc.Timestamp{WallTime: 9999}, + ExportAllRevisions: allRevisions, + } + fingerprint, rangeKeySST, summary, resumeKey := fingerprint(opts, engine) + require.Empty(t, resumeKey) + rangeKeys := getRangeKeys(t, rangeKeySST) + if allRevisions { + require.Equal(t, kvSize*3+rangeKeySize, summary.DataSize) + require.Equal(t, 1, len(rangeKeys)) + } else { + // Rangekey masks the point key 1@1000, so we only see 2@2000. + require.Equal(t, kvSize*1, summary.DataSize) + // StartTime is empty, so we don't read rangekeys when not exporting all + // revisions. + require.Empty(t, rangeKeys) + } + verifyFingerprintAgainstOracle(fingerprint, getRangeKeys(t, rangeKeySST), opts, engine) + }) + + t.Run("outside-point-key-bounds", func(t *testing.T) { + opts := MVCCExportOptions{ + StartKey: MVCCKey{Key: key(3).Next()}, + EndKey: keys.MaxKey, + StartTS: hlc.Timestamp{}, + EndTS: hlc.Timestamp{WallTime: 9999}, + ExportAllRevisions: allRevisions, + } + fingerprint, rangeKeySST, summary, resumeKey := fingerprint(opts, engine) + require.Empty(t, resumeKey) + rangeKeys := getRangeKeys(t, rangeKeySST) + require.Equal(t, uint64(0), fingerprint) + if allRevisions { + require.Equal(t, rangeKeySize, summary.DataSize) + require.Len(t, rangeKeys, 1) + } else { + require.Equal(t, int64(0), summary.DataSize) + require.Empty(t, rangeKeys) + } + verifyFingerprintAgainstOracle(fingerprint, getRangeKeys(t, rangeKeySST), opts, engine) + }) + + t.Run("time-bounds", func(t *testing.T) { + opts := MVCCExportOptions{ + StartKey: MVCCKey{Key: key(1)}, + EndKey: keys.MaxKey, + StartTS: ts(1000).Prev(), + EndTS: ts(1000), + ExportAllRevisions: allRevisions, + } + fingerprint, rangeKeySST, summary, resumeKey := fingerprint(opts, engine) + require.Empty(t, resumeKey) + rangeKeys := getRangeKeys(t, rangeKeySST) + require.Empty(t, rangeKeys) + require.Equal(t, kvSize*2, summary.DataSize) + verifyFingerprintAgainstOracle(fingerprint, getRangeKeys(t, rangeKeySST), opts, engine) + }) + + t.Run("outside-point-key-time-bounds", func(t *testing.T) { + opts := MVCCExportOptions{ + StartKey: MVCCKey{Key: key(1)}, + EndKey: keys.MaxKey, + StartTS: ts(2000), + EndTS: ts(3000), + ExportAllRevisions: allRevisions, + } + fingerprint, rangeKeySST, summary, resumeKey := fingerprint(opts, engine) + require.Empty(t, resumeKey) + rangeKeys := getRangeKeys(t, rangeKeySST) + require.Equal(t, rangeKeySize, summary.DataSize) + require.Len(t, rangeKeys, 1) + require.Equal(t, uint64(0), fingerprint) + verifyFingerprintAgainstOracle(fingerprint, getRangeKeys(t, rangeKeySST), opts, engine) + }) + + t.Run("assert-hash-is-per-kv", func(t *testing.T) { + // Fingerprint point keys 1 and 2. + opts := MVCCExportOptions{ + StartKey: MVCCKey{Key: key(1)}, + EndKey: key(2).Next(), + StartTS: hlc.Timestamp{}, + EndTS: hlc.Timestamp{WallTime: 9999}, + ExportAllRevisions: allRevisions, + } + fingerprint1, _, summary, resumeKey := fingerprint(opts, engine) + require.Empty(t, resumeKey) + if allRevisions { + require.Equal(t, 3*kvSize+rangeKeySize, summary.DataSize) + } else { + // Rangekey masking means we only see 2@2000. + require.Equal(t, kvSize, summary.DataSize) + } + + // Fingerprint point key 3. + opts = MVCCExportOptions{ + StartKey: MVCCKey{Key: key(3)}, + EndKey: keys.MaxKey, + StartTS: hlc.Timestamp{}, + EndTS: hlc.Timestamp{WallTime: 9999}, + ExportAllRevisions: allRevisions, + } + fingerprint2, _, summary2, resumeKey2 := fingerprint(opts, engine) + require.Empty(t, resumeKey2) + if allRevisions { + require.Equal(t, kvSize+rangeKeySize, summary2.DataSize) + } else { + require.Equal(t, kvSize, summary2.DataSize) + } + + // Fingerprint point keys 1 to 3. + opts = MVCCExportOptions{ + StartKey: MVCCKey{Key: key(1)}, + EndKey: keys.MaxKey, + StartTS: hlc.Timestamp{}, + EndTS: hlc.Timestamp{WallTime: 9999}, + ExportAllRevisions: allRevisions, + } + fingerprint3, _, summary3, resumeKey3 := fingerprint(opts, engine) + require.Empty(t, resumeKey3) + if allRevisions { + require.Equal(t, 4*kvSize+2*rangeKeySize, summary3.DataSize) + } else { + require.Equal(t, 2*kvSize, summary3.DataSize) + } + + // Verify that fp3 = fp1 ^ fp2 + require.Equal(t, fingerprint3, fingerprint1^fingerprint2) + }) + }) +} + +type fingerprintOracle struct { + st *cluster.Settings + engine Engine + opts *MVCCExportOptions +} + +func makeFingerprintOracle( + st *cluster.Settings, engine Engine, opts MVCCExportOptions, +) *fingerprintOracle { + return &fingerprintOracle{ + opts: &opts, + engine: engine, + st: st, + } +} + +// getFingerprintAndRangeKeys can be used to generate the fingerprint of point +// keys in an interval determined by the supplied `MVCCExportOptions`. This +// fingerprint is generated by exporting the point and range keys to a pebble +// SST using `MVCCExportToSST` and then maintaining a XOR aggregate of the hash +// of every point key in the SST. Range keys are not fingerprinted but instead +// returned as is to the caller. +func (f *fingerprintOracle) getFingerprintAndRangeKeys( + ctx context.Context, t *testing.T, +) (uint64, []MVCCRangeKeyStack) { + t.Helper() + + dest := &MemFile{} + _, _, err := MVCCExportToSST(ctx, f.st, f.engine, *f.opts, dest) + require.NoError(t, err) + return f.fingerprintPointKeys(t, dest.Data()), getRangeKeys(t, dest.Data()) +} + +func (f *fingerprintOracle) fingerprintPointKeys(t *testing.T, dataSST []byte) uint64 { + t.Helper() + + hasher := fnv.New64() + var xorAgg uint64 + iterOpts := IterOptions{ + KeyTypes: IterKeyTypePointsOnly, + LowerBound: keys.LocalMax, + UpperBound: keys.MaxKey, + } + iter, err := NewMemSSTIterator(dataSST, false, iterOpts) + if err != nil { + t.Fatal(err) + } + defer iter.Close() + + for iter.SeekGE(MVCCKey{Key: keys.MinKey}); ; iter.Next() { + if valid, err := iter.Valid(); !valid || err != nil { + if err != nil { + t.Fatal(err) + } + break + } + k := iter.UnsafeKey() + if k.Timestamp.IsEmpty() { + _, err := hasher.Write(k.Key) + require.NoError(t, err) + _, err = hasher.Write(iter.UnsafeValue()) + require.NoError(t, err) + } else { + _, err := hasher.Write(k.Key) + require.NoError(t, err) + _, err = hasher.Write([]byte(k.Timestamp.String())) + require.NoError(t, err) + _, err = hasher.Write(iter.UnsafeValue()) + require.NoError(t, err) + } + xorAgg = xorAgg ^ hasher.Sum64() + hasher.Reset() + } + + return xorAgg +} + +func getRangeKeys(t *testing.T, dataSST []byte) []MVCCRangeKeyStack { + t.Helper() + + iterOpts := IterOptions{ + KeyTypes: IterKeyTypeRangesOnly, + LowerBound: keys.LocalMax, + UpperBound: keys.MaxKey, + } + iter, err := NewMemSSTIterator(dataSST, false, iterOpts) + require.NoError(t, err) + defer iter.Close() + + allRangeKeys := make([]MVCCRangeKeyStack, 0) + for iter.SeekGE(MVCCKey{Key: keys.MinKey}); ; iter.Next() { + if ok, err := iter.Valid(); err != nil { + t.Fatal(err) + } else if !ok { + break + } + rangeKeys := iter.RangeKeys() + allRangeKeys = append(allRangeKeys, rangeKeys.Clone()) + } + return allRangeKeys +} + // mvccGetRaw fetches a raw MVCC value, for use in tests. func mvccGetRaw(t *testing.T, r Reader, key MVCCKey) []byte { value, err := mvccGetRawWithError(t, r, key)