oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package roster
2
3import (
4 "context"
5 "database/sql"
6 "path/filepath"
7 "sync"
8 "testing"
9 "time"
10
11 _ "modernc.org/sqlite"
12)
13
14type sqliteEvidence struct {
15 db *sql.DB
16 mu sync.Mutex
17 entering chan struct{}
18}
19
20func (s *sqliteEvidence) Load(ctx context.Context) ([]byte, error) {
21 var b []byte
22 e := s.db.QueryRowContext(ctx, "SELECT value FROM evidence WHERE id=1").Scan(&b)
23 if e == sql.ErrNoRows {
24 return nil, ErrNoEvidence
25 }
26 return b, e
27}
28func (s *sqliteEvidence) Save(ctx context.Context, b []byte) error {
29 s.mu.Lock()
30 entered := s.entering
31 s.entering = nil
32 s.mu.Unlock()
33 if entered != nil {
34 close(entered)
35 }
36 _, e := s.db.ExecContext(ctx, "INSERT INTO evidence(id,value) VALUES(1,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value", b)
37 return e
38}
39
40func TestLeaseDoesNotWaitForSQLiteWriterPublication(t *testing.T) {
41 f := setup(t)
42 db, e := sql.Open("sqlite", filepath.Join(t.TempDir(), "roster.db"))
43 if e != nil {
44 t.Fatal(e)
45 }
46 defer db.Close()
47 db.SetMaxOpenConns(1)
48 if _, e = db.Exec("CREATE TABLE evidence(id INTEGER PRIMARY KEY,value BLOB NOT NULL)"); e != nil {
49 t.Fatal(e)
50 }
51 disk := &sqliteEvidence{db: db}
52 f.cfg.Durable = disk
53 c := f.cache()
54 d := f.check(c, 1)
55 state(t, d, Member)
56 tx, e := db.BeginTx(context.Background(), nil)
57 if e != nil {
58 t.Fatal(e)
59 }
60 defer tx.Rollback()
61 if _, e = tx.Exec("UPDATE evidence SET value=value WHERE id=1"); e != nil {
62 t.Fatal(e)
63 }
64 before := db.Stats().WaitCount
65 entered := make(chan struct{})
66 disk.mu.Lock()
67 disk.entering = entered
68 disk.mu.Unlock()
69 f.clock.step(30 * time.Second)
70 refreshed := make(chan error, 1)
71 go func() { refreshed <- c.Refresh(context.Background()) }()
72 <-entered
73 // WaitCount proves Save is waiting on the real one-connection writer pool,
74 // not merely scheduled to enter a fake callback. Publication owns Cache.mu.
75 deadline := time.Now().Add(time.Second)
76 for db.Stats().WaitCount == before && time.Now().Before(deadline) {
77 time.Sleep(time.Millisecond)
78 }
79 if db.Stats().WaitCount == before {
80 _ = tx.Rollback()
81 t.Fatal("publication never waited for writer")
82 }
83 valid := make(chan bool, 1)
84 remaining := make(chan time.Duration, 1)
85 go func() { valid <- d.Lease.Valid() }()
86 go func() { remaining <- d.Lease.Remaining() }()
87 var v bool
88 var left time.Duration
89 timedOut := false
90 select {
91 case v = <-valid:
92 case <-time.After(250 * time.Millisecond):
93 timedOut = true
94 }
95 select {
96 case left = <-remaining:
97 case <-time.After(250 * time.Millisecond):
98 timedOut = true
99 }
100 // Always release the writer before reporting a mutation failure, so neither
101 // the refresh goroutine nor the blocked accessor can leak into another test.
102 _ = tx.Rollback()
103 if e = <-refreshed; e != nil {
104 t.Fatal(e)
105 }
106 if timedOut {
107 t.Fatal("lease accessor blocked behind SQLite writer publication")
108 }
109 if v || left != 0 {
110 t.Fatal("contended lease granted standing")
111 }
112 next := f.check(c, 1)
113 state(t, next, Member)
114 if !next.Lease.Valid() {
115 t.Fatal("retry could not use published lease")
116 }
117 if d.Lease.Valid() {
118 t.Fatal("old lease survived publication version")
119 }
120}
121
122func TestLeaseContentionRefusesWithoutChangingAgeOrStanding(t *testing.T) {
123 f := setup(t)
124 c := f.cache()
125 d := f.check(c, 1)
126 f.clock.step(10 * time.Second)
127 before := d.Lease.Remaining()
128 c.mu.Lock()
129 valid := make(chan bool, 1)
130 left := make(chan time.Duration, 1)
131 go func() { valid <- d.Lease.Valid() }()
132 go func() { left <- d.Lease.Remaining() }()
133 timedOut := false
134 var v bool
135 var n time.Duration
136 select {
137 case v = <-valid:
138 case <-time.After(250 * time.Millisecond):
139 timedOut = true
140 }
141 select {
142 case n = <-left:
143 case <-time.After(250 * time.Millisecond):
144 timedOut = true
145 }
146 c.mu.Unlock()
147 if timedOut {
148 t.Fatal("contended accessor waited")
149 }
150 if v || n != 0 {
151 t.Fatal("contention granted standing")
152 }
153 if !d.Lease.Valid() || d.Lease.Remaining() != before {
154 t.Fatal("contention changed the snapshot or deadline")
155 }
156 f.clock.step(50 * time.Second)
157 if d.Lease.Valid() || d.Lease.Remaining() != 0 {
158 t.Fatal("contention extended age")
159 }
160}
161
162func TestLeaseContentionDoesNotSkipLaterClockFence(t *testing.T) {
163 f := setup(t)
164 c := f.cache()
165 d := f.check(c, 1)
166 f.clock.wallStep(-ClockTolerance - time.Nanosecond)
167 c.mu.Lock()
168 done := make(chan bool, 1)
169 go func() { done <- d.Lease.Valid() }()
170 select {
171 case v := <-done:
172 if v {
173 c.mu.Unlock()
174 t.Fatal("contention granted")
175 }
176 case <-time.After(250 * time.Millisecond):
177 c.mu.Unlock()
178 t.Fatal("contention blocked")
179 }
180 c.mu.Unlock()
181 if d.Lease.Valid() || d.Lease.Remaining() != 0 {
182 t.Fatal("successful lock skipped backward-clock fence")
183 }
184}
185
186func TestUnknownSigningKeyRefused(t *testing.T) {
187 f := setup(t)
188 raw, e := SignProvisioning(f.op, "unprovisioned", f.record)
189 if e != nil {
190 t.Fatal(e)
191 }
192 if _, e = VerifyProvisioning(raw, []Key{{"operator", f.record.Keys[0].Public}}, f.record.Bindings); e != ErrRefused {
193 t.Fatalf("unknown operator key result %v", e)
194 }
195 keys, _ := publicKeys(f.record.Keys)
196 // A valid signature under a real key still refuses when its identifier was
197 // not provisioned. The map guard is explicit rather than verifier-dependent.
198 b, e := encode(f.key, "unprovisioned", PageContext, struct {
199 Version int `json:"version"`
200 }{1})
201 if e != nil {
202 t.Fatal(e)
203 }
204 if _, e = signedPayload(b, keys, PageContext); e != ErrRefused {
205 t.Fatalf("unknown roster key result %v", e)
206 }
207}
208