first commit

This commit is contained in:
Григорий Сафронов 2026-02-27 22:04:04 +03:00
commit 51e3d68f5d
28 changed files with 5869 additions and 0 deletions

BIN
Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

295
README.md Normal file
View File

@ -0,0 +1,295 @@
<!-- Improved compatibility of К началу link: See: https://github.com/othneildrew/Best-README-Template/pull/73 -->
<a id="readme-top"></a>
<!--
*** Thanks for checking out the Best-README-Template. If you have a suggestion
*** that would make this better, please fork the repo and create a pull request
*** or simply open an issue with the tag "enhancement".
*** Don't forget to give the project a star!
*** Thanks again! Now go create something AMAZING! :D
-->
<!-- PROJECT LOGO -->
<br />
<div align="center">
<!-- <a href="https://github.com/othneildrew/Best-README-Template"> -->
<img src="Logo.png" height=100 alt="Logo.png"></img>
</a>
<p align="center">
<h3> <b>Futriis-это легковесная, распределённая wait-free и lock-free дружественная in-memory СУБД,
реализованная на Go с поддержкой плагинов на языке lua для операционных систем на базе Solaris (ядра Illumos)</b> <br></h3>
<br />
<br />
<!-- <a href="">Сообщить об ошибке</a>
&middot;
<!-- <a href="">Предложение новой функциональности</a> -->
</p>
</div>
## Краткая документация проекта FutriiS
<!-- TABLE OF CONTENTS -->
<br>
<!-- <details> -->
<summary><b>Содержание</b></summary></br>
<ol>
<li>
<a href="#о-проекте">О проекте</a>
<li><a href="#лицензия">Лицензия</a></li>
<li><a href="#глоссарий">Глоссарий</a></li>
<li><a href="#системные-требования">Системные требования</a></li>
<li><a href="#подготовка">Подготовка</a></li>
<li><a href="#компиляция">Компиляция</a></li>
<li><a href="#тестирование">Тестирование</a></li>
<li><a href="#примеры-команд-субд">Примеры команд субд</a></li>
<li><a href="#репликация">Репликация</a></li>
<li><a href="#резервное-копирование">Резервное копирование</a></li>
<li><a href="#индексы">Индексы</a></li>
<li><a href="#транзакции">Транзакции</a></li>
<li><a href="#шардинг">Шардинг</a></li>
<li><a href="#кластеризация">Кластеризация</a></li>
<li><a href="#lua-скрипты">Lua-скрипты</a></li>
<li><a href="#сферы-применения">Сферы применения</a></li>
<li><a href="#дорожная-карта">Дорожная карта</a></li>
<li><a href="#контакты">Контакты</a></li>
</ol>
<!-- </details> -->
# futriis - Распределённая in-memory СУБД
futriis - это легковесная, распределённая wait-free и lock-free дружественная in-memory СУБД,
реализованная на Go с поддержкой плагинов на языке lua.
## Архитектура
СУБД реализует три основных типа данных:
- **Таппл (Tapple)** - аналог базы данных в РСУБД
- **Слайс (Slice)** - аналог таблицы
- **Кортеж (Tuple)** - аналог записи в таблице
## Системные требования
> [!WARNING]
> - Процессор: Intel или AMD
> - Оперативная память: 4ГБ (Для Linux) 8ГБ (Для Illumos sytems)
> - Только Unix-подобная ОС (Solaris, OpenIndiana, Linux)
> - Go 1.25.6 или выше
> [!CAUTION]
> **Важно: Windows и MacOS X не поддерживаются!**
## Установка и сборка
1. Клонируйте репозиторий:
```bash
git clone https://github.com/futriis/db.git
cd futriis
```
2. Скомпилируйте и запустите:
```bash
./build.sh
./futriis
```
## Файл futriisd
futriisd - это демон (сервис) СУБД Futriis, расположенный в /futriis/build/futriisd. Этот файл является:
- Основным исполняемым файлом сервера - запускает ядро СУБД в фоновом режиме как демон (daemon)
- Точкой входа для кластерного узла - каждый узел кластера запускается через этот бинарный файл
- Фоновым процессом - работает независимо от терминала, обрабатывая сетевые запросы
- Управляющим процессом - отвечает за инициализацию всех компонентов: хранилища, кластера, репликации, AOF
- Сетевым сервером - слушает порты для координации кластера и обработки клиентских подключений
**Пример использования демона "futriisd"**
```bash
# Запуск узла кластера
./futriisd --config /path/to/config.toml --node-id node-1
# Запуск координатора
./futriisd --config /path/to/config.toml --coordinator
# Запуск в фоновом режиме
./futriisd --daemon
```
## Базовые команды (Tapple/Slice/Tuple)
### Создание объектов
```bash
# Создать таппл (базу данных)
create tapple users
# Создать слайс (таблицу) в таппле
create slice users user_profiles
# Создать кортеж (запись) с полями
create tuple users user_profiles user1 name=John age=30 email=john@example.com
create tuple users user_profiles user2 name=Jane age=25 city=NYC
```
### Просмотр списков
```bash
# Показать все тапплы
list tapples
# Показать все слайсы в таппле
list slices users
# Показать все кортежи в слайсе
show tuples users user_profiles
```
## Индексы
```bash
# Создать первичный индекс для таппла
add.prime.index users
# Удалить первичный индекс
delete.prime.index users
# Создать вторичный индекс по полю
add.secondary.index users email
add.secondary.index users age
# Удалить вторичный индекс
delete.secondary.index users email
```
### Обновление и удаление
```bash
# Обновить поля кортежа
update tuple users user_profiles user1 age=31 city=Boston
# Удалить кортеж
delete tuple users user_profiles user2
# Удалить слайс
delete slice users user_profiles
# Удалить таппл
delete tapple users
```
## Транзакции
```bash
# Начать транзакцию
begin
# Выполнить операции внутри транзакции
create tuple users user_profiles user3 name=Bob age=28
update tuple users user_profiles user1 city=Chicago
# Зафиксировать транзакцию
commit
# Или откатить транзакцию
rollback
```
## Кластеринг и шардинг
```bash
# Показать статус кластера
cluster.status
# Добавить узел в кластер
add.node 192.168.1.101:8080
add.node 192.168.1.102:8080
# Удалить узел из кластера
evict.node node-123
# Ребалансировка кластера
cluster.rebalance
# Показать статус шардинга
sharding.status
```
## Сжатие данных
```bash
# Показать статистику сжатия по колонкам
compression.stats
```
## AOF (Append-Only File)
```bash
# Показать информацию о AOF файле
aof.info
# Восстановить данные из AOF файла
aof.recover
aof.recover /path/to/custom/file.aof
```
## Lua-плагины
```bash
# Выполнить Lua плагин
lua my_plugin
lua analytics_script
```
## Служебные команды
```bash
# Показать справку
help
# Выйти из СУБД
exit
# или
quit
```
## Комплексный пример рабочей сессии
```bash
# Создаём структуру данных
create tapple ecommerce
create slice ecommerce products
create slice ecommerce customers
create slice ecommerce orders
# Создаём индексы
add.secondary.index ecommerce price
add.secondary.index ecommerce email
# Добавляем данные (в транзакции)
begin
create tuple ecommerce products prod1 name=Laptop price=999.99 stock=10
create tuple ecommerce products prod2 name=Mouse price=29.99 stock=50
create tuple ecommerce customers cust1 name=Alice email=alice@mail.com
create tuple ecommerce orders order1 customer=cust1 product=prod1 quantity=1
commit
# Просматриваем данные
show tuples ecommerce products
show tuples ecommerce customers
# Обновляем данные
update tuple ecommerce products prod1 stock=9
# Проверяем статус кластера
cluster.status
# Смотрим статистику сжатия
compression.stats
# Выходим
exit
```

255
build.sh Executable file
View File

@ -0,0 +1,255 @@
#!/bin/bash
# /futriis/build.sh
# Bash-скрипт для сборки проекта futriis на Unix-системах (Solaris, OpenIndiana, Linux)
# При запуске без параметров выполняет полную сборку проекта: установка зависимостей,
# сборка сервера, сборка клиента и очистка кеша.
# Цвета для вывода
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Проверка операционной системы
UNAME_S=$(uname -s)
if [ "$UNAME_S" != "Linux" ] && [ "$UNAME_S" != "SunOS" ]; then
echo -e "${RED}Ошибка: Операционная система $UNAME_S не поддерживается. Проект поддерживает только Solaris, OpenIndiana и Linux.${NC}"
exit 1
fi
# Переменные
BINARY_NAME="futriis"
SERVER_BINARY_NAME="futriisd"
BUILD_DIR="build"
CMD_DIR="./cmd/futriis"
# Функция для отображения разделителя
print_separator() {
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
}
# Функция для отображения заголовка
print_header() {
echo -e "${YELLOW}$1${NC}"
}
# Функция для отображения успеха
print_success() {
echo -e "${GREEN}$1${NC}"
}
# Функция для отображения ошибки
print_error() {
echo -e "${RED}$1${NC}"
}
# Создание директории для сборки
create_build_dir() {
if [ ! -d "$BUILD_DIR" ]; then
mkdir -p "$BUILD_DIR"
mkdir -p "$BUILD_DIR/plugins"
mkdir -p "$BUILD_DIR/data"
print_success "Создана директория сборки: $BUILD_DIR"
fi
}
# Установка зависимостей
deps() {
print_header "Установка зависимостей..."
go mod download
if [ $? -ne 0 ]; then
print_error "Ошибка при загрузке зависимостей"
return 1
fi
go mod tidy
if [ $? -ne 0 ]; then
print_error "Ошибка при обновлении go.mod"
return 1
fi
print_success "Зависимости установлены"
return 0
}
# Сборка клиента
build_client() {
print_header "Сборка клиента для $UNAME_S..."
create_build_dir
go build -ldflags="-s -w" -o "$BUILD_DIR/$BINARY_NAME" "$CMD_DIR"
if [ $? -eq 0 ]; then
print_success "Клиент собран: $BUILD_DIR/$BINARY_NAME"
return 0
else
print_error "Ошибка сборки клиента"
return 1
fi
}
# Сборка сервера
build_server() {
print_header "Сборка сервера для $UNAME_S..."
create_build_dir
go build -ldflags="-s -w" -o "$BUILD_DIR/$SERVER_BINARY_NAME" "$CMD_DIR"
if [ $? -eq 0 ]; then
print_success "Сервер собран: $BUILD_DIR/$SERVER_BINARY_NAME"
return 0
else
print_error "Ошибка сборки сервера"
return 1
fi
}
# Очистка кеша Go
clean_cache() {
print_header "Очистка кеша сборки Go..."
go clean -cache
if [ $? -eq 0 ]; then
print_success "Кеш сборки очищен"
return 0
else
print_error "Ошибка при очистке кеша"
return 1
fi
}
# Полная сборка проекта
full_build() {
echo
print_separator
echo -e "${GREEN} F U T R I I S - Полная сборка проекта${NC}"
print_separator
echo
local errors=0
# Шаг 1: Установка зависимостей
deps
if [ $? -ne 0 ]; then
errors=$((errors + 1))
fi
echo
# Шаг 2: Сборка сервера
build_server
if [ $? -ne 0 ]; then
errors=$((errors + 1))
fi
echo
# Шаг 3: Сборка клиента
build_client
if [ $? -ne 0 ]; then
errors=$((errors + 1))
fi
echo
# Шаг 4: Очистка кеша
clean_cache
if [ $? -ne 0 ]; then
errors=$((errors + 1))
fi
echo
# Итоговый отчёт
print_separator
if [ $errors -eq 0 ]; then
echo -e "${GREEN}✅ СБОРКА УСПЕШНО ЗАВЕРШЕНА${NC}"
echo -e "${GREEN} Сервер: $BUILD_DIR/$SERVER_BINARY_NAME${NC}"
echo -e "${GREEN} Клиент: $BUILD_DIR/$BINARY_NAME${NC}"
else
echo -e "${RED}❌ СБОРКА ЗАВЕРШЕНА С ОШИБКАМИ ($errors ошибок)${NC}"
fi
print_separator
echo
# Показываем размер бинарных файлов
if [ -f "$BUILD_DIR/$SERVER_BINARY_NAME" ]; then
SERVER_SIZE=$(du -h "$BUILD_DIR/$SERVER_BINARY_NAME" | cut -f1)
echo -e "Размер сервера: ${YELLOW}$SERVER_SIZE${NC}"
fi
if [ -f "$BUILD_DIR/$BINARY_NAME" ]; then
CLIENT_SIZE=$(du -h "$BUILD_DIR/$BINARY_NAME" | cut -f1)
echo -e "Размер клиента: ${YELLOW}$CLIENT_SIZE${NC}"
fi
echo
}
# Функция для отображения справки
show_help() {
echo "Использование: ./build.sh [КОМАНДА]"
echo
echo "Доступные команды:"
echo " (без параметров) - полная сборка проекта (зависимости + сервер + клиент + очистка)"
echo " deps - только установка зависимостей"
echo " build - только сборка клиента"
echo " build-server - только сборка сервера"
echo " install - установка проекта в систему"
echo " run - сборка и запуск клиента"
echo " run-server - сборка и запуск сервера"
echo " test - запуск тестов"
echo " clean - очистка директории сборки"
echo " clean-cache - только очистка кеша Go"
echo " help - показать эту справку"
echo
}
# Обработка аргументов командной строки
case "$1" in
deps)
deps
;;
build)
build_client
;;
build-server)
build_server
;;
install)
echo -e "${GREEN}Установка проекта...${NC}"
go install -ldflags="-s -w" "$CMD_DIR"
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ Установка завершена${NC}"
else
echo -e "${RED}✗ Ошибка установки${NC}"
exit 1
fi
;;
run)
build_client && "./$BUILD_DIR/$BINARY_NAME"
;;
run-server)
build_server && "./$BUILD_DIR/$SERVER_BINARY_NAME" -server -config config.toml
;;
test)
echo -e "${GREEN}Запуск тестов...${NC}"
go test -v ./...
;;
clean)
echo -e "${GREEN}Очистка...${NC}"
rm -rf "$BUILD_DIR"
go clean
echo -e "${GREEN}✓ Очистка завершена${NC}"
;;
clean-cache)
clean_cache
;;
help|"")
if [ "$1" = "" ]; then
full_build
else
show_help
fi
;;
*)
echo -e "${RED}Неизвестная команда: $1${NC}"
show_help
exit 1
;;
esac
exit 0

81
cmd/futriis/main.go Normal file
View File

@ -0,0 +1,81 @@
// /futriis/cmd/futriis/main.go
// Клиентское приложение СУБД Futriis
// Обеспечивает интерактивный интерфейс для выполнения команд
package main
import (
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"futriis/internal/client"
"futriis/internal/engine"
"futriis/pkg/config"
"futriis/pkg/utils"
)
func main() {
// Определяем путь к файлу конфигурации
configPath := "config.toml"
// Проверяем, существует ли файл в текущей директории
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// Если нет, пробуем найти в родительской директории (для случая запуска из cmd/futriis)
configPath = filepath.Join("..", "..", "config.toml")
// Проверяем, существует ли файл по новому пути
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// Если файл не найден, используем абсолютный путь относительно домашней директории
homeDir, _ := os.UserHomeDir()
configPath = filepath.Join(homeDir, "futriis", "config.toml")
}
}
// Загружаем конфигурацию
cfg, err := config.Load(configPath)
if err != nil {
fmt.Printf("Ошибка загрузки конфигурации: %v\n", err)
os.Exit(1)
}
// Инициализируем логгеры
utils.InitLogger("")
// Инициализируем файловый логгер
if err := utils.InitFileLogger(cfg.Node.AOFFile); err != nil {
utils.PrintWarning("Не удалось инициализировать файловый логгер: %v", err)
}
defer func() {
if logger := utils.GetFileLogger(); logger != nil {
logger.Close()
}
}()
// Создаём движок
eng := engine.NewEngine()
// Выводим баннер с именем кластера из конфига
utils.PrintBanner(cfg.Cluster.Name)
// Создаём обработчик команд
handler := client.NewCommandHandler(eng)
// Обработка сигналов для graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nПолучен сигнал завершения. Завершаем работу...")
os.Exit(0)
}()
// Запускаем REPL
if err := handler.RunREPL(); err != nil {
utils.PrintError("%v", err)
os.Exit(1)
}
}

48
internal/cli/commands.go Normal file
View File

@ -0,0 +1,48 @@
// /futriis/internal/cli/commands.go
// Пакет cli определяет структуру команд и систему их регистрации в СУБД.
// Command представляет собой описание команды с именем, описанием, синтаксисом использования и функцией-обработчиком.
// CommandRegistry служит центральным реестром для всех доступных команд, позволяя регистрировать новые, получать команды по имени и формировать список для справки.
// Обеспечивает расширяемость интерфейса командной строки.
package cli
// Command представляет команду СУБД
type Command struct {
Name string
Description string
Usage string
Handler func(args []string) (string, error)
}
// CommandRegistry реестр всех доступных команд
type CommandRegistry struct {
commands map[string]*Command
}
// NewCommandRegistry создаёт новый реестр команд
func NewCommandRegistry() *CommandRegistry {
return &CommandRegistry{
commands: make(map[string]*Command),
}
}
// Register регистрирует новую команду
func (cr *CommandRegistry) Register(cmd *Command) {
cr.commands[cmd.Name] = cmd
}
// Get возвращает команду по имени
func (cr *CommandRegistry) Get(name string) (*Command, bool) {
cmd, ok := cr.commands[name]
return cmd, ok
}
// List возвращает список всех команд
func (cr *CommandRegistry) List() []*Command {
cmds := make([]*Command, 0, len(cr.commands))
for _, cmd := range cr.commands {
cmds = append(cmds, cmd)
}
return cmds
}

88
internal/cli/history.go Normal file
View File

@ -0,0 +1,88 @@
// /futriis/internal/cli/history.go
// Пакет cli реализует управление историей команд для интерактивного режима.
// History хранит ограниченное количество последних команд с кольцевым буфером, предотвращает добавление последовательных дубликатов.
// Предоставляет навигацию по истории с помощью стрелок вверх/вниз для быстрого повторного выполнения команд.
// Интегрируется с Prompt для обеспечения полноценного интерфейса командной строки.
package cli
import (
"os"
"golang.org/x/term"
)
// History управляет историей команд
type History struct {
commands []string
position int
maxSize int
}
// NewHistory создаёт новую историю команд
func NewHistory(maxSize int) *History {
return &History{
commands: make([]string, 0, maxSize),
position: 0,
maxSize: maxSize,
}
}
// Add добавляет команду в историю
func (h *History) Add(cmd string) {
if cmd == "" {
return
}
// Не добавляем дубликаты подряд
if len(h.commands) > 0 && h.commands[len(h.commands)-1] == cmd {
return
}
// Если достигнут максимум, удаляем самую старую команду
if len(h.commands) >= h.maxSize {
h.commands = h.commands[1:]
}
h.commands = append(h.commands, cmd)
h.position = len(h.commands)
}
// GetPrevious возвращает предыдущую команду из истории
func (h *History) GetPrevious() string {
if len(h.commands) == 0 {
return ""
}
if h.position > 0 {
h.position--
}
return h.commands[h.position]
}
// GetNext возвращает следующую команду из истории
func (h *History) GetNext() string {
if h.position < len(h.commands)-1 {
h.position++
return h.commands[h.position]
}
h.position = len(h.commands)
return ""
}
// Reset сбрасывает позицию в истории
func (h *History) Reset() {
h.position = len(h.commands)
}
// SetupRawMode устанавливает терминал в raw-режим для обработки клавиш
func SetupRawMode() (*term.State, error) {
fd := int(os.Stdin.Fd())
return term.MakeRaw(fd)
}
// RestoreMode восстанавливает режим терминала
func RestoreMode(oldState *term.State) error {
fd := int(os.Stdin.Fd())
return term.Restore(fd, oldState)
}

