From 51e3d68f5d10dc30cdbfa1e0cff2ec023fd18c87 Mon Sep 17 00:00:00 2001 From: gvsafronov Date: Fri, 27 Feb 2026 22:04:04 +0300 Subject: [PATCH] first commit --- Logo.png | Bin 0 -> 36296 bytes README.md | 295 +++++++++ build.sh | 255 ++++++++ cmd/futriis/main.go | 81 +++ internal/cli/commands.go | 48 ++ internal/cli/history.go | 88 +++ internal/cli/prompt.go | 204 +++++++ internal/client/handler.go | 105 ++++ internal/cluster/node.go | 810 +++++++++++++++++++++++++ internal/cluster/sharding.go | 448 ++++++++++++++ internal/engine/engine.go | 993 +++++++++++++++++++++++++++++++ internal/lua/plugin.go | 154 +++++ internal/msgpack/deserializer.go | 44 ++ internal/msgpack/serializer.go | 68 +++ internal/replication/aof.go | 244 ++++++++ internal/server/server.go | 129 ++++ internal/storage/compression.go | 240 ++++++++ internal/storage/index.go | 231 +++++++ internal/storage/slice.go | 196 ++++++ internal/storage/storage.go | 39 ++ internal/storage/tapple.go | 156 +++++ internal/storage/tuple.go | 288 +++++++++ internal/transaction/tx.go | 205 +++++++ pkg/config/config.go | 142 +++++ pkg/types/id.go | 18 + pkg/types/types.go | 188 ++++++ pkg/utils/colors.go | 131 ++++ pkg/utils/logger.go | 69 +++ 28 files changed, 5869 insertions(+) create mode 100644 Logo.png create mode 100644 README.md create mode 100755 build.sh create mode 100644 cmd/futriis/main.go create mode 100644 internal/cli/commands.go create mode 100644 internal/cli/history.go create mode 100644 internal/cli/prompt.go create mode 100644 internal/client/handler.go create mode 100644 internal/cluster/node.go create mode 100644 internal/cluster/sharding.go create mode 100644 internal/engine/engine.go create mode 100644 internal/lua/plugin.go create mode 100644 internal/msgpack/deserializer.go create mode 100644 internal/msgpack/serializer.go create mode 100644 internal/replication/aof.go create mode 100644 internal/server/server.go create mode 100644 internal/storage/compression.go create mode 100644 internal/storage/index.go create mode 100644 internal/storage/slice.go create mode 100644 internal/storage/storage.go create mode 100644 internal/storage/tapple.go create mode 100644 internal/storage/tuple.go create mode 100644 internal/transaction/tx.go create mode 100644 pkg/config/config.go create mode 100644 pkg/types/id.go create mode 100644 pkg/types/types.go create mode 100644 pkg/utils/colors.go create mode 100644 pkg/utils/logger.go diff --git a/Logo.png b/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e9636943d6de8c482d55808e001412f1cbe373c2 GIT binary patch literal 36296 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 + + + + + +
+
+ +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() + } +}