This repository has been archived by the owner on Sep 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
DB Backend: report explicit error when transactions are used concurrently #37172
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d63d365
make concurrent transaction usage loud
camdencheek 3eb6b8b
update comment
camdencheek f6b62b2
demote error to log message and update tests
camdencheek ba7ea7a
add witty aphorism
camdencheek 23b28b6
fix unset logger
camdencheek 61b0a38
Update internal/database/basestore/handle.go
camdencheek a5d2f3c
use sleeps instead of 100 goroutines
camdencheek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,14 +8,17 @@ import ( | |
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/keegancsmith/sqlf" | ||
"github.com/sourcegraph/log/logtest" | ||
"github.com/stretchr/testify/require" | ||
"golang.org/x/sync/errgroup" | ||
|
||
"github.com/sourcegraph/sourcegraph/internal/database/dbtest" | ||
"github.com/sourcegraph/sourcegraph/internal/database/dbutil" | ||
"github.com/sourcegraph/sourcegraph/lib/errors" | ||
) | ||
|
||
func TestTransaction(t *testing.T) { | ||
db := dbtest.NewDB(t) | ||
db := dbtest.NewRawDB(t) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unrelated, but we don't need a db with a frontend schema in these tests |
||
setupStoreTest(t, db) | ||
store := testStore(db) | ||
|
||
|
@@ -61,8 +64,53 @@ func TestTransaction(t *testing.T) { | |
assertCounts(t, db, map[int]int{1: 42, 3: 44}) | ||
} | ||
|
||
func TestConcurrentTransactions(t *testing.T) { | ||
db := dbtest.NewRawDB(t) | ||
setupStoreTest(t, db) | ||
store := testStore(db) | ||
ctx := context.Background() | ||
|
||
t.Run("creating transactions concurrently does not fail", func(t *testing.T) { | ||
var g errgroup.Group | ||
for i := 0; i < 2; i++ { | ||
g.Go(func() (err error) { | ||
tx, err := store.Transact(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { err = tx.Done(err) }() | ||
|
||
return tx.Exec(ctx, sqlf.Sprintf(`select pg_sleep(0.1)`)) | ||
}) | ||
} | ||
require.NoError(t, g.Wait()) | ||
}) | ||
|
||
t.Run("parallel insertion on a single transaction does not fail but logs an error", func(t *testing.T) { | ||
tx, err := store.Transact(ctx) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
capturingLogger, export := logtest.Captured(t) | ||
tx.handle.(*txHandle).logger = capturingLogger | ||
|
||
var g errgroup.Group | ||
for i := 0; i < 2; i++ { | ||
g.Go(func() (err error) { | ||
return tx.Exec(ctx, sqlf.Sprintf(`select pg_sleep(0.1)`)) | ||
}) | ||
} | ||
err = g.Wait() | ||
require.NoError(t, err) | ||
|
||
captured := export() | ||
require.Greater(t, len(captured), 0) | ||
require.Equal(t, "transaction used concurrently", captured[0].Message) | ||
}) | ||
} | ||
|
||
func TestSavepoints(t *testing.T) { | ||
db := dbtest.NewDB(t) | ||
db := dbtest.NewRawDB(t) | ||
setupStoreTest(t, db) | ||
|
||
NumSavepointTests := 10 | ||
|
@@ -88,7 +136,7 @@ func TestSavepoints(t *testing.T) { | |
} | ||
|
||
func TestSetLocal(t *testing.T) { | ||
db := dbtest.NewDB(t) | ||
db := dbtest.NewRawDB(t) | ||
setupStoreTest(t, db) | ||
store := testStore(db) | ||
|
||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
woah, a valid use of the new
TryLock()
!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have to admit I'm a bit skeptical of using
TryLock()
followed byLock()
in the same function (and there's caveats around usingTryLock
). But I also don't see another way around it that doesn't end up doing the same thing under the hood (semaphore, atomic compare and swap, etc.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So...Russ Cox is totally correct, including in this case. It is always incorrect to use a transaction concurrently, including after this PR.
However, my concerns are slightly different than Russ's. I want to do anything I can to prevent a panic in production, which might include implementing some imprecise logic that might avoid a handful of panics.
That said, the more important thing here IMO is that we report on incorrect usage, which this does with basically the same consistency as any race detector. This is what
TryLock
allows us to do: report when the invariant that a transaction should never be used concurrently is violated. A plainLock
cannot do this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I was just wondering whether we need the
TryLock
for example or could use a one-word value instead?(Edit: ... and that's what I meant with "it all ends up being the same" 😄 because this is not different than what TryLock does somewhere under the hood, but I guess TryLock is now the officially supported version, since I'm not even 100% sure 1-word vars are safe to read concurrently like that.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not safe to read
inUse
outside the mutex though, right?