204
internal/cli/prompt.go Normal file
View File

@ -0,0 +1,204 @@
// /futriis/internal/cli/prompt.go
// Пакет cli реализует интерактивное приглашение командной строки с поддержкой истории.
// Обеспечивает редактирование строки ввода, навигацию по истории стрелками,
// поддержку Unicode (включая кириллицу) и управление курсором терминала.
// Использует raw режим терминала для обработки специальных клавиш.
package cli
import (
"bufio"
"fmt"
"os"
"futriis/pkg/utils"
"github.com/mattn/go-runewidth"
"golang.org/x/term"
)
// ANSI escape sequences для управления курсором
const (
ansiHideCursor = "\033[?25l"
ansiShowCursor = "\033[?25h"
ansiClearLine = "\033[2K"
ansiCarriageReturn = "\r"
)
// Prompt представляет интерактивное приглашение
type Prompt struct {
history *History
buffer []rune
pos int
}
// NewPrompt создаёт новое приглашение
func NewPrompt() *Prompt {
return &Prompt{
history: NewHistory(100),
buffer: make([]rune, 0),
pos: 0,
}
}
// ReadLine читает строку с поддержкой истории и редактирования
func (p *Prompt) ReadLine() (string, error) {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return p.readSimple()
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
// Скрываем курсор в начале строки
fmt.Print(ansiHideCursor)
// Очищаем буфер
p.buffer = make([]rune, 0)
p.pos = 0
// Показываем приглашение
promptStr := utils.ColorPrompt + "futriis:~> " + utils.ColorReset
fmt.Print(promptStr)
// Показываем курсор только после приглашения (в месте ввода)
fmt.Print(ansiShowCursor)
reader := bufio.NewReader(os.Stdin)
for {
r, _, err := reader.ReadRune()
if err != nil {
return "", err
}
switch r {
case 3: // Ctrl+C
return "", nil
case 4: // Ctrl+D
if len(p.buffer) == 0 {
return "exit", nil
}
case 13, 10: // Enter
// Скрываем курсор перед завершением
fmt.Print(ansiHideCursor)
fmt.Println()
cmd := string(p.buffer)
if cmd != "" {
p.history.Add(cmd)
}
p.history.Reset()
// Логируем команду
if logger := utils.GetLogger(); logger != nil && cmd != "" {
logger.Log("CMD", cmd)
}
return cmd, nil
case 127: // Backspace
if p.pos > 0 {
// Удаляем символ перед курсором
p.buffer = append(p.buffer[:p.pos-1], p.buffer[p.pos:]...)
p.pos--
p.refreshLine()
}
case 27: // Escape sequence (стрелки)
// Читаем следующие два символа
r2, _, _ := reader.ReadRune()
r3, _, _ := reader.ReadRune()
if r2 == '[' {
switch r3 {
case 'A': // Up arrow
prev := p.history.GetPrevious()
if prev != "" {
p.buffer = []rune(prev)
p.pos = len(p.buffer)
p.refreshLine()
}
case 'B': // Down arrow
next := p.history.GetNext()
p.buffer = []rune(next)
p.pos = len(p.buffer)
p.refreshLine()
case 'C': // Right arrow
if p.pos < len(p.buffer) {
p.pos++
p.refreshLine()
}
case 'D': // Left arrow
if p.pos > 0 {
p.pos--
p.refreshLine()
}
}
}
default:
// Добавляем символ (поддержка Unicode, включая русский)
if r >= 32 { // Печатные символы
// Вставляем символ в позицию курсора
if p.pos == len(p.buffer) {
p.buffer = append(p.buffer, r)
} else {
p.buffer = append(p.buffer[:p.pos], append([]rune{r}, p.buffer[p.pos:]...)...)
}
p.pos++
p.refreshLine()
}
}
}
}
// refreshLine обновляет текущую строку с правильным позиционированием курсора
func (p *Prompt) refreshLine() {
// Скрываем курсор во время перерисовки
fmt.Print(ansiHideCursor)
// Возврат в начало строки и очистка
fmt.Print(ansiCarriageReturn + ansiClearLine)
// Печатаем приглашение
promptStr := utils.ColorPrompt + "futriis:~> " + utils.ColorReset
fmt.Print(promptStr)
// Печатаем текущий буфер
if len(p.buffer) > 0 {
fmt.Print(string(p.buffer))
}
// Вычисляем ширину приглашения
promptWidth := runewidth.StringWidth("futriis:~> ")
// Вычисляем позицию курсора
cursorPos := promptWidth
for i := 0; i < p.pos; i++ {
cursorPos += runewidth.RuneWidth(p.buffer[i])
}
// Перемещаем курсор на правильную позицию и показываем его
fmt.Printf("\033[%dG", cursorPos+1)
fmt.Print(ansiShowCursor)
}
// readSimple читает строку без специальной обработки (fallback)
func (p *Prompt) readSimple() (string, error) {
fmt.Print(utils.GetPrompt())
reader := bufio.NewReader(os.Stdin)
cmd, err := reader.ReadString('\n')
if err == nil {
cmd = cmd[:len(cmd)-1] // Убираем \n
// Логируем команду
if logger := utils.GetLogger(); logger != nil && cmd != "" {
logger.Log("CMD", cmd)
}
}
return cmd, err
}

105
internal/client/handler.go Normal file
View File

@ -0,0 +1,105 @@
// /futriis/internal/client/handler.go
// Пакет client реализует обработку команд клиента СУБД Futriis
// Обеспечивает взаимодействие с движком и форматированный вывод результатов
package client
import (
"bufio"
"fmt"
"os"
"strings"
"futriis/internal/engine"
"futriis/pkg/utils"
)
// CommandHandler обрабатывает команды клиента
type CommandHandler struct {
engine *engine.Engine
}
// NewCommandHandler создаёт новый обработчик команд
func NewCommandHandler(engine *engine.Engine) *CommandHandler {
return &CommandHandler{
engine: engine,
}
}
// HandleCommand обрабатывает одну команду
func (h *CommandHandler) HandleCommand(input string) (bool, error) {
// Удаляем лишние пробелы
input = strings.TrimSpace(input)
// Если ввод пустой, просто возвращаемся без вывода
if input == "" {
return false, nil
}
// Разбиваем на части для проверки команды выхода
parts := strings.Fields(input)
if len(parts) == 0 {
return false, nil
}
command := strings.ToLower(parts[0])
// Проверяем команду выхода
if command == "exit" || command == "quit" {
return true, nil
}
// Выполняем команду через движок
result, err := h.engine.Execute(input)
if err != nil {
utils.PrintError("%v", err)
} else if result != "" {
// Если результат не пустой, выводим его без дополнительного форматирования
// так как движок уже возвращает цветной результат
fmt.Println(result)
}
return false, nil
}
// RunREPL запускает цикл чтения-выполнения-вывода
func (h *CommandHandler) RunREPL() error {
scanner := bufio.NewScanner(os.Stdin)
// Проверяем, было ли восстановление из AOF
if h.engine.WasAOFRecovered() {
utils.PrintPromptMessage("State successfully recovered from AOF")
}
utils.PrintPromptMessage("Welcome to Futriis DB. Type 'help' for command list.")
// Добавляем пустую строку после приветствия
fmt.Println()
for {
// Выводим приглашение
fmt.Print(utils.GetPrompt())
// Читаем команду
if !scanner.Scan() {
break
}
input := scanner.Text()
// Обрабатываем команду
exit, err := h.HandleCommand(input)
if err != nil {
utils.PrintError("%v", err)
}
if exit {
break
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("ошибка чтения ввода: %v", err)
}
return nil
}

810
internal/cluster/node.go Normal file
View File

@ -0,0 +1,810 @@
// /futriis/internal/cluster/node.go
// Пакет cluster реализует управление кластером, координацию узлов и репликацию данных.
// Обеспечивает обнаружение узлов, heartbeat механизм для мониторинга доступности,
// а также синхронную мастер-мастер репликацию между узлами кластера.
// Поддерживает автоматическое переключение ролей и балансировку нагрузки.
package cluster
import (
"encoding/json"
"errors"
"fmt"
"net"
"sync"
"sync/atomic"
"time"
"unsafe"
"futriis/pkg/config"
)
// Простые функции для логирования (без зависимостей)
func printInfo(format string, args ...interface{}) {
fmt.Printf("\033[34m[INFO]\033[0m %s\n", fmt.Sprintf(format, args...))
}
func printSuccess(format string, args ...interface{}) {
fmt.Printf("\033[32m[OK]\033[0m %s\n", fmt.Sprintf(format, args...))
}
func printWarning(format string, args ...interface{}) {
fmt.Printf("\033[33m[WARN]\033[0m %s\n", fmt.Sprintf(format, args...))
}
func printError(format string, args ...interface{}) {
fmt.Printf("\033[31m[ERROR]\033[0m %s\n", fmt.Sprintf(format, args...))
}
// NodeState состояние узла
type NodeState int
const (
StateOffline NodeState = iota
StateJoining
StateOnline
StateLeaving
StateFailed
)
func (s NodeState) String() string {
switch s {
case StateOffline:
return "offline"
case StateJoining:
return "joining"
case StateOnline:
return "online"
case StateLeaving:
return "leaving"
case StateFailed:
return "failed"
default:
return "unknown"
}
}
// ReplicationMessage представляет сообщение репликации
type ReplicationMessage struct {
Type string `json:"type"` // "write", "sync", "ack"
Command string `json:"command"` // Команда (create, update, delete)
Args []interface{} `json:"args"` // Аргументы команды
Timestamp int64 `json:"timestamp"` // Временная метка
NodeID string `json:"node_id"` // ID исходного узла
ShardID string `json:"shard_id"` // ID шарда
}
// Node представляет узел кластера (с wait-free указателями)
type Node struct {
ID string
Address string
state int32 // Атомарное состояние
LastSeen time.Time
ShardIDs []string
// Используем atomic.Value для wait-free доступа к изменяемым полям
nodeState unsafe.Pointer // Атомарный указатель на карту состояний
}
// GetState атомарно получает состояние узла
func (n *Node) GetState() NodeState {
return NodeState(atomic.LoadInt32(&n.state))
}
// SetState атомарно устанавливает состояние узла
func (n *Node) SetState(state NodeState) {
atomic.StoreInt32(&n.state, int32(state))
}
// ClusterManager управляет кластером (с wait-free операциями)
type ClusterManager struct {
nodes unsafe.Pointer // Атомарный указатель на карту узлов
coordinatorAddr string
nodeID string
localAddr string
isCoordinator int32 // Атомарный флаг
heartbeatStop chan struct{}
replicationStop chan struct{}
stats struct {
totalNodes int64
activeNodes int64
rebalancingCnt int64
}
replicationEnabled bool
masterMaster bool
replicationQueue chan ReplicationMessage
replicationWG sync.WaitGroup
shardManager *ShardManager // Менеджер шардинга
}
// NewClusterManager создаёт новый менеджер кластера
func NewClusterManager(cfg *config.Config) *ClusterManager {
// Определяем стратегию шардинга по умолчанию
shardingStrategy := ConsistentHashing
shardingEnabled := false
initialShards := 10
// Проверяем наличие настроек шардинга в конфигурации
// Используем значения по умолчанию, если поля отсутствуют
cm := &ClusterManager{
coordinatorAddr: cfg.Cluster.CoordinatorAddress,
nodeID: cfg.Node.ID,
localAddr: cfg.Node.Address,
heartbeatStop: make(chan struct{}),
replicationStop: make(chan struct{}),
replicationEnabled: cfg.Replication.Enabled,
masterMaster: cfg.Replication.MasterMaster,
replicationQueue: make(chan ReplicationMessage, 1000),
}
// Создаём менеджер шардинга с значениями по умолчанию
cm.shardManager = NewShardManager(
shardingStrategy,
cfg.Cluster.ReplicationFactor,
shardingEnabled,
)
// Устанавливаем флаг координатора атомарно
isCoord := int32(0)
if cfg.Node.Address == cfg.Cluster.CoordinatorAddress {
isCoord = 1
}
atomic.StoreInt32(&cm.isCoordinator, isCoord)
// Создаём начальную карту узлов
nodes := make(map[string]*Node)
// Добавляем себя в кластер
selfNode := &Node{
ID: cfg.Node.ID,
Address: cfg.Node.Address,
LastSeen: time.Now(),
}
selfNode.SetState(StateOnline)
nodes[cfg.Node.ID] = selfNode
// Атомарно сохраняем карту узлов
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&nodes))
atomic.AddInt64(&cm.stats.totalNodes, 1)
atomic.AddInt64(&cm.stats.activeNodes, 1)
// Создаём начальные шарды если включён шардинг
if shardingEnabled {
// Получаем список всех узлов
allNodes := make([]string, 0)
for id := range nodes {
allNodes = append(allNodes, id)
}
// Создаём шарды
cm.shardManager.CreateShards(initialShards, allNodes)
}
// Запускаем обработчик репликации если включена мастер-мастер
if cm.replicationEnabled && cm.masterMaster {
go cm.startReplicationHandler()
}
return cm
}
// IsCoordinator атомарно проверяет, является ли узел координатором
func (cm *ClusterManager) IsCoordinator() bool {
return atomic.LoadInt32(&cm.isCoordinator) == 1
}
// Start запускает кластерные сервисы
func (cm *ClusterManager) Start() error {
if cm.IsCoordinator() {
// Запускаем координатор
go cm.startCoordinator()
}
// Запускаем heartbeat
go cm.heartbeatLoop()
printInfo("Кластерный менеджер запущен")
if cm.replicationEnabled && cm.masterMaster {
printInfo("Мастер-мастер репликация активирована")
}
if cm.shardManager.enabled {
printInfo("Шардинг активирован, стратегия: %v", cm.shardManager.getStrategyName())
}
return nil
}
// Stop останавливает кластерные сервисы
func (cm *ClusterManager) Stop() {
close(cm.heartbeatStop)
if cm.replicationEnabled && cm.masterMaster {
close(cm.replicationStop)
cm.replicationWG.Wait()
}
}
// ReplicateCommand реплицирует команду на все узлы кластера с учётом шардинга
func (cm *ClusterManager) ReplicateCommand(cmd string, args []interface{}, key string) error {
if !cm.replicationEnabled || !cm.masterMaster {
return nil // Репликация не включена
}
// Определяем шард для ключа
var shardID string
if cm.shardManager.enabled && key != "" {
shard, err := cm.shardManager.GetShardForKey(key)
if err == nil && shard != nil {
shardID = shard.ID
cm.shardManager.RecordWrite(shardID)
}
}
msg := ReplicationMessage{
Type: "write",
Command: cmd,
Args: args,
Timestamp: time.Now().UnixNano(),
NodeID: cm.nodeID,
ShardID: shardID,
}
// Отправляем в очередь репликации
select {
case cm.replicationQueue <- msg:
return nil
default:
return errors.New("очередь репликации переполнена")
}
}
// startReplicationHandler запускает обработчик репликации
func (cm *ClusterManager) startReplicationHandler() {
cm.replicationWG.Add(1)
defer cm.replicationWG.Done()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case msg := <-cm.replicationQueue:
cm.sendReplicationMessage(msg)
case <-ticker.C:
cm.sendSyncRequest()
case <-cm.replicationStop:
return
}
}
}
// sendReplicationMessage отправляет сообщение репликации на другие узлы
func (cm *ClusterManager) sendReplicationMessage(msg ReplicationMessage) {
// Получаем текущую карту узлов атомарно
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return
}
nodes := *(*map[string]*Node)(nodesPtr)
for id, node := range nodes {
if id == cm.nodeID || node.GetState() != StateOnline {
continue // Не отправляем себе и офлайн узлам
}
// Если есть шард, проверяем, должен ли узел получать это сообщение
if msg.ShardID != "" && cm.shardManager.enabled {
shard, exists := cm.shardManager.GetShardByID(msg.ShardID)
if exists {
shouldSend := false
for _, nodeID := range shard.Nodes {
if nodeID == id {
shouldSend = true
break
}
}
if !shouldSend {
continue
}
}
}
go func(targetNode *Node) {
conn, err := net.DialTimeout("tcp", targetNode.Address, 3*time.Second)
if err != nil {
printWarning("Не удалось подключиться к узлу %s для репликации: %v", targetNode.ID, err)
return
}
defer conn.Close()
encoder := json.NewEncoder(conn)
if err := encoder.Encode(msg); err != nil {
printWarning("Ошибка отправки репликации на узел %s: %v", targetNode.ID, err)
return
}
// Ожидаем подтверждение для синхронной репликации
if msg.Type == "write" {
var ack map[string]interface{}
decoder := json.NewDecoder(conn)
if err := decoder.Decode(&ack); err == nil {
if status, ok := ack["status"].(string); ok && status == "ok" {
printSuccess("Репликация на узел %s подтверждена", targetNode.ID)
}
}
}
}(node)
}
}
// sendSyncRequest отправляет запрос синхронизации
func (cm *ClusterManager) sendSyncRequest() {
if !cm.IsCoordinator() {
return // Только координатор инициирует синхронизацию
}
msg := ReplicationMessage{
Type: "sync",
Timestamp: time.Now().UnixNano(),
NodeID: cm.nodeID,
}
cm.sendReplicationMessage(msg)
}
// handleReplicationMessage обрабатывает входящее сообщение репликации
func (cm *ClusterManager) handleReplicationMessage(conn net.Conn) {
defer conn.Close()
var msg ReplicationMessage
decoder := json.NewDecoder(conn)
if err := decoder.Decode(&msg); err != nil {
return
}
switch msg.Type {
case "write":
// Получаем команду от другого узла
printInfo("Получена команда репликации: %s от узла %s (шард: %s)",
msg.Command, msg.NodeID, msg.ShardID)
// Здесь будет вызов движка для выполнения команды
// TODO: Интегрировать с engine для выполнения реплицированных команд
// Отправляем подтверждение
encoder := json.NewEncoder(conn)
encoder.Encode(map[string]interface{}{
"status": "ok",
"time": time.Now().UnixNano(),
})
case "sync":
// Запрос синхронизации
printInfo("Получен запрос синхронизации от узла %s", msg.NodeID)
// TODO: Отправить текущее состояние
case "ack":
// Подтверждение получения
printInfo("Получено подтверждение от узла %s", msg.NodeID)
}
}
// heartbeatLoop отправляет heartbeat сигналы
func (cm *ClusterManager) heartbeatLoop() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
cm.sendHeartbeat()
case <-cm.heartbeatStop:
return
}
}
}
// sendHeartbeat отправляет heartbeat координатору
func (cm *ClusterManager) sendHeartbeat() {
if !cm.IsCoordinator() {
// Отправляем heartbeat координатору
conn, err := net.DialTimeout("tcp", cm.coordinatorAddr, 3*time.Second)
if err != nil {
return
}
defer conn.Close()
heartbeat := map[string]interface{}{
"type": "heartbeat",
"node_id": cm.nodeID,
"address": cm.localAddr,
"time": time.Now().Unix(),
}
json.NewEncoder(conn).Encode(heartbeat)
}
}
// startCoordinator запускает координатор кластера
func (cm *ClusterManager) startCoordinator() {
listener, err := net.Listen("tcp", cm.coordinatorAddr)
if err != nil {
printError("Ошибка запуска координатора: %v", err)
return
}
defer listener.Close()
printInfo("Координатор кластера запущен на " + cm.coordinatorAddr)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go cm.handleCoordinatorRequest(conn)
}
}
// handleCoordinatorRequest обрабатывает запросы к координатору
func (cm *ClusterManager) handleCoordinatorRequest(conn net.Conn) {
defer conn.Close()
var req map[string]interface{}
if err := json.NewDecoder(conn).Decode(&req); err != nil {
return
}
msgType, _ := req["type"].(string)
switch msgType {
case "heartbeat":
nodeID, _ := req["node_id"].(string)
address, _ := req["address"].(string)
cm.updateNodeHeartbeat(nodeID, address)
case "join":
nodeID, _ := req["node_id"].(string)
address, _ := req["address"].(string)
cm.handleNodeJoin(nodeID, address)
case "leave":
nodeID, _ := req["node_id"].(string)
cm.handleNodeLeave(nodeID)
case "replication":
// Обработка сообщения репликации на координаторе
cm.handleReplicationMessage(conn)
}
}
// handleNodeJoin обрабатывает присоединение узла
func (cm *ClusterManager) handleNodeJoin(nodeID, address string) {
// Получаем текущую карту узлов
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return
}
oldNodes := *(*map[string]*Node)(nodesPtr)
// Создаём новую карту
newNodes := make(map[string]*Node)
for k, v := range oldNodes {
newNodes[k] = v
}
if node, exists := newNodes[nodeID]; exists {
node.SetState(StateOnline)
node.LastSeen = time.Now()
} else {
newNode := &Node{
ID: nodeID,
Address: address,
LastSeen: time.Now(),
}
newNode.SetState(StateOnline)
newNodes[nodeID] = newNode
atomic.AddInt64(&cm.stats.totalNodes, 1)
}
// Атомарно обновляем карту узлов
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
atomic.AddInt64(&cm.stats.activeNodes, 1)
printSuccess("Узел %s (%s) присоединился к кластеру", nodeID, address)
// Если включена мастер-мастер репликация, отправляем текущее состояние новому узлу
if cm.replicationEnabled && cm.masterMaster {
go cm.sendInitialSync(nodeID)
}
// Если включён шардинг, обновляем распределение шардов
if cm.shardManager.enabled {
cm.rebalanceShards()
}
}
// sendInitialSync отправляет начальную синхронизацию новому узлу
func (cm *ClusterManager) sendInitialSync(targetNodeID string) {
// Получаем текущую карту узлов
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return
}
nodes := *(*map[string]*Node)(nodesPtr)
targetNode, exists := nodes[targetNodeID]
if !exists || targetNode.GetState() != StateOnline {
return
}
// TODO: Отправить текущее состояние базы данных новому узлу
printInfo("Отправка начальной синхронизации узлу %s", targetNodeID)
}
// handleNodeLeave обрабатывает отключение узла
func (cm *ClusterManager) handleNodeLeave(nodeID string) {
// Получаем текущую карту узлов
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return
}
oldNodes := *(*map[string]*Node)(nodesPtr)
// Создаём новую карту
newNodes := make(map[string]*Node)
for k, v := range oldNodes {
if k == nodeID {
v.SetState(StateOffline)
newNodes[k] = v
} else {
newNodes[k] = v
}
}
// Атомарно обновляем карту узлов
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
atomic.AddInt64(&cm.stats.activeNodes, -1)
printWarning("Узел %s покинул кластер", nodeID)
// Если включён шардинг, обновляем распределение шардов
if cm.shardManager.enabled {
cm.rebalanceShards()
}
}
// updateNodeHeartbeat обновляет время последнего heartbeat узла
func (cm *ClusterManager) updateNodeHeartbeat(nodeID, address string) {
// Получаем текущую карту узлов
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return
}
oldNodes := *(*map[string]*Node)(nodesPtr)
// Проверяем, нужно ли обновление
node, exists := oldNodes[nodeID]
if exists && node.GetState() == StateOnline && time.Since(node.LastSeen) < 5*time.Second {
// Обновление не требуется, просто обновляем LastSeen в существующей карте
node.LastSeen = time.Now()
return
}
// Создаём новую карту для обновления
newNodes := make(map[string]*Node)
for k, v := range oldNodes {
newNodes[k] = v
}
if exists {
node.LastSeen = time.Now()
if node.GetState() == StateOffline {
node.SetState(StateOnline)
atomic.AddInt64(&cm.stats.activeNodes, 1)
}
} else {
newNode := &Node{
ID: nodeID,
Address: address,
LastSeen: time.Now(),
}
newNode.SetState(StateOnline)
newNodes[nodeID] = newNode
atomic.AddInt64(&cm.stats.totalNodes, 1)
atomic.AddInt64(&cm.stats.activeNodes, 1)
}
// Атомарно обновляем карту узлов
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
}
// rebalanceShards выполняет ребалансировку шардов при изменении состава кластера
func (cm *ClusterManager) rebalanceShards() {
if !cm.shardManager.enabled || !cm.IsCoordinator() {
return
}
// Получаем текущие узлы
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return
}
nodes := *(*map[string]*Node)(nodesPtr)
// Собираем онлайн узлы
onlineNodes := make([]string, 0)
for id, node := range nodes {
if node.GetState() == StateOnline {
onlineNodes = append(onlineNodes, id)
}
}
// Здесь должна быть логика ребалансировки шардов
// В демо-версии просто перераспределяем существующие шарды
cm.shardManager.Rebalance()
atomic.AddInt64(&cm.stats.rebalancingCnt, 1)
}
// AddNode добавляет новый узел в кластер
func (cm *ClusterManager) AddNode(address string) error {
if !cm.IsCoordinator() {
return errors.New("только координатор может добавлять узлы")
}
// Получаем текущую карту узлов
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return errors.New("карта узлов не инициализирована")
}
oldNodes := *(*map[string]*Node)(nodesPtr)
// Генерируем ID для нового узла
nodeID := fmt.Sprintf("node-%d", len(oldNodes)+1)
// Создаём новую карту
newNodes := make(map[string]*Node)
for k, v := range oldNodes {
newNodes[k] = v
}
newNode := &Node{
ID: nodeID,
Address: address,
LastSeen: time.Now(),
}
newNode.SetState(StateJoining)
newNodes[nodeID] = newNode
// Атомарно обновляем карту узлов
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
atomic.AddInt64(&cm.stats.totalNodes, 1)
printSuccess("Узел %s (%s) добавлен в кластер", nodeID, address)
// Если включён шардинг, обновляем распределение
if cm.shardManager.enabled {
cm.rebalanceShards()
}
return nil
}
// RemoveNode удаляет узел из кластера
func (cm *ClusterManager) RemoveNode(nodeID string) error {
if !cm.IsCoordinator() {
return errors.New("только координатор может удалять узлы")
}
// Получаем текущую карту узлов
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return errors.New("карта узлов не инициализирована")
}
oldNodes := *(*map[string]*Node)(nodesPtr)
if _, exists := oldNodes[nodeID]; !exists {
return errors.New("узел не найден")
}
// Создаём новую карту без удаляемого узла
newNodes := make(map[string]*Node)
for k, v := range oldNodes {
if k != nodeID {
newNodes[k] = v
}
}
// Атомарно обновляем карту узлов
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
atomic.AddInt64(&cm.stats.totalNodes, -1)
printSuccess("Узел %s удален из кластера", nodeID)
// Если включён шардинг, обновляем распределение
if cm.shardManager.enabled {
cm.rebalanceShards()
}
return nil
}
// GetClusterStatus возвращает статус кластера
func (cm *ClusterManager) GetClusterStatus() map[string]interface{} {
status := make(map[string]interface{})
status["total_nodes"] = atomic.LoadInt64(&cm.stats.totalNodes)
status["active_nodes"] = atomic.LoadInt64(&cm.stats.activeNodes)
status["is_coordinator"] = cm.IsCoordinator()
status["coordinator"] = cm.coordinatorAddr
status["replication_enabled"] = cm.replicationEnabled
status["master_master"] = cm.masterMaster
// Добавляем информацию о шардинге
if cm.shardManager.enabled {
shardStats := cm.shardManager.GetShardStats()
status["sharding"] = shardStats
}
// Получаем текущую карту узлов атомарно
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr != nil {
nodes := *(*map[string]*Node)(nodesPtr)
nodesList := make([]map[string]interface{}, 0, len(nodes))
for id, node := range nodes {
nodesList = append(nodesList, map[string]interface{}{
"id": id,
"address": node.Address,
"state": node.GetState().String(),
"last_seen": node.LastSeen.Format(time.RFC3339),
})
}
status["nodes"] = nodesList
}
return status
}
// RebalanceCluster выполняет ребалансировку всего кластера
func (cm *ClusterManager) RebalanceCluster() error {
if !cm.IsCoordinator() {
return errors.New("only coordinator can rebalance the cluster")
}
printInfo("Starting cluster rebalance...")
// Получаем текущие узлы
nodesPtr := atomic.LoadPointer(&cm.nodes)
if nodesPtr == nil {
return errors.New("node map not initialized")
}
nodes := *(*map[string]*Node)(nodesPtr)
// Собираем онлайн узлы
onlineNodes := make([]string, 0)
for id, node := range nodes {
if node.GetState() == StateOnline {
onlineNodes = append(onlineNodes, id)
}
}
if len(onlineNodes) == 0 {
return errors.New("no online nodes available for rebalance")
}
// Если включён шардинг, ребалансируем шарды
if cm.shardManager.enabled {
err := cm.shardManager.Rebalance()
if err != nil {
return err
}
}
atomic.AddInt64(&cm.stats.rebalancingCnt, 1)
printSuccess("Cluster rebalance completed successfully")
return nil
}

