diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..0d7412c --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,27 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21.6" + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a51194c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..5be0fd4 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +A key-value store inspired by [BitCask](https://github.com/basho/bitcask) + +``` +go run . +go test -bench=. # for benchmarks +``` diff --git a/daemon.go b/daemon.go new file mode 100644 index 0000000..7a41169 --- /dev/null +++ b/daemon.go @@ -0,0 +1,54 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "strings" +) + +func main() { + kv, err := NewKV() + if err != nil { + log.Fatalln("Error initializing KV store", err) + } + defer func(kv *KV) { + err := kv.Close() + if err != nil { + log.Fatalln("Failed to close") + } + }(kv) + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() + spaceIdx := strings.Index(line, " ") + command := line + if spaceIdx != -1 { + command = line[0:spaceIdx] + } + + switch command { + case "GET": + value, err := kv.Get(line[spaceIdx+1:]) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(value) + } + case "SET": + args := line[spaceIdx+1:] + spaceIdx := strings.Index(args, " ") + if spaceIdx == -1 { + fmt.Println("Syntax SET ") + continue + } + key := args[0:spaceIdx] + value := args[spaceIdx+1:] + err := kv.Set(key, value) + if err != nil { + fmt.Println("Error while setting", err) + } + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..227d01d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module sattar.dev/kv + +go 1.21.6 diff --git a/kv.go b/kv.go new file mode 100644 index 0000000..e2125e8 --- /dev/null +++ b/kv.go @@ -0,0 +1,126 @@ +package main + +import ( + "encoding/binary" + "io" + "os" +) + +type KV struct { + file *os.File + index map[string]KeyDir +} + +type KeyDir struct { + valueSize int64 + valueOffset int64 +} + +func NewKV() (*KV, error) { + file, err := os.OpenFile("data", os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return nil, err + } + index := make(map[string]KeyDir) + kv := KV{file: file, index: index} + err = buildIndex(&kv) + if err != nil { + return nil, err + } + return &kv, nil +} + +func buildIndex(kv *KV) error { + _, err := kv.file.Seek(0, io.SeekStart) + if err != nil { + return err + } + offset := int64(0) + for { + keyValueSizeBytes := make([]byte, 8+8) + _, err = kv.file.Read(keyValueSizeBytes) + if err != nil { + if err.Error() == "EOF" { + return nil + } + return err + } + keySize := int64(binary.BigEndian.Uint64(keyValueSizeBytes[0:8])) + valueSize := int64(binary.BigEndian.Uint64(keyValueSizeBytes[8:])) + + offset += 8 + 8 + + keyValueBytes := make([]byte, keySize+valueSize) + _, err = kv.file.Read(keyValueBytes) + if err != nil { + return err + } + key := string(keyValueBytes[:keySize]) + + kv.index[key] = KeyDir{valueSize: valueSize, valueOffset: offset + keySize} + + offset += keySize + valueSize + } +} + +func (kv *KV) Close() error { + return kv.file.Close() +} + +func (kv *KV) Get(key string) (string, error) { + keyDir, exists := kv.index[key] + if !exists { + err := buildIndex(kv) + if err != nil { + return "", err + } + } + return readAt(kv.file, keyDir.valueOffset, keyDir.valueSize) +} + +func readAt(file *os.File, offset int64, size int64) (string, error) { + _, err := file.Seek(offset, io.SeekStart) + if err != nil { + return "", err + } + + contentBytes := make([]byte, size) + _, err = file.Read(contentBytes) + if err != nil { + return "", err + } + + return string(contentBytes), nil +} + +func (kv *KV) Set(key string, value string) error { + keyBytes := []byte(key) + valueBytes := []byte(value) + keySizeBytes := intToBuffer(uint64(len(keyBytes))) + valueSizeBytes := intToBuffer(uint64(len(valueBytes))) + + offset, err := kv.file.Seek(0, io.SeekCurrent) + if err != nil { + return err + } + + order := [][]byte{ + keySizeBytes, valueSizeBytes, keyBytes, valueBytes, + } + var bytesToWrite []byte + for _, r := range order { + bytesToWrite = append(bytesToWrite, r...) + } + + if _, err := kv.file.Write(bytesToWrite); err != nil { + return err + } + kv.index[key] = KeyDir{valueSize: int64(len(valueBytes)), valueOffset: offset + int64(8+8) + int64(len(keyBytes))} + return nil +} + +func intToBuffer(number uint64) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, number) + return buf +} diff --git a/kv_test.go b/kv_test.go new file mode 100644 index 0000000..88cd5a4 --- /dev/null +++ b/kv_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "testing" +) + +func BenchmarkWrites(b *testing.B) { + kv, err := NewKV() + defer func(kv *KV) { + err := kv.Close() + if err != nil { + fmt.Println(err) + panic(err) + } + }(kv) + if err != nil { + fmt.Println(err) + panic(err) + } + for i := 0; i < b.N; i++ { + err := kv.Set(fmt.Sprintf("key%d", i), "value") + if err != nil { + fmt.Println(err) + panic(err) + } + } +}