-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix race in lazy computation of column offset tab (#118)
This patch fixes a race in the way that the column offset tables were getting computed. Previously, multiple goroutines could attempt to Scan a record of the same type at once and then compute the offset table at the same time. They could then write different slice headers at the same time, potentially corrupting data. The fix is to guard each offset table with a RWMutex. The scan routine acquires a read lock whenever it accesses the table and the update routine acquires a write lock before updating it. We could make the critical section for the update routine shorter (by just wrapping the actual slice update), but I thought it would be better to prevent readers from enqueueing more writers and causing thrash. I did not really benchmark this. This race is unlikely to occur unless a service is under heavy load, but I was able to write a test that confirms that it is in fact a real issue. Threading code is tricky, so when we audit this for bugs, we want to make sure that: - Scan has released all locks by the time it returns (I needed fairly fine grained control of when the lock was held so I had to kick it C style rather than using defer). - No read lock can ever be held by a given goroutine when it calls fillColPosTab, as this would deadlock. Fixes #115
- Loading branch information
1 parent
57e04aa
commit 6abe0df
Showing
23 changed files
with
409 additions
and
181 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -431,3 +431,6 @@ | |
|
||
[[table]] | ||
name = "funky_enums" | ||
|
||
[[table]] | ||
name = "offset_table_fillings" |
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,76 @@ | ||
package test | ||
|
||
import ( | ||
"fmt" | ||
"sync" | ||
"testing" | ||
|
||
"github.com/opendoor-labs/pggen/cmd/pggen/test/models" | ||
) | ||
|
||
// file: race_test.go | ||
// | ||
// this file contains tests that attempt to exercise potentially racy code | ||
// | ||
|
||
// NOTE: I have been able to confirm that this test will reliably turn up | ||
// data races by commenting out all the lock calls in the generated Scan routine | ||
// for OffsetTableFilling and then running `go test -race --run TestOffsetTableFilling` | ||
// in a loop. Re-generating the models code makes the race warnings go away. | ||
func TestOffsetTableFilling(t *testing.T) { | ||
nscanners := 100 | ||
nmods := 10 | ||
|
||
// insert some data so the results are not empty, not really needed but somehow | ||
// makes me fell better. | ||
id, err := pgClient.InsertOffsetTableFilling(ctx, &models.OffsetTableFilling{ | ||
I1: 1, | ||
}) | ||
chkErr(t, err) | ||
|
||
// start mucking about with the table | ||
errchan := make(chan error) | ||
modRoutine := func() { | ||
for i := 0; i < nmods; i++ { | ||
_, err := pgClient.Handle().ExecContext( | ||
ctx, fmt.Sprintf("ALTER TABLE offset_table_fillings ADD COLUMN i%d integer", i+2)) | ||
errchan <- err | ||
} | ||
} | ||
|
||
wg := sync.WaitGroup{} | ||
wg.Add(nscanners) | ||
scanRoutine := func(tid int) { | ||
for i := 0; i < 100; i++ { | ||
// don't check the error, as it might be something like | ||
// "sorry, too many clients already" or "cached plan must not change result type". | ||
// we don't actually care about these issues, we just want to see if we will | ||
// get a race with `go test -race` | ||
pgClient.GetOffsetTableFilling(ctx, id) // nolint: errcheck | ||
} | ||
wg.Done() | ||
} | ||
|
||
for i := 0; i < (nscanners / 2); i++ { | ||
go scanRoutine(i) | ||
} | ||
go modRoutine() | ||
for i := 0; i < (nscanners / 2); i++ { | ||
go scanRoutine((nscanners / 2) + i) | ||
} | ||
|
||
wg.Wait() | ||
for i := 0; i < nmods; i++ { | ||
err := <-errchan | ||
chkErr(t, err) | ||
} | ||
|
||
_, err = pgClient.Handle().ExecContext(ctx, ` | ||
DROP TABLE offset_table_fillings; | ||
CREATE TABLE offset_table_fillings ( | ||
id SERIAL PRIMARY KEY, | ||
i1 integer NOT NULL | ||
); | ||
`) | ||
chkErr(t, err) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.