View File

@ -0,0 +1,448 @@
// /futriis/internal/cluster/sharding.go
// Пакет cluster реализует шардинг данных для распределённого хранения
// Данный файл содержит реализацию менеджера шардинга, который управляет
// распределением данных по шардам с поддержкой различных стратегий (consistent hashing, range-based, hash-based) и обеспечивает ребалансировку)
package cluster
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"hash/crc32"
"sort"
"sync/atomic"
"time"
"futriis/pkg/utils"
)
// ShardingStrategy стратегия шардинга
type ShardingStrategy int
const (
ConsistentHashing ShardingStrategy = iota
RangeBased
HashBased
)
// Shard представляет шард данных
type Shard struct {
ID string
Nodes []string // ID узлов, содержащих этот шард
Range *KeyRange
DataSize int64
CreatedAt time.Time
stats struct {
reads int64
writes int64
rebalances int64
}
}
// KeyRange диапазон ключей для range-based шардинга
type KeyRange struct {
Start string
End string
}
// ShardManager управляет шардированием
type ShardManager struct {
strategy ShardingStrategy
shards map[string]*Shard
virtualNodes int // Количество виртуальных нод для consistent hashing
hashRing []uint32 // Хэш-кольцо
hashToShard map[uint32]string // Хэш -> ID шарда
nodeToShards map[string][]string // Узел -> список шардов
shardToNodes map[string][]string // Шард -> список узлов
replicationFactor int
enabled bool
stats struct {
totalShards int64
totalMoves int64
rebalances int64
}
}
// NewShardManager создаёт новый менеджер шардинга
func NewShardManager(strategy ShardingStrategy, replicationFactor int, enabled bool) *ShardManager {
sm := &ShardManager{
strategy: strategy,
shards: make(map[string]*Shard),
hashRing: make([]uint32, 0),
hashToShard: make(map[uint32]string),
nodeToShards: make(map[string][]string),
shardToNodes: make(map[string][]string),
virtualNodes: 100, // По умолчанию 100 виртуальных нод
replicationFactor: replicationFactor,
enabled: enabled,
}
if enabled {
utils.PrintInfo("Шардинг активирован, стратегия: %v, фактор репликации: %d",
sm.getStrategyName(), replicationFactor)
}
return sm
}
// getStrategyName возвращает название стратегии
func (sm *ShardManager) getStrategyName() string {
switch sm.strategy {
case ConsistentHashing:
return "consistent_hashing"
case RangeBased:
return "range_based"
case HashBased:
return "hash_based"
default:
return "unknown"
}
}
// CreateShards создаёт начальные шарды
func (sm *ShardManager) CreateShards(numShards int, nodes []string) error {
if !sm.enabled {
return nil
}
switch sm.strategy {
case ConsistentHashing:
return sm.createConsistentHashShards(numShards, nodes)
case RangeBased:
return sm.createRangeBasedShards(numShards, nodes)
case HashBased:
return sm.createHashBasedShards(numShards, nodes)
}
return nil
}
// createConsistentHashShards создаёт шарды для consistent hashing
func (sm *ShardManager) createConsistentHashShards(numShards int, nodes []string) error {
// Очищаем кольцо
sm.hashRing = make([]uint32, 0)
sm.hashToShard = make(map[uint32]string)
// Создаём шарды
for i := 0; i < numShards; i++ {
shardID := fmt.Sprintf("shard-%d", i+1)
shard := &Shard{
ID: shardID,
Nodes: make([]string, 0),
CreatedAt: time.Now(),
}
sm.shards[shardID] = shard
// Добавляем виртуальные ноды для шарда в хэш-кольцо
for j := 0; j < sm.virtualNodes; j++ {
vnodeKey := fmt.Sprintf("%s:%d", shardID, j)
hash := crc32.ChecksumIEEE([]byte(vnodeKey))
sm.hashRing = append(sm.hashRing, hash)
sm.hashToShard[hash] = shardID
}
}
// Сортируем кольцо
sort.Slice(sm.hashRing, func(i, j int) bool {
return sm.hashRing[i] < sm.hashRing[j]
})
// Распределяем шарды по узлам
sm.distributeShardsToNodes(nodes)
atomic.AddInt64(&sm.stats.totalShards, int64(numShards))
utils.PrintSuccess("Создано %d шардов с consistent hashing", numShards)
return nil
}
// createRangeBasedShards создаёт шарды на основе диапазонов
func (sm *ShardManager) createRangeBasedShards(numShards int, nodes []string) error {
// Разбиваем ключевое пространство на диапазоны
// Используем hex-строки от "00" до "ff" для простоты
totalRange := 256 // 0x00 - 0xff
rangeSize := totalRange / numShards
for i := 0; i < numShards; i++ {
start := fmt.Sprintf("%02x", i*rangeSize)
end := fmt.Sprintf("%02x", (i+1)*rangeSize-1)
if i == numShards-1 {
end = "ff"
}
shardID := fmt.Sprintf("range-shard-%d", i+1)
shard := &Shard{
ID: shardID,
Range: &KeyRange{
Start: start,
End: end,
},
Nodes: make([]string, 0),
CreatedAt: time.Now(),
}
sm.shards[shardID] = shard
}
// Распределяем шарды по узлам
sm.distributeShardsToNodes(nodes)
atomic.AddInt64(&sm.stats.totalShards, int64(numShards))
utils.PrintSuccess("Создано %d range-based шардов", numShards)
return nil
}
// createHashBasedShards создаёт шарды на основе хэша
func (sm *ShardManager) createHashBasedShards(numShards int, nodes []string) error {
for i := 0; i < numShards; i++ {
shardID := fmt.Sprintf("hash-shard-%d", i+1)
shard := &Shard{
ID: shardID,
Nodes: make([]string, 0),
CreatedAt: time.Now(),
}
sm.shards[shardID] = shard
}
// Распределяем шарды по узлам
sm.distributeShardsToNodes(nodes)
atomic.AddInt64(&sm.stats.totalShards, int64(numShards))
utils.PrintSuccess("Создано %d hash-based шардов", numShards)
return nil
}
// distributeShardsToNodes распределяет шарды по узлам
func (sm *ShardManager) distributeShardsToNodes(nodes []string) {
if len(nodes) == 0 {
return
}
nodeCount := len(nodes)
shardCount := len(sm.shards)
// Равномерно распределяем шарды по узлам
shardsPerNode := shardCount / nodeCount
remainder := shardCount % nodeCount
shardIndex := 0
shardIDs := make([]string, 0, shardCount)
for id := range sm.shards {
shardIDs = append(shardIDs, id)
}
for i, nodeID := range nodes {
numShardsForNode := shardsPerNode
if i < remainder {
numShardsForNode++
}
nodeShards := make([]string, 0)
for j := 0; j < numShardsForNode && shardIndex < len(shardIDs); j++ {
shardID := shardIDs[shardIndex]
// Добавляем узел к шарду
sm.addNodeToShard(shardID, nodeID)
nodeShards = append(nodeShards, shardID)
shardIndex++
}
sm.nodeToShards[nodeID] = nodeShards
}
}
// addNodeToShard добавляет узел к шарду
func (sm *ShardManager) addNodeToShard(shardID, nodeID string) {
shard, exists := sm.shards[shardID]
if !exists {
return
}
// Добавляем узел к шарду
shard.Nodes = append(shard.Nodes, nodeID)
// Обновляем маппинг
sm.shardToNodes[shardID] = append(sm.shardToNodes[shardID], nodeID)
// Обновляем маппинг узла к шардам
sm.nodeToShards[nodeID] = append(sm.nodeToShards[nodeID], shardID)
}
// GetShardForKey определяет шард для ключа
func (sm *ShardManager) GetShardForKey(key string) (*Shard, error) {
if !sm.enabled {
return nil, nil
}
switch sm.strategy {
case ConsistentHashing:
return sm.getShardConsistentHashing(key)
case RangeBased:
return sm.getShardRangeBased(key)
case HashBased:
return sm.getShardHashBased(key)
}
return nil, errors.New("неизвестная стратегия шардинга")
}
// getShardConsistentHashing получает шард через consistent hashing
func (sm *ShardManager) getShardConsistentHashing(key string) (*Shard, error) {
if len(sm.hashRing) == 0 {
return nil, errors.New("хэш-кольцо пусто")
}
hash := crc32.ChecksumIEEE([]byte(key))
// Бинарный поиск в кольце
idx := sort.Search(len(sm.hashRing), func(i int) bool {
return sm.hashRing[i] >= hash
})
if idx == len(sm.hashRing) {
idx = 0
}
shardID := sm.hashToShard[sm.hashRing[idx]]
shard, exists := sm.shards[shardID]
if !exists {
return nil, errors.New("шард не найден")
}
atomic.AddInt64(&shard.stats.reads, 1)
return shard, nil
}
// getShardRangeBased получает шард по диапазону
func (sm *ShardManager) getShardRangeBased(key string) (*Shard, error) {
// Вычисляем хэш ключа для определения диапазона
hash := md5.Sum([]byte(key))
keyHex := hex.EncodeToString(hash[:1]) // Используем первый байт
for _, shard := range sm.shards {
if shard.Range != nil {
if keyHex >= shard.Range.Start && keyHex <= shard.Range.End {
atomic.AddInt64(&shard.stats.reads, 1)
return shard, nil
}
}
}
return nil, errors.New("шард не найден для ключа")
}
// getShardHashBased получает шард по хэшу
func (sm *ShardManager) getShardHashBased(key string) (*Shard, error) {
hash := crc32.ChecksumIEEE([]byte(key))
shardIndex := int(hash) % len(sm.shards)
// Получаем отсортированный список ID шардов
shardIDs := make([]string, 0, len(sm.shards))
for id := range sm.shards {
shardIDs = append(shardIDs, id)
}
sort.Strings(shardIDs)
if shardIndex >= 0 && shardIndex < len(shardIDs) {
shardID := shardIDs[shardIndex]
shard, exists := sm.shards[shardID]
if exists {
atomic.AddInt64(&shard.stats.reads, 1)
return shard, nil
}
}
return nil, errors.New("шард не найден")
}
// GetShardByID возвращает шард по ID
func (sm *ShardManager) GetShardByID(shardID string) (*Shard, bool) {
shard, exists := sm.shards[shardID]
return shard, exists
}
// RecordWrite записывает статистику записи в шард
func (sm *ShardManager) RecordWrite(shardID string) {
if !sm.enabled {
return
}
shard, exists := sm.shards[shardID]
if exists {
atomic.AddInt64(&shard.stats.writes, 1)
}
}
// Rebalance выполняет ребалансировку шардов
func (sm *ShardManager) Rebalance() error {
if !sm.enabled {
return nil
}
utils.PrintInfo("Запуск ребалансировки шардов...")
atomic.AddInt64(&sm.stats.rebalances, 1)
// Здесь должна быть логика ребалансировки
// Перемещение шардов между узлами для равномерной загрузки
for _, shard := range sm.shards {
atomic.AddInt64(&shard.stats.rebalances, 1)
}
atomic.AddInt64(&sm.stats.totalMoves, int64(len(sm.shards)/2))
utils.PrintSuccess("Ребалансировка завершена")
return nil
}
// GetShardStats возвращает статистику шардов
func (sm *ShardManager) GetShardStats() map[string]interface{} {
stats := make(map[string]interface{})
stats["enabled"] = sm.enabled
stats["strategy"] = sm.getStrategyName()
stats["total_shards"] = atomic.LoadInt64(&sm.stats.totalShards)
stats["total_rebalances"] = atomic.LoadInt64(&sm.stats.rebalances)
stats["total_moves"] = atomic.LoadInt64(&sm.stats.totalMoves)
shardStats := make([]map[string]interface{}, 0)
for id, shard := range sm.shards {
sStats := map[string]interface{}{
"id": id,
"nodes": shard.Nodes,
"reads": atomic.LoadInt64(&shard.stats.reads),
"writes": atomic.LoadInt64(&shard.stats.writes),
"rebalances": atomic.LoadInt64(&shard.stats.rebalances),
"created": shard.CreatedAt.Format(time.RFC3339),
}
if shard.Range != nil {
sStats["range_start"] = shard.Range.Start
sStats["range_end"] = shard.Range.End
}
shardStats = append(shardStats, sStats)
}
stats["shards"] = shardStats
return stats
}

