internal/store/contention_test.go

v1.34.0
gitbay/internal/store/contention_test.go history · blame · raw

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}