Skip to content

Commit 76e9512

Browse files
authored
Backup (#71)
1 parent f07a773 commit 76e9512

File tree

12 files changed

+262
-83
lines changed

12 files changed

+262
-83
lines changed

backup.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package pogreb
2+
3+
import (
4+
"io"
5+
"os"
6+
7+
"github.com/akrylysov/pogreb/fs"
8+
)
9+
10+
func touchFile(fsys fs.FileSystem, path string) error {
11+
f, err := fsys.OpenFile(path, os.O_CREATE|os.O_TRUNC, os.FileMode(0640))
12+
if err != nil {
13+
return err
14+
}
15+
return f.Close()
16+
}
17+
18+
// Backup creates a database backup at the specified path.
19+
func (db *DB) Backup(path string) error {
20+
// Make sure the compaction is not running during backup.
21+
db.maintenanceMu.Lock()
22+
defer db.maintenanceMu.Unlock()
23+
24+
if err := db.opts.rootFS.MkdirAll(path, 0755); err != nil {
25+
return err
26+
}
27+
28+
db.mu.RLock()
29+
var segments []*segment
30+
activeSegmentSizes := make(map[uint16]int64)
31+
for _, seg := range db.datalog.segmentsBySequenceID() {
32+
segments = append(segments, seg)
33+
if !seg.meta.Full {
34+
// Save the size of the active segments to copy only the data persisted up to the point
35+
// of when the backup started.
36+
activeSegmentSizes[seg.id] = seg.size
37+
}
38+
}
39+
db.mu.RUnlock()
40+
41+
srcFS := db.opts.FileSystem
42+
dstFS := fs.Sub(db.opts.rootFS, path)
43+
44+
for _, seg := range segments {
45+
name := segmentName(seg.id, seg.sequenceID)
46+
mode := os.FileMode(0640)
47+
srcFile, err := srcFS.OpenFile(name, os.O_RDONLY, mode)
48+
if err != nil {
49+
return err
50+
}
51+
52+
dstFile, err := dstFS.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, mode)
53+
if err != nil {
54+
return err
55+
}
56+
57+
if srcSize, ok := activeSegmentSizes[seg.id]; ok {
58+
if _, err := io.CopyN(dstFile, srcFile, srcSize); err != nil {
59+
return err
60+
}
61+
} else {
62+
if _, err := io.Copy(dstFile, srcFile); err != nil {
63+
return err
64+
}
65+
}
66+
67+
if err := srcFile.Close(); err != nil {
68+
return err
69+
}
70+
if err := dstFile.Close(); err != nil {
71+
return err
72+
}
73+
}
74+
75+
if err := touchFile(dstFS, lockName); err != nil {
76+
return err
77+
}
78+
79+
return nil
80+
}

backup_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package pogreb
2+
3+
import (
4+
"testing"
5+
6+
"github.com/akrylysov/pogreb/internal/assert"
7+
)
8+
9+
const testDBBackupName = testDBName + ".backup"
10+
11+
func TestBackup(t *testing.T) {
12+
opts := &Options{
13+
maxSegmentSize: 1024,
14+
compactionMinSegmentSize: 520,
15+
compactionMinFragmentation: 0.02,
16+
}
17+
18+
run := func(name string, f func(t *testing.T, db *DB)) bool {
19+
return t.Run(name, func(t *testing.T) {
20+
db, err := createTestDB(opts)
21+
assert.Nil(t, err)
22+
f(t, db)
23+
assert.Nil(t, db.Close())
24+
_ = cleanDir(testDBBackupName)
25+
})
26+
}
27+
28+
run("empty", func(t *testing.T, db *DB) {
29+
assert.Nil(t, db.Backup(testDBBackupName))
30+
db2, err := Open(testDBBackupName, opts)
31+
assert.Nil(t, err)
32+
assert.Nil(t, db2.Close())
33+
})
34+
35+
run("single segment", func(t *testing.T, db *DB) {
36+
assert.Nil(t, db.Put([]byte{0}, []byte{0}))
37+
assert.Equal(t, 1, countSegments(t, db))
38+
assert.Nil(t, db.Backup(testDBBackupName))
39+
db2, err := Open(testDBBackupName, opts)
40+
assert.Nil(t, err)
41+
v, err := db2.Get([]byte{0})
42+
assert.Equal(t, []byte{0}, v)
43+
assert.Nil(t, err)
44+
assert.Nil(t, db2.Close())
45+
})
46+
47+
run("multiple segments", func(t *testing.T, db *DB) {
48+
for i := byte(0); i < 100; i++ {
49+
assert.Nil(t, db.Put([]byte{i}, []byte{i}))
50+
}
51+
assert.Equal(t, 3, countSegments(t, db))
52+
assert.Nil(t, db.Backup(testDBBackupName))
53+
db2, err := Open(testDBBackupName, opts)
54+
assert.Equal(t, 3, countSegments(t, db2))
55+
assert.Nil(t, err)
56+
for i := byte(0); i < 100; i++ {
57+
v, err := db2.Get([]byte{i})
58+
assert.Nil(t, err)
59+
assert.Equal(t, []byte{i}, v)
60+
}
61+
assert.Nil(t, db2.Close())
62+
})
63+
}

compaction.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package pogreb
22

33
import (
4-
"sync/atomic"
5-
64
"github.com/akrylysov/pogreb/internal/errors"
75
)
86