993
internal/engine/engine.go Normal file
View File

@ -0,0 +1,993 @@
// /futriis/internal/engine/engine.go
// Пакет engine реализует ядро СУБД Futriis, координирующее все операции.
// Выступает в роли центрального компонента, связывающего хранилище, кластерное управление, транзакции, Lua плагины и AOF (Append-Only File) для персистентности.
package engine
import (
"fmt"
"reflect"
"strings"
"futriis/internal/cluster"
"futriis/internal/lua"
"futriis/internal/replication"
"futriis/internal/storage"
"futriis/internal/transaction"
"futriis/pkg/config"
"futriis/pkg/types"
"futriis/pkg/utils"
)
// Engine представляет ядро СУБД
type Engine struct {
storage *storage.Storage
clusterMgr *cluster.ClusterManager
txMgr *transaction.TransactionManager
luaMgr *lua.PluginManager
aofMgr *replication.AOFManager
cfg *config.Config
aofRecovered bool // Флаг, было ли восстановление из AOF
}
// NewEngine создаёт новый экземпляр ядра СУБД
func NewEngine() *Engine {
cfg := config.Get()
// Создаём AOF менеджер
aofMgr, _ := replication.NewAOFManager(cfg.Node.AOFFile, cfg.Node.AOFEnabled)
// Воспроизводим AOF если нужно
aofRecovered := false
if aofMgr != nil && cfg.Node.AOFEnabled {
// Восстановление состояния из AOF
if err := replayAOF(aofMgr); err != nil {
utils.PrintError("Error recovering from AOF: %v", err)
} else {
aofRecovered = true
// Сообщение будет показано в handler.go
}
}
return &Engine{
storage: storage.NewStorage(),
clusterMgr: cluster.NewClusterManager(cfg),
txMgr: transaction.NewTransactionManager(),
luaMgr: lua.NewPluginManager(&cfg.Lua),
aofMgr: aofMgr,
cfg: cfg,
aofRecovered: aofRecovered,
}
}
// GetConfig возвращает конфигурацию
func (e *Engine) GetConfig() *config.Config {
return e.cfg
}
// WasAOFRecovered возвращает флаг восстановления из AOF
func (e *Engine) WasAOFRecovered() bool {
return e.aofRecovered
}
// replayAOF воспроизводит команды из AOF файла для восстановления состояния
func replayAOF(aofMgr *replication.AOFManager) error {
commands, err := aofMgr.ReadAll()
if err != nil {
return fmt.Errorf("failed to read AOF: %v", err)
}
// Создаём временное хранилище для восстановления
tempStorage := storage.NewStorage()
for i, cmd := range commands {
// Пропускаем команды транзакций при восстановлении
if cmd.Name == "begin" || cmd.Name == "commit" || cmd.Name == "rollback" {
continue
}
// Преобразуем аргументы в строки
args := make([]string, len(cmd.Args))
for j, arg := range cmd.Args {
if str, ok := arg.(string); ok {
args[j] = str
} else {
args[j] = fmt.Sprint(arg)
}
}
// Выполняем команду на временном хранилище
if err := executeRestoreCommand(tempStorage, cmd.Name, args); err != nil {
utils.PrintWarning("Error replaying command #%d (%s): %v", i+1, cmd.Name, err)
// Продолжаем восстановление, несмотря на ошибки
}
}
// TODO: Перенести восстановленные данные в основное хранилище
// Это упрощённая реализация, в реальности нужно синхронизировать состояния
return nil
}
// executeRestoreCommand выполняет команду при восстановлении из AOF
func executeRestoreCommand(storage *storage.Storage, cmdName string, args []string) error {
switch cmdName {
case "create":
if len(args) < 2 {
return nil
}
switch args[0] {
case "tapple":
if len(args) < 2 {
return nil
}
_, err := storage.GetTappleManager().CreateTapple(args[1])
return err
case "slice":
if len(args) < 3 {
return nil
}
tapple, err := storage.GetTappleManager().GetTapple(args[1])
if err != nil {
return err
}
_, err = storage.GetTappleManager().GetSliceManager().CreateSlice(tapple, args[2])
return err
case "tuple":
if len(args) < 4 {
return nil
}
tapple, err := storage.GetTappleManager().GetTapple(args[1])
if err != nil {
return err
}
slice, err := storage.GetTappleManager().GetSliceManager().GetSlice(tapple, args[2])
if err != nil {
return err
}
fields := make(map[string]interface{})
for i := 4; i < len(args); i++ {
parts := strings.SplitN(args[i], "=", 2)
if len(parts) == 2 {
fields[parts[0]] = parts[1]
}
}
_, err = storage.GetTappleManager().GetSliceManager().GetTupleManager().CreateTuple(slice, args[3], fields)
return err
}
case "update":
if len(args) < 4 || args[0] != "tuple" {
return nil
}
tapple, err := storage.GetTappleManager().GetTapple(args[1])
if err != nil {
return err
}
slice, err := storage.GetTappleManager().GetSliceManager().GetSlice(tapple, args[2])
if err != nil {
return err
}
fields := make(map[string]interface{})
for i := 4; i < len(args); i++ {
parts := strings.SplitN(args[i], "=", 2)
if len(parts) == 2 {
fields[parts[0]] = parts[1]
}
}
_, err = storage.GetTappleManager().GetSliceManager().GetTupleManager().UpdateTuple(slice, args[3], fields)
return err
case "delete":
if len(args) < 2 {
return nil
}
switch args[0] {
case "tapple":
if len(args) < 2 {
return nil
}
return storage.GetTappleManager().DeleteTapple(args[1])
case "slice":
if len(args) < 3 {
return nil
}
tapple, err := storage.GetTappleManager().GetTapple(args[1])
if err != nil {
return err
}
return storage.GetTappleManager().GetSliceManager().DeleteSlice(tapple, args[2])
case "tuple":
if len(args) < 4 {
return nil
}
tapple, err := storage.GetTappleManager().GetTapple(args[1])
if err != nil {
return err
}
slice, err := storage.GetTappleManager().GetSliceManager().GetSlice(tapple, args[2])
if err != nil {
return err
}
return storage.GetTappleManager().GetSliceManager().GetTupleManager().DeleteTuple(slice, args[3])
}
}
return nil
}
// Execute выполняет команду и возвращает результат
func (e *Engine) Execute(input string) (string, error) {
// Разбираем ввод
parts := strings.Fields(input)
if len(parts) == 0 {
return "", nil
}
command := strings.ToLower(parts[0])
args := parts[1:]
// Записываем команду в AOF (кроме команд транзакций и служебных команд)
if e.aofMgr != nil && command != "begin" && command != "commit" && command != "rollback" &&
command != "cluster.status" && command != "help" && command != "exit" && command != "quit" &&
command != "aof.recover" && command != "aof.info" &&
!strings.HasPrefix(command, "add.prime.index") && !strings.HasPrefix(command, "delete.prime.index") &&
!strings.HasPrefix(command, "add.secondary.index") && !strings.HasPrefix(command, "delete.secondary.index") &&
command != "cluster.rebalance" {
argsInterface := make([]interface{}, len(args))
for i, v := range args {
argsInterface[i] = v
}
e.aofMgr.Append(command, argsInterface)
}
// Обработка команд
switch command {
case "help":
return e.help(), nil
case "exit", "quit":
return "exit", nil
case "create":
return e.handleCreate(args)
case "delete":
return e.handleDelete(args)
case "update":
return e.handleUpdate(args)
case "list":
return e.handleList(args)
case "show":
return e.handleShow(args)
case "begin":
return e.handleBegin()
case "commit":
return e.handleCommit()
case "rollback":
return e.handleRollback()
case "cluster.status":
return e.handleClusterStatus()
case "cluster.rebalance":
return e.handleClusterRebalance(args)
case "add.node":
return e.handleAddNode(args)
case "evict.node":
return e.handleEvictNode(args)
case "lua":
return e.handleLua(args)
case "aof.recover":
return e.handleAOFRecover(args)
case "aof.info":
return e.handleAOFInfo()
case "add.prime.index":
return e.handleAddPrimaryIndex(args)
case "delete.prime.index":
return e.handleDeletePrimaryIndex(args)
case "add.secondary.index":
return e.handleAddSecondaryIndex(args)
case "delete.secondary.index":
return e.handleDeleteSecondaryIndex(args)
case "compression.stats":
return e.handleCompressionStats(args)
case "sharding.status":
return e.handleShardingStatus()
default:
return "", fmt.Errorf("unknown command: %s", command)
}
}
// handleAOFRecover восстанавливает данные из AOF файла
func (e *Engine) handleAOFRecover(args []string) (string, error) {
if e.aofMgr == nil {
return "", fmt.Errorf("AOF manager not initialized")
}
// Проверяем, указан ли путь к файлу
filePath := e.cfg.Node.AOFFile
if len(args) > 0 {
filePath = args[0]
}
// Создаём временный AOF менеджер для чтения указанного файла
tmpAOF, err := replication.NewAOFManager(filePath, true)
if err != nil {
return "", fmt.Errorf("failed to open AOF file: %v", err)
}
defer tmpAOF.Close()
// Читаем все команды
commands, err := tmpAOF.ReadAll()
if err != nil {
return "", fmt.Errorf("failed to read AOF file: %v", err)
}
if len(commands) == 0 {
return utils.ColorYellow + "AOF file is empty" + utils.ColorReset, nil
}
// Создаём временное хранилище для проверки
tempStorage := storage.NewStorage()
successCount := 0
errorCount := 0
for i, cmd := range commands {
if cmd.Name == "begin" || cmd.Name == "commit" || cmd.Name == "rollback" {
continue
}
args := make([]string, len(cmd.Args))
for j, arg := range cmd.Args {
if str, ok := arg.(string); ok {
args[j] = str
} else {
args[j] = fmt.Sprint(arg)
}
}
if err := executeRestoreCommand(tempStorage, cmd.Name, args); err != nil {
utils.PrintWarning("Error in command #%d: %v", i+1, err)
errorCount++
} else {
successCount++
}
}
return fmt.Sprintf(utils.ColorGreen+"Recovery completed. Successful: %d, Errors: %d"+utils.ColorReset,
successCount, errorCount), nil
}
// handleAOFInfo показывает информацию о AOF файле
func (e *Engine) handleAOFInfo() (string, error) {
if e.aofMgr == nil {
return "", fmt.Errorf("AOF manager not initialized")
}
// Получаем информацию о файле напрямую
filePath := e.cfg.Node.AOFFile
commands, err := e.aofMgr.ReadAll()
if err != nil {
return "", fmt.Errorf("failed to read AOF file: %v", err)
}
// Получаем размер файла
fileInfo, err := e.aofMgr.GetFileInfo()
if err != nil {
fileInfo = "unavailable"
}
result := utils.ColorCyan + "AOF Information:" + utils.ColorReset + "\n"
result += fmt.Sprintf(" File: %s\n", filePath)
result += fmt.Sprintf(" Size: %v\n", fileInfo)
result += fmt.Sprintf(" Commands: %d\n", len(commands))
if len(commands) > 0 {
lastCmd := commands[len(commands)-1]
result += fmt.Sprintf(" Last write: %d\n", lastCmd.Time)
} else {
result += " Last write: no records\n"
}
return result, nil
}
// handleClusterRebalance выполняет ребалансировку кластера
func (e *Engine) handleClusterRebalance(args []string) (string, error) {
clusterName := "futriis-cluster"
if len(args) > 0 {
clusterName = args[0]
}
err := e.clusterMgr.RebalanceCluster()
if err != nil {
return "", fmt.Errorf("cluster rebalance failed: %v", err)
}
return utils.ColorGreen + "Cluster '" + clusterName + "' rebalanced successfully" + utils.ColorReset, nil
}
// handleCreate обрабатывает команды создания
func (e *Engine) handleCreate(args []string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("insufficient arguments for create command")
}
switch args[0] {
case "tapple":
if len(args) < 2 {
return "", fmt.Errorf("tapple name not specified")
}
return e.createTapple(args[1])
case "slice":
if len(args) < 3 {
return "", fmt.Errorf("insufficient arguments for slice creation")
}
return e.createSlice(args[1], args[2])
case "tuple":
if len(args) < 4 {
return "", fmt.Errorf("insufficient arguments for tuple creation")
}
return e.createTuple(args[1], args[2], args[3], args[4:])
default:
return "", fmt.Errorf("unknown creation type: %s", args[0])
}
}
// handleDelete обрабатывает команды удаления
func (e *Engine) handleDelete(args []string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("insufficient arguments for delete command")
}
switch args[0] {
case "tapple":
if len(args) < 2 {
return "", fmt.Errorf("tapple name not specified")
}
return e.deleteTapple(args[1])
case "slice":
if len(args) < 3 {
return "", fmt.Errorf("insufficient arguments for slice deletion")
}
return e.deleteSlice(args[1], args[2])
case "tuple":
if len(args) < 4 {
return "", fmt.Errorf("insufficient arguments for tuple deletion")
}
return e.deleteTuple(args[1], args[2], args[3])
default:
return "", fmt.Errorf("unknown deletion type: %s", args[0])
}
}
// handleUpdate обрабатывает команды обновления
func (e *Engine) handleUpdate(args []string) (string, error) {
if len(args) < 4 || args[0] != "tuple" {
return "", fmt.Errorf("invalid update command")
}
return e.updateTuple(args[1], args[2], args[3], args[4:])
}
// handleList обрабатывает команды списка
func (e *Engine) handleList(args []string) (string, error) {
if len(args) < 1 {
return "", fmt.Errorf("insufficient arguments for list command")
}
switch args[0] {
case "tapples":
return e.listTapples()
case "slices":
if len(args) < 2 {
return "", fmt.Errorf("tapple name not specified")
}
return e.listSlices(args[1])
default:
return "", fmt.Errorf("unknown list type: %s", args[0])
}
}
// handleShow обрабатывает команды показа
func (e *Engine) handleShow(args []string) (string, error) {
if len(args) < 2 || args[0] != "tuples" {
return "", fmt.Errorf("invalid show command")
}
if len(args) < 3 {
return "", fmt.Errorf("insufficient arguments for show tuples")
}
return e.showTuples(args[1], args[2])
}
// handleBegin начинает транзакцию
func (e *Engine) handleBegin() (string, error) {
id, err := e.txMgr.Begin()
if err != nil {
return "", err
}
return utils.ColorGreen + "Transaction started. ID: " + id + utils.ColorReset, nil
}
// handleCommit фиксирует транзакцию
func (e *Engine) handleCommit() (string, error) {
err := e.txMgr.Commit()
if err != nil {
return "", err
}
return utils.ColorGreen + "Transaction committed" + utils.ColorReset, nil
}
// handleRollback откатывает транзакцию
func (e *Engine) handleRollback() (string, error) {
err := e.txMgr.Rollback()
if err != nil {
return "", err
}
return utils.ColorGreen + "Transaction rolled back" + utils.ColorReset, nil
}
// handleClusterStatus показывает статус кластера
func (e *Engine) handleClusterStatus() (string, error) {
status := e.clusterMgr.GetClusterStatus()
result := utils.ColorCyan + "Cluster Status:" + utils.ColorReset + "\n"
result += fmt.Sprintf(" Total nodes: %d\n", status["total_nodes"])
result += fmt.Sprintf(" Active nodes: %d\n", status["active_nodes"])
result += fmt.Sprintf(" Coordinator: %v\n", status["coordinator"])
result += fmt.Sprintf(" Master-master replication: %v\n", status["master_master"])
nodes, _ := status["nodes"].([]map[string]interface{})
if len(nodes) > 0 {
result += utils.ColorCyan + "\nCluster Nodes:" + utils.ColorReset + "\n"
for _, node := range nodes {
result += fmt.Sprintf(" %s (%s) - %s, last seen: %s\n",
node["id"], node["address"], node["state"], node["last_seen"])
}
}
return result, nil
}
// handleAddNode добавляет узел в кластер
func (e *Engine) handleAddNode(args []string) (string, error) {
if len(args) < 1 {
return "", fmt.Errorf("specify node address")
}
address := args[0]
err := e.clusterMgr.AddNode(address)
if err != nil {
return "", err
}
return utils.ColorGreen + "Node " + address + " added to cluster" + utils.ColorReset, nil
}
// handleEvictNode удаляет узел из кластера
func (e *Engine) handleEvictNode(args []string) (string, error) {
if len(args) < 1 {
return "", fmt.Errorf("specify node ID or address")
}
nodeID := args[0]
err := e.clusterMgr.RemoveNode(nodeID)
if err != nil {
return "", err
}
return utils.ColorGreen + "Node " + nodeID + " removed from cluster" + utils.ColorReset, nil
}
// handleLua выполняет Lua скрипт
func (e *Engine) handleLua(args []string) (string, error) {
if len(args) < 1 {
return "", fmt.Errorf("specify plugin name")
}
pluginName := args[0]
err := e.luaMgr.ExecutePlugin(pluginName)
if err != nil {
return "", err
}
return utils.ColorGreen + "Plugin executed" + utils.ColorReset, nil
}
// handleAddPrimaryIndex обрабатывает создание первичного индекса
func (e *Engine) handleAddPrimaryIndex(args []string) (string, error) {
if len(args) < 1 {
return "", fmt.Errorf("specify tapple name")
}
tappleName := args[0]
// Получаем таппл
_, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", fmt.Errorf("tapple not found: %v", err)
}
// Создаём первичный индекс
indexManager := e.storage.GetTappleManager().GetIndexManager()
err = indexManager.CreatePrimaryIndex(tappleName)
if err != nil {
return "", err
}
return utils.ColorGreen + "Primary index for tapple '" + tappleName + "' created successfully" + utils.ColorReset, nil
}
// handleDeletePrimaryIndex обрабатывает удаление первичного индекса
func (e *Engine) handleDeletePrimaryIndex(args []string) (string, error) {
if len(args) < 1 {
return "", fmt.Errorf("specify tapple name")
}
tappleName := args[0]
indexManager := e.storage.GetTappleManager().GetIndexManager()
err := indexManager.DeletePrimaryIndex(tappleName)
if err != nil {
return "", err
}
return utils.ColorGreen + "Primary index for tapple '" + tappleName + "' deleted successfully" + utils.ColorReset, nil
}
// handleAddSecondaryIndex обрабатывает создание вторичного индекса
func (e *Engine) handleAddSecondaryIndex(args []string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("specify tapple name and field name")
}
tappleName := args[0]
fieldName := args[1]
// Получаем таппл
_, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", fmt.Errorf("tapple not found: %v", err)
}
// Создаём вторичный индекс
indexManager := e.storage.GetTappleManager().GetIndexManager()
err = indexManager.CreateSecondaryIndex(tappleName, fieldName)
if err != nil {
return "", err
}
return utils.ColorGreen + "Secondary index for tapple '" + tappleName + "' on field '" + fieldName + "' created successfully" + utils.ColorReset, nil
}
// handleDeleteSecondaryIndex обрабатывает удаление вторичного индекса
func (e *Engine) handleDeleteSecondaryIndex(args []string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("specify tapple name and field name")
}
tappleName := args[0]
fieldName := args[1]
indexManager := e.storage.GetTappleManager().GetIndexManager()
err := indexManager.DeleteSecondaryIndex(tappleName, fieldName)
if err != nil {
return "", err
}
return utils.ColorGreen + "Secondary index for tapple '" + tappleName + "' on field '" + fieldName + "' deleted successfully" + utils.ColorReset, nil
}
// handleCompressionStats показывает статистику сжатия
func (e *Engine) handleCompressionStats(args []string) (string, error) {
result := utils.ColorCyan + "Compression Statistics:" + utils.ColorReset + "\n"
result += " Compression statistics available at slice level\n"
result += " Use 'show compression <tapple> <slice>' for detailed information"
return result, nil
}
// handleShardingStatus показывает статус шардинга
func (e *Engine) handleShardingStatus() (string, error) {
status := e.clusterMgr.GetClusterStatus()
shardingInfo, exists := status["sharding"]
if !exists {
return utils.ColorYellow + "Sharding is not activated" + utils.ColorReset, nil
}
shardStats := shardingInfo.(map[string]interface{})
result := utils.ColorCyan + "Sharding Status:" + utils.ColorReset + "\n"
result += fmt.Sprintf(" Enabled: %v\n", shardStats["enabled"])
result += fmt.Sprintf(" Strategy: %s\n", shardStats["strategy"])
result += fmt.Sprintf(" Total shards: %d\n", shardStats["total_shards"])
shards, _ := shardStats["shards"].([]map[string]interface{})
if len(shards) > 0 {
result += utils.ColorCyan + "\nShards:" + utils.ColorReset + "\n"
for _, shard := range shards {
result += fmt.Sprintf(" %s: nodes=%v, reads=%d, writes=%d\n",
shard["id"], shard["nodes"], shard["reads"], shard["writes"])
}
}
return result, nil
}
// Методы для работы с тапплами
func (e *Engine) createTapple(name string) (string, error) {
tapple, err := e.storage.GetTappleManager().CreateTapple(name)
if err != nil {
return "", err
}
return utils.ColorGreen + "Tapple '" + tapple.Name + "' created successfully" + utils.ColorReset, nil
}
func (e *Engine) deleteTapple(name string) (string, error) {
err := e.storage.GetTappleManager().DeleteTapple(name)
if err != nil {
return "", err
}
return utils.ColorGreen + "Tapple '" + name + "' deleted successfully" + utils.ColorReset, nil
}
func (e *Engine) listTapples() (string, error) {
tapples := e.storage.GetTappleManager().ListTapples()
if len(tapples) == 0 {
return utils.ColorYellow + "No tapples found" + utils.ColorReset, nil
}
result := utils.ColorCyan + "List of tapples:" + utils.ColorReset + "\n"
for _, t := range tapples {
result += " " + utils.ColorGreen + t + utils.ColorReset + "\n"
}
return result, nil
}
// Методы для работы со слайсами
func (e *Engine) createSlice(tappleName, sliceName string) (string, error) {
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", err
}
slice, err := e.storage.GetTappleManager().GetSliceManager().CreateSlice(tapple, sliceName)
if err != nil {
return "", err
}
return utils.ColorGreen + "Slice '" + slice.Name + "' in tapple '" + tappleName + "' created successfully" + utils.ColorReset, nil
}
func (e *Engine) deleteSlice(tappleName, sliceName string) (string, error) {
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", err
}
err = e.storage.GetTappleManager().GetSliceManager().DeleteSlice(tapple, sliceName)
if err != nil {
return "", err
}
return utils.ColorGreen + "Slice '" + sliceName + "' in tapple '" + tappleName + "' deleted successfully" + utils.ColorReset, nil
}
func (e *Engine) listSlices(tappleName string) (string, error) {
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", err
}
slices := e.storage.GetTappleManager().GetSliceManager().ListSlices(tapple)
if len(slices) == 0 {
return utils.ColorYellow + "No slices found in tapple '" + tappleName + "'" + utils.ColorReset, nil
}
result := utils.ColorCyan + "List of slices in tapple '" + tappleName + "':" + utils.ColorReset + "\n"
for _, s := range slices {
result += " " + utils.ColorGreen + s + utils.ColorReset + "\n"
}
return result, nil
}
// Методы для работы с кортежами
func (e *Engine) createTuple(tappleName, sliceName, tupleID string, fieldsArgs []string) (string, error) {
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", err
}
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
if err != nil {
return "", err
}
// Парсим поля
fields := make(map[string]interface{})
for _, arg := range fieldsArgs {
parts := strings.SplitN(arg, "=", 2)
if len(parts) == 2 {
fields[parts[0]] = parts[1]
}
}
tuple, err := e.storage.GetTappleManager().GetSliceManager().GetTupleManager().CreateTuple(slice, tupleID, fields)
if err != nil {
return "", err
}
return utils.ColorGreen + "Tuple '" + tuple.ID + "' in slice '" + sliceName + "' created successfully" + utils.ColorReset, nil
}
func (e *Engine) deleteTuple(tappleName, sliceName, tupleID string) (string, error) {
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", err
}
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
if err != nil {
return "", err
}
err = e.storage.GetTappleManager().GetSliceManager().GetTupleManager().DeleteTuple(slice, tupleID)
if err != nil {
return "", err
}
return utils.ColorGreen + "Tuple '" + tupleID + "' in slice '" + sliceName + "' deleted successfully" + utils.ColorReset, nil
}
func (e *Engine) updateTuple(tappleName, sliceName, tupleID string, fieldsArgs []string) (string, error) {
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", err
}
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
if err != nil {
return "", err
}
// Парсим поля
fields := make(map[string]interface{})
for _, arg := range fieldsArgs {
parts := strings.SplitN(arg, "=", 2)
if len(parts) == 2 {
fields[parts[0]] = parts[1]
}
}
tuple, err := e.storage.GetTappleManager().GetSliceManager().GetTupleManager().UpdateTuple(slice, tupleID, fields)
if err != nil {
return "", err
}
return utils.ColorGreen + "Tuple '" + tuple.ID + "' in slice '" + sliceName + "' updated successfully" + utils.ColorReset, nil
}
// showTuples показывает все кортежи в слайсе
func (e *Engine) showTuples(tappleName, sliceName string) (string, error) {
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
if err != nil {
return "", err
}
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
if err != nil {
return "", err
}
// Получаем все кортежи из слайса через рефлексию
tuples, err := e.getAllTuplesFromSlice(slice)
if err != nil {
return "", err
}
if len(tuples) == 0 {
return utils.ColorYellow + "No tuples found in slice '" + sliceName + "'" + utils.ColorReset, nil
}
result := utils.ColorCyan + "List of tuples in slice '" + sliceName + "':" + utils.ColorReset + "\n"
for id, tuple := range tuples {
result += " " + utils.ColorGreen + "ID: " + id + utils.ColorReset + "\n"
for k, v := range tuple.Fields {
result += " " + utils.ColorYellow + k + utils.ColorReset + ": " + utils.ColorPromptCode + fmt.Sprint(v) + utils.ColorReset + "\n"
}
result += "\n"
}
return result, nil
}
// вспомогательная функция для получения всех кортежей из слайса через рефлексию
func (e *Engine) getAllTuplesFromSlice(slice *types.Slice) (map[string]*types.Tuple, error) {
if slice == nil {
return nil, fmt.Errorf("slice is nil")
}
// Используем рефлексию для доступа к неэкспортируемому полю tuples
v := reflect.ValueOf(slice).Elem()
field := v.FieldByName("tuples")
if !field.IsValid() || field.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot access tuples field in slice")
}
result := make(map[string]*types.Tuple)
iter := field.MapRange()
for iter.Next() {
key := iter.Key().String()
value := iter.Value().Interface()
if tuple, ok := value.(*types.Tuple); ok {
result[key] = tuple
}
}
return result, nil
}
// help возвращает справку по командам
func (e *Engine) help() string {
help := utils.ColorCyan + "Available commands:" + utils.ColorReset + "\n"
help += "\n" + utils.ColorYellow + "Basic commands:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "create tapple <name>" + utils.ColorReset + " - create a new tapple (database)\n"
help += " " + utils.ColorGreen + "create slice <tapple> <name>" + utils.ColorReset + " - create a new slice (table)\n"
help += " " + utils.ColorGreen + "create tuple <tapple> <slice> <id> [key=value...]" + utils.ColorReset + " - create a new tuple (record)\n"
help += " " + utils.ColorGreen + "delete tapple <name>" + utils.ColorReset + " - delete a tapple\n"
help += " " + utils.ColorGreen + "delete slice <tapple> <name>" + utils.ColorReset + " - delete a slice\n"
help += " " + utils.ColorGreen + "delete tuple <tapple> <slice> <id>" + utils.ColorReset + " - delete a tuple\n"
help += " " + utils.ColorGreen + "update tuple <tapple> <slice> <id> [key=value...]" + utils.ColorReset + " - update a tuple\n"
help += " " + utils.ColorGreen + "list tapples" + utils.ColorReset + " - show all tapples\n"
help += " " + utils.ColorGreen + "list slices <tapple>" + utils.ColorReset + " - show all slices in a tapple\n"
help += " " + utils.ColorGreen + "show tuples <tapple> <slice>" + utils.ColorReset + " - show all tuples in a slice\n"
help += "\n" + utils.ColorYellow + "Index management:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "add.prime.index <tapple>" + utils.ColorReset + " - create primary index for tapple\n"
help += " " + utils.ColorGreen + "delete.prime.index <tapple>" + utils.ColorReset + " - delete primary index\n"
help += " " + utils.ColorGreen + "add.secondary.index <tapple> <field>" + utils.ColorReset + " - create secondary index on field\n"
help += " " + utils.ColorGreen + "delete.secondary.index <tapple> <field>" + utils.ColorReset + " - delete secondary index\n"
help += "\n" + utils.ColorYellow + "Transactions:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "begin" + utils.ColorReset + " - start a transaction\n"
help += " " + utils.ColorGreen + "commit" + utils.ColorReset + " - commit a transaction\n"
help += " " + utils.ColorGreen + "rollback" + utils.ColorReset + " - rollback a transaction\n"
help += "\n" + utils.ColorYellow + "Cluster and sharding management:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "cluster.status" + utils.ColorReset + " - show cluster status\n"
help += " " + utils.ColorGreen + "cluster.rebalance [cluster_name]" + utils.ColorReset + " - rebalance the cluster\n"
help += " " + utils.ColorGreen + "sharding.status" + utils.ColorReset + " - show sharding status\n"
help += " " + utils.ColorGreen + "add.node <address>" + utils.ColorReset + " - add a node to the cluster\n"
help += " " + utils.ColorGreen + "evict.node <node_id>" + utils.ColorReset + " - remove a node from the cluster\n"
help += "\n" + utils.ColorYellow + "Compression:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "compression.stats" + utils.ColorReset + " - show compression statistics\n"
help += "\n" + utils.ColorYellow + "AOF management:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "aof.recover [file]" + utils.ColorReset + " - recover data from AOF file\n"
help += " " + utils.ColorGreen + "aof.info" + utils.ColorReset + " - show AOF file information\n"
help += "\n" + utils.ColorYellow + "Lua plugins:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "lua <plugin_name>" + utils.ColorReset + " - execute Lua plugin\n"
help += "\n" + utils.ColorYellow + "Other:" + utils.ColorReset + "\n"
help += " " + utils.ColorGreen + "exit/quit" + utils.ColorReset + " - exit the DBMS\n"
return help
}

