diff --git a/example-databases/go/log/Makefile b/example-databases/go/log/Makefile new file mode 100644 index 0000000..332428e --- /dev/null +++ b/example-databases/go/log/Makefile @@ -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) \ No newline at end of file diff --git a/example-databases/go/log/README.md b/example-databases/go/log/README.md new file mode 100644 index 0000000..038aeec --- /dev/null +++ b/example-databases/go/log/README.md @@ -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` + diff --git a/example-databases/go/log/cmd/main.go b/example-databases/go/log/cmd/main.go new file mode 100644 index 0000000..1ac3140 --- /dev/null +++ b/example-databases/go/log/cmd/main.go @@ -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()) + } +} diff --git a/example-databases/go/log/go.mod b/example-databases/go/log/go.mod new file mode 100644 index 0000000..6d59820 --- /dev/null +++ b/example-databases/go/log/go.mod @@ -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 +) diff --git a/example-databases/go/log/go.sum b/example-databases/go/log/go.sum new file mode 100644 index 0000000..16f1ef5 --- /dev/null +++ b/example-databases/go/log/go.sum @@ -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= diff --git a/example-databases/go/log/pkg/db/db.go b/example-databases/go/log/pkg/db/db.go new file mode 100644 index 0000000..90a1793 --- /dev/null +++ b/example-databases/go/log/pkg/db/db.go @@ -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 +} diff --git a/example-databases/go/log/pkg/server/logserver.go b/example-databases/go/log/pkg/server/logserver.go new file mode 100644 index 0000000..13af97c --- /dev/null +++ b/example-databases/go/log/pkg/server/logserver.go @@ -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)) + +}