Skip to content

Commit 599de69

Browse files
authored
Merge pull request #1058 from ahrtr/20250818_writeto_1.4
[release-1.4] Update (*Tx)WriteTo to reuse the already opened file if WriteFlag not set
2 parents 76cd3d4 + 8fd1b83 commit 599de69

File tree

2 files changed

+163
-14
lines changed

2 files changed

+163
-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
"strings"
1516
"sync"
1617
"testing"
@@ -760,6 +761,114 @@ func TestDB_Concurrent_WriteTo_and_ConsistentRead(t *testing.T) {
760761
}
761762
}
762763

764+
// TestDB_WriteTo_and_Overwrite verifies that `(tx *Tx) WriteTo` can still
765+
// work even the underlying file is overwritten between the time a read-only
766+
// transaction is created and the time the file is actually opened
767+
func TestDB_WriteTo_and_Overwrite(t *testing.T) {
768+
testCases := []struct {
769+
name string
770+
writeFlag int
771+
}{
772+
{
773+
name: "writeFlag not set",
774+
writeFlag: 0,
775+
},
776+
/* syscall.O_DIRECT not supported on some platforms, i.e. Windows and MacOS
777+
{
778+
name: "writeFlag set",
779+
writeFlag: syscall.O_DIRECT,
780+
},*/
781+
}
782+
783+
fRead := func(db *bolt.DB, bucketName []byte) map[string]string {
784+
data := make(map[string]string)
785+
_ = db.View(func(tx *bolt.Tx) error {
786+
b := tx.Bucket(bucketName)
787+
berr := b.ForEach(func(k, v []byte) error {
788+
data[string(k)] = string(v)
789+
return nil
790+
})
791+
require.NoError(t, berr)
792+
return nil
793+
})
794+
return data
795+
}
796+
797+
for _, tc := range testCases {
798+
t.Run(tc.name, func(t *testing.T) {
799+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{
800+
PageSize: 4096,
801+
})
802+
filePathOfDb := db.Path()
803+
804+
var (
805+
bucketName = []byte("data")
806+
dataExpected map[string]string
807+
dataActual map[string]string
808+
)
809+
810+
t.Log("Populate some data")
811+
err := db.Update(func(tx *bolt.Tx) error {
812+
b, berr := tx.CreateBucket(bucketName)
813+
if berr != nil {
814+
return berr
815+
}
816+
for k := 0; k < 10; k++ {
817+
key, value := fmt.Sprintf("key_%d", rand.Intn(10)), fmt.Sprintf("value_%d", rand.Intn(100))
818+
if perr := b.Put([]byte(key), []byte(value)); perr != nil {
819+
return perr
820+
}
821+
}
822+
return nil
823+
})
824+
require.NoError(t, err)
825+
826+
t.Log("Read all the data before calling WriteTo")
827+
dataExpected = fRead(db.DB, bucketName)
828+
829+
t.Log("Create a readonly transaction for WriteTo")
830+
rtx, rerr := db.Begin(false)
831+
require.NoError(t, rerr)
832+
833+
// Some platforms (i.e. Windows) don't support renaming a file
834+
// when the target file already exist and is opened.
835+
if runtime.GOOS == "linux" {
836+
t.Log("Create another empty db file")
837+
db2 := btesting.MustCreateDBWithOption(t, &bolt.Options{
838+
PageSize: 4096,
839+
})
840+
db2.MustClose()
841+
filePathOfDb2 := db2.Path()
842+
843+
t.Logf("Renaming the new empty db file (%s) to the original db path (%s)", filePathOfDb2, filePathOfDb)
844+
err = os.Rename(filePathOfDb2, filePathOfDb)
845+
require.NoError(t, err)
846+
} else {
847+
t.Log("Ignore renaming step on non-Linux platform")
848+
}
849+
850+
t.Logf("Call WriteTo to copy the data of the original db file")
851+
f := filepath.Join(t.TempDir(), "-backup-db")
852+
err = rtx.CopyFile(f, 0600)
853+
require.NoError(t, err)
854+
require.NoError(t, rtx.Rollback())
855+
856+
t.Logf("Read all the data from the backup db after calling WriteTo")
857+
newDB, err := bolt.Open(f, 0600, &bolt.Options{
858+
ReadOnly: true,
859+
})
860+
require.NoError(t, err)
861+
dataActual = fRead(newDB, bucketName)
862+
err = newDB.Close()
863+
require.NoError(t, err)
864+
865+
t.Log("Compare the dataExpected and dataActual")
866+
same := reflect.DeepEqual(dataExpected, dataActual)
867+
require.True(t, same, fmt.Sprintf("found inconsistent data, dataExpected: %v, ddataActual : %v", dataExpected, dataActual))
868+
})
869+
}
870+
}
871+
763872
// Ensure that opening a transaction while the DB is closed returns an error.
764873
func TestDB_BeginRW_Closed(t *testing.T) {
765874
var db bolt.DB

tx.go

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -387,16 +387,43 @@ func (tx *Tx) Copy(w io.Writer) error {
387387
// WriteTo writes the entire database to a writer.
388388
// If err == nil then exactly tx.Size() bytes will be written into the writer.
389389
func (tx *Tx) WriteTo(w io.Writer) (n int64, err error) {
390-
// Attempt to open reader with WriteFlag
391-
f, err := tx.db.openFile(tx.db.path, os.O_RDONLY|tx.WriteFlag, 0)
392-
if err != nil {
393-
return 0, err
394-
}
395-
defer func() {
396-
if cerr := f.Close(); err == nil {
397-
err = cerr
390+
var f *os.File
391+
// There is a risk that between the time a read-only transaction
392+
// is created and the time the file is actually opened, the
393+
// underlying db file at tx.db.path may have been replaced
394+
// (e.g. via rename). In that case, opening the file again would
395+
// unexpectedly point to a different file, rather than the one
396+
// the transaction was based on.
397+
//
398+
// To overcome this, we reuse the already opened file handle when
399+
// WritFlag not set. When the WriteFlag is set, we reopen the file
400+
// but verify that it still refers to the same underlying file
401+
// (by device and inode). If it does not, we fall back to
402+
// reusing the existing already opened file handle.
403+
if tx.WriteFlag != 0 {
404+
// Attempt to open reader with WriteFlag
405+
f, err = tx.db.openFile(tx.db.path, os.O_RDONLY|tx.WriteFlag, 0)
406+
if err != nil {
407+
return 0, err
398408
}
399-
}()
409+
410+
if ok, err := sameFile(tx.db.file, f); !ok {
411+
lg := tx.db.Logger()
412+
if cerr := f.Close(); cerr != nil {
413+
lg.Errorf("failed to close the file (%s): %v", tx.db.path, cerr)
414+
}
415+
lg.Warningf("The underlying file has changed, so reuse the already opened file (%s): %v", tx.db.path, err)
416+
f = tx.db.file
417+
} else {
418+
defer func() {
419+
if cerr := f.Close(); err == nil {
420+
err = cerr
421+
}
422+
}()
423+
}
424+
} else {
425+
f = tx.db.file
426+
}
400427

401428
// Generate a meta page. We use the same page data for both meta pages.
402429
buf := make([]byte, tx.db.pageSize)
@@ -423,13 +450,13 @@ func (tx *Tx) WriteTo(w io.Writer) (n int64, err error) {
423450
return n, fmt.Errorf("meta 1 copy: %s", err)
424451
}
425452

426-
// Move past the meta pages in the file.
427-
if _, err := f.Seek(int64(tx.db.pageSize*2), io.SeekStart); err != nil {
428-
return n, fmt.Errorf("seek: %s", err)
429-
}
453+
// Copy data pages using a SectionReader to avoid affecting f's offset.
454+
dataOffset := int64(tx.db.pageSize * 2)
455+
dataSize := tx.Size() - dataOffset
456+
sr := io.NewSectionReader(f, dataOffset, dataSize)
430457

431458
// Copy data pages.
432-
wn, err := io.CopyN(w, f, tx.Size()-int64(tx.db.pageSize*2))
459+
wn, err := io.CopyN(w, sr, dataSize)
433460
n += wn
434461
if err != nil {
435462
return n, err
@@ -438,6 +465,19 @@ func (tx *Tx) WriteTo(w io.Writer) (n int64, err error) {
438465
return n, nil
439466
}
440467

468+
func sameFile(f1, f2 *os.File) (bool, error) {
469+
fi1, err := f1.Stat()
470+
if err != nil {
471+
return false, fmt.Errorf("failed to get fileInfo of the first file (%s): %w", f1.Name(), err)
472+
}
473+
fi2, err := f2.Stat()
474+
if err != nil {
475+
return false, fmt.Errorf("failed to get fileInfo of the second file (%s): %w", f2.Name(), err)
476+
}
477+
478+
return os.SameFile(fi1, fi2), nil
479+
}
480+
441481
// CopyFile copies the entire database to file at the given path.
442482
// A reader transaction is maintained during the copy so it is safe to continue
443483
// using the database while a copy is in progress.

0 commit comments

Comments
 (0)