Skip to content

Commit 4b0d919

Browse files
Sync active memtable and value log on Db.Sync (#1847) (#1953)
Fixes #1847 Currently, `DB.Sync()` only syncs the value log but not the WAL of the active memtable, however recovery happens from the WAL of the active memtable as a part of the `DB.Open()` method. This change attempts to sync both the logs, however there can be an issue in syncing one of the logs. This change lists all of the possible cases that can arise during the sync operations. Some of these issues will be taken separately. For example, the issue #1954 can arise and shall be handled separately. This change adds a few tests to ensure that the Sync behavior is not broken. This change also adds a learning test `(db2_test.go -> TestAssertValueLogIsNotWrittenToOnStartup)` to ensure that value log is only read from (and not written to) during startup. This learning shall be used in the next issue (the one described above (*))
1 parent f9b9e4d commit 4b0d919

File tree

5 files changed

+210
-1
lines changed

5 files changed

+210
-1
lines changed

db.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,38 @@ const (
676676
// Sync syncs database content to disk. This function provides
677677
// more control to user to sync data whenever required.
678678
func (db *DB) Sync() error {
679-
return db.vlog.sync()
679+
/**
680+
Make an attempt to sync both the logs, the active memtable's WAL and the vLog (1847).
681+
Cases:
682+
- All_ok :: If both the logs sync successfully.
683+
684+
- Entry_Lost :: If an entry with a value pointer was present in the active memtable's WAL,
685+
:: and the WAL was synced but there was an error in syncing the vLog.
686+
:: The entry will be considered lost and this case will need to be handled during recovery.
687+
688+
- Entries_Lost :: If there were errors in syncing both the logs, multiple entries would be lost.
689+
690+
- Entries_Lost :: If the active memtable's WAL is not synced but the vLog is synced, it will
691+
:: result in entries being lost because recovery of the active memtable is done from its WAL.
692+
:: Check `UpdateSkipList` in memtable.go.
693+
694+
- Nothing_lost :: If an entry with its value was present in the active memtable's WAL, and the WAL was synced,
695+
:: but there was an error in syncing the vLog.
696+
:: Nothing is lost for this very specific entry because the entry is completely present in the memtable's WAL.
697+
698+
- Partially_lost :: If entries were written partially in either of the logs,
699+
:: the logs will be truncated during recovery.
700+
:: As a result of truncation, some entries might be lost.
701+
:: Assume that 4KB of data is to be synced and invoking `Sync` results only in syncing 3KB
702+
:: of data and then the machine shuts down or the disk failure happens,
703+
:: this will result in partial writes. [[This case needs verification]]
704+
*/
705+
db.lock.RLock()
706+
memtableSyncError := db.mt.SyncWAL()
707+
db.lock.RUnlock()
708+
709+
vLogSyncError := db.vlog.sync()
710+
return y.CombineErrors(memtableSyncError, vLogSyncError)
680711
}
681712

682713
// getMemtables returns the current memtables and get references.

db2_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,3 +1059,79 @@ func TestKeyCount(t *testing.T) {
10591059
require.NoError(t, stream.Orchestrate(context.Background()))
10601060
require.Equal(t, N, uint64(count))
10611061
}
1062+
1063+
func TestAssertValueLogIsNotWrittenToOnStartup(t *testing.T) {
1064+
opt := DefaultOptions("").WithValueLogFileSize(1 << 20).WithValueThreshold(1 << 4)
1065+
1066+
dir, err := os.MkdirTemp(".", "badger-test")
1067+
require.NoError(t, err)
1068+
defer removeDir(dir)
1069+
1070+
openDb := func(readonly bool) *DB {
1071+
opts := &opt
1072+
opts.Dir = dir
1073+
opts.ValueDir = dir
1074+
if readonly {
1075+
opts.ReadOnly = true
1076+
}
1077+
1078+
if opts.InMemory {
1079+
opts.Dir = ""
1080+
opts.ValueDir = ""
1081+
}
1082+
db, err := Open(*opts)
1083+
require.NoError(t, err)
1084+
1085+
return db
1086+
}
1087+
1088+
key := func(i int) string {
1089+
return fmt.Sprintf("key%100d", i)
1090+
}
1091+
1092+
assertOnLoadDb := func(db *DB) uint32 {
1093+
data := []byte(fmt.Sprintf("value%100d", 1))
1094+
for i := 0; i < 20; i++ {
1095+
err := db.Update(func(txn *Txn) error {
1096+
return txn.SetEntry(NewEntry([]byte(key(i)), data))
1097+
})
1098+
require.NoError(t, err)
1099+
}
1100+
return db.vlog.maxFid
1101+
}
1102+
1103+
latestVLogFileSize := func(db *DB, vLogId uint32) uint32 {
1104+
return db.vlog.filesMap[vLogId].size.Load()
1105+
}
1106+
1107+
assertOnReadDb := func(db *DB) {
1108+
for i := 0; i < 20; i++ {
1109+
err := db.View(func(txn *Txn) error {
1110+
item, err := txn.Get([]byte(key(i)))
1111+
require.NoError(t, err, "Getting key: %s", key(i))
1112+
err = item.Value(func(v []byte) error {
1113+
_ = v
1114+
return nil
1115+
})
1116+
require.NoError(t, err, "Getting value for the key: %s", key(i))
1117+
return nil
1118+
})
1119+
require.NoError(t, err)
1120+
}
1121+
}
1122+
1123+
db := openDb(false)
1124+
vLogFileSize := latestVLogFileSize(db, assertOnLoadDb(db))
1125+
assertOnReadDb(db)
1126+
1127+
require.NoError(t, db.Sync())
1128+
require.NoError(t, db.Close())
1129+
1130+
db = openDb(true)
1131+
defer func() {
1132+
require.NoError(t, db.Close())
1133+
}()
1134+
1135+
assertOnReadDb(db)
1136+
require.Equal(t, latestVLogFileSize(db, db.vlog.maxFid), vLogFileSize)
1137+
}

db_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,6 +2035,67 @@ func TestSyncForRace(t *testing.T) {
20352035
<-doneChan
20362036
}
20372037

2038+
func TestSyncForNoErrors(t *testing.T) {
2039+
dir, err := os.MkdirTemp("", "badger-test")
2040+
require.NoError(t, err)
2041+
defer removeDir(dir)
2042+
2043+
db, err := Open(DefaultOptions(dir).WithSyncWrites(false))
2044+
require.NoError(t, err)
2045+
defer func() { require.NoError(t, db.Close()) }()
2046+
2047+
txn := db.NewTransaction(true)
2048+
for i := 0; i < 10; i++ {
2049+
require.NoError(
2050+
t,
2051+
txn.SetEntry(NewEntry(
2052+
[]byte(fmt.Sprintf("key%d", i)),
2053+
[]byte(fmt.Sprintf("value%d", i)),
2054+
)),
2055+
)
2056+
}
2057+
require.NoError(t, txn.Commit())
2058+
2059+
if err := db.Sync(); err != nil {
2060+
require.NoError(t, err)
2061+
}
2062+
}
2063+
2064+
func TestSyncForReadingTheEntriesThatWereSynced(t *testing.T) {
2065+
dir, err := os.MkdirTemp("", "badger-test")
2066+
require.NoError(t, err)
2067+
defer removeDir(dir)
2068+
2069+
db, err := Open(DefaultOptions(dir).WithSyncWrites(false))
2070+
require.NoError(t, err)
2071+
defer func() { require.NoError(t, db.Close()) }()
2072+
2073+
txn := db.NewTransaction(true)
2074+
for i := 0; i < 10; i++ {
2075+
require.NoError(
2076+
t,
2077+
txn.SetEntry(NewEntry(
2078+
[]byte(fmt.Sprintf("key%d", i)),
2079+
[]byte(fmt.Sprintf("value%d", i)),
2080+
)),
2081+
)
2082+
}
2083+
require.NoError(t, txn.Commit())
2084+
2085+
if err := db.Sync(); err != nil {
2086+
require.NoError(t, err)
2087+
}
2088+
2089+
readOnlyTxn := db.NewTransaction(false)
2090+
for i := 0; i < 10; i++ {
2091+
item, err := readOnlyTxn.Get([]byte(fmt.Sprintf("key%d", i)))
2092+
require.NoError(t, err)
2093+
2094+
value := getItemValue(t, item)
2095+
require.Equal(t, []byte(fmt.Sprintf("value%d", i)), value)
2096+
}
2097+
}
2098+
20382099
func TestForceFlushMemtable(t *testing.T) {
20392100
dir, err := os.MkdirTemp("", "badger-test")
20402101
require.NoError(t, err, "temp dir for badger could not be created")

y/error.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,16 @@ func Wrapf(err error, format string, args ...interface{}) error {
8484
}
8585
return errors.Wrapf(err, format, args...)
8686
}
87+
88+
func CombineErrors(one, other error) error {
89+
if one != nil && other != nil {
90+
return fmt.Errorf("%v; %v", one, other)
91+
}
92+
if one != nil && other == nil {
93+
return fmt.Errorf("%v", one)
94+
}
95+
if one == nil && other != nil {
96+
return fmt.Errorf("%v", other)
97+
}
98+
return nil
99+
}

y/error_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package y
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestCombineWithBothErrorsPresent(t *testing.T) {
11+
combinedError := CombineErrors(errors.New("one"), errors.New("two"))
12+
require.Equal(t, "one; two", combinedError.Error())
13+
}
14+
15+
func TestCombineErrorsWithOneErrorPresent(t *testing.T) {
16+
combinedError := CombineErrors(errors.New("one"), nil)
17+
require.Equal(t, "one", combinedError.Error())
18+
}
19+
20+
func TestCombineErrorsWithOtherErrorPresent(t *testing.T) {
21+
combinedError := CombineErrors(nil, errors.New("other"))
22+
require.Equal(t, "other", combinedError.Error())
23+
}
24+
25+
func TestCombineErrorsWithBothErrorsAsNil(t *testing.T) {
26+
combinedError := CombineErrors(nil, nil)
27+
require.NoError(t, combinedError)
28+
}

0 commit comments

Comments
 (0)