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

SVI Добавление кода, документация

SVI 2 жил өмнө
parent
commit
469dbd1d31

+ 16 - 2
README.md

@@ -1,6 +1,6 @@
 # gostore
 
-Простейшее сетевое хранилще на `golang`.
+Простейшее сетевое хранилище на `golang`.
 
 ## Описание
 
@@ -13,7 +13,7 @@
 * [ ] `/del/:login/:pass/:key` (POST, удалить из хранилища).
 * [x] `/time` (GET, возвращает текущее время на сервере; для поддержания `KeepAlive`).
 
-Запросы сделанными классическими специально, чтобы гарантировать праильную работу через прокси.
+Запросы сделанными классическими специально, чтобы гарантировать правильную работу через прокси.
 
 Поддерживается `KeepAlive` с ожиданием следующего запроса 10 сек. Клиент должен
 присылать запросы не реже, чтобы поддерживать открытым соединение.
@@ -23,12 +23,26 @@
 
 Максимальный размер значения ключа не может быть более 10 МБ (но можно настроить в коде).
 
+## Кеш в памяти
+
+Для ускорения работы хранилище имеют копию кеш в памяти. Количество записей в кеше
+нстраивается через переменную окружения, например:
+
+> Задано 5_000 записей. При предельном размере записи 10 МБ общая память под кешем
+> займёт 50 ГБ.
+>
+> Задано 50_000, но известно ,что размер записи не превысит 500 байт. Тогда
+> память под кешем максимум будет 25 Мб.
+
+В любом случае, поиск по словарю в своей памяти всегда будет быстрее, чем поиск в БД на диске.
+
 ## Переменные окружения
 
 ```bash
 export STORE_USER="dev"
 export STORE_USER_PASS="dev"
 export STORE_HTTP_PORT="25000"
+export STORE_MEM_RECORD_POOL_SIZE="5_000"
 ```
 
 ## Команды сборки

+ 1 - 0
dev.sh

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

+ 38 - 0
internal/store_mem/life_time/life_time.go

@@ -0,0 +1,38 @@
+// package life_time -- глобальный счётчик древности записи в паамяти
+package life_time
+
+import (
+	"math"
+	"sync"
+
+	"git.p78su.freemyip.com/svi/gostore/pkg/types"
+)
+
+// LifeTime -- глобальный счётчик древности записи в паамяти
+type LifeTime struct {
+	timeLife int // Глобальный счётчик
+	block    sync.Mutex
+}
+
+// NewLifeTime -- возвращает новый глобальный счётчик
+func NewLifeTime() types.ILifeTime {
+	sf := &LifeTime{
+		timeLife: math.MaxInt,
+	}
+	return sf
+}
+
+// Next -- возвращает следующую метку времени и признак перебора
+func (sf *LifeTime) Next() int {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	sf.timeLife--
+	defer func() {
+		if sf.timeLife <= 0 {
+			sf.timeLife = math.MaxInt
+		}
+	}()
+
+	return sf.timeLife
+
+}

+ 76 - 0
internal/store_mem/mem_pool_record/mem_pool_record.go

