Skip to content
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
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# https://hub.docker.com/_/golang/
FROM golang:1.11.3-alpine3.8

RUN mkdir -p /go/src/github.com/ITandElectronics/GoHomework
WORKDIR /go/src/github.com/ITandElectronics/GoHomework
COPY . .

RUN CGO_ENABLED=0 go test -v ./...

RUN CGO_ENABLED=0 go install -v ./...

FROM alpine:3.8

WORKDIR /root/
COPY --from=0 /go/bin/client .
COPY --from=0 /go/bin/server .

CMD ["./server"]
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
build:
CGO_ENABLED=0 go install ./...
docker build -t redislight .
test:
go test -coverprofile=./coverage.profile ./...
go tool cover --func=./coverage.profile > coverage.out
check:
go get -u golang.org/x/lint/golint
go get -u golang.org/x/tools/cmd/goimports

${GOPATH}/bin/goimports -w .
go vet ./...
${GOPATH}/bin/golint ./...
run:
CGO_ENABLED=0 go run ./cmd/server
Binary file added Project.pdf
Binary file not shown.
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Redislight
### Simplified version of a redis. Supports only GET, SET and DEL commands

## Usage
### Pre Requisites
- Docker

Redis light consists of two application: client and server. They both bundled into a single docker image which can be built by running following command:
```bash
$ docker build -t redislight .
```
### Server
In order to run server:
```bash
$ docker run -p 9090:9090 redislight
```
Server supports following options:
```bash
./server --help
Usage of server:
--mode, -m string
Storage options. One of [disk] (default "disk")
--port, -p int
Port to listen on (default 9090)
```

### Client
In order to run client:
```bash
$ docker run redislight ./client
```

Client supports following options:
```bash
./client --help
Usage of client:
--host, -h string
Remote server address (default "127.0.0.1")
--port, -p int
Remote server port (default 9090)
```

### Supported commands
**GET** *key* - return value associated with proveded *key* or 'key is not exists' error
**SET** *key* *value* - create or update *value* associated with the *key*
**DEL** *key* - remove value associated with the *key*. If *key* is not exists, it will return 'not exists error'


## Development
### Pre Requisites
- Go >= 1.11
- Docker
- Make

Clone this repository into GOPATH/src/{repository}/redislight:
```bash
$ git clone .../redislight.git
```
Run linters:
```bash
$ make check
```
Run tests(also will produce code coverage in coverage.out file):
```bash
$ make tests
```
Build:
```bash
$ make build
```

Run server:
```bash
$ make run
```
76 changes: 76 additions & 0 deletions cmd/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"bufio"
"flag"
"fmt"
"io"
"log"
"net"
"os"

"github.com/ITandElectronics/GoHomework/protocol"
)

