Skip to content

Commit 000e70e

Browse files
rscFiloSottile
authored andcommitted
mpt: copy rsc.io/tmp/mpt here
rsc.io/tmp/mpt was my workspace for building up to this production implementation. It has reached the point where it belongs in a more permanent home, so copy it to torchwood. This corresponds to rsc/tmp@b6bdb3d0c98a aka rsc.io/tmp/mpt@v0.2.0.
1 parent c903b96 commit 000e70e

12 files changed

Lines changed: 4728 additions & 0 deletions

File tree

mpt/DESIGN.md

Lines changed: 733 additions & 0 deletions
Large diffs are not rendered by default.

mpt/disk.go

Lines changed: 510 additions & 0 deletions
Large diffs are not rendered by default.

mpt/disk_test.go

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package mpt
6+
7+
import (
8+
"bytes"
9+
"crypto/sha256"
10+
"encoding/base64"
11+
"encoding/hex"
12+
"fmt"
13+
"io"
14+
"maps"
15+
"math/rand/v2"
16+
"runtime/debug"
17+
"slices"
18+
"testing"
19+
)
20+
21+
// A memFile is an in-memory file with ReadAt, WriteAt, Close, and Sync methods.
22+
type memFile struct {
23+
readOnly bool
24+
data []byte
25+
}
26+
27+
func (f *memFile) ReadAt(data []byte, off int64) (int, error) {
28+
if off < 0 || off >= int64(len(f.data)) {
29+
return 0, io.EOF
30+
}
31+
n := copy(data, f.data[off:])
32+
if n < len(data) {
33+
return n, io.ErrUnexpectedEOF
34+
}
35+
return n, nil
36+
}
37+
38+
func (f *memFile) WriteAt(data []byte, off int64) (int, error) {
39+
if f.readOnly {
40+
panic("write to read-only file")
41+
}
42+
if off > int64(len(f.data)) {
43+
// Fill hole in file.
44+
f.data = append(f.data, make([]byte, int(off)-len(f.data))...)
45+
}
46+
n := copy(f.data[off:], data)
47+
f.data = append(f.data, data[n:]...)
48+
return len(data), nil
49+
}
50+
51+
func (f *memFile) Close() error {
52+
return nil
53+
}
54+
55+
func (f *memFile) Sync() error {
56+
return nil
57+
}
58+
59+
func memHash(t *diskTree) string {
60+
h := sha256.New()
61+
h.Write(t.mem)
62+
n := 1 + (len(t.mem)-hdrSize)/nodeSize
63+
const pmemHdrSize = 16
64+
leaf := pmemHdrSize + n*64
65+
switch f := t.leaf.(type) {
66+
default:
67+
panic(fmt.Sprintf("unknown leaf type %T", t.leaf))
68+
case *memFile:
69+
h.Write(f.data[:leaf])
70+
case *testFile:
71+
if len(f.data) != leaf {
72+
panic(fmt.Sprintf("unexpected leaf size in real tree: %d != %d (t.mem=%d)", len(f.data), leaf, len(t.mem)))
73+
}
74+
h.Write(f.data)
75+
}
76+
s := base64.StdEncoding.EncodeToString(h.Sum(nil))
77+
return fmt.Sprintf("%s/%#x", s[:7], len(t.mem))
78+
}
79+
80+
// A tester is a two-file simulator that checks after each write that
81+
// reopening the disk works properly, even if the write only happens
82+
// partially or even gets corrupted (unlikely but we can handle it).
83+
type tester struct {
84+
t *testing.T
85+
tree *diskTree // in-memory tree
86+
file [3]testFile // files backing tree
87+
valid map[string]bool // hashes of acceptable tree memory images
88+
replay []int // replay log for recovery
89+
}
90+
91+
// A testFile is a single simulated file.
92+
type testFile struct {
93+
memFile
94+
tester *tester
95+
sync int // offset of last sync; writes only append
96+
current bool // whether file is current
97+
}
98+
99+
func (f *testFile) name() string {
100+
if f.tester == nil {
101+
return "???"
102+
}
103+
for i := range 3 {
104+
if f == &f.tester.file[i] {
105+
return fmt.Sprint("file", i+1)
106+
}
107+
}
108+
return "???"
109+
}
110+
111+
func (f *testFile) setCurrent(current bool) {
112+
f.current = current
113+
}
114+
115+
func (f *testFile) clone() *memFile {
116+
return &memFile{readOnly: true, data: bytes.Clone(f.data)}
117+
}
118+
119+
// WriteAt writes to the test file.
120+
func (f *testFile) WriteAt(data []byte, off int64) (int, error) {
121+
// Writes to the current file should only ever append;
122+
// not overwriting is part of our reliability story.
123+
// Writes to the next file can be scattered, because
124+
// we are writing the tree interleaved with new patches.
125+
if f.current && off != int64(len(f.data)) {
126+
return 0, fmt.Errorf("non-appending write\n\n%s", debug.Stack())
127+
}
128+
f.tester.t.Logf("%s write %#x+%#x = %#x", f.name(), off, len(data), off+int64(len(data)))
129+
return f.memFile.WriteAt(data, off)
130+
}
131+
132+
// Sync syncs the test file.
133+
// Now bytes before the current offset cannot be lost or corrupted.
134+
func (f *testFile) Sync() error {
135+
if f.tester == nil {
136+
panic("sync of read-only file")
137+
}
138+
139+
f.sync = len(f.data)
140+
f.tester.t.Logf("%s sync at %#x", f.name(), f.sync)
141+
return nil
142+
}
143+
144+
func (tt *tester) markOK() {
145+
h := memHash(tt.tree)
146+
tt.t.Logf("ok %v", h)
147+
tt.valid[h] = true
148+
}
149+
150+
func (tt *tester) test(minVer int64, minExact bool) {
151+
tt.try(&tt.file[0], minVer, minExact)
152+
tt.try(&tt.file[1], minVer, minExact)
153+
}
154+
155+
// try tries reopening the files with various i/o problems.
156+
func (tt *tester) try(f *testFile, minVer int64, minExact bool) {
157+
if tt.tree == nil {
158+
// Initial tree not created yet.
159+
return
160+
}
161+
162+
// Test file with write actually succeeding.
163+
tt.reopen(minVer, minExact, "as written")
164+
}
165+
166+
func (tt *tester) reopen(minVer int64, minExact bool, format string, args ...any) {
167+
kind := fmt.Sprintf(format, args...)
168+
f1 := tt.file[0].clone()
169+
f2 := tt.file[1].clone()
170+
f3 := tt.file[2].clone()
171+
f3.readOnly = false
172+
tree, err := New(f1, f2, f3)
173+
if err != nil {
174+
tt.t.Fatalf("reopen: %s: %v", kind, err)
175+
}
176+
defer tree.Close()
177+
178+
version, exact := tree.Version()
179+
if err != nil {
180+
tt.t.Fatalf("reopen: %s: %v", kind, err)
181+
}
182+
if version < minVer || minExact != exact {
183+
tt.t.Fatalf("reopen: %s: version = %d,%v, want ≥ %d,%v", kind, version, exact, minVer, minExact)
184+
}
185+
if !exact {
186+
f1.readOnly = false
187+
f2.readOnly = false
188+
189+
// Find [-1, version] marking snapshot of recorded version.
190+
i := 0
191+
if version > 0 {
192+
for i < len(tt.replay) && (tt.replay[i] != -1 || int64(tt.replay[i+1]) != version) {
193+
i += 2
194+
}
195+
if i >= len(tt.replay) {
196+
tt.t.Fatalf("reopen: %s: recover %d %v: cannot find version %d", kind, version, exact, version)
197+
}
198+
i += 2
199+
}
200+
// Replay rest of log.
201+
for ; i < len(tt.replay); i += 2 {
202+
if tt.replay[i] == -1 {
203+
if _, err := tree.Snap(int64(tt.replay[i+1])); err != nil {
204+
tt.t.Fatalf("reopen: %s: Snap: %v", kind, err)
205+
}
206+
} else {
207+
if err := tree.Set(Key(v(tt.replay[i])), v(tt.replay[i+1])); err != nil {
208+
tt.t.Fatalf("reopen: %s: Set: %v", kind, err)
209+
}
210+
}
211+
}
212+
}
213+
214+
h := memHash(tree.(*diskTree))
215+
if !tt.valid[h] {
216+
tt.t.Fatalf("reopen (%d %d): %s: (%d %v): invalid hash %v want %v\n\n%s\nactual tree:\n%s\nrecovered tree:\n%s\nactual leaf:\n%s\nrecovered leaf (%v):\n%s",
217+
len(tt.file[0].data), len(tt.file[1].data), kind,
218+
version, exact,
219+
h, slices.Sorted(maps.Keys(tt.valid)),
220+
debug.Stack(),
221+
hexDump(tt.tree.mem),
222+
hexDump(tree.(*diskTree).mem),
223+
hexDump(tt.tree.leaf.(*testFile).data),
224+
tree.(*diskTree).leaf.(*memFile) == f3,
225+
hexDump(tree.(*diskTree).leaf.(*memFile).data))
226+
}
227+
}
228+
229+
func hexDump(data []byte) string {
230+
return hex.Dump(data[:min(len(data), 1024)])
231+
}
232+
233+
// TODO maybe for testing enable a pmem mode that
234+
// writes every mutation to a separate patch,
235+
// and then reopen after every file write?
236+
237+
func TestDiskRecovery(t *testing.T) {
238+
for i := range 10 {
239+
t.Run(fmt.Sprint(i), testDiskRecovery)
240+
}
241+
}
242+
243+
func testDiskRecovery(t *testing.T) {
244+
tt := &tester{t: t}
245+
for i := range tt.file {
246+
tt.file[i].tester = tt
247+
}
248+
249+
xtree, err := New(&tt.file[0], &tt.file[1], &tt.file[2])
250+
if err != nil {
251+
t.Fatal(err)
252+
}
253+
tree := xtree.(*diskTree)
254+
defer tree.Close() // relelase pmem on test failure
255+
256+
tree.pmem.SetConstantFlushing(true)
257+
tt.tree = tree
258+
tt.valid = make(map[string]bool)
259+
tt.markOK()
260+
version := int64(0)
261+
exact := false
262+
syncVersion := version
263+
syncExact := false
264+
265+
for range 10 {
266+
switch r := rand.N(10); r {
267+
default:
268+
i := rand.N(100)
269+
j := rand.N(100)
270+
t.Logf("set %d %d", i, j)
271+
tt.replay = append(tt.replay, i, j)
272+
check(t, tree.Set(Key(v(i)), v(j)))
273+
exact = false
274+
syncExact = false
275+
tt.markOK()
276+
tt.test(syncVersion, syncExact)
277+
278+
case 0, 1:
279+
version++
280+
exact = true
281+
t.Logf("snap %d", version)
282+
tt.replay = append(tt.replay, -1, int(version))
283+
_, err := tree.Snap(version)
284+
check(t, err)
285+
tt.markOK()
286+
tt.test(syncVersion, syncExact)
287+
fallthrough
288+
289+
case 3:
290+
t.Log("sync")
291+
check(t, tree.Sync())
292+
_, exact = tree.Version()
293+
syncVersion = version
294+
syncExact = exact
295+
clear(tt.valid)
296+
tt.markOK()
297+
tt.test(syncVersion, syncExact)
298+
}
299+
}
300+
301+
check(t, tree.Close())
302+
}
303+
304+
func TestDiskReopen(t *testing.T) {
305+
// Test that very basic tree written to disk can be reopened, restored.
306+
// Simulations are all well and good, but test real files a bit too.
307+
dir := t.TempDir()
308+
tree1, err := Create(dir+"/tree1", dir+"/tree2", dir+"/disk")
309+
if err != nil {
310+
t.Fatal(err)
311+
}
312+
check(t, err)
313+
defer tree1.Close()
314+
315+
for i := range 10 {
316+
check(t, tree1.Set(Key(v(i)), v(i)))
317+
}
318+
319+
_, err = tree1.Snap(1)
320+
check(t, err)
321+
check(t, tree1.Sync())
322+
323+
tree2, err := Open(dir+"/tree1", dir+"/tree2", dir+"/disk")
324+
check(t, err)
325+
defer tree2.Close()
326+
327+
if !bytes.Equal(tree1.(*diskTree).mem, tree2.(*diskTree).mem) {
328+
t.Fatalf("tree memory differs\n\n%s\n\n%s",
329+
hex.Dump(tree1.(*diskTree).mem[:1024]),
330+
hex.Dump(tree2.(*diskTree).mem[:1024]))
331+
}
332+
}
333+
334+
func check(t *testing.T, err error) {
335+
t.Helper()
336+
if err != nil {
337+
t.Fatal(err)
338+
}
339+
}

0 commit comments

Comments
 (0)