@@ -0,0 +1,76 @@
+// package mem_pool_record -- пул записей для базы в памяти
+package mem_pool_record
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"git.p78su.freemyip.com/svi/gostore/internal/store_mem/mem_record"
+	"git.p78su.freemyip.com/svi/gostore/pkg/types"
+)
+
+// MemPoolRecord -- пул записей для базы в памяти
+type MemPoolRecord struct {
+	lstRec   []types.IMemRecord    // Список записей в пуле
+	chGet    chan types.IMemRecord // Канал отдачи записей из пула
+	chPut    chan types.IMemRecord // Канал приёма записей из кеша в памяти
+	sizePool int                   // Размер пула
+}
+
+// NewMemPoolRecord -- возвращает новый пул записей в памяти
+func NewMemPoolRecord() (types.IMemPoolRecord, error) {
+	strSize := os.Getenv("STORE_MEM_RECORD_POOL_SIZE")
+	if strSize == "" {
+		return nil, fmt.Errorf("NewMemPoolRecord(): env STORE_MEM_RECORD_POOL_SIZE not set")
+	}
+	iSize, err := strconv.Atoi(strSize)
+	if err != nil {
+		return nil, fmt.Errorf("NewMemPoolRecord(): env STORE_MEM_RECORD_POOL_SIZE(%q) not integer", strSize)
+	}
+	sf := &MemPoolRecord{
+		sizePool: iSize,
+		chGet:    make(chan types.IMemRecord, 50),
+		chPut:    make(chan types.IMemRecord, 50),
+		lstRec:   make([]types.IMemRecord, 0),
+	}
+	for len(sf.lstRec) < iSize {
+		rec := mem_record.NewMemRecord()
+		sf.lstRec = append(sf.lstRec, rec)
+	}
+	go sf.run()
+	return sf, nil
+}
+
+// Cap -- возвращает ёмкость пула
+func (sf *MemPoolRecord) Cap() int {
+	return sf.sizePool
+}
+
+// Put -- помещает освободившееся записи в пул
+func (sf *MemPoolRecord) Put(rec types.IMemRecord) {
+	sf.chPut <- rec
+}
+
+// Get -- возвращает запись из пула
+func (sf *MemPoolRecord) Get() types.IMemRecord {
+	rec := <-sf.chGet
+	return rec
+}
+
+// В отдельном потоке принимает запросы на выделение записей и возврат записей
+func (sf *MemPoolRecord) run() {
+	for {
+		select {
+		case rec := <-sf.chPut: // Прилетела свободная запись
+			sf.lstRec = append(sf.lstRec, rec)
+		default: // По умолчанию записи отдаются
+			_len := len(sf.lstRec)
+			if _len > 0 {
+				rec := sf.lstRec[0]
+				sf.lstRec = sf.lstRec[1:]
+				sf.chGet <- rec
+			}
+		}
+	}
+}

+ 65 - 0
internal/store_mem/mem_record/mem_record.go

@@ -0,0 +1,65 @@
+// package mem_record -- запись хранения кеша в памяти
+package mem_record
+
+import (
+	"bytes"
+	"log"
+	"sync"
+
+	"git.p78su.freemyip.com/svi/gostore/pkg/types"
+)
+
+// MemRecord -- элемент хранения кеша в памяти
+type MemRecord struct {
+	buf      *bytes.Buffer // Буфер для хранения данных
+	timeLife int     // Как давно создана запись (чем древнее запись, тем это значение больше)
+	block    sync.RWMutex
+}
+
+// NewMemRecord -- возвращает новую запись, для размещения в памяти
+func NewMemRecord() types.IMemRecord {
+	sf := &MemRecord{
+		buf: bytes.NewBuffer([]byte{}),
+	}
+	return sf
+}
+
+// IsOld -- проверяет, что запись не древнее, чем указанная
+func (sf *MemRecord) IsOld(timeLife int) bool {
+	sf.block.RLock()
+	defer sf.block.RUnlock()
+	return sf.timeLife >= timeLife
+}
+
+// Update -- обновляет метку времени записи
+func (sf *MemRecord) Update(timeLife int) {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	sf.timeLife = timeLife
+}
+
+// TimeLife -- как давно создана запись (чем древнее запись, тем это значение больше)
+func (sf *MemRecord) TimeLife() int {
+	sf.block.RLock()
+	defer sf.block.RUnlock()
+	return sf.timeLife
+}
+
+// Set -- устанавливает хранмое значение
+func (sf *MemRecord) Set(val []byte, timeLife int) {
+	sf.block.Lock()
+	defer sf.block.RUnlock()
+	_, err := sf.buf.Write(val)
+	if err != nil {
+		log.Printf("MemRecord.Set(): in write buffer, err=\n\t%v\n", err)
+		return
+	}
+	sf.timeLife = timeLife
+}
+
+// Get -- возвращает хранимое значение
+func (sf *MemRecord) Get() []byte {
+	sf.block.RLock()
+	defer sf.block.RLock()
+	return sf.buf.Bytes()
+}

+ 101 - 0
internal/store_mem/store_mem.go

