commit 51e3d68f5d10dc30cdbfa1e0cff2ec023fd18c87 Author: gvsafronov Date: Fri Feb 27 22:04:04 2026 +0300 first commit diff --git a/Logo.png b/Logo.png new file mode 100644 index 0000000..e963694 Binary files /dev/null and b/Logo.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..edeb9f5 --- /dev/null +++ b/README.md @@ -0,0 +1,295 @@ + + + + + + +
+
+ +Logo.png + + +

+

Futriis-это легковесная, распределённая wait-free и lock-free дружественная in-memory СУБД, +реализованная на Go с поддержкой плагинов на языке lua для операционных систем на базе Solaris (ядра Illumos)

+
+
+ +

+
+ + ## Краткая документация проекта FutriiS + + +
+ + Содержание
+
    +
  1. + О проекте +
  2. Лицензия
  3. +
  4. Глоссарий
  5. +
  6. Системные требования
  7. +
  8. Подготовка
  9. +
  10. Компиляция
  11. +
  12. Тестирование
  13. +
  14. Примеры команд субд
  15. +
  16. Репликация
  17. +
  18. Резервное копирование
  19. +
  20. Индексы
  21. +
  22. Транзакции
  23. +
  24. Шардинг
  25. +
  26. Кластеризация
  27. +
  28. Lua-скрипты
  29. +
  30. Сферы применения
  31. +
  32. Дорожная карта
  33. +
  34. Контакты
  35. +
