Skip to content

Commit fb5406f

Browse files
committed
chore: cosmetic fixes
1 parent 581be94 commit fb5406f

File tree

9 files changed

+292
-52
lines changed

9 files changed

+292
-52
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ barrel*.lock
1818
barrel*.db
1919
barrel.hints
2020
config.toml
21+
bin/
22+
data/
23+
coverage.txt

Makefile

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
1+
APP-BIN := ./bin/barreldb.bin
2+
13
LAST_COMMIT := $(shell git rev-parse --short HEAD)
24
LAST_COMMIT_DATE := $(shell git show -s --format=%ci ${LAST_COMMIT})
3-
VERSION := $(shell git describe --abbrev=1)
4-
BUILDSTR := ${VERSION} (build "\\\#"${LAST_COMMIT} $(shell date '+%Y-%m-%d %H:%M:%S'))
5-
6-
BIN := ./bin/barreldb.bin
5+
VERSION := $(shell git describe --tags)
6+
BUILDSTR := ${VERSION} (Commit: ${LAST_COMMIT_DATE} (${LAST_COMMIT}), Build: $(shell date +"%Y-%m-%d% %H:%M:%S %z"))
77

88
.PHONY: build
9-
build: $(BIN)
10-
11-
$(BIN): $(shell find . -type f -name "*.go")
12-
CGO_ENABLED=0 go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}'" ./cmd/server/*.go
9+
build: ## Build binary.
10+
go build -o ${APP-BIN} -ldflags="-X 'main.buildString=${BUILDSTR}'" ./cmd/server/
1311

1412
.PHONY: run
15-
run:
16-
CGO_ENABLED=0 go run -ldflags="-s -w -X 'main.buildString=${BUILDSTR}'" ./cmd/server --config=cmd/server/config.toml
13+
run: ## Run binary.
14+
./${APP-BIN} --config=./cmd/server/config.sample.toml
15+
16+
.PHONY: clean
17+
clean: ## Remove temporary files and the `bin` folder.
18+
rm -rf bin
19+
rm -rf data/barrel_*
20+
21+
.PHONY: fresh
22+
fresh: build run
1723

1824
.PHONY: test
1925
test:
20-
go test ./...
21-
22-
# Use goreleaser to do a dry run producing local builds.
23-
.PHONY: release-dry
24-
release-dry:
25-
goreleaser --parallelism 1 --rm-dist --snapshot --skip-validate --skip-publish
26+
go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./...
2627

27-
# Use goreleaser to build production releases and publish them.
28-
.PHONY: release
29-
release:
30-
goreleaser --parallelism 1 --rm-dist --skip-validate
28+
.PHONY: bench
29+
bench:
30+
go test -bench=. -benchmem ./pkg/barrel/...

README.md

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,122 @@
1+
<p align="center">
2+
<img src="./_docs/logo.png" alt="logo" width="15%" />
3+
</p>
4+
15
# barreldb
2-
A disk based KV store (based on Bitcask implementation)
36

