-
Notifications
You must be signed in to change notification settings - Fork 0
/
bitcask_store.go
121 lines (103 loc) · 2.92 KB
/
bitcask_store.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
package bitcaskgolite
import (
"errors"
"io"
"io/fs"
"os"
"time"
)
// BitcaskStore is a Log-Structured Hash Table as described in the Bitcask paper. We
// keep appending the data to a file, like a log. DiskStorage maintains an in-memory
// hash table called KeyDir, which keeps the row's metadata, including location, on
// the disk.
//
// BitcaskStore provides two simple operations to get and set key value pairs. Both key
// and value need to be of string type, and all the data is persisted to disk.
// During startup, DiskStorage loads all the existing key-value pair metadata, and it will
// throw an error if the file is invalid or corrupt.
//
// Note that if the database file is large, the initialisation will take time
// accordingly. The initialisation is also a blocking operation; until it is completed,
// we cannot use the database.
//
// Typical usage example:
//
// ```
//
// db, _ := NewBitcaskStore("anime.db")
// db.Set("title", "One Piece")
// animeTitle := db.Get("title")
//
// ```
type BitcaskStore struct {
file *os.File
keyDir map[string]KeyEntry
lastWrittenPos int64
}
func isFileExists(fileName string) bool {
// https://stackoverflow.com/a/12518877
if _, err := os.Stat(fileName); err == nil || errors.Is(err, fs.ErrExist) {
return true
}
return false
}
func NewBitcaskStore(fileName string) (*BitcaskStore, error) {
b := &BitcaskStore{}
b.keyDir = make(map[string]KeyEntry)
if isFileExists(fileName) {
b.initKeyDir(fileName)
}
file, err := os.OpenFile(fileName, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
b.file = file
return b, err
}
func (b *BitcaskStore) Get(key string) string {
keyEntry, isKeyPresent := b.keyDir[key]
if !isKeyPresent {
return ""
}
data := make([]byte, keyEntry.entrySize)
// todo: handle errors
b.file.ReadAt(data, keyEntry.entryPos)
_, _, value := decodeKV(data)
return value
}
func (b *BitcaskStore) Set(key string, value string) {
timestamp := uint32(time.Now().Unix())
entrySize, kv := encodeKV(timestamp, key, value)
// todo: handle errors
bytesWritten, _ := b.file.Write(kv)
b.keyDir[key] = NewKeyEntry(entrySize, b.lastWrittenPos, timestamp)
b.lastWrittenPos += int64(bytesWritten)
}
func (b *BitcaskStore) Close() bool {
err := b.file.Close()
if err != nil {
clear(b.keyDir)
b.lastWrittenPos = 0
return true
}
return false
}
func (b *BitcaskStore) initKeyDir(fileName string) {
file, _ := os.Open(fileName)
defer file.Close()
header := make([]byte, headerSize)
for {
_, headerErr := io.ReadFull(file, header)
if headerErr != nil {
break
}
timestamp, keySize, valueSize := decodeHeader(header)
dataSize := uint(keySize + valueSize)
data := make([]byte, dataSize)
_, dataErr := io.ReadFull(file, data)
if dataErr != nil {
break
}
key, _ := decodeKVNoHeader(data, keySize)
entrySize := headerSize + dataSize
b.keyDir[key] = NewKeyEntry(entrySize, b.lastWrittenPos, timestamp)
b.lastWrittenPos += int64(entrySize)
}
}