Эх сурвалжийг харах

SVI Реализация методов веб-сервера

SVI 2 жил өмнө
parent
commit
91822adc1f

+ 13 - 2
Makefile

@@ -12,6 +12,17 @@ dev.run:
 mod:
 	clear
 	go get -u ./...
-	go mod tidy -compat=1.21.1
+	go mod tidy -compat=1.21
 	go mod vendor
-	go fmt ./...
+	go fmt ./...
+test.run:
+	clear
+	go fmt ./...
+	go test -vet=all -race -timeout 30s -coverprofile cover.out ./...
+	go tool cover -func=cover.out
+lint:
+	clear
+	go fmt ./...
+	golangci-lint run ./cmd/serv_old/...
+	golangci-lint run ./cmd/client_pwa/...
+	golangci-lint run ./internal/...

+ 17 - 6
README.md

@@ -9,14 +9,16 @@
 Выполняет следующие запросы:
 
 * `/put/:login/:pass/:key` (POST, поместить в хранилище);
-  * [ ] в памяти
-  * [ ] на диске
+  * [x] в памяти
+  * [x] на диске
 * `/get/:login/:pass/:key` (POST, извлечь из хранилища);
-  * [ ] в памяти
-  * [ ] на диске
+  * [x] в памяти
+  * [x] на диске
 * `/del/:login/:pass/:key` (POST, удалить из хранилища).
-  * [ ] в памяти
-  * [ ] на диске
+  * [x] в памяти
+  * [x] на диске
+* `/find/:login/:pass/:key` (POST, искать ключи по префиксу).
+  * [x] на диске
 * [x] `/time` (GET, возвращает текущее время на сервере; для поддержания `KeepAlive`).
 
 Запросы сделанными классическими специально, чтобы гарантировать правильную работу через прокси.
@@ -29,6 +31,15 @@
 
 Максимальный размер значения ключа не может быть более 10 МБ (но можно настроить в коде).
 
+## Пределы использования
+
+Примерные лимиты эффективности хранилища находится в следующих пределах
+
+* эффективный размер 20 ТБ (допустимо до 60 ТБ);
+* эффективное количество ключей 10Г (допустимо 20Т).
+
+Необходимо помнить, что в любом случае физические параметра оперативной памяти и дисковой подсистемы сильно ограничены.
+
 ## Кеш в памяти
 
 Для ускорения работы хранилище имеют копию кеш в памяти. Количество записей в кеше

+ 1 - 1
dev.sh

@@ -1,6 +1,6 @@
 export STORE_USER="dev"
 export STORE_USER_PASS="dev"
 export STORE_HTTP_PORT="25000"
-export STORE_MEM_RECORD_POOL_SIZE="5_000"
+export STORE_MEM_RECORD_POOL_SIZE="5000"
 cd ./bin_dev && \
 ./gostore_dev

+ 95 - 1
internal/serv_http/serv_http.go

@@ -13,9 +13,13 @@ import (
 
 // ServHttp -- встроенный HTTP-сервер
 type ServHttp struct {
-	serv     types.IServHttp
+	serv     types.IService
 	fiberApp *fiber.App
 	port     string
+	login    string // Логин для входа не сервер
+	pass     string // Пароль для входа на сервер
+	mem      types.IStoreMem
+	disk     types.IStoreDisk
 }
 
 // NewServHttp -- возвращает новый HTTP-сервер
@@ -86,11 +90,82 @@ func NewServHttp(serv types.IService) (types.IServHttp, error) {
 		serv:     serv,
 		fiberApp: app,
 		port:     port,
+		login:    serv.User().Login(),
+		pass:     serv.User().Pass(),
+		mem:      serv.StoreMem(),
+		disk:     serv.StoreDisk(),
 	}
 	sf.fiberApp.Get("/time", sf.getTime)
+	sf.fiberApp.Post("/put/:login/:pass/:key", sf.postLogin, sf.postPut)
+	sf.fiberApp.Post("/get/:login/:pass/:key", sf.postLogin, sf.postGet)
+	sf.fiberApp.Post("/del/:login/:pass/:key", sf.postLogin, sf.postDel)
+	sf.fiberApp.Post("/find/:login/:pass/:key", sf.postLogin, sf.postFind)
+
 	return sf, nil
 }
 