4-
Bitcask is a log-structured hash table (LSHT) database that is designed for use as a storage engine for key-value data. It uses a log-structured approach to data management, which means that data is written to a log file and is periodically merged with other log files in the database to create new, larger log files.
7+
_A disk based key-value store based on [Bitcask](https://en.wikipedia.org/wiki/Bitcask)_.
8+
9+
---
10+
11+
BarrelDB is a Golang implementation of [Bitcask by Riak](https://riak.com/assets/bitcask-intro.pdf) paper and aims to closely follow the spec.
12+
13+
Bitcask is based on a log-structured hash table to store key-value data on disk. It opens a "datafile" (term used for a Bitcask DB file) in an _append-only_ mode and all the writes are sequentially written to this file. Additionally, it also updates an in-memory hash table which maps the key with the offset of the record in the file. This clever yet simple design decision makes it possible to retrieve records from the disk using a _single_ disk seek.
14+
15+
### Benefits of this approach
16+
17+
- Low Latency: Write queries are handled with a single O(1) disk seek. Keys lookup happen in memory using a hash table lookup. This makes it possible to achieve low latency even with a lot of keys/values in the database. Bitcask also relies on the filesystem read-ahead cache for a faster reads.
18+
- High Throughput: Since the file is opened in "append only" mode, it can handle large volumes of write operations with ease.
19+
- Predictable performance. The DB has a consistent performance even with growing number of records. This can be seen in benchmarks as well.
20+
- Crash friendly. Bitcask commits each record to the disk and also generates a "hints" file which makes it easy to recover in case of a crash.
21+
- Elegant design. Bitcask achieves a lot just by keeping the architecture simple and relying on filesystem primitives for complex scenarios (backup/recovery, cache etc).
22+
- Ability to handle datasets larger than RAM.
23+
24+
### Limitations
25+
26+
- The main limitation is that all the keys must fit in RAM since they're held inside as an in-memory hash table. A potential workaround for this could be to shard the keys in multiple buckets. Incoming records can be hashed into different buckets based on the key. A shard based approach allows each bucket to have limited RAM usage.
27+
28+
## Internals
29+
30+
You can refer to https://mrkaran.dev/ for a post which goes over the internals of Bitcask and also explains how BarrelDB works.
31+
32+
## Usage
33+
34+
### Library
35+
36+
37+
```go
38+
import (
39+
"github.com/mr-karan/barreldb/pkg/barrel"
40+
)
41+
42+
barrel, _ := barrel.Init(barrel.WithDir("data/"))
43+
44+
// Set a key.
45+
barrel.Put("hello", []byte("world"))
46+
47+
// Fetch the key.
48+
v, _ := barrel.Get("hello")
49+
50+
// Delete a key.
51+
barrel.Delete("hello")
52+
53+
// Set with expiry.
54+
barrel.PutEx("hello", []byte("world"), time.Second * 3)
55+
```
56+
57+
For a complete example, visit [examples](./examples/main.go).
58+
59+
### Redis Client
60+
61+
`barreldb` implements the API over a simple Redis-compatible server (`barreldb`):
62+
63+
```
64+
127.0.0.1:6379> set hello world
65+
OK
66+
127.0.0.1:6379> get hello
67+
"world"
68+
127.0.0.1:6379> set goodbye world 10s
69+
OK
70+
127.0.0.1:6379> get goodbye
71+
"world"
72+
127.0.0.1:6379> get goodbye
73+
ERR: invalid key: key is already expired
74+
```
75+
76+
## Benchmarks
77+
78+
Using `make bench`:
79+
80+
```
81+
go test -bench=. -benchmem ./pkg/barrel/...
82+
HELLO
83+
goos: linux
84+
goarch: amd64
85+
pkg: github.com/mr-karan/barreldb/pkg/barrel
86+
cpu: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
87+
BenchmarkPut/DisableSync-8 385432 3712 ns/op 1103.48 MB/s 88 B/op 4 allocs/op
88+
BenchmarkPut/AlwaysSync-8 222 5510563 ns/op 0.74 MB/s 115 B/op 4 allocs/op
89+
BenchmarkGet-8 840627 1304 ns/op 3142.20 MB/s 4976 B/op 5 allocs/op
90+
PASS
91+
ok github.com/mr-karan/barreldb/pkg/barrel 10.751s
92+
```
93+
94+
Using `redis-benchmark`:
95+
96+
```
97+
$ redis-benchmark -p 6379 -t set -n 10000 -r 100000000
98+
Summary:
99+
throughput summary: 140845.06 requests per second
100+
latency summary (msec):
101+
avg min p50 p95 p99 max
102+
0.196 0.016 0.175 0.255 1.031 2.455
103+
104+
$ redis-benchmark -p 6379 -t set -n 200000 -r 100000000
105+
Summary:
106+
throughput summary: 143678.17 requests per second
107+
latency summary (msec):
108+
avg min p50 p95 p99 max
109+
0.184 0.016 0.183 0.223 0.455 2.183
110+
111+
$ redis-benchmark -p 6379 -t get -n 100000 -r 100000000
112+
Summary:
113+
throughput summary: 170068.03 requests per second
114+
latency summary (msec):
115+
avg min p50 p95 p99 max
116+
0.153 0.040 0.143 0.199 0.367 1.447
117+
```
5118

6-
The process of merging log files in Bitcask is called compaction. When compaction occurs, the Bitcask database will select two or more log files to merge, and it will create a new log file that contains the combined data from the selected log files. This new log file will be used in place of the old log files, and the old log files will be deleted to free up space.
119+
## References
7120

8-
The advantage of this approach is that it allows Bitcask to store data efficiently and to perform well even when dealing with large amounts of data. By periodically merging log files, Bitcask can avoid the need to split or resize its data files, which can be slow and expensive operations. It also allows Bitcask to perform efficient lookups of data by key, since the keys are stored in a hash table that is stored in memory.
121+
- [Bitcask paper](https://riak.com/assets/bitcask-intro.pdf)
122+
- [Highscalability article on Bitcask](http://highscalability.com/blog/2011/1/10/riaks-bitcask-a-log-structured-hash-table-for-fast-keyvalue.html)

TODO.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,3 @@ Availability with N+1 node with raft
5050
- [ ] Merge
5151
- [ ] Hints file
5252
- [ ] Rotate size
53-

_docs/logo.png

27.6 KB
Loading

cmd/server/config.sample.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,5 @@ address = ":6379"
33

44
[app]
55
debug = false # Enable debug logging
6-
dir = "/data/barreldb" # Directory to store .db files
6+
dir = "./data" # Directory to store .db files
77
read_only = false # Whether to run barreldb in a read only mode. Write operations are not allowed in this mode.
8-
always_fsync = false # Whether to call `fsync(2)` on every write call. This significantly affects the performance but if data durability is a big concern, consider turning it on.
9-
fsync_interval = "5s" # If always_fsync is turned off, barreldb can flush filesystem buffers periodically at this given interval.
10-
max_file_size = 1000000 # Maximum size of one datafile (.db file) in bytes. After this size has reached, it'll get rotated and a new .db file will be used for storing newer data.
11-
eval_file_size_interval = "1m" # Periodic interval to check if the size of the active .db file has reached `max_file_size`.
12-
compaction_interval = "6h" # Periodic interval to perform compaction for optimising disk usage. It cleans up deleted/expired keys and merges old datafiles into a single file.

examples/main.go

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,72 @@
11
package main
22

33
import (
4-
"fmt"
4+
"log"
5+
"os"
56
"time"
67

78
"github.com/mr-karan/barreldb/pkg/barrel"
89
)
910

11+
var (
12+
lo = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile)
13+
)
14+
1015
func main() {
11-
barrel, err := barrel.Init()
16+
// Initialise.
17+
barrel, err := barrel.Init(barrel.WithDir("data/"), barrel.WithAutoSync())
1218
if err != nil {
13-
panic(err)
19+
lo.Fatalf("error initialising barrel: %v", err)
1420
}
1521

16-
if err := barrel.PutEx("hello", []byte("world"), time.Second*5); err != nil {
17-
panic(err)
18-
}
19-
if err := barrel.Put("good", []byte("bye")); err != nil {
20-
panic(err)
22+
var (
23+
key = "hello"
24+
val = []byte("world")
25+
)
26+
27+
// Set a key.
28+
if err := barrel.Put(key, val); err != nil {
29+
lo.Fatalf("error setting a key: %v", err)
2130
}
2231

23-
val, err := barrel.Get("hello")
32+
// Fetch the key.
33+
v, err := barrel.Get(key)
2434
if err != nil {
25-
panic(err)
35+
lo.Fatalf("error fetching key %s: %v", key, err)
36+
}
37+
lo.Printf("fetched val: %s\n", string(v))
38+
39+
// Set a new key with an expiry.
40+
key = "fruit"
41+
val = []byte("apple")
42+
ex := time.Second * 2
43+
if err := barrel.PutEx(key, val, ex); err != nil {
44+
lo.Fatalf("error setting a key with ex: %v", err)
2645
}
27-
fmt.Println(string(val))
2846

29-
val, err = barrel.Get("good")
47+
// Wait for 3 seconds for expiry.
48+
wait := time.Second * 3
49+
lo.Printf("waiting for %s for the key to get expired", wait.String())
50+
time.Sleep(wait)
51+
52+
// Try fetching the expired key.
53+
_, err = barrel.Get(key)
3054
if err != nil {
31-
panic(err)
55+
lo.Printf("error fetching key %s: %v\n", key, err)
3256
}
3357

34-
fmt.Println(string(val))
58+
// Delete the key.
59+
if err := barrel.Delete(key); err != nil {
60+
lo.Fatalf("error deleting key %s: %v", key, err)
61+
}
3562

63+
// Fetch list of keys.
3664
keys := barrel.List()
37-
fmt.Println(keys)
38-
39-
val, err = barrel.Get("hello")
40-
if err != nil {
41-
panic(err)
65+
for i, k := range keys {
66+
lo.Printf("key %d is %s\n", i, k)
4267
}
43-
fmt.Println(string(val))
4468

45-
barrel.Shutdown()
69+
if err := barrel.Shutdown(); err != nil {
70+
lo.Fatalf("error closing barrel: %v", err)
71+
}
4672
}

pkg/barrel/barrel_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package barrel
22

33
import (
4+
"fmt"
45
"os"
6+
"strings"
57
"testing"
68
"time"
79

@@ -114,6 +116,15 @@ func TestAPI(t *testing.T) {
114116
assert.Equal(len, 1)
115117
})
116118

119+
t.Run("Fold", func(t *testing.T) {
120+
upper := func(s string) error {
121+
fmt.Println(strings.ToUpper(s))
122+
return nil
123+
}
124+
err = brl.Fold(upper)
125+
assert.NoError(err)
126+
})
127+
117128
t.Run("Expiry", func(t *testing.T) {
118129
err = brl.PutEx("keywithexpiry", []byte("valwithex"), time.Second*2)
119130
assert.NoError(err)

0 commit comments

Comments
 (0)