Skip to content

Commit 99e98d3

Browse files
committed
feat(compact): add archive logic
1 parent e733fe4 commit 99e98d3

File tree

6 files changed

+255
-26
lines changed

6 files changed

+255
-26
lines changed

docs/wal_compact.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
### compaction approach
2+
3+
- wal file exists
4+
5+
- every operation runs through wal
6+
7+
- check wal file size on regular basis
8+
9+
- have a default size configured
10+
11+
- if current size exceeds configured size
12+
13+
- start compaction
14+
15+
- create a new wal file
16+
17+
- redirect operations to newly created file
18+
19+
- create a tar file
20+
21+
- add wal file with data to tar
22+
23+
- remove the old file
24+
25+
#### considerations
26+
27+
- how to handle operations to wal file during compaction?
28+
29+
create a new wal file, redirect operations to new file
30+
31+
- how to handle any error or failure during compaction?
32+
33+
remove the tar file created
34+
35+
raise a warn level message to indicate failure
36+
37+
**Todo**
38+
39+
- [ ] find a way to recover from failure and deal with multiple wal files during recovery cycle
40+
41+
- [ ] what happens when drive used to store archive is full
42+
43+
- [ ] dependency on `.local` folder in `home` location

wal/compact.go

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// compact check file size
2+
// when size exceeds configured value
3+
// it creates archive and uses a new file
4+
package wal
5+
6+
import (
7+
"archive/tar"
8+
"compress/gzip"
9+
"fmt"
10+
"io"
11+
"io/fs"
12+
"os"
13+
"path/filepath"
14+
"strconv"
15+
"time"
16+
)
17+
18+
var defaultMaxFileSize int64
19+
20+
const DEFAULT_SIZE = "DEFAULT_SIZE"
21+
22+
// getDefaultFileSize gets the configured
23+
// allowed file size for wal, when none defined
24+
// it uses default `3*4096` as size
25+
func getDefaultFileSize() {
26+
ev := os.Getenv(DEFAULT_SIZE)
27+
if ev == "" {
28+
ev = getDefaultSize()
29+
}
30+
fs, err := strconv.Atoi(ev)
31+
if err != nil {
32+
fmt.Fprintf(os.Stderr, "%v\n", "error getting wal file size from env var")
33+
os.Exit(1)
34+
}
35+
defaultMaxFileSize = int64(fs)
36+
}
37+
38+
func getDefaultSize() string {
39+
dsize := strconv.Itoa(3 * 4096)
40+
return dsize
41+
}
42+
43+
// compact checks the wal file size
44+
// when size exceeds predefined size `4*4096`
45+
// it triggers the compact routine
46+
// creates a new archive file and save it disk
47+
func (w *Wal) compact() {
48+
// check existing wal file size
49+
walInfo, err := w.getWalFileInfo()
50+
if err != nil {
51+
fmt.Fprintf(os.Stderr, "error:[%v] getting wal file size", err)
52+
}
53+
if walInfo.Size() > defaultMaxFileSize {
54+
fmt.Fprintf(os.Stdout, "[info]: start compacting:%s\n", walInfo.Name())
55+
archive, err := createArchive(archiveFileName())
56+
if err != nil {
57+
fmt.Fprintf(os.Stderr,
58+
"error:[%v] creating archive with name-%s\n", err, archiveFileName())
59+
}
60+
if err := w.writeToArchive(archive, walInfo, archiveFileName()); err != nil {
61+
fmt.Fprintf(os.Stderr, "error:[%v] writing file to archive", err)
62+
}
63+
}
64+
}
65+
66+
func (w *Wal) writeToArchive(archiveFile io.ReadWriter,
67+
walInfo fs.FileInfo, archiveFileName string) error {
68+
gzr := gzip.NewWriter(archiveFile)
69+
defer gzr.Flush()
70+
defer gzr.Close()
71+
twr := tar.NewWriter(gzr)
72+
defer twr.Close()
73+
tarHeader, err := tar.FileInfoHeader(walInfo, walInfo.Name())
74+
if err != nil {
75+
fmt.Fprintf(os.Stderr,
76+
"error:[%v] generating tar file header for %q",
77+
err, walInfo.Name())
78+
}
79+
tarHeader.Name = walInfo.Name()
80+
if err := twr.WriteHeader(tarHeader); err != nil {
81+
fmt.Fprintf(os.Stderr,
82+
"error:[%v] writing header to tar archive-%s",
83+
err, archiveFileName)
84+
}
85+
f, err := os.OpenFile(w.fileName, os.O_RDONLY, fs.FileMode(os.O_RDONLY))
86+
if err != nil {
87+
fmt.Fprintf(os.Stderr,
88+
"error:[%v] opening wal file-%s to copy into archive",
89+
err, walInfo.Name())
90+
}
91+
if _, err := io.Copy(twr, f); err != nil {
92+
fmt.Fprintf(os.Stderr,
93+
"error:[%v] writing file-%s to archive-%s",
94+
err, walInfo.Name(), archiveFile)
95+
}
96+
return f.Close()
97+
}
98+
99+
func (w *Wal) getWalFileInfo() (fs.FileInfo, error) {
100+
f, err := os.Open(w.fileName)
101+
defer func() {
102+
if err := f.Close(); err != nil {
103+
fmt.Fprintf(os.Stderr,
104+
"error:[%v] closing wal file-%s\n", err, w.WalFile())
105+
}
106+
}()
107+
if err != nil {
108+
fmt.Fprintf(os.Stderr, "error:[%v] opening file\n", err)
109+
return nil, err
110+
}
111+
info, err := f.Stat()
112+
if err != nil {
113+
fmt.Fprintf(os.Stderr, "error:[%v] getting file stats\n", err)
114+
return nil, err
115+
}
116+
return info, nil
117+
}
118+
119+
// createArchive creates new archive file
120+
func createArchive(fname string) (*os.File, error) {
121+
archiveFile, err := os.Create(fname)
122+
if err != nil {
123+
fmt.Fprintf(os.Stderr, "error:[%v] creating archive file with name-%s", err, fname)
124+
return archiveFile, err
125+
}
126+
return archiveFile, err
127+
}
128+
129+
// archiveFileName generates file name to be used for archive
130+
// with timestamp numeric in unix micro format
131+
// ex - archive-17188751512222238.tar
132+
func archiveFileName() string {
133+
now := time.Now().UnixMicro()
134+
fname := fmt.Sprintf("archive-%d.tar", now)
135+
local := ".local"
136+
home := os.Getenv("HOME")
137+
fileName := filepath.Join(home, local, fname)
138+
return fileName
139+
}