func main() {
var (
host string
port int
)

flag.IntVar(&port, "port", 9090, "Remote server port")
flag.IntVar(&port, "p", 9090, "Remote server port")
flag.StringVar(&host, "host", "127.0.0.1", "Remote server address")
flag.StringVar(&host, "h", "127.0.0.1", "Remote server address")
flag.Usage = func() {
usage := `Usage of %s:
--host, -h string
Remote server address (default "127.0.0.1")
--port, -p int
Remote server port (default 9090)
`
fmt.Fprintf(os.Stderr, usage, os.Args[0])
}
flag.Parse()

conn, err := net.Dial("tcp4", fmt.Sprintf("%s:%d", host, port))
if err != nil {
log.Fatalf("couldn't connect to server: %v\n", err)
}

input := bufio.NewReader(os.Stdin)
response := bufio.NewReader(conn)
for {
fmt.Print("Enter command: ")
line, _, err := input.ReadLine()
if err != nil {
fmt.Printf("[error] couldn't read line from stdin: %v\n", err)
continue
}
msg, err := protocol.DecodeMessage(line)
if err != nil {
fmt.Printf("[error] invalid message format: %v\n", err)
continue
}
if err := protocol.ValidateMessage(msg); err != nil {
fmt.Printf("validation failed: %v\n", err)
continue
}
if _, err = conn.Write(append(line, '\n')); err != nil {
fmt.Printf("[error] coudn't write to connection: %v\n", err)
continue
}

resp, _, err := response.ReadLine()
if err != nil {
if err == io.EOF {
fmt.Println("connection is closed")
return
}
fmt.Printf("[error] couldn't read response: %v\n", err)
continue
}
fmt.Printf("[server] %s\n", string(resp))
}

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

import (
"flag"
"fmt"
"log"
"os"

"github.com/ITandElectronics/GoHomework/disk"
"github.com/ITandElectronics/GoHomework/server"
)

func main() {
const storagePath = "./db.json"

var (
port int
mode string
)

flag.IntVar(&port, "port", 9090, "Port to listen on")
flag.IntVar(&port, "p", 9090, "Port to listen on")
flag.StringVar(&mode, "mode", "disk", "Storage options. One of [disk]")
flag.StringVar(&mode, "m", "disk", "Storage options. One of [disk]")
flag.Usage = func() {
usage := `Usage of %s:
--mode, -m string
Storage options. One of [disk] (default "disk")
--port, -p int
Port to listen on (default 9090)
`
fmt.Fprintf(os.Stderr, usage, os.Args[0])

}
flag.Parse()

fmt.Printf("server is going to start on '%d' and work in '%s' mode\n", port, mode)
storage, err := disk.New(storagePath)
if err != nil {
log.Fatalf("coudn't create stroage: %v\n", err)
}
s, err := server.New(port, storage)
if err != nil {
log.Fatalf("coudln't create server: %v\n", err)
}
log.Fatalf("unable to start server: %v\n", s.Run())
}
Binary file added cmd/server/server
Binary file not shown.
74 changes: 74 additions & 0 deletions disk/disk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package disk

import (
"encoding/json"
"fmt"
"io"
"os"
"sync"

redislight "github.com/ITandElectronics/GoHomework"
)

// New construct new storage
func New(path string) (*Disk, error) {
fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(0640))
if err != nil {
return nil, err
}
data := make(map[string]string)
if err := json.NewDecoder(fd).Decode(&data); err != nil && err != io.EOF {
return nil, err
}
return &Disk{
m: &sync.RWMutex{},
fd: fd,
data: data,
}, nil
}

// Disk represent disk storage
type Disk struct {
m *sync.RWMutex
fd *os.File
data map[string]string
}

// Get checks whether key exists and either return appropriate value or KeyIsNotExists error
func (d *Disk) Get(key string) (string, error) {
d.m.RLock()
defer d.m.RUnlock()
val, ok := d.data[key]
if !ok {
return "", redislight.ErrKeyIsNotExists
}
return val, nil
}

// Set create new or update existing key value
func (d *Disk) Set(key, value string) error {
d.m.Lock()
defer d.m.Unlock()
d.data[key] = value
return d.sync()
}

// Del remove entry related to provided key from the storage
func (d *Disk) Del(key string) error {
d.m.Lock()
defer d.m.Unlock()
if _, ok := d.data[key]; !ok {
return redislight.ErrKeyIsNotExists
}
delete(d.data, key)
return d.sync()
}

// all sync calls should be protected by mutex
func (d *Disk) sync() error {
_, err := d.fd.Seek(0, 0)
if err != nil {
return fmt.Errorf("coudn't seek to the start of the file: %v", err)
}
return json.NewEncoder(d.fd).Encode(d.data)
}
77 changes: 77 additions & 0 deletions disk/disk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package disk

import (
"fmt"
"io/ioutil"
"os"
"testing"

redislight "github.com/ITandElectronics/GoHomework"
)

func createTempDir(t *testing.T) string {
dir, err := ioutil.TempDir("", "redis-light-tests")
if err != nil {
t.Fatal(err)
}
return dir
}

func TestNew(t *testing.T) {
dir := createTempDir(t)
defer os.RemoveAll(dir)

_, err := New(fmt.Sprintf("%s/test_db.json", dir))
if err != nil {
t.Fatal(err)
}
}

func TestDiskGet(t *testing.T) {
disk, err := New("./fixtures/db.json")
if err != nil {
t.Fatal(err)
}
_, err = disk.Get("not_existing_key")
if err == nil {
t.Fatal("should return error")
}
if err != redislight.ErrKeyIsNotExists {
t.Fatalf("%v - is not expected", err)
}
val, err := disk.Get("exists")
if err != nil {
t.Fatal(err)
}
if val == "" {
t.Fatal("should return some value")
}

}

func TestDiskSet(t *testing.T) {
disk, err := New("./fixtures/db.json")
if err != nil {
t.Fatal(err)
}
err = disk.Set("new", "world")
if err != nil {
t.Fatal(err)
}
val, err := disk.Get("new")
if err != nil {
t.Fatal(err)
}
if val != "world" {
t.Fatal("Set is not working")
}

}

func TestDiskDel(t *testing.T) {

}

func TestDiskSync(t *testing.T) {

}
Loading