154
internal/lua/plugin.go Normal file
View File

@ -0,0 +1,154 @@
// /futriis/internal/lua/plugin.go
// Пакет lua реализует систему плагинов на языке Lua для расширения функциональности СУБД.
// PluginManager управляет загрузкой, хранением и выполнением Lua-скриптов из указанной директории.
// Предоставляет мост между Go и Lua через регистрацию функций, доступных из скриптов.
// Позволяет динамически расширять возможности базы данных без перекомпиляции основного кода.
// Поддерживает возможность отключения через конфигурацию для минимизации ресурсов.
package lua
import (
"fmt"
"io/ioutil"
"path/filepath"
"sync"
"futriis/pkg/config"
"futriis/pkg/utils"
"github.com/yuin/gopher-lua"
)
// PluginManager управляет Lua плагинами
type PluginManager struct {
state *lua.LState
plugins map[string]*lua.LFunction
mu sync.RWMutex
enabled bool
}
// NewPluginManager создаёт новый менеджер плагинов
func NewPluginManager(cfg *config.LuaConfig) *PluginManager {
pm := &PluginManager{
plugins: make(map[string]*lua.LFunction),
enabled: cfg.Enabled,
}
if cfg.Enabled {
pm.state = lua.NewState()
pm.registerFunctions()
}
return pm
}
// registerFunctions регистрирует функции Go, доступные из Lua
func (pm *PluginManager) registerFunctions() {
if !pm.enabled || pm.state == nil {
return
}
// Регистрируем функции для работы с данными
pm.state.SetGlobal("print", pm.state.NewFunction(func(L *lua.LState) int {
top := L.GetTop()
args := make([]interface{}, top)
for i := 1; i <= top; i++ {
args[i-1] = L.Get(i).String()
}
utils.PrintInfo(fmt.Sprint(args...))
return 0
}))
pm.state.SetGlobal("get", pm.state.NewFunction(func(L *lua.LState) int {
key := L.ToString(1)
// TODO: получить значение из хранилища
L.Push(lua.LString(key))
return 1
}))
pm.state.SetGlobal("set", pm.state.NewFunction(func(L *lua.LState) int {
key := L.ToString(1)
value := L.ToString(2)
// TODO: установить значение в хранилище
utils.PrintInfo("Lua set: %s = %s", key, value)
return 0
}))
}
// LoadPlugins загружает все Lua плагины из директории
func (pm *PluginManager) LoadPlugins(pluginsDir string) error {
if !pm.enabled || pm.state == nil {
return nil
}
files, err := ioutil.ReadDir(pluginsDir)
if err != nil {
return err
}
for _, file := range files {
if filepath.Ext(file.Name()) == ".lua" {
if err := pm.LoadPlugin(filepath.Join(pluginsDir, file.Name())); err != nil {
utils.PrintError("Ошибка загрузки плагина %s: %v", file.Name(), err)
}
}
}
return nil
}
// LoadPlugin загружает один Lua плагин
func (pm *PluginManager) LoadPlugin(path string) error {
if !pm.enabled || pm.state == nil {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
fn, err := pm.state.LoadString(string(data))
if err != nil {
return err
}
pm.mu.Lock()
pm.plugins[filepath.Base(path)] = fn
pm.mu.Unlock()
utils.PrintSuccess("Загружен плагин: %s", filepath.Base(path))
return nil
}
// ExecutePlugin выполняет функцию плагина
func (pm *PluginManager) ExecutePlugin(name string, args ...lua.LValue) error {
if !pm.enabled || pm.state == nil {
return nil
}
pm.mu.RLock()
fn, exists := pm.plugins[name]
pm.mu.RUnlock()
if !exists {
return fmt.Errorf("плагин %s не найден", name)
}
pm.state.Push(fn)
for _, arg := range args {
pm.state.Push(arg)
}
if err := pm.state.PCall(len(args), lua.MultRet, nil); err != nil {
return err
}
return nil
}
// Close закрывает Lua состояние
func (pm *PluginManager) Close() {
if pm.state != nil {
pm.state.Close()
}
}

View File

@ -0,0 +1,44 @@
// /futriis/internal/msgpack/deserializer.go
// Пакет msgpack расширяет функциональность десериализации для работы с динамическими типами.
// Deserializer предоставляет дополнительные методы для десериализации произвольных значений и map-структур из формата MessagePack.
// Используется когда точный тип данных неизвестен заранее, например, при обработке полей кортежей с различными типами значений.
// Интегрируется с основным сериализатором для полного цикла преобразований.
package msgpack
import (
"futriis/pkg/types"
"github.com/vmihailenco/msgpack/v5"
)
// Deserializer расширяет функциональность десериализации
type Deserializer struct {
serializer *Serializer
}
// NewDeserializer создаёт новый экземпляр десериализатора
func NewDeserializer() *Deserializer {
return &Deserializer{
serializer: NewSerializer(),
}
}
// DeserializeValue десериализует значение произвольного типа
func (d *Deserializer) DeserializeValue(data []byte) (interface{}, error) {
var value interface{}
err := msgpack.Unmarshal(data, &value)
if err != nil {
return nil, err
}
return value, nil
}
// DeserializeMap десериализует данные в карту
func (d *Deserializer) DeserializeMap(data []byte) (map[string]interface{}, error) {
var m map[string]interface{}
err := msgpack.Unmarshal(data, &m)
if err != nil {
return nil, err
}
return m, nil
}

View File

@ -0,0 +1,68 @@
// /futriis/internal/msgpack/serializer.go
// Пакет msgpack реализует сериализацию структур данных в формат MessagePack.
// Serializer предоставляет методы для преобразования кортежей, слайсов и тапплов в компактное бинарное представление и обратно.
// Использует стороннюю библиотеку msgpack/v5 для эффективной упаковки данных. Критически важен для сохранения и загрузки состояния базы данных,/
// А также для сетевого обмена данными между клиентом и сервером.
package msgpack
import (
"futriis/pkg/types"
"github.com/vmihailenco/msgpack/v5"
)
// Serializer предоставляет методы для сериализации данных
type Serializer struct {
enc *msgpack.Encoder
dec *msgpack.Decoder
}
// NewSerializer создаёт новый экземпляр сериализатора
func NewSerializer() *Serializer {
return &Serializer{}
}
// SerializeTuple сериализует кортеж в формат MessagePack
func (s *Serializer) SerializeTuple(tuple *types.Tuple) ([]byte, error) {
return msgpack.Marshal(tuple)
}
// DeserializeTuple десериализует кортеж из формата MessagePack
func (s *Serializer) DeserializeTuple(data []byte) (*types.Tuple, error) {
var tuple types.Tuple
err := msgpack.Unmarshal(data, &tuple)
if err != nil {
return nil, err
}
return &tuple, nil
}
// SerializeSlice сериализует слайс в формат MessagePack
func (s *Serializer) SerializeSlice(slice *types.Slice) ([]byte, error) {
return msgpack.Marshal(slice)
}
// DeserializeSlice десериализует слайс из формата MessagePack
func (s *Serializer) DeserializeSlice(data []byte) (*types.Slice, error) {
var slice types.Slice
err := msgpack.Unmarshal(data, &slice)
if err != nil {
return nil, err
}
return &slice, nil
}
// SerializeTapple сериализует таппл в формат MessagePack
func (s *Serializer) SerializeTapple(tapple *types.Tapple) ([]byte, error) {
return msgpack.Marshal(tapple)
}
// DeserializeTapple десериализует таппл из формата MessagePack
func (s *Serializer) DeserializeTapple(data []byte) (*types.Tapple, error) {
var tapple types.Tapple
err := msgpack.Unmarshal(data, &tapple)
if err != nil {
return nil, err
}
return &tapple, nil
}

244
internal/replication/aof.go Normal file
View File

@ -0,0 +1,244 @@
// /futriis/internal/replication/aof.go
// Пакет replication реализует Append-Only File данных
// AOF обеспечивает постоянную запись команд в лог-файл для восстановления состояния после перезапуска
// Поддерживает запись, чтение, синхронизацию и перезапись (rewrite) лог-файла
package replication
import (
"bufio"
"encoding/json"
"fmt"
"os"
"sync"
"time"
)
// AOFCommand представляет команду для записи в AOF
type AOFCommand struct {
Name string `json:"name"`
Args []interface{} `json:"args"`
Time int64 `json:"time"`
}
// AOFManager управляет Append-Only File
type AOFManager struct {
file *os.File
writer *bufio.Writer
enabled bool
filePath string
mu sync.Mutex
}
// NewAOFManager создаёт новый менеджер AOF
func NewAOFManager(filePath string, enabled bool) (*AOFManager, error) {
if !enabled {
return &AOFManager{
enabled: false,
}, nil
}
// Открываем файл для записи и чтения
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return nil, fmt.Errorf("не удалось открыть AOF файл: %v", err)
}
return &AOFManager{
file: file,
writer: bufio.NewWriter(file),
enabled: true,
filePath: filePath,
}, nil
}
// Append добавляет команду в AOF
func (aof *AOFManager) Append(name string, args []interface{}) error {
if !aof.enabled {
return nil
}
aof.mu.Lock()
defer aof.mu.Unlock()
cmd := AOFCommand{
Name: name,
Args: args,
Time: time.Now().Unix(),
}
data, err := json.Marshal(cmd)
if err != nil {
return fmt.Errorf("ошибка сериализации команды: %v", err)
}
// Записываем команду в файл
if _, err := aof.writer.Write(data); err != nil {
return fmt.Errorf("ошибка записи в AOF: %v", err)
}
if err := aof.writer.WriteByte('\n'); err != nil {
return fmt.Errorf("ошибка записи разделителя в AOF: %v", err)
}
// Сбрасываем буфер на диск
if err := aof.writer.Flush(); err != nil {
return fmt.Errorf("ошибка сброса AOF на диск: %v", err)
}
return nil
}
// ReadAll читает все команды из AOF файла
func (aof *AOFManager) ReadAll() ([]AOFCommand, error) {
if !aof.enabled {
return nil, fmt.Errorf("AOF отключён")
}
aof.mu.Lock()
defer aof.mu.Unlock()
// Сбрасываем буфер на диск перед чтением
if err := aof.writer.Flush(); err != nil {
return nil, fmt.Errorf("ошибка сброса AOF на диск: %v", err)
}
// Перемещаем указатель в начало файла
if _, err := aof.file.Seek(0, 0); err != nil {
return nil, fmt.Errorf("ошибка перемещения в начало AOF файла: %v", err)
}
var commands []AOFCommand
scanner := bufio.NewScanner(aof.file)
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 {
continue
}
var cmd AOFCommand
if err := json.Unmarshal(line, &cmd); err != nil {
// Пропускаем повреждённые записи
continue
}
commands = append(commands, cmd)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("ошибка чтения AOF файла: %v", err)
}
// Возвращаем указатель в конец файла для продолжения записи
if _, err := aof.file.Seek(0, 2); err != nil {
return nil, fmt.Errorf("ошибка перемещения в конец AOF файла: %v", err)
}
return commands, nil
}
// GetFileInfo возвращает информацию о файле
func (aof *AOFManager) GetFileInfo() (string, error) {
if !aof.enabled || aof.file == nil {
return "AOF отключён", nil
}
stat, err := aof.file.Stat()
if err != nil {
return "", err
}
return fmt.Sprintf("%d байт", stat.Size()), nil
}
// Close закрывает AOF файл
func (aof *AOFManager) Close() error {
if !aof.enabled || aof.file == nil {
return nil
}
aof.mu.Lock()
defer aof.mu.Unlock()
if err := aof.writer.Flush(); err != nil {
return err
}
return aof.file.Close()
}
// Sync принудительно синхронизирует AOF с диском
func (aof *AOFManager) Sync() error {
if !aof.enabled {
return nil
}
aof.mu.Lock()
defer aof.mu.Unlock()
if err := aof.writer.Flush(); err != nil {
return err
}
return aof.file.Sync()
}
// Rewrite выполняет перезапись AOF файла (упрощённая версия)
func (aof *AOFManager) Rewrite(commands []AOFCommand) error {
if !aof.enabled {
return nil
}
aof.mu.Lock()
defer aof.mu.Unlock()
// Создаём временный файл
tmpFile := aof.filePath + ".tmp"
file, err := os.Create(tmpFile)
if err != nil {
return fmt.Errorf("не удалось создать временный AOF файл: %v", err)
}
defer file.Close()
writer := bufio.NewWriter(file)
// Записываем все команды
for _, cmd := range commands {
data, err := json.Marshal(cmd)
if err != nil {
continue
}
if _, err := writer.Write(data); err != nil {
return err
}
if err := writer.WriteByte('\n'); err != nil {
return err
}
}
if err := writer.Flush(); err != nil {
return err
}
// Закрываем текущий файл
aof.file.Close()
// Заменяем старый файл новым
if err := os.Rename(tmpFile, aof.filePath); err != nil {
return err
}
// Открываем новый файл
file, err = os.OpenFile(aof.filePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return err
}
aof.file = file
aof.writer = bufio.NewWriter(file)
return nil
}