+type ListValRequest struct {
+	Val_ []string `json:"val" form:"val"`
+}
+
+// postFind -- возвращает список ключей по префиксу
+func (sf *ServHttp) postFind(ctx *fiber.Ctx) error {
+	key := ctx.Params("key")
+	lstKey, err := sf.disk.Find(key)
+	if err != nil {
+		return fiber.NewError(40, "ServHttp.postFind(): in get keys by prefix from db")
+	}
+	return ctx.JSON(&ListValRequest{
+		Val_: lstKey,
+	})
+}
+
+type ValRequest struct {
+	Val_ []byte `json:"val" form:"val"`
+}
+
+// postDel -- удаляет содержимое ключа
+func (sf *ServHttp) postDel(ctx *fiber.Ctx) error {
+	key := ctx.Params("key")
+	sf.mem.Del(key)
+	sf.disk.Del(key)
+	return nil
+}
+
+// postGet -- возвращает содержимое ключа
+func (sf *ServHttp) postGet(ctx *fiber.Ctx) error {
+	key := ctx.Params("key")
+	val, err := sf.mem.Get(key)
+	if err == nil {
+		return ctx.JSON(&ValRequest{
+			Val_: val,
+		})
+	}
+	val, err = sf.disk.Get(key)
+	if err != nil {
+		return fiber.NewError(30, "ServHttp.postGet(): in get val from db")
+	}
+	_ = sf.mem.Put(key, val)
+	return ctx.JSON(&ValRequest{
+		Val_: val,
+	})
+}
+
+// postPut -- сохраняет содержимое ключа
+func (sf *ServHttp) postPut(ctx *fiber.Ctx) error {
+	key := ctx.Params("key")
+	req := &ValRequest{}
+	if err := ctx.BodyParser(req); err != nil {
+		return fiber.NewError(20, "ServHttp.postPut(): bad FORM")
+	}
+	_ = sf.mem.Put(key, req.Val_)
+	err := sf.disk.Put(key, req.Val_)
+	if err != nil {
+		return fiber.NewError(21, "ServHttp.postPut(): in put val on db")
+	}
+	return nil
+}
+
 // getTime -- возвращает ответ с меткой времени (для KeepAlive)
 func (sf *ServHttp) getTime(ctx *fiber.Ctx) error {
 	ctx.Response().Header.Add("Cache-Control", "no-cache") // Cache-Control: no-cache
@@ -99,9 +174,28 @@ func (sf *ServHttp) getTime(ctx *fiber.Ctx) error {
 
 // Run -- запускает веб-сервер в работу
 func (sf *ServHttp) Run() error {
+	defer sf.serv.CancelApp()
 	err := sf.fiberApp.Listen(":" + sf.port)
 	if err != nil {
 		return fmt.Errorf("ServHttp.Run(): in listen port(%q), err=\n\t%w", sf.port, err)
 	}
 	return nil
 }
+
+// Проверяет логин/пароль в запросе
+func (sf *ServHttp) postLogin(ctx *fiber.Ctx) error {
+	login := ctx.Params("login")
+	if login != sf.login {
+		return fiber.NewError(1, "ServHttp.postLogin(): bad login/pass")
+	}
+	pass := ctx.Params("pass")
+	if pass != sf.pass {
+		return fiber.NewError(2, "ServHttp.postLogin(): bad login/pass")
+	}
+	key := ctx.Params("key")
+	if key == "" {
+		return fiber.NewError(3, "ServHttp.postLogin(): key is empty")
+	}
+	ctx.Next()
+	return nil
+}

+ 56 - 5
internal/service/service.go

@@ -2,9 +2,13 @@
 package service
 
 import (
+	"context"
 	"fmt"
+	"log"
 
 	"git.p78su.freemyip.com/svi/gostore/internal/serv_http"
+	"git.p78su.freemyip.com/svi/gostore/internal/store_disk"
+	"git.p78su.freemyip.com/svi/gostore/internal/store_mem"
 	"git.p78su.freemyip.com/svi/gostore/internal/store_user"
 	"git.p78su.freemyip.com/svi/gostore/pkg/types"
 )
@@ -13,6 +17,11 @@ import (
 type Service struct {
 	user     types.IStoreUser
 	servHttp types.IServHttp
+	dbMem    types.IStoreMem
+	dbDisk   types.IStoreDisk
+	ctxBg    context.Context // Неотменяемый контекст
+	ctx      context.Context // Контекст приложения
+	fnCancel func()          // Функция отмены контекста
 }
 
 // NewService -- возвращает новый объект сервиса
@@ -21,8 +30,22 @@ func NewService() (types.IService, error) {
 	if err != nil {
 		return nil, fmt.Errorf("NewService(): in create IStoreUser, err=\n\t%w", err)
 	}
+
+	ctxBg := context.Background()
+	ctx, fnCancel := context.WithCancel(ctxBg)
 	sf := &Service{
-		user: user,
+		user:     user,
+		ctxBg:    ctxBg,
+		ctx:      ctx,
+		fnCancel: fnCancel,
+	}
+	sf.dbMem, err = store_mem.NewStoreMem()
+	if err != nil {
+		return nil, fmt.Errorf("NewService(): in create IStoreMem, err=\n\t%w", err)
+	}
+	sf.dbDisk, err = store_disk.NewStoreDisk(sf)
+	if err != nil {
+		return nil, fmt.Errorf("NewService(): in create IStoreDisk, err=\n\t%w", err)
 	}
 	sf.servHttp, err = serv_http.NewServHttp(sf)
 	if err != nil {
@@ -31,11 +54,39 @@ func NewService() (types.IService, error) {
 	return sf, nil
 }
 
+// User -- возвращает хпользователя для авторизации на сервере
+func (sf *Service) User() types.IStoreUser {
+	return sf.user
+}
+
+// StoreDisk -- возвращает хранилище на диске
+func (sf *Service) StoreDisk() types.IStoreDisk {
+	return sf.dbDisk
+}
+
+// StoreMem -- возвращает хранилище в памяти
+func (sf *Service) StoreMem() types.IStoreMem {
+	return sf.dbMem
+}
+
+// CancelApp -- отменяет контекст приложения
+func (sf *Service) CancelApp() {
+	sf.fnCancel()
+}
+
+// Ctx -- возвращает контекст приложения
+func (sf *Service) Ctx() context.Context {
+	return sf.ctx
+}
+
 // Run -- запускает сервис в работу
 func (sf *Service) Run() error {
-	err := sf.servHttp.Run()
-	if err != nil {
-		return fmt.Errorf("Service.Run(): in run IServHttp, err=\n\t%w", err)
-	}
+	go func() {
+		err := sf.servHttp.Run()
+		if err != nil {
+			log.Printf("Service.Run(): in run IServHttp, err=\n\t%v\n", err)
+		}
+	}()
+	<-sf.ctx.Done()
 	return nil
 }

+ 92 - 0
internal/store_disk/store_disk.go

@@ -0,0 +1,92 @@
+// package store_disk -- хранилище на диске
+package store_disk
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/syndtr/goleveldb/leveldb"
+	"github.com/syndtr/goleveldb/leveldb/util"
+
+	"git.p78su.freemyip.com/svi/gostore/pkg/types"
+)
+
+// StoreDisk -- хранилище на диске
+type StoreDisk struct {
+	serv types.IService
+	db   *leveldb.DB
+}
+
+// NewStoreDisk -- возвращает новое хранилище на диске
+func NewStoreDisk(serv types.IService) (types.IStoreDisk, error) {
+	if serv == nil {
+		return nil, fmt.Errorf("NewStoreDisk(): IService==nil")
+	}
+	_ = os.MkdirAll("./store/db", 0750)
+	db, err := leveldb.OpenFile("./store/db", nil)
+	if err != nil {
+		if !strings.Contains(err.Error(), "leveldb: manifest corrupted") {
+			return nil, fmt.Errorf("NewStoreDisk(): in create IStoreDisk, err=\n\t%w", err)
+		}
+		db, err = leveldb.RecoverFile("./stor/db", nil)
+		if err != nil {
+			return nil, fmt.Errorf("NewStoreDisk(): in recovery DB, err=\n\t%w", err)
+		}
+	}
+	sf := &StoreDisk{
+		serv: serv,
+		db:   db,
+	}
+	go sf.close()
+	return sf, nil
+}
+
+// Put -- размещает в хранилище ключ и значение
+func (sf *StoreDisk) Put(key string, val []byte) error {
+	err := sf.db.Put([]byte(key), val, nil)
+	if err != nil {
+		return fmt.Errorf("StoreDisk.Put(): key=%q\terr=\n\t%w", key, err)
+	}
+	return nil
+}
+
+// Get -- возвращает значение ключа
+func (sf *StoreDisk) Get(key string) ([]byte, error) {
+	val, err := sf.db.Get([]byte(key), nil)
+	if err != nil {
+		return nil, fmt.Errorf("StoreDisk.Get(): key=%q\terr=\n\t%w", key, err)
+	}
+	return val, nil
+}
+
+// Find -- ищет ключи по префиксу
+func (sf *StoreDisk) Find(prefixKey string) ([]string, error) {
+	lstKey := []string{}
+	iter := sf.db.NewIterator(util.BytesPrefix([]byte(prefixKey)), nil)
+	for iter.Next() {
+		key := iter.Key()
+		lstKey = append(lstKey, string(key))
+	}
+	iter.Release()
+	err := iter.Error()
+	if err != nil {
+		return nil, fmt.Errorf("StoreDisk.Find(): preefixKey=%q\terr=\n\t%w", prefixKey, err)
+	}
+	return lstKey, nil
+}
+
+// Del -- удаление ключа из базы
+func (sf *StoreDisk) Del(key string) {
+	err := sf.db.Delete([]byte(key), nil)
+	if err != nil {
+		log.Printf("StoreDisk.Del(): key=%q\terr=\n\t%v\n", key, err)
+	}
+}
+
+// Ожидание закрытия приложения
+func (sf *StoreDisk) close() {
+	<-sf.serv.Ctx().Done()
+	sf.db.Close()
+}

+ 1 - 1
internal/store_mem/mem_record/mem_record.go

@@ -12,7 +12,7 @@ import (
 // MemRecord -- элемент хранения кеша в памяти
 type MemRecord struct {
 	buf      *bytes.Buffer // Буфер для хранения данных
-	timeLife int     // Как давно создана запись (чем древнее запись, тем это значение больше)
+	timeLife int           // Как давно создана запись (чем древнее запись, тем это значение больше)
 	block    sync.RWMutex
 }
 

+ 19 - 6
internal/store_mem/store_mem.go

@@ -19,7 +19,7 @@ type StoreMem struct {
 }
 
 // NewStoreMem -- возвращает новое хранилище в памяти
-func NewStoreMem() (types.IMemStore, error) {
+func NewStoreMem() (types.IStoreMem, error) {
 	pool, err := mem_pool_record.NewMemPoolRecord()
 	if err != nil {
 		return nil, fmt.Errorf("NewStoreMem(): in create IMemPoolRecord, err=\n\t%w", err)
@@ -40,8 +40,20 @@ func (sf *StoreMem) reset() {
 	sf.dict = map[string]types.IMemRecord{}
 }
 
+// Del -- удаляет запись по ключу
+func (sf *StoreMem) Del(key string) {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	rec := sf.dict[key]
+	if rec == nil {
+		return
+	}
+	delete(sf.dict, key)
+	sf.pool.Put(rec)
+}
+
 // Put -- размещает запись в памяти
-func (sf *StoreMem) Put(key string, val []byte) {
+func (sf *StoreMem) Put(key string, val []byte) error {
 	sf.block.Lock()
 	defer sf.block.Unlock()
 	nextTime := sf.lifeTime.Next()
@@ -53,7 +65,7 @@ func (sf *StoreMem) Put(key string, val []byte) {
 	rec := sf.dict[key]
 	if rec != nil {
 		rec.Set(val, nextTime)
-		return
+		return nil
 	}
 	// Такой записи нет, проверить что словарь не переполнен
 	if len(sf.dict) > sf.cap {
@@ -62,6 +74,7 @@ func (sf *StoreMem) Put(key string, val []byte) {
 	rec = sf.pool.Get()
 	rec.Set(val, nextTime)
 	sf.dict[key] = rec
+	return nil
 }
 
 // Снижает уровень заполнения словаря меньше 90% по требованию
@@ -83,12 +96,12 @@ func (sf *StoreMem) clear() {
 }
 
 // Get -- возвращает хранимую запись (если есть)
-func (sf *StoreMem) Get(key string) []byte {
+func (sf *StoreMem) Get(key string) ([]byte, error) {
 	sf.block.Lock()
 	defer sf.block.Unlock()
 	rec := sf.dict[key]
 	if rec == nil {
-		return nil
+		return nil, fmt.Errorf("StoreMem.Get(): not found")
 	}
 	nextTime := sf.lifeTime.Next()
 	if nextTime <= 0 {
@@ -96,6 +109,6 @@ func (sf *StoreMem) Get(key string) []byte {
 		nextTime = sf.lifeTime.Next()
 	}
 	rec.Update(nextTime)
-	return rec.Get()
+	return rec.Get(), nil
 
 }

+ 3 - 3
pkg/types/imem_pool_record.go

@@ -1,11 +1,11 @@
 package types
 
 // IMemPoolRecord -- пул записей в памяти
-type IMemPoolRecord interface{
+type IMemPoolRecord interface {
 	// Get -- возвращает запись в памяти
-	Get()IMemRecord
+	Get() IMemRecord
 	// Put -- возвращает запись в пул
 	Put(IMemRecord)
 	// Cap -- возвращает ёмкость пула
-	Cap()int
+	Cap() int
 }

+ 0 - 9
pkg/types/imem_store.go

@@ -1,9 +0,0 @@
-package types
-
-// IMemStore -- хранилище записей в памяти
-type IMemStore interface {
-	// Get -- возвращает значение по ключу (если есть)
-	Get(key string) []byte
-	// Put -- помещает запись в хранилище в памяти
-	Put(key string, val []byte)
-}

+ 12 - 0
pkg/types/iservice.go

@@ -1,8 +1,20 @@
 // package types -- содержит интерфейсы проекта
 package types
 
+import "context"
+
 // IService -- объект сервиса
 type IService interface {
 	// Run -- запускает сервис в работу
 	Run() error
+	// Ctx -- возвращает контекст приложения
+	Ctx() context.Context
+	// CancelApp -- отменяет контекст приложения
+	CancelApp()
+	// StoreMem -- хранилище в памяти
+	StoreMem() IStoreMem
+	// StoreDisk -- хранилище на диске
+	StoreDisk() IStoreDisk
+	// User -- возвращает пользователя для авторизации на сервере
+	User() IStoreUser
 }

+ 11 - 0
pkg/types/istore_base.go

@@ -0,0 +1,11 @@
+package types
+
+// IStoreBase -- базовый интерфейс для хранилищ
+type IStoreBase interface {
+	// Put -- разместить запись
+	Put(key string, val []byte) error
+	// Get -- получить запись
+	Get(key string) (val []byte, err error)
+	// Del -- удаляет запись по ключу
+	Del(key string)
+}

+ 8 - 0
pkg/types/istore_disk.go

@@ -0,0 +1,8 @@
+package types
+
+// IStoreDisk -- дисковое хранилище
+type IStoreDisk interface {
+	IStoreBase
+	// Find -- найти ключи по префиксу
+	Find(prefixKey string) ([]string, error)
+}

+ 6 - 0
pkg/types/istore_mem.go

@@ -0,0 +1,6 @@
+package types
+
+// IStoreMem -- хранилище записей в памяти
+type IStoreMem interface {
+	IStoreBase
+}