@@ -135,11 +133,11 @@ func (db *DB) Compact() (CompactionResult, error) {
135133
cr := CompactionResult{}
136134

137135
// Run only a single compaction at a time.
138-
if !atomic.CompareAndSwapInt32(&db.compactionRunning, 0, 1) {
136+
if !db.maintenanceMu.TryLock() {
139137
return cr, errBusy
140138
}
141139
defer func() {
142-
atomic.StoreInt32(&db.compactionRunning, 0)
140+
db.maintenanceMu.Unlock()
143141
}()
144142

145143
db.mu.RLock()

compaction_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pogreb
33
import (
44
"os"
55
"path/filepath"
6+
"runtime"
67
"sync"
78
"sync/atomic"
89
"testing"
@@ -224,15 +225,20 @@ func TestCompaction(t *testing.T) {
224225
wg := sync.WaitGroup{}
225226
wg.Add(1)
226227
db.mu.Lock()
228+
var goroutineRunning int32
227229
go func() {
228230
// The compaction is blocked until we unlock the mutex.
229231
defer wg.Done()
232+
atomic.StoreInt32(&goroutineRunning, 1)
230233
_, err := db.Compact()
231234
assert.Nil(t, err)
232235
}()
233-
// Make sure the compaction is running.
236+
// Make sure the compaction goroutine is running.
237+
// It's unlikely, but still possible the compaction goroutine doesn't reach the maintenance
238+
// lock, which makes the test flaky.
239+
runtime.Gosched()
234240
assert.CompleteWithin(t, time.Minute, func() bool {
235-
return atomic.LoadInt32(&db.compactionRunning) == 1
241+
return atomic.LoadInt32(&goroutineRunning) == 1
236242
})
237243
_, err := db.Compact()
238244
assert.Equal(t, errBusy, err)

db.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,17 @@ const (
3030
// DB represents the key-value storage.
3131
// All DB methods are safe for concurrent use by multiple goroutines.
3232
type DB struct {
33-
mu sync.RWMutex // Allows multiple database readers or a single writer.
34-
opts *Options
35-
index *index
36-
datalog *datalog
37-
lock fs.LockFile // Prevents opening multiple instances of the same database.
38-
hashSeed uint32
39-
metrics *Metrics
40-
syncWrites bool
41-
cancelBgWorker context.CancelFunc
42-
closeWg sync.WaitGroup
43-
compactionRunning int32 // Prevents running compactions concurrently.
33+
mu sync.RWMutex // Allows multiple database readers or a single writer.
34+
opts *Options
35+
index *index
36+
datalog *datalog
37+
lock fs.LockFile // Prevents opening multiple instances of the same database.
38+
hashSeed uint32
39+
metrics *Metrics
40+
syncWrites bool
41+
cancelBgWorker context.CancelFunc
42+
closeWg sync.WaitGroup
43+
maintenanceMu sync.Mutex // Ensures there only one maintenance task running at a time.
4444
}
4545

4646
type dbMeta struct {
@@ -52,7 +52,7 @@ type dbMeta struct {
5252
func Open(path string, opts *Options) (*DB, error) {
5353
opts = opts.copyWithDefaults(path)
5454

55-
if err := os.MkdirAll(path, 0755); err != nil {
55+
if err := opts.rootFS.MkdirAll(path, 0755); err != nil {
5656
return nil, err
5757
}
5858

db_test.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,12 @@ func TestMain(m *testing.M) {
4141
fmt.Printf("DEBUG\tFS=%T\n", fsys)
4242
os.Exit(exitCode)
4343
}
44+
_ = cleanDir(testDBName)
45+
_ = cleanDir(testDBBackupName)
4446
}
45-
_ = cleanDir(testDBName)
4647
os.Exit(0)
4748
}
4849

49-
func touchFile(fsys fs.FileSystem, path string) error {
50-
f, err := fsys.OpenFile(path, os.O_CREATE|os.O_TRUNC, os.FileMode(0640))
51-
if err != nil {
52-
return err
53-
}
54-
return f.Close()
55-
}
56-
5750
func appendFile(path string, data []byte) error {
5851
f, err := testFS.OpenFile(path, os.O_RDWR, os.FileMode(0640))
5952
if err != nil {
@@ -93,7 +86,7 @@ func cleanDir(path string) error {
9386
return err
9487
}
9588
for _, file := range files {
96-
_ = testFS.Remove(filepath.Join(testDBName, file.Name()))
89+
_ = testFS.Remove(filepath.Join(path, file.Name()))
9790
}
9891
return nil
9992
}

file_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ func (fs *errfs) ReadDir(name string) ([]os.DirEntry, error) {
3434
return nil, errfileError
3535
}
3636

37+
func (fs *errfs) MkdirAll(path string, perm os.FileMode) error {
38+
return errfileError
39+
}
40+
3741
type errfile struct{}
3842

3943
var errfileError = errors.New("errfile error")

fs/fs.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var (
1414
)
1515

1616
// File is the interface compatible with os.File.
17+
// All methods are not thread-safe, except for ReadAt, Slice and Stat.
1718
type File interface {
1819
io.Closer
1920
io.Reader
@@ -60,4 +61,7 @@ type FileSystem interface {
6061

6162
// CreateLockFile creates a lock file.
6263
CreateLockFile(name string, perm os.FileMode) (LockFile, bool, error)
64+
65+
// MkdirAll creates a directory named path.
66+
MkdirAll(path string, perm os.FileMode) error
6367
}

0 commit comments

Comments
 (0)