129
internal/server/server.go Normal file
View File

@ -0,0 +1,129 @@
// /futriis/internal/server/server.go
// Пакет server реализует TCP-сервер для клиент-серверной архитектуры СУБД.
// Server обрабатывает входящие соединения, принимает команды в текстовом или JSON формате, выполняет их через движок и возвращает результаты клиентам.
// Поддерживает множественные одновременные подключения, каждый обрабатывается в отдельной горутине.
// Обеспечивает корректное завершение работы с закрытием всех соединений и освобождением порта.
package server
import (
"bufio"
"encoding/json"
"net"
"sync"
"futriis/internal/engine"
"futriis/pkg/utils"
)
// Server представляет сервер СУБД
type Server struct {
address string
engine *engine.Engine
listener net.Listener
clients map[net.Conn]bool
mu sync.RWMutex
stopChan chan struct{}
}
// NewServer создаёт новый сервер
func NewServer(address string, engine *engine.Engine) *Server {
return &Server{
address: address,
engine: engine,
clients: make(map[net.Conn]bool),
stopChan: make(chan struct{}),
}
}
// Start запускает сервер
func (s *Server) Start() error {
listener, err := net.Listen("tcp", s.address)
if err != nil {
return err
}
s.listener = listener
utils.PrintSuccess("Сервер запущен на %s", s.address)
go s.acceptLoop()
return nil
}
// acceptLoop принимает входящие соединения
func (s *Server) acceptLoop() {
for {
select {
case <-s.stopChan:
return
default:
conn, err := s.listener.Accept()
if err != nil {
continue
}
s.mu.Lock()
s.clients[conn] = true
s.mu.Unlock()
go s.handleClient(conn)
}
}
}
// handleClient обрабатывает клиентское соединение
func (s *Server) handleClient(conn net.Conn) {
defer func() {
s.mu.Lock()
delete(s.clients, conn)
s.mu.Unlock()
conn.Close()
}()
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
line := scanner.Text()
// Парсим JSON запрос
var req map[string]interface{}
if err := json.Unmarshal([]byte(line), &req); err != nil {
// Если не JSON, обрабатываем как простую команду
result, err := s.engine.Execute(line)
s.sendResponse(conn, result, err)
} else {
// Обрабатываем JSON запрос
cmd, _ := req["command"].(string)
result, err := s.engine.Execute(cmd)
s.sendResponse(conn, result, err)
}
}
}
// sendResponse отправляет ответ клиенту
func (s *Server) sendResponse(conn net.Conn, result string, err error) {
response := make(map[string]interface{})
if err != nil {
response["error"] = err.Error()
} else {
response["result"] = result
}
data, _ := json.Marshal(response)
conn.Write(append(data, '\n'))
}
// Stop останавливает сервер
func (s *Server) Stop() {
close(s.stopChan)
if s.listener != nil {
s.listener.Close()
}
s.mu.Lock()
for conn := range s.clients {
conn.Close()
}
s.mu.Unlock()
}

View File

@ -0,0 +1,240 @@
// /futriis/internal/storage/compression.go
// Пакет storage реализует простейшее сжатие для колонок с одинаковыми типами данных
package storage
import (
"encoding/binary"
"math"
"strconv"
"sync/atomic"
)
// CompressionType тип сжатия
type CompressionType int
const (
NoCompression CompressionType = iota
RLECompression // Run-length encoding для повторяющихся значений
DeltaCompression // Дельта-сжатие для чисел
DictionaryCompression // Словарное сжатие для строк
)
// ColumnCompressor предоставляет сжатие для колонки
type ColumnCompressor struct {
colType string
compType CompressionType
stats struct {
originalSize int64
compressedSize int64
savings int64
}
}
// NewColumnCompressor создаёт новый компрессор для колонки
func NewColumnCompressor(colType string) *ColumnCompressor {
cc := &ColumnCompressor{
colType: colType,
compType: NoCompression,
}
// Автоматически выбираем тип сжатия на основе типа данных
switch colType {
case "int", "int64", "float64":
cc.compType = DeltaCompression
case "string":
cc.compType = DictionaryCompression
default:
cc.compType = RLECompression
}
return cc
}
// Compress сжимает данные
func (cc *ColumnCompressor) Compress(data []interface{}) ([]byte, error) {
var compressed []byte
var err error
switch cc.compType {
case RLECompression:
compressed, err = cc.rleCompress(data)
case DeltaCompression:
compressed, err = cc.deltaCompress(data)
case DictionaryCompression:
compressed, err = cc.dictionaryCompress(data)
default:
// Без сжатия - просто сериализуем
compressed, err = cc.noCompress(data)
}
if err != nil {
return nil, err
}
// Обновляем статистику
originalSize := int64(len(data) * 8) // Примерная оценка
compressedSize := int64(len(compressed))
atomic.AddInt64(&cc.stats.originalSize, originalSize)
atomic.AddInt64(&cc.stats.compressedSize, compressedSize)
atomic.AddInt64(&cc.stats.savings, originalSize-compressedSize)
return compressed, nil
}
// rleCompress реализует сжатие повторяющихся значений
func (cc *ColumnCompressor) rleCompress(data []interface{}) ([]byte, error) {
if len(data) == 0 {
return []byte{}, nil
}
result := make([]byte, 0)
current := data[0]
count := 1
for i := 1; i < len(data); i++ {
if data[i] == current {
count++
} else {
// Записываем значение и счётчик
result = append(result, []byte(encodeValue(current))...)
result = append(result, byte(count))
current = data[i]
count = 1
}
}
// Записываем последнее значение
result = append(result, []byte(encodeValue(current))...)
result = append(result, byte(count))
return result, nil
}
// deltaCompress реализует дельта-сжатие для чисел
func (cc *ColumnCompressor) deltaCompress(data []interface{}) ([]byte, error) {
if len(data) == 0 {
return []byte{}, nil
}
result := make([]byte, 8) // Первое значение храним полностью
// Преобразуем первое значение
first, ok := data[0].(float64)
if !ok {
if i, ok := data[0].(int); ok {
first = float64(i)
} else {
return cc.noCompress(data)
}
}
binary.LittleEndian.PutUint64(result, math.Float64bits(first))
// Для остальных храним дельты
for i := 1; i < len(data); i++ {
var curr float64
switch v := data[i].(type) {
case float64:
curr = v
case int:
curr = float64(v)
default:
return cc.noCompress(data)
}
prev, _ := data[i-1].(float64)
if iPrev, ok := data[i-1].(int); ok {
prev = float64(iPrev)
}
delta := int16(curr - prev)
deltaBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(deltaBytes, uint16(delta))
result = append(result, deltaBytes...)
}
return result, nil
}
// dictionaryCompress реализует словарное сжатие для строк
func (cc *ColumnCompressor) dictionaryCompress(data []interface{}) ([]byte, error) {
// Строим словарь уникальных значений
dict := make(map[string]byte)
values := make([]byte, len(data))
nextCode := byte(0)
for i, val := range data {
str, ok := val.(string)
if !ok {
return cc.noCompress(data)
}
code, exists := dict[str]
if !exists {
code = nextCode
dict[str] = code
nextCode++
}
values[i] = code
}
// Кодируем: сначала словарь, затем значения
result := make([]byte, 0)
// Записываем размер словаря
result = append(result, byte(len(dict)))
// Записываем словарь
for str, code := range dict {
result = append(result, code)
result = append(result, byte(len(str)))
result = append(result, []byte(str)...)
}
// Записываем значения
result = append(result, values...)
return result, nil
}
// noCompress без сжатия
func (cc *ColumnCompressor) noCompress(data []interface{}) ([]byte, error) {
result := make([]byte, 0)
for _, val := range data {
result = append(result, []byte(encodeValue(val))...)
}
return result, nil
}
// encodeValue кодирует значение в строку
func encodeValue(val interface{}) string {
switch v := val.(type) {
case string:
return v
case int:
return strconv.Itoa(v)
case int64:
return strconv.FormatInt(v, 10)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case bool:
return strconv.FormatBool(v)
default:
return ""
}
}
// GetStats возвращает статистику сжатия
func (cc *ColumnCompressor) GetStats() map[string]int64 {
return map[string]int64{
"original_size": atomic.LoadInt64(&cc.stats.originalSize),
"compressed_size": atomic.LoadInt64(&cc.stats.compressedSize),
"savings": atomic.LoadInt64(&cc.stats.savings),
}
}

231
internal/storage/index.go Normal file
View File

@ -0,0 +1,231 @@
// /futriis/internal/storage/index.go
// Пакет storage реализует систему индексов для ускорения доступа к данным
// Поддерживает первичные и вторичные индексы с wait-free операциями
package storage
import (
"errors"
"fmt"
"sync/atomic"
"time"
"unsafe"
"futriis/pkg/utils"
)
// IndexType тип индекса
type IndexType int
const (
PrimaryIndex IndexType = iota
SecondaryIndex
)
func (it IndexType) String() string {
switch it {
case PrimaryIndex:
return "primary"
case SecondaryIndex:
return "secondary"
default:
return "unknown"
}
}
// IndexEntry представляет запись в индексе
type IndexEntry struct {
Key string
Value unsafe.Pointer // Указатель на кортеж
Timestamp time.Time
}
// Index представляет структуру индекса
type Index struct {
Name string
Type IndexType
FieldName string // Для вторичных индексов - поле, по которому построен индекс
entries map[string]unsafe.Pointer
stats struct {
lookups int64
inserts int64
deletes int64
collisions int64
}
}
// NewPrimaryIndex создаёт новый первичный индекс
func NewPrimaryIndex(name string) *Index {
return &Index{
Name: name,
Type: PrimaryIndex,
entries: make(map[string]unsafe.Pointer),
}
}
// NewSecondaryIndex создаёт новый вторичный индекс
func NewSecondaryIndex(name, fieldName string) *Index {
return &Index{
Name: name,
Type: SecondaryIndex,
FieldName: fieldName,
entries: make(map[string]unsafe.Pointer),
}
}
// Insert добавляет запись в индекс (wait-free)
func (idx *Index) Insert(key string, tuplePtr unsafe.Pointer) {
// Атомарная операция записи в map не поддерживается напрямую в Go
// Используем атомарный указатель для значения, но сама map требует блокировки
// Для демонстрации wait-free подхода используем атомарные операции для статистики
atomic.AddInt64(&idx.stats.inserts, 1)
// Проверяем существование записи
if oldPtr, exists := idx.entries[key]; exists {
if oldPtr != tuplePtr {
atomic.AddInt64(&idx.stats.collisions, 1)
}
}
idx.entries[key] = tuplePtr
}
// Lookup выполняет поиск по индексу (wait-free для чтения)
func (idx *Index) Lookup(key string) (unsafe.Pointer, bool) {
atomic.AddInt64(&idx.stats.lookups, 1)
ptr, exists := idx.entries[key]
return ptr, exists
}
// Delete удаляет запись из индекса
func (idx *Index) Delete(key string) {
atomic.AddInt64(&idx.stats.deletes, 1)
delete(idx.entries, key)
}
// GetStats возвращает статистику индекса
func (idx *Index) GetStats() map[string]int64 {
return map[string]int64{
"lookups": atomic.LoadInt64(&idx.stats.lookups),
"inserts": atomic.LoadInt64(&idx.stats.inserts),
"deletes": atomic.LoadInt64(&idx.stats.deletes),
"collisions": atomic.LoadInt64(&idx.stats.collisions),
"size": int64(len(idx.entries)),
}
}
// IndexManager управляет индексами
type IndexManager struct {
primaryIndices map[string]*Index // Имя таппла -> первичный индекс
secondaryIndices map[string][]*Index // Имя таппла -> список вторичных индексов
stats struct {
totalIndices int64
}
}
// NewIndexManager создаёт новый менеджер индексов
func NewIndexManager() *IndexManager {
return &IndexManager{
primaryIndices: make(map[string]*Index),
secondaryIndices: make(map[string][]*Index),
}
}
// CreatePrimaryIndex создаёт первичный индекс для таппла
func (im *IndexManager) CreatePrimaryIndex(tappleName string) error {
if _, exists := im.primaryIndices[tappleName]; exists {
return errors.New("первичный индекс уже существует для данного таппла")
}
idx := NewPrimaryIndex(tappleName + "_primary")
im.primaryIndices[tappleName] = idx
atomic.AddInt64(&im.stats.totalIndices, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", fmt.Sprintf("Создан первичный индекс для таппла: %s", tappleName))
}
return nil
}
// DeletePrimaryIndex удаляет первичный индекс
func (im *IndexManager) DeletePrimaryIndex(tappleName string) error {
if _, exists := im.primaryIndices[tappleName]; !exists {
return errors.New("первичный индекс не найден")
}
delete(im.primaryIndices, tappleName)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", fmt.Sprintf("Удалён первичный индекс для таппла: %s", tappleName))
}
return nil
}
// CreateSecondaryIndex создаёт вторичный индекс
func (im *IndexManager) CreateSecondaryIndex(tappleName, fieldName string) error {
indices, exists := im.secondaryIndices[tappleName]
if !exists {
indices = make([]*Index, 0)
}
// Проверяем, нет ли уже индекса по этому полю
for _, idx := range indices {
if idx.FieldName == fieldName {
return errors.New("вторичный индекс для данного поля уже существует")
}
}
idx := NewSecondaryIndex(tappleName+"_secondary_"+fieldName, fieldName)
indices = append(indices, idx)
im.secondaryIndices[tappleName] = indices
atomic.AddInt64(&im.stats.totalIndices, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", fmt.Sprintf("Создан вторичный индекс для таппла %s по полю: %s", tappleName, fieldName))
}
return nil
}
// DeleteSecondaryIndex удаляет вторичный индекс
func (im *IndexManager) DeleteSecondaryIndex(tappleName, fieldName string) error {
indices, exists := im.secondaryIndices[tappleName]
if !exists {
return errors.New("вторичные индексы не найдены")
}
for i, idx := range indices {
if idx.FieldName == fieldName {
// Удаляем индекс
im.secondaryIndices[tappleName] = append(indices[:i], indices[i+1:]...)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", fmt.Sprintf("Удалён вторичный индекс для таппла %s по полю: %s", tappleName, fieldName))
}
return nil
}
}
return errors.New("вторичный индекс не найден")
}
// GetPrimaryIndex возвращает первичный индекс
func (im *IndexManager) GetPrimaryIndex(tappleName string) (*Index, bool) {
idx, exists := im.primaryIndices[tappleName]
return idx, exists
}
// GetSecondaryIndices возвращает все вторичные индексы для таппла
func (im *IndexManager) GetSecondaryIndices(tappleName string) ([]*Index, bool) {
indices, exists := im.secondaryIndices[tappleName]
return indices, exists
}

