oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package lease
2
3import (
4 "context"
5 "crypto/sha256"
6 "database/sql"
7 "fmt"
8 "io"
9 "net/url"
10 "os"
11 "path/filepath"
12 "testing"
13 "time"
14
15 lsfile "github.com/benbjohnson/litestream/file"
16 "modernc.org/sqlite"
17)
18
19func TestCheckerClosedCheckpointFinalizationProbe(t *testing.T) {
20 backend := &lockStore{}
21 h := localHeld(t, backend, 30*time.Second)
22 rn := h.rn
23 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
24 defer cancel()
25 path := filepath.Join(t.TempDir(), "source.db")
26 seedDB(t, path)
27 replica := t.TempDir()
28 h.replicaClient = lsfile.NewReplicaClient(replica)
29 receipt, err := h.Checkpoint(ctx, path)
30 if err != nil {
31 t.Fatal(err)
32 }
33 if receipt.CompletedAt.IsZero() || rn != h.rn {
34 t.Fatal("checkpoint ownership")
35 }
36 for _, suffix := range []string{"-wal", "-shm"} {
37 s, e := os.Stat(path + suffix)
38 if e != nil {
39 t.Fatalf("expected PERSIST_WAL sidecar%s: %v", suffix, e)
40 }
41 t.Logf("after Litestream Close%s size=%d", suffix, s.Size())
42 }
43 u := url.URL{Scheme: "file", Path: path}
44 q := u.Query()
45 q.Set("mode", "rw")
46 u.RawQuery = q.Encode()
47 d, err := sql.Open("sqlite", u.String())
48 if err != nil {
49 t.Fatal(err)
50 }
51 d.SetMaxOpenConns(1)
52 d.SetMaxIdleConns(1)
53 conn, err := d.Conn(ctx)
54 if err != nil {
55 t.Fatal(err)
56 }
57 if err = conn.Raw(func(raw any) error {
58 f, ok := raw.(sqlite.FileControl)
59 if !ok {
60 return fmt.Errorf("no file control")
61 }
62 before, e := f.FileControlPersistWAL("main", -1)
63 if e != nil {
64 return e
65 }
66 t.Logf("ordinary PERSIST_WAL before explicit clear=%d", before)
67 after, e := f.FileControlPersistWAL("main", 0)
68 if e != nil {
69 return e
70 }
71 if after != 0 {
72 return fmt.Errorf("persist remains%d", after)
73 }
74 return nil
75 }); err != nil {
76 t.Fatal(err)
77 }
78 var integrity string
79 if err = conn.QueryRowContext(ctx, "PRAGMA integrity_check").Scan(&integrity); err != nil || integrity != "ok" {
80 t.Fatalf("integrity=%q err=%v", integrity, err)
81 }
82 var busy, log, done int
83 if err = conn.QueryRowContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)").Scan(&busy, &log, &done); err != nil || busy != 0 || log != 0 || done != 0 {
84 t.Fatalf("checkpoint %d %d %d err=%v", busy, log, done, err)
85 }
86 if err = conn.Close(); err != nil {
87 t.Fatal(err)
88 }
89 if err = d.Close(); err != nil {
90 t.Fatal(err)
91 }
92 absent := func() {
93 t.Helper()
94 for _, suffix := range []string{"-wal", "-shm", "-journal"} {
95 if _, e := os.Lstat(path + suffix); !os.IsNotExist(e) {
96 t.Fatalf("sidecar remains%s err=%v", suffix, e)
97 }
98 }
99 }
100 absent()
101 if rn != h.rn {
102 t.Fatal("renewer changed")
103 }
104 if _, err = h.SafeUntil(); err != nil {
105 t.Fatal(err)
106 }
107 if _, deletes := backend.counts(); deletes != 0 {
108 t.Fatal("lease released")
109 }
110 if peer, e := acquireTTL(ctx, "test-bucket", "one.example", "test-region", 30*time.Second); e == nil {
111 peer.Release(ctx)
112 t.Fatal("peer acquired")
113 }
114 pin, err := os.Open(path)
115 if err != nil {
116 t.Fatal(err)
117 }
118 defer pin.Close()
119 beforeInfo, err := pin.Stat()
120 if err != nil {
121 t.Fatal(err)
122 }
123 hash := func() [32]byte {
124 t.Helper()
125 if _, e := pin.Seek(0, 0); e != nil {
126 t.Fatal(e)
127 }
128 h := sha256.New()
129 if _, e := io.Copy(h, pin); e != nil {
130 t.Fatal(e)
131 }
132 var out [32]byte
133 copy(out[:], h.Sum(nil))
134 return out
135 }
136 before := hash()
137 q.Set("mode", "ro")
138 q.Set("immutable", "1")
139 u.RawQuery = q.Encode()
140 reader, err := sql.Open("sqlite", u.String())
141 if err != nil {
142 t.Fatal(err)
143 }
144 reader.SetMaxOpenConns(1)
145 var count int
146 if err = reader.QueryRowContext(ctx, "SELECT count(*) FROM t").Scan(&count); err != nil || count != 50 {
147 t.Fatalf("immutable rows=%d err=%v", count, err)
148 }
149 if err = reader.Close(); err != nil {
150 t.Fatal(err)
151 }
152 after := hash()
153 afterInfo, err := pin.Stat()
154 if err != nil {
155 t.Fatal(err)
156 }
157 if before != after || !os.SameFile(beforeInfo, afterInfo) {
158 t.Fatal("immutable check changed main")
159 }
160 absent()
161 if count := restoreCount(t, replica, filepath.Join(t.TempDir(), "restored.db")); count != 50 {
162 t.Fatalf("replica rows=%d", count)
163 }
164 if _, err = h.SafeUntil(); err != nil {
165 t.Fatal(err)
166 }
167 if _, deletes := backend.counts(); deletes != 0 {
168 t.Fatal("prep/check released lease")
169 }
170 t.Logf("PASS same Held, SQLite cleanup without unlink, immutable read preserves main SHA256=%x and absent sidecars; restored rows=50", after)
171}
172