wal/compact_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package wal
2+
3+
import (
4+
"fmt"
5+
"io/fs"
6+
"os"
7+
"testing"
8+
)
9+
10+
func Test_compact(t *testing.T) {
11+
// create a sample wal file locally
12+
w := new()
13+
if err := w.createFile(); err != nil {
14+
t.Fatalf("error-[%v] creating wal file-%q\n", err, w.WalFile())
15+
}
16+
// insert some dummy data
17+
f, err := os.OpenFile(w.WalFile(), os.O_APPEND|os.O_WRONLY, fs.FileMode(os.O_APPEND))
18+
if err != nil {
19+
t.Fatalf("error-[%v] opening wal file-%q", err, w.WalFile())
20+
}
21+
for i := 1; i <= 3; i++ {
22+
if _, err := fmt.Fprintf(f, "some test data-%d\n", i); err != nil {
23+
t.Fatalf("error-[%v] writing data to wal file %q", err, w.WalFile())
24+
}
25+
}
26+
fInfo, err := w.getWalFileInfo()
27+
if err != nil {
28+
t.Fatalf("error-[%v] getting wal file info", err)
29+
}
30+
// set a default file size
31+
defaultMaxFileSize = int64(fInfo.Size() - 1)
32+
t.Logf("file size:%d\n", defaultMaxFileSize)
33+
if _, err := fmt.Fprintf(f, "some more data to increase size of exisiting file\n"); err != nil {
34+
t.Fatalf("error-[%v] adding data to trigger compact action", err)
35+
}
36+
// test if condition to compact is triggered
37+
t.Logf("new file size:%d\n", int64(fInfo.Size()))
38+
if err := f.Close(); err != nil {
39+
t.Fatalf("error-[%v] closing file %q", err, w.WalFile())
40+
}
41+
w.compact()
42+
}

