Skip to content

Commit 10c954b

Browse files
authored
Merge pull request #1059 from ahrtr/20250818_writeto_1.3
[release-1.3] Update (*Tx)WriteTo to reuse the already opened file if WriteFlag not set
2 parents 1898f36 + 3914986 commit 10c954b

File tree

2 files changed

+160
-14
lines changed

2 files changed

+160
-14
lines changed

db_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"path/filepath"
1313
"reflect"
14+
"runtime"
1415
"sync"
1516
"testing"
1617
"time"
@@ -724,6 +725,114 @@ func TestDB_Concurrent_WriteTo(t *testing.T) {
724725
wg.Wait()
725726
}
726727

728+
// TestDB_WriteTo_and_Overwrite verifies that `(tx *Tx) WriteTo` can still
729+
// work even the underlying file is overwritten between the time a read-only
730+
// transaction is created and the time the file is actually opened
731+
func TestDB_WriteTo_and_Overwrite(t *testing.T) {
732+
testCases := []struct {
733+
name string
734+
writeFlag int
735+
}{
736+
{
737+
name: "writeFlag not set",
738+
writeFlag: 0,
739+
},
740+
/* syscall.O_DIRECT not supported on some platforms, i.e. Windows and MacOS
741+
{
742+
name: "writeFlag set",
743+
writeFlag: syscall.O_DIRECT,
744+
},*/
745+
}
746+
747+
fRead := func(db *bolt.DB, bucketName []byte) map[string]string {
748+
data := make(map[string]string)
749+
_ = db.View(func(tx *bolt.Tx) error {
750+
b := tx.Bucket(bucketName)
751+
berr := b.ForEach(func(k, v []byte) error {
752+
data[string(k)] = string(v)
753+
return nil
754+
})
755+
require.NoError(t, berr)
756+
return nil
757+
})
758+
return data
759+
}
760+
761+
for _, tc := range testCases {
762+
t.Run(tc.name, func(t *testing.T) {
763+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{
764+
PageSize: 4096,
765+
})
766+
filePathOfDb := db.Path()
767+
768+
var (
769+
bucketName = []byte("data")
770+
dataExpected map[string]string
771+
dataActual map[string]string
772+
)
773+
774+
t.Log("Populate some data")
775+
err := db.Update(func(tx *bolt.Tx) error {
776+
b, berr := tx.CreateBucket(bucketName)
777+
if berr != nil {
778+
return berr
779+
}
780+
for k := 0; k < 10; k++ {
781+
key, value := fmt.Sprintf("key_%d", rand.Intn(10)), fmt.Sprintf("value_%d", rand.Intn(100))
782+
if perr := b.Put([]byte(key), []byte(value)); perr != nil {
783+
return perr
784+
}
785+
}
786+
return nil
787+
})
788+
require.NoError(t, err)
789+
790+
t.Log("Read all the data before calling WriteTo")
791+
dataExpected = fRead(db.DB, bucketName)
792+
793+
t.Log("Create a readonly transaction for WriteTo")
794+
rtx, rerr := db.Begin(false)
795+
require.NoError(t, rerr)
796+
797+
// Some platforms (i.e. Windows) don't support renaming a file
798+
// when the target file already exist and is opened.
799+
if runtime.GOOS == "linux" {
800+
t.Log("Create another empty db file")
801+
db2 := btesting.MustCreateDBWithOption(t, &bolt.Options{
802+
PageSize: 4096,
803+
})
804+
db2.MustClose()
805+
filePathOfDb2 := db2.Path()
806+
807+
t.Logf("Renaming the new empty db file (%s) to the original db path (%s)", filePathOfDb2, filePathOfDb)
808+
err = os.Rename(filePathOfDb2, filePathOfDb)
809+
require.NoError(t, err)
810+
} else {
811+
t.Log("Ignore renaming step on non-Linux platform")
812+
}
813+
814+
t.Logf("Call WriteTo to copy the data of the original db file")
815+
f := filepath.Join(t.TempDir(), "-backup-db")
816+
err = rtx.CopyFile(f, 0600)
817+
require.NoError(t, err)
818+
require.NoError(t, rtx.Rollback())
819+
820+
t.Logf("Read all the data from the backup db after calling WriteTo")
821+
newDB, err := bolt.Open(f, 0600, &bolt.Options{
822+
ReadOnly: true,
823+
})
824+
require.NoError(t, err)
825+
dataActual = fRead(newDB, bucketName)
826+
err = newDB.Close()
827+
require.NoError(t, err)
828+
829+
t.Log("Compare the dataExpected and dataActual")
830+
same := reflect.DeepEqual(dataExpected, dataActual)
831+
require.True(t, same, fmt.Sprintf("found inconsistent data, dataExpected: %v, ddataActual : %v", dataExpected, dataActual))
832+
})
833+
}
834+
}
835+
727836
// Ensure that opening a transaction while the DB is closed returns an error.
728837
func TestDB_BeginRW_Closed(t *testing.T) {
729838
var db bolt.DB

tx.go

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -344,16 +344,40 @@ func (tx *Tx) Copy(w io.Writer) error {
344344
// WriteTo writes the entire database to a writer.
345345
// If err == nil then exactly tx.Size() bytes will be written into the writer.
346346
func (tx *Tx) WriteTo(w io.Writer) (n int64, err error) {
347-
// Attempt to open reader with WriteFlag
348-
f, err := tx.db.openFile(tx.db.path, os.O_RDONLY|tx.WriteFlag, 0)
349-
if err != nil {
350-
return 0, err
351-
}
352-
defer func() {
353-
if cerr := f.Close(); err == nil {
354-
err = cerr
347+
var f *os.File
348+
// There is a risk that between the time a read-only transaction
349+
// is created and the time the file is actually opened, the
350+
// underlying db file at tx.db.path may have been replaced
351+
// (e.g. via rename). In that case, opening the file again would
352+
// unexpectedly point to a different file, rather than the one
353+
// the transaction was based on.
354+
//
355+
// To overcome this, we reuse the already opened file handle when
356+
// WritFlag not set. When the WriteFlag is set, we reopen the file
357+
// but verify that it still refers to the same underlying file
358+
// (by device and inode). If it does not, we fall back to
359+
// reusing the existing already opened file handle.
360+
if tx.WriteFlag != 0 {
361+
// Attempt to open reader with WriteFlag
362+
f, err = tx.db.openFile(tx.db.path, os.O_RDONLY|tx.WriteFlag, 0)
363+
if err != nil {
364+
return 0, err
355365
}
356-
}()
366+
367+
if ok, err := sameFile(tx.db.file, f); !ok {
368+
_ = f.Close() // ignore the error
369+
370+
f = tx.db.file
371+
} else {
372+
defer func() {
373+
if cerr := f.Close(); err == nil {
374+
err = cerr
375+
}
376+
}()
377+
}
378+
} else {
379+
f = tx.db.file
380+
}
357381

358382
// Generate a meta page. We use the same page data for both meta pages.
359383
buf := make([]byte, tx.db.pageSize)
@@ -380,13 +404,13 @@ func (tx *Tx) WriteTo(w io.Writer) (n int64, err error) {
380404
return n, fmt.Errorf("meta 1 copy: %s", err)
381405
}
382406

383-
// Move past the meta pages in the file.
384-
if _, err := f.Seek(int64(tx.db.pageSize*2), io.SeekStart); err != nil {
385-
return n, fmt.Errorf("seek: %s", err)
386-
}
407+
// Copy data pages using a SectionReader to avoid affecting f's offset.
408+
dataOffset := int64(tx.db.pageSize * 2)
409+
dataSize := tx.Size() - dataOffset
410+
sr := io.NewSectionReader(f, dataOffset, dataSize)
387411

388412
// Copy data pages.
389-
wn, err := io.CopyN(w, f, tx.Size()-int64(tx.db.pageSize*2))
413+
wn, err := io.CopyN(w, sr, dataSize)
390414
n += wn
391415
if err != nil {
392416
return n, err
@@ -395,6 +419,19 @@ func (tx *Tx) WriteTo(w io.Writer) (n int64, err error) {
395419
return n, nil
396420
}
397421

422+
func sameFile(f1, f2 *os.File) (bool, error) {
423+
fi1, err := f1.Stat()
424+
if err != nil {
425+
return false, fmt.Errorf("failed to get fileInfo of the first file (%s): %w", f1.Name(), err)
426+
}
427+
fi2, err := f2.Stat()
428+
if err != nil {
429+
return false, fmt.Errorf("failed to get fileInfo of the second file (%s): %w", f2.Name(), err)
430+
}
431+
432+
return os.SameFile(fi1, fi2), nil
433+
}
434+
398435
// CopyFile copies the entire database to file at the given path.
399436
// A reader transaction is maintained during the copy so it is safe to continue
400437
// using the database while a copy is in progress.

0 commit comments

Comments
 (0)