/
queuefka.go
335 lines (280 loc) · 8.05 KB
/
queuefka.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Copyright (c) 2015-2016 John W. Leimgruber III <blog.ubergarm.com>
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// Package queuefka implements Append Only Log functionality. It wraps a
// bufio.Reader or bufio.Writer object, creating another object (Reader or
// Writer) that also implements the interface but handles stream framing,
// CRCs, and segment file management.
package queuefka
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"sync"
"github.com/vova616/xxhash"
)
var (
ErrInvalidTopic = errors.New("queuefka: Read() invalid topic path")
ErrEndOfLog = errors.New("queuefka: Read() end of log")
ErrOutOfBounds = errors.New("queuefka: Read() topic address out of bounds")
ErrBadChecksum = errors.New("queuefka: Read() checksum mismatch")
)
// Reader implements Append Only Log functionality for an bufio.Reader object.
type Reader struct {
topic string // path to directory which holds *.slab files
base uint64 // address of first message in current slab file e.g. <base>.slab
fp *os.File
rd *bufio.Reader
}
// Seek sets up Reader file pointer, bufio reader, for a given absoulute log address
func (rd *Reader) Seek(topic string, address uint64) error {
// close any existing file pointer
if rd.fp != nil {
rd.fp.Close()
}
slabs := SlabFiles(rd.topic)
// error if there are no .slab files found
if len(slabs) <= 0 {
return ErrInvalidTopic
}
// sequentially search through all slab files until one contains offset
// assumes fixed style slab file name e.g. "< 20 characters >.slab"
slabFile := slabs[0]
for i := 0; i < len(slabs); i++ {
basename := slabs[i][(len(slabs[i]) - 25):(len(slabs[i]) - 5)]
d, _ := strconv.Atoi(basename)
if address < uint64(d) {
break
}
slabFile = slabs[i]
rd.base = uint64(d)
}
// open file
fp, err := os.OpenFile(slabFile, os.O_RDONLY, 0600)
if err != nil {
return err
}
rd.fp = fp
// check out of bounds
stat, _ := rd.fp.Stat()
if (address - rd.base) > uint64(stat.Size()) {
return ErrOutOfBounds
}
// check if end of log
if (address - rd.base) == uint64(stat.Size()) {
// new buffered reader at begginning of fp
rd.rd = bufio.NewReader(rd.fp)
return ErrEndOfLog
}
// seek file cursor to offset
offset := int64(rd.base - address)
_, err = rd.fp.Seek(offset, os.SEEK_SET)
if err != nil {
return err
}
// new buffered reader at the cursor location of fp
rd.rd = bufio.NewReader(rd.fp)
return nil
}
// NewReader returns a new Reader starting at the specified topic and address
func NewReader(topic string, address uint64) (*Reader, error) {
rd := &Reader{topic: topic}
err := rd.Seek(topic, address)
if err != nil {
return rd, err
}
return rd, nil
}
// TODO: possibly optimize by having caller pass in a buffer reference?
// also need to give user the address so they can keep track of it
// returns single messages sequentially
func (rd *Reader) Read() ([]byte, error) {
var dlen, xx32 uint32
buf := make([]byte, 4)
// read 4 bytes length
for cnt := 0; cnt < 4; {
rx, err := rd.rd.Read(buf[cnt:])
if err == io.EOF {
offset, _ := rd.fp.Seek(0, os.SEEK_CUR)
//TODO test this reader changing slab file code, seems brittle
// issues with reader outpacing writer?? file locks? ugh?
rd.base += uint64(offset)
err := rd.Seek(rd.topic, rd.base)
if err != nil {
return nil, err
}
continue
} else if err != nil {
return nil, err
}
cnt += rx
}
dlen = binary.LittleEndian.Uint32(buf)
// read 4 bytes crc
for cnt := 0; cnt < 4; {
rx, err := rd.rd.Read(buf[cnt:])
if err != nil {
return nil, err
}
cnt += rx
}
xx32 = binary.LittleEndian.Uint32(buf)
// read data payload
buf = make([]byte, dlen)
for cnt := 0; uint32(cnt) < dlen; {
rx, err := rd.rd.Read(buf[cnt:])
if err != nil {
return nil, err
}
cnt += rx
}
// check crc
if xx32 != xxhash.Checksum32(buf) {
return buf, ErrBadChecksum
}
return buf, nil
}
// cleanup Reader
func (rd *Reader) Close() error {
return rd.fp.Close()
}
// Writer implements Append Only Log functionality for a bufio.Writer object.
type Writer struct {
topic string // path to directory which holds *.slab files
address uint64 // absolute address of whole log in bytes
base uint64 // absolute offset of current slab file e.g. <base>.slab
fp *os.File // file pointer for writing to log address
wt *bufio.Writer
slabSizeHint uint64 // once a slab exceeds this size roll a fresh one
sync.Mutex // mutex to lock while writing to log address
}
// return names of all slab files present in wt.topic
func SlabFiles(topic string) []string {
files, err := filepath.Glob(topic + "/*.slab")
if err != nil {
log.Panic(err)
}
return files
}
// load and validate *.slab files from wt.topic
func (wt *Writer) load() {
files, err := filepath.Glob(wt.topic + "/*.slab")
if err != nil {
log.Panic(err)
}
latest := files[len(files)-1]
// open slab file with highest log address in name
fp, err := os.OpenFile(latest, os.O_APPEND|os.O_RDWR, 0600)
if err != nil {
log.Panic(err)
}
// the absolute address is (biggest segment name + biggest segment size)
stat, _ := fp.Stat()
i, _ := strconv.Atoi(stat.Name()[:len(stat.Name())-5])
wt.base = uint64(i)
wt.address = wt.base + uint64(stat.Size())
wt.fp = fp
wt.wt = bufio.NewWriter(wt.fp)
wt.Flush()
}
// create a new log slab in wt.topic
func (wt *Writer) create() error {
// create topic if necessary
err := os.MkdirAll(wt.topic, 0700)
if err != nil {
return err
}
// create a new slab file
fname := fmt.Sprintf("%s/%020d.slab", wt.topic, wt.address)
wt.base = wt.address
fp, err := os.OpenFile(fname, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return err
}
// TODO trunc or hints depending on size to prealloc ext4/xfs etc?
// could possibly optimize this here for sequential writes etc...
// Don't truncate for now as it confuses finding address on a new file
// fp.Truncate(int64(wt.slabSizeHint))
wt.fp = fp
wt.wt = bufio.NewWriter(wt.fp)
wt.Flush()
return nil
}
// NewWriter returns a Writer after creating a topic or seeking address properly
func NewWriter(topic string, slabSizeHint uint64) (*Writer, error) {
var wt *Writer
wt = &Writer{slabSizeHint: slabSizeHint}
wt.topic = topic
if len(SlabFiles(wt.topic)) == 0 {
// create a new topic
wt.create()
} else {
// load existing topic with cursor at the end of the highest address file
wt.load()
}
return wt, nil
}
func (wt *Writer) Close() error {
wt.Flush()
return wt.fp.Close()
}
func (wt *Writer) Write(d []byte) error {
var dlen, xx32 uint32
buf := make([]byte, 4)
dlen = uint32(len(d))
xx32 = xxhash.Checksum32(d)
wt.Lock()
// FIXME -- make a function like WriteAll() to write until all written
// e.g.
// for cnt = 0; cnt < len(key); {
// tx, _ := fp.Write(key[cnt:])
// cnt += tx
// }
// write header
binary.LittleEndian.PutUint32(buf, dlen)
tx, err := wt.wt.Write(buf)
if err != nil {
return err
}
binary.LittleEndian.PutUint32(buf, xx32)
tx, err = wt.wt.Write(buf)
if err != nil {
return err
}
// write payload
tx, err = wt.wt.Write(d)
if err != nil {
return err
}
// update address
wt.address = wt.address + uint64(8+tx)
// roll over slab file if it is big enough
if (wt.address - wt.base) > wt.slabSizeHint {
wt.Flush()
wt.fp.Close()
wt.create()
}
wt.Unlock()
return nil
}
func (wt *Writer) Flush() error {
return wt.wt.Flush()
}
func (wt *Writer) Status() {
stat, _ := wt.fp.Stat()
log.Printf("===================================================\n")
log.Printf("Queuefka Log Status\n")
log.Printf(" absolute address : %d\n", wt.address)
log.Printf(" no of segments : %d\n", len(SlabFiles(wt.topic)))
log.Printf(" total size : %.1fMB\n", float32(wt.address/1024.0/1024.0))
log.Printf(" log directory : %s\n", wt.topic)
log.Printf(" current segment : %s\n", stat.Name())
log.Printf(" segment size : %.1fMB\n", float32((stat.Size() / 1024.0 / 1024.0)))
log.Printf("===================================================\n")
}