196
internal/storage/slice.go Normal file
View File

@ -0,0 +1,196 @@
// /futriis/internal/storage/slice.go
// Пакет storage реализует операции со слайсами (таблицами) - контейнерами для кортежей.
// SliceManager управляет созданием, получением, удалением и списком слайсов внутри тапплов.
// Интегрируется с TupleManager для операций с кортежами на более низком уровне.
// Обеспечивает wait-free доступ к слайсам через атомарные операции.
package storage
import (
"errors"
"reflect"
"sync"
"sync/atomic"
"time"
"futriis/pkg/types"
"futriis/pkg/utils"
)
// SliceManager управляет операциями со слайсами
type SliceManager struct {
tupleManager *TupleManager
stats struct {
created int64
deleted int64
}
// Добавляем собственную блокировку для управления доступом к тапплам
mu sync.RWMutex
}
// NewSliceManager создаёт новый менеджер слайсов
func NewSliceManager() *SliceManager {
return &SliceManager{
tupleManager: NewTupleManager(),
}
}
// вспомогательная функция для доступа к неэкспортируемому полю slices
func getSlicesFromTapple(tapple *types.Tapple) map[string]*types.Slice {
// Используем рефлексию для доступа к неэкспортируемому полю
v := reflect.ValueOf(tapple).Elem()
field := v.FieldByName("slices")
if field.IsValid() && field.Kind() == reflect.Map {
// Преобразуем в map[string]*types.Slice
result := make(map[string]*types.Slice)
iter := field.MapRange()
for iter.Next() {
key := iter.Key().String()
value := iter.Value().Interface()
if slice, ok := value.(*types.Slice); ok {
result[key] = slice
}
}
return result
}
return make(map[string]*types.Slice)
}
// вспомогательная функция для добавления слайса в неэкспортируемое поле slices
func addSliceToTapple(tapple *types.Tapple, name string, slice *types.Slice) error {
v := reflect.ValueOf(tapple).Elem()
field := v.FieldByName("slices")
if field.IsValid() && field.Kind() == reflect.Map {
// Создаём новую map, если она nil
if field.IsNil() {
newMap := reflect.MakeMap(reflect.TypeOf(map[string]*types.Slice{}))
field.Set(newMap)
}
// Устанавливаем значение
key := reflect.ValueOf(name)
value := reflect.ValueOf(slice)
field.SetMapIndex(key, value)
return nil
}
return errors.New("cannot access slices field in tapple")
}
// вспомогательная функция для удаления слайса из неэкспортируемого поля slices
func removeSliceFromTapple(tapple *types.Tapple, name string) error {
v := reflect.ValueOf(tapple).Elem()
field := v.FieldByName("slices")
if field.IsValid() && field.Kind() == reflect.Map {
key := reflect.ValueOf(name)
field.SetMapIndex(key, reflect.Value{})
return nil
}
return errors.New("cannot access slices field in tapple")
}
// CreateSlice создаёт новый слайс в указанном таппле с временной меткой
func (sm *SliceManager) CreateSlice(tapple *types.Tapple, name string) (*types.Slice, error) {
if tapple == nil {
return nil, errors.New("tapple is nil")
}
// Используем блокировку менеджера для доступа к тапплу
sm.mu.Lock()
defer sm.mu.Unlock()
// Получаем слайсы через рефлексию
slices := getSlicesFromTapple(tapple)
_, exists := slices[name]
if exists {
return nil, errors.New("slice already exists")
}
slice := types.NewSlice(name)
slice.CreatedAt = time.Now()
slice.UpdatedAt = time.Now()
// Добавляем слайс через рефлексию
err := addSliceToTapple(tapple, name, slice)
if err != nil {
return nil, err
}
atomic.AddInt64(&sm.stats.created, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", "Created slice: "+name+" at "+slice.CreatedAt.Format(time.RFC3339))
}
return slice, nil
}
// GetSlice возвращает слайс по имени
func (sm *SliceManager) GetSlice(tapple *types.Tapple, name string) (*types.Slice, error) {
if tapple == nil {
return nil, errors.New("tapple is nil")
}
sm.mu.RLock()
defer sm.mu.RUnlock()
slices := getSlicesFromTapple(tapple)
slice, exists := slices[name]
if !exists {
return nil, errors.New("slice not found")
}
return slice, nil
}
// DeleteSlice удаляет слайс
func (sm *SliceManager) DeleteSlice(tapple *types.Tapple, name string) error {
if tapple == nil {
return errors.New("tapple is nil")
}
sm.mu.Lock()
defer sm.mu.Unlock()
slices := getSlicesFromTapple(tapple)
_, exists := slices[name]
if !exists {
return errors.New("slice not found")
}
err := removeSliceFromTapple(tapple, name)
if err != nil {
return err
}
atomic.AddInt64(&sm.stats.deleted, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", "Deleted slice: "+name)
}
return nil
}
// ListSlices возвращает список всех слайсов в таппле
func (sm *SliceManager) ListSlices(tapple *types.Tapple) []string {
if tapple == nil {
return nil
}
sm.mu.RLock()
defer sm.mu.RUnlock()
slices := getSlicesFromTapple(tapple)
result := make([]string, 0, len(slices))
for name := range slices {
result = append(result, name)
}
return result
}
// GetTupleManager возвращает менеджер кортежей
func (sm *SliceManager) GetTupleManager() *TupleManager {
return sm.tupleManager
}

View File

@ -0,0 +1,39 @@
// /futriis/internal/storage/storage.go
// Пакет storage предоставляет единую точку доступа ко всем операциям с хранилищем данных.
// Структура Storage агрегирует TappleManager и служит фасадом для работы с тапплами, слайсами и кортежами.
// Предоставляет методы для выполнения команд и создания резервных копий всех данных.
// Является основным интерфейсом для взаимодействия движка СУБД с хранилищем, обеспечивая централизованное управление всеми компонентами хранения.
package storage
import (
"futriis/pkg/types"
)
// Storage представляет основное хранилище данных
type Storage struct {
tappleManager *TappleManager
}
// NewStorage создаёт новое хранилище
func NewStorage() *Storage {
return &Storage{
tappleManager: NewTappleManager(),
}
}
// GetTappleManager возвращает менеджер тапплов
func (s *Storage) GetTappleManager() *TappleManager {
return s.tappleManager
}
// ExecuteCommand выполняет команду над хранилищем
func (s *Storage) ExecuteCommand(cmd string, args []string) (interface{}, error) {
// Будет расширяться по мере добавления команд
return nil, nil
}
// Backup создаёт резервную копию всех данных
func (s *Storage) Backup() map[string]*types.Tapple {
return s.tappleManager.GetAllTapples()
}

156
internal/storage/tapple.go Normal file
View File

@ -0,0 +1,156 @@
// /futriis/internal/storage/tapple.go
// Пакет storage реализует операции с тапплами (базами данных) - контейнерами верхнего уровня.
// TappleManager управляет созданием, удалением и получением тапплов, каждый из которых содержит коллекцию слайсов (таблиц).
// Интегрируется с SliceManager для операций со слайсами.
// Обеспечивает wait-free хранение тапплов в памяти с использованием атомарных указателей.
package storage
import (
"errors"
"sync/atomic"
"time"
"unsafe"
"futriis/pkg/types"
"futriis/pkg/utils"
)
// TappleManager управляет операциями с тапплами (wait-free)
type TappleManager struct {
tapples unsafe.Pointer // Атомарный указатель на map[string]*types.Tapple
sliceManager *SliceManager
indexManager *IndexManager
stats struct {
created int64
deleted int64
}
}
// NewTappleManager создаёт новый менеджер тапплов
func NewTappleManager() *TappleManager {
// Инициализируем пустую карту
tapples := make(map[string]*types.Tapple)
return &TappleManager{
tapples: unsafe.Pointer(&tapples),
sliceManager: NewSliceManager(),
indexManager: NewIndexManager(),
}
}
// CreateTapple создаёт новый таппл с временной меткой
func (tm *TappleManager) CreateTapple(name string) (*types.Tapple, error) {
// Получаем текущую карту
oldPtr := atomic.LoadPointer(&tm.tapples)
oldTapples := *(*map[string]*types.Tapple)(oldPtr)
// Проверяем существование
if _, exists := oldTapples[name]; exists {
return nil, errors.New("tapple already exists")
}
// Создаём новый таппл с временной меткой
tapple := types.NewTapple(name)
tapple.CreatedAt = time.Now()
tapple.UpdatedAt = time.Now()
// Создаём новую карту
newTapples := make(map[string]*types.Tapple)
for k, v := range oldTapples {
newTapples[k] = v
}
newTapples[name] = tapple
// Атомарно обновляем указатель
atomic.StorePointer(&tm.tapples, unsafe.Pointer(&newTapples))
atomic.AddInt64(&tm.stats.created, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", "Created tapple: "+name+" at "+tapple.CreatedAt.Format(time.RFC3339))
}
return tapple, nil
}
// GetTapple возвращает таппл по имени (wait-free)
func (tm *TappleManager) GetTapple(name string) (*types.Tapple, error) {
// Атомарно загружаем указатель
ptr := atomic.LoadPointer(&tm.tapples)
tapples := *(*map[string]*types.Tapple)(ptr)
tapple, exists := tapples[name]
if !exists {
return nil, errors.New("tapple not found")
}
return tapple, nil
}
// GetAllTapples возвращает копию всех тапплов (для Backup)
func (tm *TappleManager) GetAllTapples() map[string]*types.Tapple {
ptr := atomic.LoadPointer(&tm.tapples)
tapples := *(*map[string]*types.Tapple)(ptr)
// Создаём копию для безопасного использования
result := make(map[string]*types.Tapple)
for k, v := range tapples {
result[k] = v
}
return result
}
// DeleteTapple удаляет таппл
func (tm *TappleManager) DeleteTapple(name string) error {
// Получаем текущую карту
oldPtr := atomic.LoadPointer(&tm.tapples)
oldTapples := *(*map[string]*types.Tapple)(oldPtr)
// Проверяем существование
if _, exists := oldTapples[name]; !exists {
return errors.New("tapple not found")
}
// Создаём новую карту без удаляемого таппла
newTapples := make(map[string]*types.Tapple)
for k, v := range oldTapples {
if k != name {
newTapples[k] = v
}
}
// Атомарно обновляем указатель
atomic.StorePointer(&tm.tapples, unsafe.Pointer(&newTapples))
atomic.AddInt64(&tm.stats.deleted, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", "Deleted tapple: "+name)
}
return nil
}
// ListTapples возвращает список всех тапплов (wait-free)
func (tm *TappleManager) ListTapples() []string {
ptr := atomic.LoadPointer(&tm.tapples)
tapples := *(*map[string]*types.Tapple)(ptr)
result := make([]string, 0, len(tapples))
for name := range tapples {
result = append(result, name)
}
return result
}
// GetSliceManager возвращает менеджер слайсов
func (tm *TappleManager) GetSliceManager() *SliceManager {
return tm.sliceManager
}
// GetIndexManager возвращает менеджер индексов
func (tm *TappleManager) GetIndexManager() *IndexManager {
return tm.indexManager
}

288
internal/storage/tuple.go Normal file
View File

@ -0,0 +1,288 @@
// /futriis/internal/storage/tuple.go
// Пакет storage реализует операции с кортежами (записями) - базовыми единицами данных.
// TupleManager предоставляет wait-free операции создания, чтения, обновления и удаления кортежей
// с использованием атомарных счётчиков для статистики и поддержкой индексов.
package storage
import (
"errors"
"fmt"
"reflect"
"sync"
"sync/atomic"
"time"
"unsafe"
"futriis/pkg/types"
"futriis/pkg/utils"
)
// TupleManager управляет операциями с кортежами
type TupleManager struct {
stats struct {
created int64
updated int64
deleted int64
read int64
}
columnCompressors map[string]*ColumnCompressor // Имя колонки -> компрессор
// Добавляем мьютекс для синхронизации доступа к кортежам
mu sync.RWMutex
}
// NewTupleManager создаёт новый менеджер кортежей
func NewTupleManager() *TupleManager {
return &TupleManager{
columnCompressors: make(map[string]*ColumnCompressor),
}
}
// вспомогательная функция для доступа к неэкспортируемому полю tuples в Slice
func getTuplesFromSlice(slice *types.Slice) map[string]*types.Tuple {
v := reflect.ValueOf(slice).Elem()
field := v.FieldByName("tuples")
if field.IsValid() && field.Kind() == reflect.Map {
result := make(map[string]*types.Tuple)
iter := field.MapRange()
for iter.Next() {
key := iter.Key().String()
value := iter.Value().Interface()
if tuple, ok := value.(*types.Tuple); ok {
result[key] = tuple
}
}
return result
}
return make(map[string]*types.Tuple)
}
// вспомогательная функция для добавления кортежа в неэкспортируемое поле tuples
func addTupleToSlice(slice *types.Slice, id string, tuple *types.Tuple) error {
v := reflect.ValueOf(slice).Elem()
field := v.FieldByName("tuples")
if field.IsValid() && field.Kind() == reflect.Map {
if field.IsNil() {
newMap := reflect.MakeMap(reflect.TypeOf(map[string]*types.Tuple{}))
field.Set(newMap)
}
key := reflect.ValueOf(id)
value := reflect.ValueOf(tuple)
field.SetMapIndex(key, value)
return nil
}
return errors.New("cannot access tuples field in slice")
}
// вспомогательная функция для удаления кортежа из неэкспортируемого поля tuples
func removeTupleFromSlice(slice *types.Slice, id string) error {
v := reflect.ValueOf(slice).Elem()
field := v.FieldByName("tuples")
if field.IsValid() && field.Kind() == reflect.Map {
key := reflect.ValueOf(id)
field.SetMapIndex(key, reflect.Value{})
return nil
}
return errors.New("cannot access tuples field in slice")
}
// вспомогательная функция для проверки существования кортежа
func tupleExistsInSlice(slice *types.Slice, id string) bool {
tuples := getTuplesFromSlice(slice)
_, exists := tuples[id]
return exists
}
// CreateTuple создаёт новый кортеж в указанном слайсе с временной меткой
// Wait-free операция: использует атомарные операции для счётчиков
func (tm *TupleManager) CreateTuple(slice *types.Slice, id string, fields map[string]interface{}) (*types.Tuple, error) {
if slice == nil {
return nil, errors.New("slice is nil")
}
tm.mu.Lock()
defer tm.mu.Unlock()
// Проверяем существование кортежа
if tupleExistsInSlice(slice, id) {
return nil, errors.New("tuple already exists")
}
// Создаём новый кортеж с временной меткой
tuple := types.NewTuple(id)
tuple.CreatedAt = time.Now()
tuple.UpdatedAt = time.Now()
for k, v := range fields {
tuple.Fields[k] = v
// Создаём компрессор для колонки, если его нет
if _, ok := tm.columnCompressors[k]; !ok {
colType := getFieldType(v)
tm.columnCompressors[k] = NewColumnCompressor(colType)
}
}
// Добавляем в слайс
err := addTupleToSlice(slice, id, tuple)
if err != nil {
return nil, err
}
// Обновляем индексы, если они есть
// Получаем доступ к индексам через таппл
// В реальном коде здесь должна быть ссылка на IndexManager
atomic.AddInt64(&tm.stats.created, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", fmt.Sprintf("Created tuple: %s at %s", id, tuple.CreatedAt.Format(time.RFC3339)))
}
return tuple, nil
}
// ReadTuple читает кортеж по ID
// Wait-free операция для чтения
func (tm *TupleManager) ReadTuple(slice *types.Slice, id string) (*types.Tuple, error) {
if slice == nil {
return nil, errors.New("slice is nil")
}
tm.mu.RLock()
defer tm.mu.RUnlock()
tuples := getTuplesFromSlice(slice)
tuple, exists := tuples[id]
if !exists {
return nil, errors.New("tuple not found")
}
atomic.AddInt64(&tm.stats.read, 1)
return tuple, nil
}
// UpdateTuple обновляет поля кортежа с обновлением временной метки
func (tm *TupleManager) UpdateTuple(slice *types.Slice, id string, fields map[string]interface{}) (*types.Tuple, error) {
if slice == nil {
return nil, errors.New("slice is nil")
}
tm.mu.Lock()
defer tm.mu.Unlock()
tuples := getTuplesFromSlice(slice)
tuple, exists := tuples[id]
if !exists {
return nil, errors.New("tuple not found")
}
// Обновляем поля
for k, v := range fields {
tuple.Fields[k] = v
}
// Обновляем временную метку
tuple.UpdatedAt = time.Now()
atomic.AddInt64(&tm.stats.updated, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", fmt.Sprintf("Updated tuple: %s at %s", id, tuple.UpdatedAt.Format(time.RFC3339)))
}
return tuple, nil
}
// DeleteTuple удаляет кортеж
func (tm *TupleManager) DeleteTuple(slice *types.Slice, id string) error {
if slice == nil {
return errors.New("slice is nil")
}
tm.mu.Lock()
defer tm.mu.Unlock()
if !tupleExistsInSlice(slice, id) {
return errors.New("tuple not found")
}
err := removeTupleFromSlice(slice, id)
if err != nil {
return err
}
atomic.AddInt64(&tm.stats.deleted, 1)
logger := utils.GetLogger()
if logger != nil {
logger.Log("INFO", "Deleted tuple: "+id)
}
return nil
}
// FindTuplesByIndex выполняет поиск кортежей по индексу
func (tm *TupleManager) FindTuplesByIndex(index *Index, key string) ([]unsafe.Pointer, error) {
if index == nil {
return nil, errors.New("index is nil")
}
ptr, exists := index.Lookup(key)
if !exists {
return nil, nil
}
return []unsafe.Pointer{ptr}, nil
}
// CompressColumn сжимает данные колонки
func (tm *TupleManager) CompressColumn(columnName string, data []interface{}) ([]byte, error) {
compressor, exists := tm.columnCompressors[columnName]
if !exists {
// Создаём компрессор по умолчанию
compressor = NewColumnCompressor("unknown")
tm.columnCompressors[columnName] = compressor
}
return compressor.Compress(data)
}
// GetCompressionStats возвращает статистику сжатия
func (tm *TupleManager) GetCompressionStats() map[string]interface{} {
stats := make(map[string]interface{})
for colName, compressor := range tm.columnCompressors {
stats[colName] = compressor.GetStats()
}
return stats
}
// getFieldType определяет тип поля
func getFieldType(v interface{}) string {
switch v.(type) {
case int, int64, int32:
return "int"
case float32, float64:
return "float64"
case string:
return "string"
case bool:
return "bool"
default:
return "unknown"
}
}
// GetStats возвращает статистику операций
func (tm *TupleManager) GetStats() map[string]int64 {
return map[string]int64{
"created": atomic.LoadInt64(&tm.stats.created),
"updated": atomic.LoadInt64(&tm.stats.updated),
"deleted": atomic.LoadInt64(&tm.stats.deleted),
"read": atomic.LoadInt64(&tm.stats.read),
}
}

205
internal/transaction/tx.go Normal file
View File

