internal/store/contention_test.go
80 lines · 2206 bytes
1package store
2
3import (
4 "sync"
5 "sync/atomic"
6 "testing"
7)
8
9// TestConcurrentWritersDoNotFail runs the shape every write transaction in
10// this package has — read a row, write it back, inside one Begin — from
11// several goroutines at once.
12//
13// Deferred transactions take the write lock at their first write, by which
14// point another writer may hold it. SQLite answers SQLITE_BUSY and does
15// not invoke the busy handler for that case, so busy_timeout cannot help
16// and the transaction simply fails. Measured before the fix, 44% of these
17// failed. The DSN opens transactions as immediate, which takes the lock up
18// front where busy_timeout applies (#121).
19//
20// This has to be a real file: :memory: gives each connection its own
21// database, so nothing contends.
22func TestConcurrentWritersDoNotFail(t *testing.T) {
23 s := open(t)
24 if err := s.MigrateUp(); err != nil {
25 t.Fatal(err)
26 }
27 uid, err := s.CreateUser("cmc", true)
28 if err != nil {
29 t.Fatal(err)
30 }
31 repoID, err := s.CreateRepo("user", uid, "lib", "public")
32 if err != nil {
33 t.Fatal(err)
34 }
35
36 const writers, each = 8, 40
37 var failures int64
38 var wg sync.WaitGroup
39 for i := 0; i < writers; i++ {
40 wg.Add(1)
41 go func() {
42 defer wg.Done()
43 for j := 0; j < each; j++ {
44 if err := func() error {
45 tx, err := s.DB.Begin()
46 if err != nil {
47 return err
48 }
49 defer tx.Rollback()
50 var n int
51 if err := tx.QueryRow(
52 "SELECT issue_counter FROM repos WHERE id = ?", repoID).Scan(&n); err != nil {
53 return err
54 }
55 if _, err := tx.Exec(
56 "UPDATE repos SET issue_counter = ? WHERE id = ?", n+1, repoID); err != nil {
57 return err
58 }
59 return tx.Commit()
60 }(); err != nil {
61 atomic.AddInt64(&failures, 1)
62 }
63 }
64 }()
65 }
66 wg.Wait()
67
68 if failures != 0 {
69 t.Fatalf("%d of %d write transactions failed", failures, writers*each)
70 }
71 // Serialised writers each read what the last one committed, so no
72 // increment is lost.
73 var got int
74 if err := s.DB.QueryRow("SELECT issue_counter FROM repos WHERE id = ?", repoID).Scan(&got); err != nil {
75 t.Fatal(err)
76 }
77 if got != writers*each {
78 t.Fatalf("counter = %d, want %d: an increment was lost", got, writers*each)
79 }
80}