Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement log database #7

Open
wants to merge 21 commits into
base: ep1-basic-log
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions example-databases/go/log/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
BINARY_NAME=nulldb-go

build:
cd cmd && go build -o $(BINARY_NAME)

run: build
cd cmd &&./$(BINARY_NAME)

clean:
rm cmd/$(BINARY_NAME)
52 changes: 52 additions & 0 deletions example-databases/go/log/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Log based Key/Value store

A go implementation of a log based key value store from the null channel's building a database series

=> link: https://www.youtube.com/playlist?list=PL5JFPVMx5WzV_7j2RoTc7hkx0os4wDJTx


Pros:
- it works?
- fast writes

Cons:
- reads are slow as the log file grows


## Todo's

- [] implement log compaction
- [] implement partitioning



## Usage

Start the server using

```bash
make run
```


### adding a key

```shell
curl -d '{"key":"hey","value":"world"}' -H "Content-Type: application/json" http://localhost:4000/set
```

### retreiving a value

```shell
curl http://localhost:4000/get/key
```

### Deleting a value
```shell
curl -d '{"key":"hey"}' -H "Content-Type: application/json" http://localhost:4000/pop
```

## Clean up

`make clean`

47 changes: 47 additions & 0 deletions example-databases/go/log/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package main

import (
"fmt"
"log"
"net/http"
"os"
"sync"

"github.com/gorilla/mux"
"github.com/null-channel/null-db/pkg/db"
"github.com/null-channel/null-db/pkg/server"
"github.com/teris-io/shortid"
)

var (
createlogfile sync.Once
logfile = "log.db"
id, _ = shortid.Generate()

// unique server id incase this ever becomes distributed??
serverid = fmt.Sprintf("[logdb-%s] ", id)
l = log.New(os.Stdout, serverid, log.LstdFlags)
)

func main() {

// create log file once server starts up
createlogfile.Do(func() {
_, err := os.Create(logfile)
if err != nil {
fmt.Println(err)
}
})
DB := db.NewDB(l, logfile)
loghandler := server.NewDbServer(l, DB)
router := mux.NewRouter()
router.HandleFunc("/get/{key}", loghandler.GetHandler).Methods(http.MethodGet)
router.HandleFunc("/set", loghandler.InsertHandler).Methods(http.MethodPost)
router.HandleFunc("/pop", loghandler.DeleteHandler).Methods(http.MethodPost)
l.Println("starting server on port 4000")
http.Handle("/", router)
err := http.ListenAndServe(":4000", nil)
if err != nil {
l.Fatalf("unable to start server on 4000 %s", err.Error())
}
}
8 changes: 8 additions & 0 deletions example-databases/go/log/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/null-channel/null-db

go 1.17

require (
github.com/gorilla/mux v1.8.0
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125
)
4 changes: 4 additions & 0 deletions example-databases/go/log/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w=
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
116 changes: 116 additions & 0 deletions example-databases/go/log/pkg/db/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package db

import (
"bufio"
"errors"
"fmt"
"log"
"os"
"strings"
"sync"
)

// Represents a deleted value
const Tombstone = "~tombstone~"

var ErrorKeyNotFound = errors.New("key not found")

type DB struct {
mu sync.RWMutex
l *log.Logger
logfile string
}

func NewDB(l *log.Logger, lf string) *DB {
return &DB{
l: l,
logfile: lf,
}
}

func ReverseSlice(data []string) []string {
m := len(data) - 1
var out = []string{}
for i := m; i >= 0; i-- {
out = append(out, data[i])
}
return out
}

func (db *DB) Get(K string) (string, error) {
defer db.mu.RUnlock()
file, err := os.Open(db.logfile)
if err != nil {
db.l.Printf("an error occured opening the file: %s", err.Error())
return "", err
}
defer file.Close()
db.mu.RLock()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
var data []string
for scanner.Scan() {
data = append(data, scanner.Text())
}
data = ReverseSlice(data)
for _, kv := range data {
key := strings.Split(kv, ":")
if key[1] == Tombstone {
return "", ErrorKeyNotFound
}
if key[0] == K {
return key[1], nil
}
}
return "", ErrorKeyNotFound
}

func (db *DB) Set(k, v string) error {
defer db.mu.Unlock()
db.mu.Lock()
file, err := os.OpenFile(db.logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer file.Close()
text := fmt.Sprintf("%s:%s\n", k, v)
_, err = file.WriteString(text)
if err != nil {
return err
}
return nil
}

func (db *DB) Delete(K string) (string, error) {
file, err := os.Open(db.logfile)
if err != nil {
db.l.Printf("an error occured opening the file: %s", err.Error())
return "", err
}
db.mu.Lock()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
var data []string
for scanner.Scan() {
data = append(data, scanner.Text())
}
ReverseSlice(data)
file.Close()
for _, kv := range data {
key := strings.Split(kv, ":")
if key[0] == K {
file, err := os.OpenFile(db.logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return "", err
}
text := fmt.Sprintf("%s:%s\n", K, Tombstone)
_, err = file.WriteString(text)
if err != nil {
return "", err
}

}
}
defer db.mu.Unlock()
return "", ErrorKeyNotFound
}
92 changes: 92 additions & 0 deletions example-databases/go/log/pkg/server/logserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package server

import (
"encoding/json"
"fmt"
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/null-channel/null-db/pkg/db"
)

type DbServer struct {
l *log.Logger
logdb *db.DB
}

type InsertRequest struct {
Key string `json:"key"`
Value string `json:"value"`
}

type GetRequest struct {
Key string `json:"key"`
}

type DeleteRequest struct {
Key string `json:"key"`
}

func NewDbServer(l *log.Logger, db *db.DB) *DbServer {
return &DbServer{l, db}
}

func (s *DbServer) InsertHandler(rw http.ResponseWriter, r *http.Request) {
s.l.Println("Recived Insert request")
req := InsertRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
s.l.Println("unable to decode request")
http.Error(rw, "unable to decode request", 500)
return
}
err = s.logdb.Set(req.Key, req.Value)
if err != nil {
s.l.Println("unable to process request")
http.Error(rw, fmt.Sprintf("unable to process request reason %s", err), 404)
return
}
s.l.Printf("Set %s to value %s", req.Key, req.Value)

rw.Write([]byte(req.Key))
}

func (s *DbServer) GetHandler(rw http.ResponseWriter, r *http.Request) {
s.l.Println("received Get request")
vars := mux.Vars(r)
key := vars["key"]
if key == "" {
s.l.Println("unable to process get request , no key supplied", )
http.Error(rw, "a key must be supplied", http.StatusBadRequest)
return

}
s.l.Printf("obtaining value for key %s", key)
val, err := s.logdb.Get(key)
if err != nil {
s.l.Println("unable to process request")
http.Error(rw, fmt.Sprintf("%s", err), 404)
return
}
rw.Write([]byte(val))
}
func (s *DbServer) DeleteHandler(rw http.ResponseWriter, r *http.Request) {
s.l.Printf("received delete request")
req := DeleteRequest{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
s.l.Println("unable to process request")
http.Error(rw, "unable to process", 500)
return
}
s.l.Printf("deleting key %s", req.Key)
resp, err := s.logdb.Delete(req.Key)
if err != nil {
s.l.Println("unable to process request")
http.Error(rw, fmt.Sprintf("%s", err), 404)
return
}
rw.Write([]byte(resp))

}