-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
kv: don't mix prefix and non-prefix iters when collecting intents
Fixes #47219. This commit addresses the bug diagnosed and explained in #47219. In that issue, we saw an assertion failure all the way up in the concurrency manager because a READ_UNCOMMITTED scan was hitting a WriteIntentError, which should not be possible. The root cause of this issue was that READ_UNCOMMITTED scans were mixing prefix and non-prefix iterators pulled from a read-only engine between the time that they were collecting intent keys and they were returning to fetch the provisional values for those keys. This mixing of iterators did not guarantee that the two stages of the operation would observe a consistent snapshot of the underlying engine, and because the READ_UNCOMMITTED scans also did not acquire latches, writes were able to slip in and change the intent while the scan wasn't looking. This caused the scan to throw a WriteIntentError for the new intent transaction, which badly confused other parts of the system (rightfully so). This commit fixes this issue in a few different ways: 1. it ensures that we always use the same iterator type (prefix or non-prefix) when retrieving the provisional values for a collection of intents retrieved by an earlier scan during READ_UNCOMMITTED operations. 2. it adds an assertion inside of batcheval.CollectIntentRows that the function never returns a WriteIntentError. This would have caught the bug much more easily, especially back before we had the concurrency manager assertion and this bug could have materialized as stuck range lookups and potentially even deadlocked splits due to the dependency cycle between those two operations. 3. it documents the limited guarantees that read-only engines provide with respect to consistent engine snapshots across iterator instances. We'll want to backport this fix as far back as possible. It won't crash earlier releases of Cockroach, but as stated above, it might cause even more disastrous results. REMINDER: when backporting, remember to change the release note. Release notes (bug fix): a bug that could cause Cockroach processes to crash due to an assertion failure with the text "expected latches held, found none" has been fixed. Release justification: fixes a high-priority bug in existing functionality. The bug became louder (now crashes servers) due to recent changes that added new assertions into the code.
- Loading branch information
1 parent
3967538
commit b9e925b
Showing
7 changed files
with
282 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright 2020 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 batcheval | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/cockroachdb/cockroach/pkg/roachpb" | ||
"github.com/cockroachdb/cockroach/pkg/storage" | ||
"github.com/cockroachdb/cockroach/pkg/testutils" | ||
"github.com/cockroachdb/cockroach/pkg/util/hlc" | ||
"github.com/cockroachdb/cockroach/pkg/util/leaktest" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
// instrumentedEngine wraps a storage.Engine and allows for various methods in | ||
// the interface to be instrumented for testing purposes. | ||
type instrumentedEngine struct { | ||
storage.Engine | ||
|
||
onNewIterator func(storage.IterOptions) | ||
// ... can be extended ... | ||
} | ||
|
||
func (ie *instrumentedEngine) NewIterator(opts storage.IterOptions) storage.Iterator { | ||
if ie.onNewIterator != nil { | ||
ie.onNewIterator(opts) | ||
} | ||
return ie.Engine.NewIterator(opts) | ||
} | ||
|
||
// TestCollectIntentsUsesSameIterator tests that all uses of CollectIntents | ||
// (currently only by READ_UNCOMMITTED Gets, Scans, and ReverseScans) use the | ||
// same cached iterator (prefix or non-prefix) for their initial read and their | ||
// provisional value collection for any intents they find. | ||
func TestCollectIntentsUsesSameIterator(t *testing.T) { | ||
defer leaktest.AfterTest(t)() | ||
|
||
ctx := context.Background() | ||
key := roachpb.Key("key") | ||
ts := hlc.Timestamp{WallTime: 123} | ||
header := roachpb.Header{ | ||
Timestamp: ts, | ||
ReadConsistency: roachpb.READ_UNCOMMITTED, | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
run func(*testing.T, storage.ReadWriter) (intents []roachpb.KeyValue, _ error) | ||
expPrefixIters int | ||
expNonPrefixIters int | ||
}{ | ||
{ | ||
name: "get", | ||
run: func(t *testing.T, db storage.ReadWriter) ([]roachpb.KeyValue, error) { | ||
req := &roachpb.GetRequest{ | ||
RequestHeader: roachpb.RequestHeader{Key: key}, | ||
} | ||
var resp roachpb.GetResponse | ||
if _, err := Get(ctx, db, CommandArgs{Args: req, Header: header}, &resp); err != nil { | ||
return nil, err | ||
} | ||
if resp.IntentValue == nil { | ||
return nil, nil | ||
} | ||
return []roachpb.KeyValue{{Key: key, Value: *resp.IntentValue}}, nil | ||
}, | ||
expPrefixIters: 2, | ||
expNonPrefixIters: 0, | ||
}, | ||
{ | ||
name: "scan", | ||
run: func(t *testing.T, db storage.ReadWriter) ([]roachpb.KeyValue, error) { | ||
req := &roachpb.ScanRequest{ | ||
RequestHeader: roachpb.RequestHeader{Key: key, EndKey: key.Next()}, | ||
} | ||
var resp roachpb.ScanResponse | ||
if _, err := Scan(ctx, db, CommandArgs{Args: req, Header: header}, &resp); err != nil { | ||
return nil, err | ||
} | ||
return resp.IntentRows, nil | ||
}, | ||
expPrefixIters: 0, | ||
expNonPrefixIters: 2, | ||
}, | ||
{ | ||
name: "reverse scan", | ||
run: func(t *testing.T, db storage.ReadWriter) ([]roachpb.KeyValue, error) { | ||
req := &roachpb.ReverseScanRequest{ | ||
RequestHeader: roachpb.RequestHeader{Key: key, EndKey: key.Next()}, | ||
} | ||
var resp roachpb.ReverseScanResponse | ||
if _, err := ReverseScan(ctx, db, CommandArgs{Args: req, Header: header}, &resp); err != nil { | ||
return nil, err | ||
} | ||
return resp.IntentRows, nil | ||
}, | ||
expPrefixIters: 0, | ||
expNonPrefixIters: 2, | ||
}, | ||
} | ||
for _, c := range testCases { | ||
t.Run(c.name, func(t *testing.T) { | ||
// Test with and without deletion intents. If a READ_UNCOMMITTED request | ||
// encounters an intent whose provisional value is a deletion tombstone, | ||
// the request should ignore the intent and should not return any | ||
// corresponding intent row. | ||
testutils.RunTrueAndFalse(t, "deletion intent", func(t *testing.T, delete bool) { | ||
db := &instrumentedEngine{Engine: storage.NewDefaultInMem()} | ||
defer db.Close() | ||
|
||
// Write an intent. | ||
val := roachpb.MakeValueFromBytes([]byte("val")) | ||
txn := roachpb.MakeTransaction("test", key, roachpb.NormalUserPriority, ts, 0) | ||
var err error | ||
if delete { | ||
err = storage.MVCCDelete(ctx, db, nil, key, ts, &txn) | ||
} else { | ||
err = storage.MVCCPut(ctx, db, nil, key, ts, val, &txn) | ||
} | ||
require.NoError(t, err) | ||
|
||
// Instrument iterator creation, count prefix vs. non-prefix iters. | ||
var prefixIters, nonPrefixIters int | ||
db.onNewIterator = func(opts storage.IterOptions) { | ||
if opts.Prefix { | ||
prefixIters++ | ||
} else { | ||
nonPrefixIters++ | ||
} | ||
} | ||
|
||
intents, err := c.run(t, db) | ||
require.NoError(t, err) | ||
|
||
// Assert proper intent values. | ||
if delete { | ||
require.Len(t, intents, 0) | ||
} else { | ||
expIntentVal := val | ||
expIntentVal.Timestamp = ts | ||
expIntentKeyVal := roachpb.KeyValue{Key: key, Value: expIntentVal} | ||
require.Len(t, intents, 1) | ||
require.Equal(t, expIntentKeyVal, intents[0]) | ||
} | ||
|
||
// Assert proper iterator use. | ||
require.Equal(t, c.expPrefixIters, prefixIters) | ||
require.Equal(t, c.expNonPrefixIters, nonPrefixIters) | ||
require.Equal(t, c.expNonPrefixIters, nonPrefixIters) | ||
}) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters