From d105b5c2457ea7c0b9d11f87826f4a199efed887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=91=D0=B5=D0=BB=D1=8F?= =?UTF-8?q?=D0=BD=D0=BA=D0=B8=D0=BD?= Date: Fri, 25 Sep 2020 12:53:00 +0300 Subject: [PATCH 1/2] #1 Add base app --- .dockerignore | 20 ++++++++++ .gitignore | 20 ++++++++++ Dockerfile | 30 ++++++++++++++ Makefile | 49 +++++++++++++++++++++++ cmd/ivy/main.go | 22 +++++++++++ configs/ivy-default.conf.yml | 0 go.mod | 3 ++ internal/server/server.go | 71 ++++++++++++++++++++++++++++++++++ internal/storage/items.go | 47 ++++++++++++++++++++++ internal/storage/storage.go | 69 +++++++++++++++++++++++++++++++++ internal/storage/temp_items.go | 31 +++++++++++++++ 11 files changed, 362 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/ivy/main.go create mode 100644 configs/ivy-default.conf.yml create mode 100644 go.mod create mode 100644 internal/server/server.go create mode 100644 internal/storage/items.go create mode 100644 internal/storage/storage.go create mode 100644 internal/storage/temp_items.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f36814f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +# common files +*.log +*.md + +# git +**/.git/ +**/.gitignore +**/.gitmodules + +# configuration files +.gitlab-ci.yml +.dockerignore +.idea/ + +# artifacts +bin/ + +# misc +docs/ +/coverage.out diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..244e644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ +!bin/.gitkeep + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Jetbrains directories +.idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fecf689 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.14.3-alpine3.11 as builder + +ENV GIT_TERMINAL_PROMPT=1 + +RUN apk --update add git openssh && \ + rm -rf /var/lib/apt/lists/* && \ + rm /var/cache/apk/* + +WORKDIR /ivy +COPY go.mod go.sum ./ + +RUN go mod download + +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o bin/ivy cmd/ivy/main.go + +FROM alpine:3.11 + +ENV TZ=Europe/Moscow +RUN apk --update add bash tzdata && \ + cp /usr/share/zoneinfo/${TZ} /etc/localtime && \ + echo ${TZ} > /etc/timezone + +WORKDIR /usr/bin +COPY --from=builder /ivy/bin/ivy ./ +COPY --from=builder /ivy/configs/ivy.conf.yml ./ivy.conf.yml +RUN chmod +x ./ivy + +ENTRYPOINT ["/usr/bin/ivy"] +CMD ["-config", "./ivy-default.conf.yml"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..561b048 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +ROOT_APP_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) + +.PHONY: all +all: run + +.PHONY: build +build: ## Build app + CGO_ENABLED=0 go build -o bin/ivy cmd/ivy/main.go + +.PHONY: build-race +build-race: ## Build app with race flag + CGO_ENABLED=1 go build -race -o bin/ivyr cmd/ivy/main.go + +.PHONY: run +run: build ## Build and run with default config + bin/ivy --config=configs/ivy-default.conf.yml + +.PHONY: clean +clean: ## Clean bin dir + rm -rf bin/ + +.PHONY: run-race +run-race: build-race ## Build with race flag and run with default config + bin/ivyr --config=configs/ivy-default.conf.yml + +.PHONY: fmt +fmt: ## Run go fmt and goimports + go fmt ./... + goimports -w ./ + +.PHONY: lint +lint: ## Run golangci-lint + golangci-lint run -v ./... + +.PHONY: test +test: ## Run tests + @go test ./internal... + +.PHONY: cover +cover: ## Run tests with cover + @go test -coverprofile=coverage.out ./internal... + +.PHONY: coverr +coverr: cover ## Run tests with cover and open report + @go tool cover -html=coverage.out + +.PHONY: help +help: ## List of commands + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' \ No newline at end of file diff --git a/cmd/ivy/main.go b/cmd/ivy/main.go new file mode 100644 index 0000000..6208f03 --- /dev/null +++ b/cmd/ivy/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/lartie/ivy/internal/server" + "github.com/lartie/ivy/internal/storage" + "log" + "os" + "os/signal" + "syscall" +) + +func main() { + storage.InitStorage() + + server.InitServer() + + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + interrupt := <-sig + + log.Printf("Received system signal: %s. Shutting down redis-puf\n", interrupt) +} diff --git a/configs/ivy-default.conf.yml b/configs/ivy-default.conf.yml new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..989066b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/lartie/ivy + +go 1.14 diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..1aa0911 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,71 @@ +package server + +import ( + "bufio" + "log" + "net" +) + +type Server interface { + ListenAndServe() error +} + +type defaultServer struct { + port string + network string +} + +func (s *defaultServer) ListenAndServe() error { + listener, err := net.Listen(s.network, s.port) + if err != nil { + return err + } + + for { + conn, err := listener.Accept() + if err != nil { + log.Println(err) + continue + } + + go func() { + err := s.handleConn(conn) + if err != nil { + log.Println(err) + } + }() + } +} + +func (s *defaultServer) handleConn(conn net.Conn) (err error) { + defer func() { + err := conn.Close() + if err != nil { + log.Println(err) + } + }() + + message, _ := bufio.NewReader(conn).ReadString('\n') + response := s.handleMessage(message) + + _, err = conn.Write([]byte(response)) + return +} + +func (s *defaultServer) handleMessage(msg string) string { + return "+OK\r\n" +} + +func newServer() Server { + return &defaultServer{ + port: ":9000", + network: "tcp", + } +} + +func InitServer() { + err := newServer().ListenAndServe() + if err != nil { + panic(err) + } +} diff --git a/internal/storage/items.go b/internal/storage/items.go new file mode 100644 index 0000000..0a66e5b --- /dev/null +++ b/internal/storage/items.go @@ -0,0 +1,47 @@ +package storage + +import ( + "time" +) + +type DataType byte + +const ( + StringType DataType = 's' + IntType = 'i' +) + +type Item struct { + key string + value interface{} + vtype DataType + ttl time.Time +} + +func (i *Item) GetKey() string { + return i.key +} + +func (i *Item) GetValue() interface{} { + return i.value +} + +func NewItem( + key string, + value interface{}, + vtype DataType, + ttl time.Time, +) *Item { + return &Item{ + key: key, + value: value, + vtype: vtype, + ttl: ttl, + } +} + +type Items map[string]*Item + +func newItems() Items { + return make(Items) +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..21bb40c --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,69 @@ +package storage + +import ( + "errors" + "sync" + "time" +) + +var ErrorItemNotFound = errors.New("item not found") + +var once sync.Once +var storage Storage + +func GetStorage() Storage { + return storage +} + +type Storage interface { + Set(key string, value *Item) + Get(key string) (item *Item, err error) +} + +type defaultStorage struct { + // Sync locker + mu sync.RWMutex + + // All items + items Items + + // Expiration keys + tempItems *TempItems +} + +func (s *defaultStorage) Set(key string, value *Item) { + now := time.Now().UnixNano() + s.mu.Lock() + defer s.mu.Unlock() + ttl := value.ttl.UnixNano() + if ttl > 0 { + if ttl < now { + return + } + go s.tempItems.Append(value) + } + s.items[key] = value +} + +func (s *defaultStorage) Get(key string) (*Item, error) { + s.mu.RLock() + item, ok := s.items[key] + s.mu.RUnlock() + if ok { + return item, nil + } + return item, ErrorItemNotFound +} + +func newDefaultStorage() Storage { + return &defaultStorage{ + items: newItems(), + tempItems: newTempItems(), + } +} + +func InitStorage() { + once.Do(func() { + storage = newDefaultStorage() + }) +} \ No newline at end of file diff --git a/internal/storage/temp_items.go b/internal/storage/temp_items.go new file mode 100644 index 0000000..86ada1d --- /dev/null +++ b/internal/storage/temp_items.go @@ -0,0 +1,31 @@ +package storage + +import "sync" + +type TempItems struct { + // Sync locker + mu sync.RWMutex + + // Expiration queue + keys map[int][]string +} + +func (t *TempItems) Append(value *Item) { + t.mu.Lock() + defer t.mu.Unlock() + + ttl := value.ttl.Nanosecond() + if v, ok := t.keys[ttl]; ok { + t.keys[ttl] = append(v, value.key) + } else { + t.keys[ttl] = []string{ + value.key, + } + } +} + +func newTempItems() *TempItems { + return &TempItems{ + keys: make(map[int][]string), + } +} From c7c2beb2c443d33c9bdb3033e56ef93e2e918a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=91=D0=B5=D0=BB=D1=8F?= =?UTF-8?q?=D0=BD=D0=BA=D0=B8=D0=BD?= Date: Fri, 25 Sep 2020 13:15:32 +0300 Subject: [PATCH 2/2] Update module name --- cmd/ivy/main.go | 4 ++-- go.mod | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/ivy/main.go b/cmd/ivy/main.go index 6208f03..99afebe 100644 --- a/cmd/ivy/main.go +++ b/cmd/ivy/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/lartie/ivy/internal/server" - "github.com/lartie/ivy/internal/storage" + "github.com/yule-labs/ivy/internal/server" + "github.com/yule-labs/ivy/internal/storage" "log" "os" "os/signal" diff --git a/go.mod b/go.mod index 989066b..5172983 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/lartie/ivy +module github.com/yule-labs/ivy go 1.14