+ + + +# 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 +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c1332fd --- /dev/null +++ b/build.sh @@ -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 diff --git a/cmd/futriis/main.go b/cmd/futriis/main.go new file mode 100644 index 0000000..0bc1bc1 --- /dev/null +++ b/cmd/futriis/main.go @@ -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) + } +} diff --git a/internal/cli/commands.go b/internal/cli/commands.go new file mode 100644 index 0000000..4bec844 --- /dev/null +++ b/internal/cli/commands.go @@ -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 +} diff --git a/internal/cli/history.go b/internal/cli/history.go new file mode 100644 index 0000000..7515bba --- /dev/null +++ b/internal/cli/history.go @@ -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) +} diff --git a/internal/cli/prompt.go b/internal/cli/prompt.go new file mode 100644 index 0000000..f655fb8 --- /dev/null +++ b/internal/cli/prompt.go @@ -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 +} diff --git a/internal/client/handler.go b/internal/client/handler.go new file mode 100644 index 0000000..4a1f9c4 --- /dev/null +++ b/internal/client/handler.go @@ -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 +} diff --git a/internal/cluster/node.go b/internal/cluster/node.go new file mode 100644 index 0000000..c9c05ff --- /dev/null +++ b/internal/cluster/node.go @@ -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 +} diff --git a/internal/cluster/sharding.go b/internal/cluster/sharding.go new file mode 100644 index 0000000..f6c131e --- /dev/null +++ b/internal/cluster/sharding.go @@ -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 +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..e8bc315 --- /dev/null +++ b/internal/engine/engine.go @@ -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 ' 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 " + utils.ColorReset + " - create a new tapple (database)\n" + help += " " + utils.ColorGreen + "create slice " + utils.ColorReset + " - create a new slice (table)\n" + help += " " + utils.ColorGreen + "create tuple [key=value...]" + utils.ColorReset + " - create a new tuple (record)\n" + help += " " + utils.ColorGreen + "delete tapple " + utils.ColorReset + " - delete a tapple\n" + help += " " + utils.ColorGreen + "delete slice " + utils.ColorReset + " - delete a slice\n" + help += " " + utils.ColorGreen + "delete tuple " + utils.ColorReset + " - delete a tuple\n" + help += " " + utils.ColorGreen + "update tuple [key=value...]" + utils.ColorReset + " - update a tuple\n" + help += " " + utils.ColorGreen + "list tapples" + utils.ColorReset + " - show all tapples\n" + help += " " + utils.ColorGreen + "list slices " + utils.ColorReset + " - show all slices in a tapple\n" + help += " " + utils.ColorGreen + "show tuples " + utils.ColorReset + " - show all tuples in a slice\n" + + help += "\n" + utils.ColorYellow + "Index management:" + utils.ColorReset + "\n" + help += " " + utils.ColorGreen + "add.prime.index " + utils.ColorReset + " - create primary index for tapple\n" + help += " " + utils.ColorGreen + "delete.prime.index " + utils.ColorReset + " - delete primary index\n" + help += " " + utils.ColorGreen + "add.secondary.index " + utils.ColorReset + " - create secondary index on field\n" + help += " " + utils.ColorGreen + "delete.secondary.index " + 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
" + utils.ColorReset + " - add a node to the cluster\n" + help += " " + utils.ColorGreen + "evict.node " + 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 " + 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 +} diff --git a/internal/lua/plugin.go b/internal/lua/plugin.go new file mode 100644 index 0000000..e6ac9a2 --- /dev/null +++ b/internal/lua/plugin.go @@ -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() + } +} diff --git a/internal/msgpack/deserializer.go b/internal/msgpack/deserializer.go new file mode 100644 index 0000000..b8b095d --- /dev/null +++ b/internal/msgpack/deserializer.go @@ -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 +} diff --git a/internal/msgpack/serializer.go b/internal/msgpack/serializer.go new file mode 100644 index 0000000..4518f20 --- /dev/null +++ b/internal/msgpack/serializer.go @@ -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 +} diff --git a/internal/replication/aof.go b/internal/replication/aof.go new file mode 100644 index 0000000..953e8d2 --- /dev/null +++ b/internal/replication/aof.go @@ -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 +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..8a14ea9 --- /dev/null +++ b/internal/server/server.go @@ -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() +} diff --git a/internal/storage/compression.go b/internal/storage/compression.go new file mode 100644 index 0000000..5c6286d --- /dev/null +++ b/internal/storage/compression.go @@ -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), + } +} diff --git a/internal/storage/index.go b/internal/storage/index.go new file mode 100644 index 0000000..448026b --- /dev/null +++ b/internal/storage/index.go @@ -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 +} diff --git a/internal/storage/slice.go b/internal/storage/slice.go new file mode 100644 index 0000000..deaae93 --- /dev/null +++ b/internal/storage/slice.go @@ -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 +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..d0f4316 --- /dev/null +++ b/internal/storage/storage.go @@ -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() +} diff --git a/internal/storage/tapple.go b/internal/storage/tapple.go new file mode 100644 index 0000000..9d1a960 --- /dev/null +++ b/internal/storage/tapple.go @@ -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 +} diff --git a/internal/storage/tuple.go b/internal/storage/tuple.go new file mode 100644 index 0000000..5fc0d5f --- /dev/null +++ b/internal/storage/tuple.go @@ -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), + } +} diff --git a/internal/transaction/tx.go b/internal/transaction/tx.go new file mode 100644 index 0000000..96abd84 --- /dev/null +++ b/internal/transaction/tx.go @@ -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") +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..1908399 --- /dev/null +++ b/pkg/config/config.go @@ -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 +} diff --git a/pkg/types/id.go b/pkg/types/id.go new file mode 100644 index 0000000..51f5345 --- /dev/null +++ b/pkg/types/id.go @@ -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) +} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..a6827a2 --- /dev/null +++ b/pkg/types/types.go @@ -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(), + } +} diff --git a/pkg/utils/colors.go b/pkg/utils/colors.go new file mode 100644 index 0000000..e227136 --- /dev/null +++ b/pkg/utils/colors.go @@ -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 +} diff --git a/pkg/utils/logger.go b/pkg/utils/logger.go new file mode 100644 index 0000000..8a6567a --- /dev/null +++ b/pkg/utils/logger.go @@ -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() + } +}