Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
abdulsattar committed Jan 25, 2024
0 parents commit 026ea53
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
data
.idea
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
A key-value store inspired by [BitCask](https://github.com/basho/bitcask)

```
go run .
go test -bench=. # for benchmarks
```
54 changes: 54 additions & 0 deletions daemon.go
Original file line number Diff line number Diff line change
@@ -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 <key> <value>")
continue
}
key := args[0:spaceIdx]
value := args[spaceIdx+1:]
err := kv.Set(key, value)
if err != nil {
fmt.Println("Error while setting", err)
}
}
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module sattar.dev/kv

go 1.21.6
126 changes: 126 additions & 0 deletions kv.go
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 28 additions & 0 deletions kv_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

0 comments on commit 026ea53

Please sign in to comment.