Skip to content

Commit cec3a95

Browse files
nakabonneclaude
andauthored
Add Snappy streaming compression to WAL segments (#70)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 64a7605 commit cec3a95

4 files changed

Lines changed: 70 additions & 13 deletions

File tree

disk_wal.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"strconv"
1414
"sync"
1515
"sync/atomic"
16+
17+
"github.com/golang/snappy"
1618
)
1719

1820
// diskWAL contains multiple segment files. One segment is responsible for one partition.
@@ -28,6 +30,8 @@ type diskWAL struct {
2830
bufferedSize int
2931
// Buffered-writer to the active segment
3032
w *bufio.Writer
33+
// Snappy streaming writer
34+
sw *snappy.Writer
3135
// File descriptor to the active segment
3236
fd *os.File
3337
index uint32
@@ -46,12 +50,29 @@ func newDiskWAL(dir string, bufferedSize int) (wal, error) {
4650
if err != nil {
4751
return nil, err
4852
}
49-
w.fd = f
50-
w.w = bufio.NewWriterSize(f, bufferedSize)
53+
w.initWriter(f)
5154

5255
return w, nil
5356
}
5457

58+
// initWriter sets up the writer chain: bufio.Writer → snappy.Writer → os.File.
59+
func (w *diskWAL) initWriter(f *os.File) {
60+
w.fd = f
61+
w.sw = snappy.NewBufferedWriter(f)
62+
w.w = bufio.NewWriterSize(w.sw, w.bufferedSize)
63+
}
64+
65+
// closeWriter flushes and closes the entire writer chain.
66+
func (w *diskWAL) closeWriter() error {
67+
if err := w.w.Flush(); err != nil {
68+
return fmt.Errorf("failed to flush buffered-data into the underlying WAL file: %w", err)
69+
}
70+
if err := w.sw.Close(); err != nil {
71+
return fmt.Errorf("failed to close snappy writer: %w", err)
72+
}
73+
return w.fd.Close()
74+
}
75+
5576
// append appends the given entry to the end of a file via the file descriptor it has.
5677
func (w *diskWAL) append(op walOperation, rows []Row) error {
5778
w.mu.Lock()
@@ -103,25 +124,24 @@ func (w *diskWAL) flush() error {
103124
if err := w.w.Flush(); err != nil {
104125
return fmt.Errorf("failed to flush buffered-data into the underlying WAL file: %w", err)
105126
}
127+
if err := w.sw.Flush(); err != nil {
128+
return fmt.Errorf("failed to flush snappy writer: %w", err)
129+
}
106130
return nil
107131
}
108132

109133
// punctuate set boundary and creates a new segment.
110134
func (w *diskWAL) punctuate() error {
111135
w.mu.Lock()
112136
defer w.mu.Unlock()
113-
if err := w.flush(); err != nil {
114-
return err
115-
}
116-
if err := w.fd.Close(); err != nil {
137+
if err := w.closeWriter(); err != nil {
117138
return err
118139
}
119140
f, err := w.createSegmentFile(w.dir)
120141
if err != nil {
121142
return err
122143
}
123-
w.fd = f
124-
w.w = bufio.NewWriterSize(f, w.bufferedSize)
144+
w.initWriter(f)
125145
return nil
126146
}
127147

@@ -143,6 +163,9 @@ func (w *diskWAL) removeOldest() error {
143163
func (w *diskWAL) removeAll() error {
144164
w.mu.Lock()
145165
defer w.mu.Unlock()
166+
// Best-effort flush/close of the writer chain before removing files.
167+
_ = w.w.Flush()
168+
_ = w.sw.Close()
146169
if err := w.fd.Close(); err != nil {
147170
return err
148171
}
@@ -164,8 +187,7 @@ func (w *diskWAL) refresh() error {
164187
if err != nil {
165188
return err
166189
}
167-
w.fd = f
168-
w.w = bufio.NewWriterSize(f, w.bufferedSize)
190+
w.initWriter(f)
169191
return nil
170192
}
171193

@@ -216,7 +238,7 @@ func (f *diskWALReader) readAll() error {
216238
}
217239
segment := &segment{
218240
file: fd,
219-
r: bufio.NewReader(fd),
241+
r: bufio.NewReader(snappy.NewReader(fd)),
220242
}
221243
for segment.next() {
222244
rec := segment.record()
@@ -230,7 +252,7 @@ func (f *diskWALReader) readAll() error {
230252
}
231253

232254
err = segment.error()
233-
if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
255+
if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) || errors.Is(err, snappy.ErrCorrupt) {
234256
// It is not unusual for a line to be invalid, as it may well terminate in the middle of writing to the WAL.
235257
return nil
236258
}

disk_wal_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@ func Test_diskWAL_append_read(t *testing.T) {
5151
assert.Equal(t, rows, got)
5252
}
5353

54+
func Test_diskWAL_snappy_compressed(t *testing.T) {
55+
tmpDir, err := os.MkdirTemp("", "tstorage-test")
56+
defer os.RemoveAll(tmpDir)
57+
require.NoError(t, err)
58+
path := filepath.Join(tmpDir, "wal")
59+
60+
wal, err := newDiskWAL(path, 4096)
61+
require.NoError(t, err)
62+
63+
rows := []Row{
64+
{Metric: "metric-1", DataPoint: DataPoint{Value: 0.1, Timestamp: 1600000000}},
65+
}
66+
err = wal.append(operationInsert, rows)
67+
require.NoError(t, err)
68+
err = wal.flush()
69+
require.NoError(t, err)
70+
71+
// Read the segment file and check for snappy stream magic bytes.
72+
files, err := os.ReadDir(path)
73+
require.NoError(t, err)
74+
require.NotEmpty(t, files)
75+
76+
data, err := os.ReadFile(filepath.Join(path, files[0].Name()))
77+
require.NoError(t, err)
78+
79+
// Snappy stream format magic: 0xff 0x06 0x00 0x00 s N a P p Y
80+
snappyMagic := []byte{0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59}
81+
require.True(t, len(data) >= len(snappyMagic), "segment file too small")
82+
assert.Equal(t, snappyMagic, data[:len(snappyMagic)], "segment file should start with snappy stream magic bytes")
83+
}
84+
5485
func Test_diskWAL_removeOldest(t *testing.T) {
5586
tmpDir, err := os.MkdirTemp("", "tstorage-test")
5687
require.NoError(t, err)

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ module github.com/nakabonne/tstorage
22

33
go 1.20
44

5+
require github.com/stretchr/testify v1.7.0
6+
57
require (
68
github.com/davecgh/go-spew v1.1.0 // indirect
9+
github.com/golang/snappy v1.0.0 // indirect
710
github.com/pmezard/go-difflib v1.0.0 // indirect
8-
github.com/stretchr/testify v1.7.0 // indirect
911
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
1012
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
22
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
4+
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
35
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
46
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

0 commit comments

Comments
 (0)