wal/sync_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ func Test_compare_file_size(t *testing.T) {
1414
// create test file and write some data
1515
// for testing
1616
testWal := new()
17-
fileName := fileName()
17+
fileName := testWal.WalFile()
1818
// test file operations
19-
if err := createFile(); err != nil {
19+
if err := testWal.createFile(); err != nil {
2020
t.Fatal(err)
2121
}
2222
// open file to add some data
@@ -52,9 +52,9 @@ func Test_upd_in_memory_cache(t *testing.T) {
5252
// create test file and write some data
5353
// for testing
5454
testWal := new()
55-
fileName := fileName()
55+
fileName := testWal.WalFile()
5656
// test file operations
57-
if err := createFile(); err != nil {
57+
if err := testWal.createFile(); err != nil {
5858
t.Fatal(err)
5959
}
6060
// open file to add some data

wal/wal.go

+23-18
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,25 @@ import (
88
"io"
99
"os"
1010
"path/filepath"
11+
"time"
1112
)
1213

1314
// receive data and persist in file
1415
// keep track of pointer up to where the data is being loaded in to cache
1516

1617
type Wal struct {
17-
filePointer int
18-
size int
19-
fileName string
20-
}
21-
22-
func fileName() string {
23-
home := os.Getenv("HOME")
24-
fileName := filepath.Join(home, ".local", "wal.txt")
25-
return fileName
18+
filePointer int
19+
size int
20+
stamp int64
21+
fileName string
22+
defaultWalDir string
2623
}
2724

2825
// createFile checks if file exist
2926
// and create new file when not found
30-
func createFile() error {
31-
if !exists() {
32-
_, err := os.Create(fileName())
27+
func (w *Wal) createFile() error {
28+
if !w.exists() {
29+
_, err := os.Create(w.fileName)
3330
if err != nil {
3431
return err
3532
}
@@ -39,23 +36,31 @@ func createFile() error {
3936

4037
// creates file if one does not exist
4138
func new() *Wal {
39+
now := time.Now().UnixMicro()
40+
home := os.Getenv("HOME")
41+
local := ".local"
4242
return &Wal{
43-
filePointer: 0,
44-
size: 4096,
45-
fileName: fileName(),
43+
filePointer: 0,
44+
size: 4096,
45+
stamp: now,
46+
defaultWalDir: filepath.Join(home, local),
47+
fileName: filepath.Join(home, local, fmt.Sprintf("wal-%d.txt", now)),
4648
}
4749
}
4850

4951
func (w *Wal) WalFile() string {
5052
return w.fileName
5153
}
5254

53-
func exists() bool {
54-
f, err := os.Open(fileName())
55+
func (w *Wal) exists() bool {
56+
f, err := os.Open(w.fileName)
5557
if err != nil && errors.Is(err, os.ErrNotExist) {
5658
return false
5759
}
58-
f.Close()
60+
if err := f.Close(); err != nil {
61+
fmt.Fprintf(os.Stderr, "error:[%v] closing-%s file\n", err, w.WalFile())
62+
os.Exit(1)
63+
}
5964
return true
6065
}
6166

wal/wal_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import (
1111

1212
func Test_encode_decode(t *testing.T) {
1313
testWal := new()
14-
fileName := fileName()
14+
fileName := testWal.WalFile()
1515
b := &bytes.Buffer{}
16-
if err := createFile(); err != nil {
16+
if err := testWal.createFile(); err != nil {
1717
t.Fatal(err)
1818
}
1919
d := &datastructure.Data{
@@ -40,9 +40,9 @@ func Test_encode_decode(t *testing.T) {
4040

4141
func Test_encode_read_at(t *testing.T) {
4242
testWal := new()
43-
fileName:=fileName()
43+
fileName := testWal.WalFile()
4444
b := &bytes.Buffer{}
45-
if err := createFile(); err != nil {
45+
if err := testWal.createFile(); err != nil {
4646
t.Fatal(err)
4747
}
4848
d := &datastructure.Data{

0 commit comments

Comments
 (0)