@ -0,0 +1,205 @@
// /futriis/internal/transaction/tx.go
// Пакет transaction реализует механизм простых транзакций (не ACID) для операций с данными.
// Предоставляет структуры для хранения состояния транзакций и управления их жизненным циклом.
// Использует wait-free операции через атомарные указатели.
package transaction
import (
"errors"
"sync/atomic"
"time"
"unsafe"
"futriis/pkg/types"
)
// TxState состояние транзакции
type TxState int32
const (
TxStateActive TxState = iota
TxStateCommited
TxStateRolledBack
)
// String возвращает строковое представление состояния
func (s TxState) String() string {
switch s {
case TxStateActive:
return "active"
case TxStateCommited:
return "committed"
case TxStateRolledBack:
return "rolled_back"
default:
return "unknown"
}
}
// Operation представляет операцию в транзакции
type Operation struct {
Type string // create, update, delete
Tapple string
Slice string
Key string
Value interface{}
OldValue interface{}
Timestamp time.Time
}
// Transaction представляет транзакцию с wait-free состоянием
type Transaction struct {
ID string
state int32 // Атомарное состояние
Operations []Operation
CreatedAt time.Time
}
// NewTransaction создаёт новую транзакцию
func NewTransaction(id string) *Transaction {
return &Transaction{
ID: id,
state: int32(TxStateActive),
Operations: make([]Operation, 0),
CreatedAt: time.Now(),
}
}
// GetState атомарно получает состояние транзакции
func (tx *Transaction) GetState() TxState {
return TxState(atomic.LoadInt32(&tx.state))
}
// SetState атомарно устанавливает состояние транзакции
func (tx *Transaction) SetState(state TxState) {
atomic.StoreInt32(&tx.state, int32(state))
}
// AddOperation добавляет операцию в транзакцию
func (tx *Transaction) AddOperation(op Operation) {
tx.Operations = append(tx.Operations, op)
}
// TransactionManager управляет транзакциями с wait-free операциями
type TransactionManager struct {
transactions unsafe.Pointer // Атомарный указатель на map[string]*Transaction
currentTx unsafe.Pointer // Атомарный указатель на текущую транзакцию
}
// NewTransactionManager создаёт новый менеджер транзакций
func NewTransactionManager() *TransactionManager {
transactions := make(map[string]*Transaction)
return &TransactionManager{
transactions: unsafe.Pointer(&transactions),
currentTx: nil,
}
}
// Begin начинает новую транзакцию
func (tm *TransactionManager) Begin() (string, error) {
// Проверяем, есть ли активная транзакция
currentPtr := atomic.LoadPointer(&tm.currentTx)
if currentPtr != nil {
currentTx := (*Transaction)(currentPtr)
if currentTx.GetState() == TxStateActive {
return "", errors.New("транзакция уже активна")
}
}
id := generateTxID()
tx := NewTransaction(id)
// Атомарно устанавливаем текущую транзакцию
atomic.StorePointer(&tm.currentTx, unsafe.Pointer(tx))
// Добавляем в список транзакций
oldPtr := atomic.LoadPointer(&tm.transactions)
oldTxns := *(*map[string]*Transaction)(oldPtr)
newTxns := make(map[string]*Transaction)
for k, v := range oldTxns {
newTxns[k] = v
}
newTxns[id] = tx
atomic.StorePointer(&tm.transactions, unsafe.Pointer(&newTxns))
return id, nil
}
// Commit фиксирует текущую транзакцию
func (tm *TransactionManager) Commit() error {
currentPtr := atomic.LoadPointer(&tm.currentTx)
if currentPtr == nil {
return errors.New("нет активной транзакции")
}
tx := (*Transaction)(currentPtr)
if tx.GetState() != TxStateActive {
return errors.New("транзакция не активна")
}
tx.SetState(TxStateCommited)
atomic.StorePointer(&tm.currentTx, nil)
return nil
}
// Rollback откатывает текущую транзакцию
func (tm *TransactionManager) Rollback() error {
currentPtr := atomic.LoadPointer(&tm.currentTx)
if currentPtr == nil {
return errors.New("нет активной транзакции")
}
tx := (*Transaction)(currentPtr)
if tx.GetState() != TxStateActive {
return errors.New("транзакция не активна")
}
tx.SetState(TxStateRolledBack)
atomic.StorePointer(&tm.currentTx, nil)
return nil
}
// AddOperation добавляет операцию в текущую транзакцию
func (tm *TransactionManager) AddOperation(op Operation) error {
currentPtr := atomic.LoadPointer(&tm.currentTx)
if currentPtr == nil {
return errors.New("нет активной транзакции")
}
tx := (*Transaction)(currentPtr)
if tx.GetState() != TxStateActive {
return errors.New("транзакция не активна")
}
op.Timestamp = time.Now()
tx.AddOperation(op)
return nil
}
// GetCurrentTx возвращает текущую транзакцию (wait-free)
func (tm *TransactionManager) GetCurrentTx() *Transaction {
currentPtr := atomic.LoadPointer(&tm.currentTx)
if currentPtr == nil {
return nil
}
return (*Transaction)(currentPtr)
}
// GetTransaction возвращает транзакцию по ID (wait-free)
func (tm *TransactionManager) GetTransaction(id string) (*Transaction, bool) {
ptr := atomic.LoadPointer(&tm.transactions)
txns := *(*map[string]*Transaction)(ptr)
tx, exists := txns[id]
return tx, exists
}
// generateTxID генерирует ID транзакции
func generateTxID() string {
return "tx-" + types.GenerateID() + "-" + time.Now().Format("20060102150405")
}

142
pkg/config/config.go Normal file
View File

@ -0,0 +1,142 @@
// /futriis/pkg/config/config.go
// Пакет config предоставляет функциональность для загрузки и управления конфигурацией СУБД Futriis.
// Он определяет структуры конфигурации для кластера, узла, хранилища, репликации и Lua плагинов.
// Пакет поддерживает загрузку из TOML файлов, установку значений по умолчанию и глобальный доступ
// к конфигурации через атомарные операции для потокобезопасности.
package config
import (
"sync/atomic"
"github.com/BurntSushi/toml"
)
// Цветовые коды ANSI (скопированы из utils для избежания циклического импорта)
const (
ColorReset = "\033[0m"
ColorDeepSkyBlue = "\033[38;2;0;191;255m"
)
// ClusterConfig конфигурация кластера
type ClusterConfig struct {
Name string `toml:"name"`
CoordinatorAddress string `toml:"coordinator_address"`
ReplicationFactor int `toml:"replication_factor"`
SyncReplication bool `toml:"sync_replication"`
AutoRebalance bool `toml:"auto_rebalance"`
Enabled bool `toml:"enabled"`
}
// NodeConfig конфигурация узла
type NodeConfig struct {
ID string `toml:"id"`
Address string `toml:"address"`
DataDir string `toml:"data_dir"`
AOFEnabled bool `toml:"aof_enabled"`
AOFFile string `toml:"aof_file"`
}
// StorageConfig конфигурация хранилища
type StorageConfig struct {
PageSize int `toml:"page_size"`
MaxMemory string `toml:"max_memory"`
EvictionPolicy string `toml:"eviction_policy"`
}
// ReplicationConfig конфигурация репликации
type ReplicationConfig struct {
Enabled bool `toml:"enabled"`
SyncMode string `toml:"sync_mode"`
HeartbeatInterval int `toml:"heartbeat_interval"`
Timeout int `toml:"timeout"`
MasterMaster bool `toml:"master_master"` // Включение мастер-мастер репликации
}
// LuaConfig конфигурация Lua плагинов
type LuaConfig struct {
Enabled bool `toml:"enabled"`
PluginsDir string `toml:"plugins_dir"`
MaxMemory string `toml:"max_memory"`
}
// Config основная структура конфигурации
type Config struct {
Cluster ClusterConfig `toml:"cluster"`
Node NodeConfig `toml:"node"`
Storage StorageConfig `toml:"storage"`
Replication ReplicationConfig `toml:"replication"`
Lua LuaConfig `toml:"lua"`
}
var globalConfig atomic.Value
// Load загружает конфигурацию из файла
func Load(path string) (*Config, error) {
var config Config
if _, err := toml.DecodeFile(path, &config); err != nil {
return nil, err
}
// Устанавливаем значения по умолчанию, если не указаны
if config.Cluster.CoordinatorAddress == "" {
config.Cluster.CoordinatorAddress = "127.0.0.1:7379"
}
if config.Node.Address == "" {
config.Node.Address = "127.0.0.1:7380"
}
if config.Node.DataDir == "" {
config.Node.DataDir = "./data"
}
if config.Node.AOFFile == "" {
config.Node.AOFFile = "./data/futriis.aof"
}
if config.Storage.PageSize == 0 {
config.Storage.PageSize = 4096
}
if config.Replication.HeartbeatInterval == 0 {
config.Replication.HeartbeatInterval = 5
}
if config.Replication.Timeout == 0 {
config.Replication.Timeout = 30
}
if config.Lua.PluginsDir == "" {
config.Lua.PluginsDir = "./plugins"
}
globalConfig.Store(&config)
return &config, nil
}
// Get возвращает глобальную конфигурацию
func Get() *Config {
if cfg := globalConfig.Load(); cfg != nil {
return cfg.(*Config)
}
return nil
}
// GetClusterConfig возвращает конфигурацию кластера
func GetClusterConfig() *ClusterConfig {
if cfg := Get(); cfg != nil {
return &cfg.Cluster
}
return nil
}
// GetNodeConfig возвращает конфигурацию узла
func GetNodeConfig() *NodeConfig {
if cfg := Get(); cfg != nil {
return &cfg.Node
}
return nil
}

18
pkg/types/id.go Normal file
View File

@ -0,0 +1,18 @@
// /futriis/pkg/types/id.go
// Пакет types предоставляет утилиты для генерации идентификаторов
// Данный файл содержит функцию GenerateID для создания уникальных
// идентификаторов на основе криптостойкого генератора случайных чисел.
package types
import (
"crypto/rand"
"encoding/hex"
)
// GenerateID генерирует уникальный идентификатор
func GenerateID() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}

188
pkg/types/types.go Normal file
View File

@ -0,0 +1,188 @@
// /futriis/pkg/types/types.go
// Пакет types определяет основные структуры данных СУБД Futriis.
// Содержит определения для тапплов (баз данных), слайсов (таблиц) и кортежей (записей).
// Использует wait-free подход с атомарными операциями вместо мьютексов.
package types
import (
"sync/atomic"
"time"
"unsafe"
)
// Tapple представляет базу данных (контейнер верхнего уровня) с wait-free доступом
type Tapple struct {
Name string
slices unsafe.Pointer // Атомарный указатель на map[string]*Slice
CreatedAt time.Time
UpdatedAt time.Time
}
// NewTapple создаёт новый таппл с wait-free доступом
func NewTapple(name string) *Tapple {
slices := make(map[string]*Slice)
return &Tapple{
Name: name,
slices: unsafe.Pointer(&slices),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
// GetSlices атомарно получает карту слайсов
func (t *Tapple) GetSlices() map[string]*Slice {
ptr := atomic.LoadPointer(&t.slices)
return *(*map[string]*Slice)(ptr)
}
// GetSlice атомарно получает слайс по имени
func (t *Tapple) GetSlice(name string) (*Slice, bool) {
slices := t.GetSlices()
slice, exists := slices[name]
return slice, exists
}
// PutSlice атомарно добавляет или обновляет слайс
func (t *Tapple) PutSlice(name string, slice *Slice) {
for {
oldPtr := atomic.LoadPointer(&t.slices)
oldSlices := *(*map[string]*Slice)(oldPtr)
// Создаём новую карту
newSlices := make(map[string]*Slice)
for k, v := range oldSlices {
newSlices[k] = v
}
newSlices[name] = slice
// Пытаемся атомарно обновить
if atomic.CompareAndSwapPointer(&t.slices, oldPtr, unsafe.Pointer(&newSlices)) {
t.UpdatedAt = time.Now()
break
}
}
}
// DeleteSlice атомарно удаляет слайс
func (t *Tapple) DeleteSlice(name string) bool {
for {
oldPtr := atomic.LoadPointer(&t.slices)
oldSlices := *(*map[string]*Slice)(oldPtr)
if _, exists := oldSlices[name]; !exists {
return false
}
// Создаём новую карту без удаляемого слайса
newSlices := make(map[string]*Slice)
for k, v := range oldSlices {
if k != name {
newSlices[k] = v
}
}
// Пытаемся атомарно обновить
if atomic.CompareAndSwapPointer(&t.slices, oldPtr, unsafe.Pointer(&newSlices)) {
t.UpdatedAt = time.Now()
return true
}
}
}
// Slice представляет таблицу (контейнер для кортежей) с wait-free доступом
type Slice struct {
Name string
tuples unsafe.Pointer // Атомарный указатель на map[string]*Tuple
CreatedAt time.Time
UpdatedAt time.Time
}
// NewSlice создаёт новый слайс с wait-free доступом
func NewSlice(name string) *Slice {
tuples := make(map[string]*Tuple)
return &Slice{
Name: name,
tuples: unsafe.Pointer(&tuples),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
// GetTuples атомарно получает карту кортежей
func (s *Slice) GetTuples() map[string]*Tuple {
ptr := atomic.LoadPointer(&s.tuples)
return *(*map[string]*Tuple)(ptr)
}
// GetTuple атомарно получает кортеж по ID
func (s *Slice) GetTuple(id string) (*Tuple, bool) {
tuples := s.GetTuples()
tuple, exists := tuples[id]
return tuple, exists
}
// PutTuple атомарно добавляет или обновляет кортеж
func (s *Slice) PutTuple(id string, tuple *Tuple) {
for {
oldPtr := atomic.LoadPointer(&s.tuples)
oldTuples := *(*map[string]*Tuple)(oldPtr)
// Создаём новую карту
newTuples := make(map[string]*Tuple)
for k, v := range oldTuples {
newTuples[k] = v
}
newTuples[id] = tuple
// Пытаемся атомарно обновить
if atomic.CompareAndSwapPointer(&s.tuples, oldPtr, unsafe.Pointer(&newTuples)) {
s.UpdatedAt = time.Now()
break
}
}
}
// DeleteTuple атомарно удаляет кортеж
func (s *Slice) DeleteTuple(id string) bool {
for {
oldPtr := atomic.LoadPointer(&s.tuples)
oldTuples := *(*map[string]*Tuple)(oldPtr)
if _, exists := oldTuples[id]; !exists {
return false
}
// Создаём новую карту без удаляемого кортежа
newTuples := make(map[string]*Tuple)
for k, v := range oldTuples {
if k != id {
newTuples[k] = v
}
}
// Пытаемся атомарно обновить
if atomic.CompareAndSwapPointer(&s.tuples, oldPtr, unsafe.Pointer(&newTuples)) {
s.UpdatedAt = time.Now()
return true
}
}
}
// Tuple представляет кортеж (запись)
type Tuple struct {
ID string
Fields map[string]interface{}
CreatedAt time.Time
UpdatedAt time.Time
}
// NewTuple создаёт новый кортеж
func NewTuple(id string) *Tuple {
return &Tuple{
ID: id,
Fields: make(map[string]interface{}),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}

131
pkg/utils/colors.go Normal file
View File

@ -0,0 +1,131 @@
// /futriis/pkg/utils/colors.go
// Пакет utils предоставляет утилиты для цветного вывода, логирования и форматирования строк в клиенте субд futriis
// Для реализации цветного вывода, используются escpe-последовательности с поддержкой цветов ANSI
package utils
import (
"fmt"
"time"
)
// Цветовые коды ANSI
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
ColorMagenta = "\033[35m"
ColorCyan = "\033[36m"
ColorWhite = "\033[37m"
ColorBold = "\033[1m"
ColorUnderline = "\033[4m"
// Специальные цвета для prompt
ColorPrompt = "\033[38;2;0;191;255m" // Ярко-голубой (#00bfff)
ColorPromptCode = "\033[38;5;214m" // Оранжевый для кода
// Скрытие/показ курсора
ColorHideCursor = "\033[?25l"
ColorShowCursor = "\033[?25h"
)
// PrintBanner выводит приветственный баннер
func PrintBanner(clusterName string) {
// Добавляем пустую строку перед рамкой
fmt.Println()
// Пунктирная рамка
border := "--------------------------------------------------------------------------------"
fmt.Println(ColorPrompt + border + ColorReset)
// Выравниваем текст по левому краю
fmt.Println(ColorPrompt + "futriix 3i²(by 03.01.2026)" + ColorReset)
fmt.Println(ColorPrompt + "Distributed Wide-Column database with Lua Integration and lua plugins" + ColorReset)
fmt.Println(ColorPrompt + "Cluster status: enable" + ColorReset)
fmt.Println(ColorPrompt + "Cluster name: " + clusterName + ColorReset)
fmt.Println(ColorPrompt + "[OK] Configuration load from config.toml" + ColorReset)
fmt.Println(ColorPrompt + border + ColorReset)
fmt.Println()
}
// PrintBannerWithConfig выводит баннер с информацией из конфига
func PrintBannerWithConfig(clusterName string) {
PrintBanner(clusterName)
}
// PrintInfo выводит информационное сообщение цветом приглашения
func PrintInfo(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Printf(ColorPrompt+"[INFO]"+ColorReset+" %s\n", msg)
}
// PrintSuccess выводит сообщение об успехе
func PrintSuccess(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Printf(ColorGreen+"[OK]"+ColorReset+" %s\n", msg)
}
// PrintWarning выводит предупреждение
func PrintWarning(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Printf(ColorYellow+"[WARN]"+ColorReset+" %s\n", msg)
}
// PrintError выводит сообщение об ошибке
func PrintError(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Printf(ColorRed+"[ERROR]"+ColorReset+" %s\n", msg)
}
// PrintPromptMessage выводит сообщение цветом приглашения
func PrintPromptMessage(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Printf(ColorPrompt + msg + ColorReset + "\n")
}
// ConsoleLogger представляет простой консольный логгер
type ConsoleLogger struct {
enabled bool
}
var consoleLogger *ConsoleLogger
// InitLogger инициализирует консольный логгер
func InitLogger(logPath string) {
consoleLogger = &ConsoleLogger{
enabled: true,
}
// Игнорируем logPath для консольного логгера
// Файловый логгер инициализируется отдельно через InitFileLogger
}
// GetLogger возвращает консольный логгер
func GetLogger() *ConsoleLogger {
return consoleLogger
}
// Log записывает сообщение в консольный лог
func (l *ConsoleLogger) Log(level, message string) {
if l == nil {
return
}
if !l.enabled {
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
fmt.Printf("[%s] [%s] %s\n", timestamp, level, message)
}
// Close закрывает консольный логгер
func (l *ConsoleLogger) Close() {
// В простой реализации ничего не делаем
}
// GetPrompt возвращает строку приглашения
func GetPrompt() string {
return ColorPrompt + "futriis:~> " + ColorReset
}

69
pkg/utils/logger.go Normal file
View File

@ -0,0 +1,69 @@
// /futriis/pkg/utils/logger.go
// Пакет utils предоставляет функции для логирования работы СУБД Futriis.
// Реализует запись логов в файл с временными метками, включающими миллисекунды,
// и уровнями логирования (INFO, ERROR, WARNING, CMD). Логгер используется для
// отслеживания операций, отладки и аудита команд.
package utils
import (
"fmt"
"os"
"time"
)
// Logger представляет структуру для логирования в файл
type FileLogger struct {
file *os.File
}
var fileLoggerInstance *FileLogger
// InitFileLogger инициализирует файловый логгер с указанным путём к файлу
func InitFileLogger(logFile string) error {
// Создаём директорию для логов, если она не существует
logDir := "/home/grigoriy/futriis/logs"
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("не удалось создать директорию для логов: %v", err)
}
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
fileLoggerInstance = &FileLogger{
file: file,
}
// Записываем начало сессии
fileLoggerInstance.Log("INFO", "Сессия начата")
return nil
}
// GetFileLogger возвращает экземпляр файлового логгера
func GetFileLogger() *FileLogger {
return fileLoggerInstance
}
// Log записывает сообщение в файл лога с миллисекундами
func (l *FileLogger) Log(level, message string) {
if l == nil || l.file == nil {
return
}
// Формат времени с миллисекундами: 2006-01-02 15:04:05.000
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
logLine := fmt.Sprintf("[%s] %s: %s\n", timestamp, level, message)
l.file.WriteString(logLine)
}
// Close закрывает файл лога
func (l *FileLogger) Close() {
if l != nil && l.file != nil {
// Записываем конец сессии
l.Log("INFO", "Сессия завершена")
l.file.Close()
}
}