ceremonyclient/pebble/vfs/syncing_file_test.go

293 lines
7.0 KiB
Go
Raw Permalink Normal View History

2024-01-03 07:31:42 +00:00
// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package vfs
import (
"bytes"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/require"
)
func TestSyncingFile(t *testing.T) {
const mb = 1 << 20
tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
require.NoError(t, err)
filename := tmpf.Name()
require.NoError(t, tmpf.Close())
defer os.Remove(filename)
f, err := Default.Create(filename)
require.NoError(t, err)
tf := &mockSyncToFile{File: f, canSyncTo: true}
sf := NewSyncingFile(tf, SyncingFileOptions{BytesPerSync: 8 << 10 /* 8 KB */})
sf.(*syncingFile).fd = 1
testCases := []struct {
n int64
expectedSyncTo int64
}{
{mb, -1},
{mb, mb},
{4 << 10, mb},
{4 << 10, mb + 8<<10},
{8 << 10, mb + 16<<10},
{16 << 10, mb + 32<<10},
}
for i, c := range testCases {
_, err := sf.Write(make([]byte, c.n))
require.NoError(t, err)
syncTo := sf.(*syncingFile).syncOffset.Load()
if c.expectedSyncTo != syncTo {
t.Fatalf("%d: expected sync to %d, but found %d", i, c.expectedSyncTo, syncTo)
}
}
}
func TestSyncingFileClose(t *testing.T) {
testCases := []struct {
canSyncTo bool
expected string
}{
{true, `sync-to(1048576): test [false,<nil>]
sync-to(2097152): test [false,<nil>]
sync-to(3145728): test [false,<nil>]
pre-close: test [offset=4194304 sync-offset=3145728]
sync-data: test [<nil>]
close: test [<nil>]
`},
// When SyncTo is not being used, the last sync call ends up syncing all
// of the data causing syncingFile.Close to elide the sync.
{false, `sync-to(1048576): test [true,<nil>]
sync-to(3145728): test [true,<nil>]
pre-close: test [offset=4194304 sync-offset=4194304]
close: test [<nil>]
`},
}
for _, c := range testCases {
t.Run(fmt.Sprintf("canSyncTo=%t", c.canSyncTo), func(t *testing.T) {
tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
require.NoError(t, err)
filename := tmpf.Name()
require.NoError(t, tmpf.Close())
defer os.Remove(filename)
f, err := Default.Create(filename)
require.NoError(t, err)
var buf bytes.Buffer
tf := &mockSyncToFile{File: f, canSyncTo: c.canSyncTo}
lf := &vfsTestFSFile{tf, "test", &buf}
s := NewSyncingFile(lf, SyncingFileOptions{BytesPerSync: 8 << 10 /* 8 KB */}).(*syncingFile)
write := func(n int64) {
t.Helper()
_, err := s.Write(make([]byte, n))
require.NoError(t, err)
}
const mb = 1 << 20
write(2 * mb)
write(mb)
write(mb)
fmt.Fprintf(&buf, "pre-close: %s [offset=%d sync-offset=%d]\n",
lf.name, s.offset.Load(), s.syncOffset.Load())
require.NoError(t, s.Close())
if s := buf.String(); c.expected != s {
t.Fatalf("expected\n%s\nbut found\n%s", c.expected, s)
}
})
}
}
type mockSyncToFile struct {
File
canSyncTo bool
}
func (f *mockSyncToFile) SyncTo(length int64) (fullSync bool, err error) {
if !f.canSyncTo {
if err = f.File.SyncData(); err != nil {
return false, err
}
return true, nil
}
// f.canSyncTo = true
if _, err = f.File.SyncTo(length); err != nil {
return false, err
}
// NB: If the underlying file performed a full sync, lie.
return false, nil
}
func TestSyncingFileNoSyncOnClose(t *testing.T) {
testCases := []struct {
useSyncTo bool
expectBefore int64
expectAfter int64
}{
{false, 2 << 20, 3<<20 + 128},
{true, 2 << 20, 3<<20 + 128},
}
for _, c := range testCases {
t.Run(fmt.Sprintf("useSyncTo=%v", c.useSyncTo), func(t *testing.T) {
tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
require.NoError(t, err)
filename := tmpf.Name()
require.NoError(t, tmpf.Close())
defer os.Remove(filename)
f, err := Default.Create(filename)
require.NoError(t, err)
tf := &mockSyncToFile{f, c.useSyncTo}
s := NewSyncingFile(tf, SyncingFileOptions{NoSyncOnClose: true, BytesPerSync: 8 << 10}).(*syncingFile)
write := func(n int64) {
t.Helper()
_, err := s.Write(make([]byte, n))
require.NoError(t, err)
}
const mb = 1 << 20
write(2 * mb) // Sync first 2MB
write(mb) // No sync because syncToOffset = 3M-1M = 2M
write(128) // No sync for the same reason
syncToBefore := s.syncOffset.Load()
require.NoError(t, s.Close())
syncToAfter := s.syncOffset.Load()
// If we're not able to non-blockingly sync using sync-to,
// NoSyncOnClose should elide the sync.
if !c.useSyncTo {
if syncToBefore != c.expectBefore || syncToAfter != c.expectAfter {
t.Fatalf("Expected syncTo before and after closing are %d %d but found %d %d",
c.expectBefore, c.expectAfter, syncToBefore, syncToAfter)
}
}
})
}
}
func BenchmarkSyncWrite(b *testing.B) {
const targetSize = 16 << 20
var wsizes []int
if testing.Verbose() {
wsizes = []int{64, 512, 1 << 10, 2 << 10, 4 << 10, 8 << 10, 16 << 10, 32 << 10}
} else {
wsizes = []int{64}
}
run := func(b *testing.B, wsize int, newFile func(string) File) {
tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
if err != nil {
b.Fatal(err)
}
filename := tmpf.Name()
_ = tmpf.Close()
defer os.Remove(filename)
var f File
var size int
buf := make([]byte, wsize)
b.SetBytes(int64(len(buf)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
if f == nil {
b.StopTimer()
f = newFile(filename)
size = 0
b.StartTimer()
}
if _, err := f.Write(buf); err != nil {
b.Fatal(err)
}
if err := f.Sync(); err != nil {
b.Fatal(err)
}
size += len(buf)
if size >= targetSize {
_ = f.Close()
f = nil
}
}
b.StopTimer()
}
b.Run("no-prealloc", func(b *testing.B) {
for _, wsize := range wsizes {
b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) {
run(b, wsize, func(filename string) File {
_ = os.Remove(filename)
t, err := os.Create(filename)
if err != nil {
b.Fatal(err)
}
return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 0})
})
})
}
})
b.Run("prealloc-4MB", func(b *testing.B) {
for _, wsize := range wsizes {
b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) {
run(b, wsize, func(filename string) File {
_ = os.Remove(filename)
t, err := os.Create(filename)
if err != nil {
b.Fatal(err)
}
return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 4 << 20})
})
})
}
})
b.Run("reuse", func(b *testing.B) {
for _, wsize := range wsizes {
b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) {
init := true
run(b, wsize, func(filename string) File {
if init {
init = false
t, err := os.OpenFile(filename, os.O_RDWR, 0755)
if err != nil {
b.Fatal(err)
}
if _, err := t.Write(make([]byte, targetSize)); err != nil {
b.Fatal(err)
}
if err := t.Sync(); err != nil {
b.Fatal(err)
}
t.Close()
}
t, err := os.OpenFile(filename, os.O_RDWR, 0755)
if err != nil {
b.Fatal(err)
}
return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 0})
})
})
}
})
}