@@ -0,0 +1,101 @@
+// package store_mem -- хранилище-кеш в памяти
+package store_mem
+
+import (
+	"fmt"
+	"sync"
+
+	"git.p78su.freemyip.com/svi/gostore/internal/store_mem/mem_pool_record"
+	"git.p78su.freemyip.com/svi/gostore/pkg/types"
+)
+
+// StoreMem -- хранилище-кеш в памяти
+type StoreMem struct {
+	dict     map[string]types.IMemRecord // Кеш в памяти с данными
+	pool     types.IMemPoolRecord        // Пул доступных записей
+	lifeTime types.ILifeTime             // Счётчик жизни для очистки кеша
+	block    sync.Mutex
+	cap      int // Предельная ёмкость кеша
+}
+
+// NewStoreMem -- возвращает новое хранилище в памяти
+func NewStoreMem() (types.IMemStore, error) {
+	pool, err := mem_pool_record.NewMemPoolRecord()
+	if err != nil {
+		return nil, fmt.Errorf("NewStoreMem(): in create IMemPoolRecord, err=\n\t%w", err)
+	}
+	sf := &StoreMem{
+		dict: map[string]types.IMemRecord{},
+		pool: pool,
+		cap:  pool.Cap() - pool.Cap()/10,
+	}
+	return sf, nil
+}
+
+// Сбрасывает словарь по требованию
+func (sf *StoreMem) reset() {
+	for _, rec := range sf.dict {
+		sf.pool.Put(rec)
+	}
+	sf.dict = map[string]types.IMemRecord{}
+}
+
+// Put -- размещает запись в памяти
+func (sf *StoreMem) Put(key string, val []byte) {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	nextTime := sf.lifeTime.Next()
+	if nextTime <= 0 {
+		sf.reset()
+		nextTime = sf.lifeTime.Next()
+	}
+	// Проверить что такая запись есть в памяти
+	rec := sf.dict[key]
+	if rec != nil {
+		rec.Set(val, nextTime)
+		return
+	}
+	// Такой записи нет, проверить что словарь не переполнен
+	if len(sf.dict) > sf.cap {
+		sf.clear()
+	}
+	rec = sf.pool.Get()
+	rec.Set(val, nextTime)
+	sf.dict[key] = rec
+}
+
+// Снижает уровень заполнения словаря меньше 90% по требованию
+func (sf *StoreMem) clear() {
+	nextTime := sf.lifeTime.Next()
+	if nextTime <= 0 {
+		sf.reset()
+		nextTime = sf.lifeTime.Next()
+	}
+	nextTime -= sf.cap
+	for key, rec := range sf.dict {
+		if rec.IsOld(nextTime) {
+			delete(sf.dict, key)
+		}
+		if len(sf.dict) < sf.cap {
+			return
+		}
+	}
+}
+
+// Get -- возвращает хранимую запись (если есть)
+func (sf *StoreMem) Get(key string) []byte {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	rec := sf.dict[key]
+	if rec == nil {
+		return nil
+	}
+	nextTime := sf.lifeTime.Next()
+	if nextTime <= 0 {
+		sf.reset()
+		nextTime = sf.lifeTime.Next()
+	}
+	rec.Update(nextTime)
+	return rec.Get()
+
+}

+ 6 - 0
pkg/types/ilife_time.go

@@ -0,0 +1,6 @@
+package types
+
+// ILifeTime -- счётчик жизни для хранилища в памяти
+type ILifeTime interface {
+	Next() (timeLife int)
+}

+ 11 - 0
pkg/types/imem_pool_record.go

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

+ 15 - 0
pkg/types/imem_record.go

@@ -0,0 +1,15 @@
+package types
+
+// IMemRecord -- запись размещаемая в памяти
+type IMemRecord interface {
+	// Get -- возвращает хранмое значени
+	Get() []byte
+	// Reset -- сбрасывает хранимое значение
+	Set(val []byte, timeLife int)
+	// Update -- обновляет метку времени записи
+	Update(timeLife int)
+	// IsOld -- проверяет, что метка времени в записи старше контрольной метки
+	IsOld(timeLife int) bool
+	// TimeLife -- возвращает время жизни
+	TimeLife() int
+}

+ 9 - 0
pkg/types/imem_store.go

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