first commit
This commit is contained in:
commit
51e3d68f5d
295
README.md
Normal file
295
README.md
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
<!-- Improved compatibility of К началу link: See: https://github.com/othneildrew/Best-README-Template/pull/73 -->
|
||||||
|
<a id="readme-top"></a>
|
||||||
|
<!--
|
||||||
|
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
||||||
|
*** that would make this better, please fork the repo and create a pull request
|
||||||
|
*** or simply open an issue with the tag "enhancement".
|
||||||
|
*** Don't forget to give the project a star!
|
||||||
|
*** Thanks again! Now go create something AMAZING! :D
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- PROJECT LOGO -->
|
||||||
|
<br />
|
||||||
|
<div align="center">
|
||||||
|
<!-- <a href="https://github.com/othneildrew/Best-README-Template"> -->
|
||||||
|
<img src="Logo.png" height=100 alt="Logo.png"></img>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<h3> <b>Futriis-это легковесная, распределённая wait-free и lock-free дружественная in-memory СУБД,
|
||||||
|
реализованная на Go с поддержкой плагинов на языке lua для операционных систем на базе Solaris (ядра Illumos)</b> <br></h3>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<!-- <a href="">Сообщить об ошибке</a>
|
||||||
|
·
|
||||||
|
<!-- <a href="">Предложение новой функциональности</a> -->
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Краткая документация проекта FutriiS
|
||||||
|
|
||||||
|
<!-- TABLE OF CONTENTS -->
|
||||||
|
<br>
|
||||||
|
<!-- <details> -->
|
||||||
|
<summary><b>Содержание</b></summary></br>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<a href="#о-проекте">О проекте</a>
|
||||||
|
<li><a href="#лицензия">Лицензия</a></li>
|
||||||
|
<li><a href="#глоссарий">Глоссарий</a></li>
|
||||||
|
<li><a href="#системные-требования">Системные требования</a></li>
|
||||||
|
<li><a href="#подготовка">Подготовка</a></li>
|
||||||
|
<li><a href="#компиляция">Компиляция</a></li>
|
||||||
|
<li><a href="#тестирование">Тестирование</a></li>
|
||||||
|
<li><a href="#примеры-команд-субд">Примеры команд субд</a></li>
|
||||||
|
<li><a href="#репликация">Репликация</a></li>
|
||||||
|
<li><a href="#резервное-копирование">Резервное копирование</a></li>
|
||||||
|
<li><a href="#индексы">Индексы</a></li>
|
||||||
|
<li><a href="#транзакции">Транзакции</a></li>
|
||||||
|
<li><a href="#шардинг">Шардинг</a></li>
|
||||||
|
<li><a href="#кластеризация">Кластеризация</a></li>
|
||||||
|
<li><a href="#lua-скрипты">Lua-скрипты</a></li>
|
||||||
|
<li><a href="#сферы-применения">Сферы применения</a></li>
|
||||||
|
<li><a href="#дорожная-карта">Дорожная карта</a></li>
|
||||||
|
<li><a href="#контакты">Контакты</a></li>
|
||||||
|
</ol>
|
||||||
|
<!-- </details> -->
|
||||||
|
|
||||||
|
|
||||||
|
# futriis - Распределённая in-memory СУБД
|
||||||
|
|
||||||
|
futriis - это легковесная, распределённая wait-free и lock-free дружественная in-memory СУБД,
|
||||||
|
реализованная на Go с поддержкой плагинов на языке lua.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
СУБД реализует три основных типа данных:
|
||||||
|
- **Таппл (Tapple)** - аналог базы данных в РСУБД
|
||||||
|
- **Слайс (Slice)** - аналог таблицы
|
||||||
|
- **Кортеж (Tuple)** - аналог записи в таблице
|
||||||
|
|
||||||
|
|
||||||
|
## Системные требования
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> - Процессор: Intel или AMD
|
||||||
|
> - Оперативная память: 4ГБ (Для Linux) 8ГБ (Для Illumos sytems)
|
||||||
|
> - Только Unix-подобная ОС (Solaris, OpenIndiana, Linux)
|
||||||
|
> - Go 1.25.6 или выше
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **Важно: Windows и MacOS X не поддерживаются!**
|
||||||
|
|
||||||
|
## Установка и сборка
|
||||||
|
|
||||||
|
1. Клонируйте репозиторий:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/futriis/db.git
|
||||||
|
cd futriis
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Скомпилируйте и запустите:
|
||||||
|
```bash
|
||||||
|
./build.sh
|
||||||
|
./futriis
|
||||||
|
```
|
||||||
|
|
||||||
|
## Файл futriisd
|
||||||
|
|
||||||
|
futriisd - это демон (сервис) СУБД Futriis, расположенный в /futriis/build/futriisd. Этот файл является:
|
||||||
|
|
||||||
|
- Основным исполняемым файлом сервера - запускает ядро СУБД в фоновом режиме как демон (daemon)
|
||||||
|
- Точкой входа для кластерного узла - каждый узел кластера запускается через этот бинарный файл
|
||||||
|
- Фоновым процессом - работает независимо от терминала, обрабатывая сетевые запросы
|
||||||
|
- Управляющим процессом - отвечает за инициализацию всех компонентов: хранилища, кластера, репликации, AOF
|
||||||
|
- Сетевым сервером - слушает порты для координации кластера и обработки клиентских подключений
|
||||||
|
|
||||||
|
**Пример использования демона "futriisd"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск узла кластера
|
||||||
|
./futriisd --config /path/to/config.toml --node-id node-1
|
||||||
|
|
||||||
|
# Запуск координатора
|
||||||
|
./futriisd --config /path/to/config.toml --coordinator
|
||||||
|
|
||||||
|
# Запуск в фоновом режиме
|
||||||
|
./futriisd --daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
## Базовые команды (Tapple/Slice/Tuple)
|
||||||
|
|
||||||
|
### Создание объектов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать таппл (базу данных)
|
||||||
|
create tapple users
|
||||||
|
|
||||||
|
# Создать слайс (таблицу) в таппле
|
||||||
|
create slice users user_profiles
|
||||||
|
|
||||||
|
# Создать кортеж (запись) с полями
|
||||||
|
create tuple users user_profiles user1 name=John age=30 email=john@example.com
|
||||||
|
create tuple users user_profiles user2 name=Jane age=25 city=NYC
|
||||||
|
```
|
||||||
|
|
||||||
|
### Просмотр списков
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать все тапплы
|
||||||
|
list tapples
|
||||||
|
|
||||||
|
# Показать все слайсы в таппле
|
||||||
|
list slices users
|
||||||
|
|
||||||
|
# Показать все кортежи в слайсе
|
||||||
|
show tuples users user_profiles
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Индексы
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать первичный индекс для таппла
|
||||||
|
add.prime.index users
|
||||||
|
|
||||||
|
# Удалить первичный индекс
|
||||||
|
delete.prime.index users
|
||||||
|
|
||||||
|
# Создать вторичный индекс по полю
|
||||||
|
add.secondary.index users email
|
||||||
|
add.secondary.index users age
|
||||||
|
|
||||||
|
# Удалить вторичный индекс
|
||||||
|
delete.secondary.index users email
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обновление и удаление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Обновить поля кортежа
|
||||||
|
update tuple users user_profiles user1 age=31 city=Boston
|
||||||
|
|
||||||
|
# Удалить кортеж
|
||||||
|
delete tuple users user_profiles user2
|
||||||
|
|
||||||
|
# Удалить слайс
|
||||||
|
delete slice users user_profiles
|
||||||
|
|
||||||
|
# Удалить таппл
|
||||||
|
delete tapple users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Транзакции
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Начать транзакцию
|
||||||
|
begin
|
||||||
|
|
||||||
|
# Выполнить операции внутри транзакции
|
||||||
|
create tuple users user_profiles user3 name=Bob age=28
|
||||||
|
update tuple users user_profiles user1 city=Chicago
|
||||||
|
|
||||||
|
# Зафиксировать транзакцию
|
||||||
|
commit
|
||||||
|
|
||||||
|
# Или откатить транзакцию
|
||||||
|
rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
## Кластеринг и шардинг
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать статус кластера
|
||||||
|
cluster.status
|
||||||
|
|
||||||
|
# Добавить узел в кластер
|
||||||
|
add.node 192.168.1.101:8080
|
||||||
|
add.node 192.168.1.102:8080
|
||||||
|
|
||||||
|
# Удалить узел из кластера
|
||||||
|
evict.node node-123
|
||||||
|
|
||||||
|
# Ребалансировка кластера
|
||||||
|
cluster.rebalance
|
||||||
|
|
||||||
|
# Показать статус шардинга
|
||||||
|
sharding.status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сжатие данных
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать статистику сжатия по колонкам
|
||||||
|
compression.stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## AOF (Append-Only File)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать информацию о AOF файле
|
||||||
|
aof.info
|
||||||
|
|
||||||
|
# Восстановить данные из AOF файла
|
||||||
|
aof.recover
|
||||||
|
aof.recover /path/to/custom/file.aof
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lua-плагины
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Выполнить Lua плагин
|
||||||
|
lua my_plugin
|
||||||
|
lua analytics_script
|
||||||
|
```
|
||||||
|
|
||||||
|
## Служебные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Показать справку
|
||||||
|
help
|
||||||
|
|
||||||
|
# Выйти из СУБД
|
||||||
|
exit
|
||||||
|
# или
|
||||||
|
quit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Комплексный пример рабочей сессии
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создаём структуру данных
|
||||||
|
create tapple ecommerce
|
||||||
|
create slice ecommerce products
|
||||||
|
create slice ecommerce customers
|
||||||
|
create slice ecommerce orders
|
||||||
|
|
||||||
|
# Создаём индексы
|
||||||
|
add.secondary.index ecommerce price
|
||||||
|
add.secondary.index ecommerce email
|
||||||
|
|
||||||
|
# Добавляем данные (в транзакции)
|
||||||
|
begin
|
||||||
|
create tuple ecommerce products prod1 name=Laptop price=999.99 stock=10
|
||||||
|
create tuple ecommerce products prod2 name=Mouse price=29.99 stock=50
|
||||||
|
create tuple ecommerce customers cust1 name=Alice email=alice@mail.com
|
||||||
|
create tuple ecommerce orders order1 customer=cust1 product=prod1 quantity=1
|
||||||
|
commit
|
||||||
|
|
||||||
|
# Просматриваем данные
|
||||||
|
show tuples ecommerce products
|
||||||
|
show tuples ecommerce customers
|
||||||
|
|
||||||
|
# Обновляем данные
|
||||||
|
update tuple ecommerce products prod1 stock=9
|
||||||
|
|
||||||
|
# Проверяем статус кластера
|
||||||
|
cluster.status
|
||||||
|
|
||||||
|
# Смотрим статистику сжатия
|
||||||
|
compression.stats
|
||||||
|
|
||||||
|
# Выходим
|
||||||
|
exit
|
||||||
|
```
|
||||||
255
build.sh
Executable file
255
build.sh
Executable file
@ -0,0 +1,255 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# /futriis/build.sh
|
||||||
|
# Bash-скрипт для сборки проекта futriis на Unix-системах (Solaris, OpenIndiana, Linux)
|
||||||
|
# При запуске без параметров выполняет полную сборку проекта: установка зависимостей,
|
||||||
|
# сборка сервера, сборка клиента и очистка кеша.
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Проверка операционной системы
|
||||||
|
UNAME_S=$(uname -s)
|
||||||
|
if [ "$UNAME_S" != "Linux" ] && [ "$UNAME_S" != "SunOS" ]; then
|
||||||
|
echo -e "${RED}Ошибка: Операционная система $UNAME_S не поддерживается. Проект поддерживает только Solaris, OpenIndiana и Linux.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Переменные
|
||||||
|
BINARY_NAME="futriis"
|
||||||
|
SERVER_BINARY_NAME="futriisd"
|
||||||
|
BUILD_DIR="build"
|
||||||
|
CMD_DIR="./cmd/futriis"
|
||||||
|
|
||||||
|
# Функция для отображения разделителя
|
||||||
|
print_separator() {
|
||||||
|
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Функция для отображения заголовка
|
||||||
|
print_header() {
|
||||||
|
echo -e "${YELLOW}▶ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Функция для отображения успеха
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✓ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Функция для отображения ошибки
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}✗ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Создание директории для сборки
|
||||||
|
create_build_dir() {
|
||||||
|
if [ ! -d "$BUILD_DIR" ]; then
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
mkdir -p "$BUILD_DIR/plugins"
|
||||||
|
mkdir -p "$BUILD_DIR/data"
|
||||||
|
print_success "Создана директория сборки: $BUILD_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
deps() {
|
||||||
|
print_header "Установка зависимостей..."
|
||||||
|
go mod download
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
print_error "Ошибка при загрузке зависимостей"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
go mod tidy
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
print_error "Ошибка при обновлении go.mod"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Зависимости установлены"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сборка клиента
|
||||||
|
build_client() {
|
||||||
|
print_header "Сборка клиента для $UNAME_S..."
|
||||||
|
create_build_dir
|
||||||
|
|
||||||
|
go build -ldflags="-s -w" -o "$BUILD_DIR/$BINARY_NAME" "$CMD_DIR"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_success "Клиент собран: $BUILD_DIR/$BINARY_NAME"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Ошибка сборки клиента"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сборка сервера
|
||||||
|
build_server() {
|
||||||
|
print_header "Сборка сервера для $UNAME_S..."
|
||||||
|
create_build_dir
|
||||||
|
|
||||||
|
go build -ldflags="-s -w" -o "$BUILD_DIR/$SERVER_BINARY_NAME" "$CMD_DIR"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_success "Сервер собран: $BUILD_DIR/$SERVER_BINARY_NAME"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Ошибка сборки сервера"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Очистка кеша Go
|
||||||
|
clean_cache() {
|
||||||
|
print_header "Очистка кеша сборки Go..."
|
||||||
|
go clean -cache
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_success "Кеш сборки очищен"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Ошибка при очистке кеша"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Полная сборка проекта
|
||||||
|
full_build() {
|
||||||
|
echo
|
||||||
|
print_separator
|
||||||
|
echo -e "${GREEN} F U T R I I S - Полная сборка проекта${NC}"
|
||||||
|
print_separator
|
||||||
|
echo
|
||||||
|
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
# Шаг 1: Установка зависимостей
|
||||||
|
deps
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Шаг 2: Сборка сервера
|
||||||
|
build_server
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Шаг 3: Сборка клиента
|
||||||
|
build_client
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Шаг 4: Очистка кеша
|
||||||
|
clean_cache
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Итоговый отчёт
|
||||||
|
print_separator
|
||||||
|
if [ $errors -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✅ СБОРКА УСПЕШНО ЗАВЕРШЕНА${NC}"
|
||||||
|
echo -e "${GREEN} Сервер: $BUILD_DIR/$SERVER_BINARY_NAME${NC}"
|
||||||
|
echo -e "${GREEN} Клиент: $BUILD_DIR/$BINARY_NAME${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ СБОРКА ЗАВЕРШЕНА С ОШИБКАМИ ($errors ошибок)${NC}"
|
||||||
|
fi
|
||||||
|
print_separator
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Показываем размер бинарных файлов
|
||||||
|
if [ -f "$BUILD_DIR/$SERVER_BINARY_NAME" ]; then
|
||||||
|
SERVER_SIZE=$(du -h "$BUILD_DIR/$SERVER_BINARY_NAME" | cut -f1)
|
||||||
|
echo -e "Размер сервера: ${YELLOW}$SERVER_SIZE${NC}"
|
||||||
|
fi
|
||||||
|
if [ -f "$BUILD_DIR/$BINARY_NAME" ]; then
|
||||||
|
CLIENT_SIZE=$(du -h "$BUILD_DIR/$BINARY_NAME" | cut -f1)
|
||||||
|
echo -e "Размер клиента: ${YELLOW}$CLIENT_SIZE${NC}"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Функция для отображения справки
|
||||||
|
show_help() {
|
||||||
|
echo "Использование: ./build.sh [КОМАНДА]"
|
||||||
|
echo
|
||||||
|
echo "Доступные команды:"
|
||||||
|
echo " (без параметров) - полная сборка проекта (зависимости + сервер + клиент + очистка)"
|
||||||
|
echo " deps - только установка зависимостей"
|
||||||
|
echo " build - только сборка клиента"
|
||||||
|
echo " build-server - только сборка сервера"
|
||||||
|
echo " install - установка проекта в систему"
|
||||||
|
echo " run - сборка и запуск клиента"
|
||||||
|
echo " run-server - сборка и запуск сервера"
|
||||||
|
echo " test - запуск тестов"
|
||||||
|
echo " clean - очистка директории сборки"
|
||||||
|
echo " clean-cache - только очистка кеша Go"
|
||||||
|
echo " help - показать эту справку"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Обработка аргументов командной строки
|
||||||
|
case "$1" in
|
||||||
|
deps)
|
||||||
|
deps
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
build_client
|
||||||
|
;;
|
||||||
|
build-server)
|
||||||
|
build_server
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
echo -e "${GREEN}Установка проекта...${NC}"
|
||||||
|
go install -ldflags="-s -w" "$CMD_DIR"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✓ Установка завершена${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Ошибка установки${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
run)
|
||||||
|
build_client && "./$BUILD_DIR/$BINARY_NAME"
|
||||||
|
;;
|
||||||
|
run-server)
|
||||||
|
build_server && "./$BUILD_DIR/$SERVER_BINARY_NAME" -server -config config.toml
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
echo -e "${GREEN}Запуск тестов...${NC}"
|
||||||
|
go test -v ./...
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
echo -e "${GREEN}Очистка...${NC}"
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
go clean
|
||||||
|
echo -e "${GREEN}✓ Очистка завершена${NC}"
|
||||||
|
;;
|
||||||
|
clean-cache)
|
||||||
|
clean_cache
|
||||||
|
;;
|
||||||
|
help|"")
|
||||||
|
if [ "$1" = "" ]; then
|
||||||
|
full_build
|
||||||
|
else
|
||||||
|
show_help
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Неизвестная команда: $1${NC}"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
81
cmd/futriis/main.go
Normal file
81
cmd/futriis/main.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// /futriis/cmd/futriis/main.go
|
||||||
|
// Клиентское приложение СУБД Futriis
|
||||||
|
// Обеспечивает интерактивный интерфейс для выполнения команд
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"futriis/internal/client"
|
||||||
|
"futriis/internal/engine"
|
||||||
|
"futriis/pkg/config"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Определяем путь к файлу конфигурации
|
||||||
|
configPath := "config.toml"
|
||||||
|
|
||||||
|
// Проверяем, существует ли файл в текущей директории
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
// Если нет, пробуем найти в родительской директории (для случая запуска из cmd/futriis)
|
||||||
|
configPath = filepath.Join("..", "..", "config.toml")
|
||||||
|
|
||||||
|
// Проверяем, существует ли файл по новому пути
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
// Если файл не найден, используем абсолютный путь относительно домашней директории
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
configPath = filepath.Join(homeDir, "futriis", "config.toml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем конфигурацию
|
||||||
|
cfg, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Ошибка загрузки конфигурации: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализируем логгеры
|
||||||
|
utils.InitLogger("")
|
||||||
|
|
||||||
|
// Инициализируем файловый логгер
|
||||||
|
if err := utils.InitFileLogger(cfg.Node.AOFFile); err != nil {
|
||||||
|
utils.PrintWarning("Не удалось инициализировать файловый логгер: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if logger := utils.GetFileLogger(); logger != nil {
|
||||||
|
logger.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Создаём движок
|
||||||
|
eng := engine.NewEngine()
|
||||||
|
|
||||||
|
// Выводим баннер с именем кластера из конфига
|
||||||
|
utils.PrintBanner(cfg.Cluster.Name)
|
||||||
|
|
||||||
|
// Создаём обработчик команд
|
||||||
|
handler := client.NewCommandHandler(eng)
|
||||||
|
|
||||||
|
// Обработка сигналов для graceful shutdown
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
fmt.Println("\nПолучен сигнал завершения. Завершаем работу...")
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Запускаем REPL
|
||||||
|
if err := handler.RunREPL(); err != nil {
|
||||||
|
utils.PrintError("%v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
internal/cli/commands.go
Normal file
48
internal/cli/commands.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// /futriis/internal/cli/commands.go
|
||||||
|
// Пакет cli определяет структуру команд и систему их регистрации в СУБД.
|
||||||
|
// Command представляет собой описание команды с именем, описанием, синтаксисом использования и функцией-обработчиком.
|
||||||
|
// CommandRegistry служит центральным реестром для всех доступных команд, позволяя регистрировать новые, получать команды по имени и формировать список для справки.
|
||||||
|
// Обеспечивает расширяемость интерфейса командной строки.
|
||||||
|
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
// Command представляет команду СУБД
|
||||||
|
type Command struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Usage string
|
||||||
|
Handler func(args []string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandRegistry реестр всех доступных команд
|
||||||
|
type CommandRegistry struct {
|
||||||
|
commands map[string]*Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandRegistry создаёт новый реестр команд
|
||||||
|
func NewCommandRegistry() *CommandRegistry {
|
||||||
|
return &CommandRegistry{
|
||||||
|
commands: make(map[string]*Command),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register регистрирует новую команду
|
||||||
|
func (cr *CommandRegistry) Register(cmd *Command) {
|
||||||
|
cr.commands[cmd.Name] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get возвращает команду по имени
|
||||||
|
func (cr *CommandRegistry) Get(name string) (*Command, bool) {
|
||||||
|
cmd, ok := cr.commands[name]
|
||||||
|
return cmd, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// List возвращает список всех команд
|
||||||
|
func (cr *CommandRegistry) List() []*Command {
|
||||||
|
cmds := make([]*Command, 0, len(cr.commands))
|
||||||
|
for _, cmd := range cr.commands {
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
88
internal/cli/history.go
Normal file
88
internal/cli/history.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// /futriis/internal/cli/history.go
|
||||||
|
// Пакет cli реализует управление историей команд для интерактивного режима.
|
||||||
|
// History хранит ограниченное количество последних команд с кольцевым буфером, предотвращает добавление последовательных дубликатов.
|
||||||
|
// Предоставляет навигацию по истории с помощью стрелок вверх/вниз для быстрого повторного выполнения команд.
|
||||||
|
// Интегрируется с Prompt для обеспечения полноценного интерфейса командной строки.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
// History управляет историей команд
|
||||||
|
type History struct {
|
||||||
|
commands []string
|
||||||
|
position int
|
||||||
|
maxSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistory создаёт новую историю команд
|
||||||
|
func NewHistory(maxSize int) *History {
|
||||||
|
return &History{
|
||||||
|
commands: make([]string, 0, maxSize),
|
||||||
|
position: 0,
|
||||||
|
maxSize: maxSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add добавляет команду в историю
|
||||||
|
func (h *History) Add(cmd string) {
|
||||||
|
if cmd == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не добавляем дубликаты подряд
|
||||||
|
if len(h.commands) > 0 && h.commands[len(h.commands)-1] == cmd {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если достигнут максимум, удаляем самую старую команду
|
||||||
|
if len(h.commands) >= h.maxSize {
|
||||||
|
h.commands = h.commands[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
h.commands = append(h.commands, cmd)
|
||||||
|
h.position = len(h.commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrevious возвращает предыдущую команду из истории
|
||||||
|
func (h *History) GetPrevious() string {
|
||||||
|
if len(h.commands) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.position > 0 {
|
||||||
|
h.position--
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.commands[h.position]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNext возвращает следующую команду из истории
|
||||||
|
func (h *History) GetNext() string {
|
||||||
|
if h.position < len(h.commands)-1 {
|
||||||
|
h.position++
|
||||||
|
return h.commands[h.position]
|
||||||
|
}
|
||||||
|
h.position = len(h.commands)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset сбрасывает позицию в истории
|
||||||
|
func (h *History) Reset() {
|
||||||
|
h.position = len(h.commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupRawMode устанавливает терминал в raw-режим для обработки клавиш
|
||||||
|
func SetupRawMode() (*term.State, error) {
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
return term.MakeRaw(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreMode восстанавливает режим терминала
|
||||||
|
func RestoreMode(oldState *term.State) error {
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
return term.Restore(fd, oldState)
|
||||||
|
}
|
||||||
204
internal/cli/prompt.go
Normal file
204
internal/cli/prompt.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// /futriis/internal/cli/prompt.go
|
||||||
|
// Пакет cli реализует интерактивное приглашение командной строки с поддержкой истории.
|
||||||
|
// Обеспечивает редактирование строки ввода, навигацию по истории стрелками,
|
||||||
|
// поддержку Unicode (включая кириллицу) и управление курсором терминала.
|
||||||
|
// Использует raw режим терминала для обработки специальных клавиш.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ANSI escape sequences для управления курсором
|
||||||
|
const (
|
||||||
|
ansiHideCursor = "\033[?25l"
|
||||||
|
ansiShowCursor = "\033[?25h"
|
||||||
|
ansiClearLine = "\033[2K"
|
||||||
|
ansiCarriageReturn = "\r"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prompt представляет интерактивное приглашение
|
||||||
|
type Prompt struct {
|
||||||
|
history *History
|
||||||
|
buffer []rune
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrompt создаёт новое приглашение
|
||||||
|
func NewPrompt() *Prompt {
|
||||||
|
return &Prompt{
|
||||||
|
history: NewHistory(100),
|
||||||
|
buffer: make([]rune, 0),
|
||||||
|
pos: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLine читает строку с поддержкой истории и редактирования
|
||||||
|
func (p *Prompt) ReadLine() (string, error) {
|
||||||
|
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
return p.readSimple()
|
||||||
|
}
|
||||||
|
defer term.Restore(int(os.Stdin.Fd()), oldState)
|
||||||
|
|
||||||
|
// Скрываем курсор в начале строки
|
||||||
|
fmt.Print(ansiHideCursor)
|
||||||
|
|
||||||
|
// Очищаем буфер
|
||||||
|
p.buffer = make([]rune, 0)
|
||||||
|
p.pos = 0
|
||||||
|
|
||||||
|
// Показываем приглашение
|
||||||
|
promptStr := utils.ColorPrompt + "futriis:~> " + utils.ColorReset
|
||||||
|
fmt.Print(promptStr)
|
||||||
|
|
||||||
|
// Показываем курсор только после приглашения (в месте ввода)
|
||||||
|
fmt.Print(ansiShowCursor)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
for {
|
||||||
|
r, _, err := reader.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case 3: // Ctrl+C
|
||||||
|
return "", nil
|
||||||
|
|
||||||
|
case 4: // Ctrl+D
|
||||||
|
if len(p.buffer) == 0 {
|
||||||
|
return "exit", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case 13, 10: // Enter
|
||||||
|
// Скрываем курсор перед завершением
|
||||||
|
fmt.Print(ansiHideCursor)
|
||||||
|
fmt.Println()
|
||||||
|
cmd := string(p.buffer)
|
||||||
|
if cmd != "" {
|
||||||
|
p.history.Add(cmd)
|
||||||
|
}
|
||||||
|
p.history.Reset()
|
||||||
|
|
||||||
|
// Логируем команду
|
||||||
|
if logger := utils.GetLogger(); logger != nil && cmd != "" {
|
||||||
|
logger.Log("CMD", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
|
||||||
|
case 127: // Backspace
|
||||||
|
if p.pos > 0 {
|
||||||
|
// Удаляем символ перед курсором
|
||||||
|
p.buffer = append(p.buffer[:p.pos-1], p.buffer[p.pos:]...)
|
||||||
|
p.pos--
|
||||||
|
p.refreshLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
case 27: // Escape sequence (стрелки)
|
||||||
|
// Читаем следующие два символа
|
||||||
|
r2, _, _ := reader.ReadRune()
|
||||||
|
r3, _, _ := reader.ReadRune()
|
||||||
|
|
||||||
|
if r2 == '[' {
|
||||||
|
switch r3 {
|
||||||
|
case 'A': // Up arrow
|
||||||
|
prev := p.history.GetPrevious()
|
||||||
|
if prev != "" {
|
||||||
|
p.buffer = []rune(prev)
|
||||||
|
p.pos = len(p.buffer)
|
||||||
|
p.refreshLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'B': // Down arrow
|
||||||
|
next := p.history.GetNext()
|
||||||
|
p.buffer = []rune(next)
|
||||||
|
p.pos = len(p.buffer)
|
||||||
|
p.refreshLine()
|
||||||
|
|
||||||
|
case 'C': // Right arrow
|
||||||
|
if p.pos < len(p.buffer) {
|
||||||
|
p.pos++
|
||||||
|
p.refreshLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'D': // Left arrow
|
||||||
|
if p.pos > 0 {
|
||||||
|
p.pos--
|
||||||
|
p.refreshLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Добавляем символ (поддержка Unicode, включая русский)
|
||||||
|
if r >= 32 { // Печатные символы
|
||||||
|
// Вставляем символ в позицию курсора
|
||||||
|
if p.pos == len(p.buffer) {
|
||||||
|
p.buffer = append(p.buffer, r)
|
||||||
|
} else {
|
||||||
|
p.buffer = append(p.buffer[:p.pos], append([]rune{r}, p.buffer[p.pos:]...)...)
|
||||||
|
}
|
||||||
|
p.pos++
|
||||||
|
p.refreshLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshLine обновляет текущую строку с правильным позиционированием курсора
|
||||||
|
func (p *Prompt) refreshLine() {
|
||||||
|
// Скрываем курсор во время перерисовки
|
||||||
|
fmt.Print(ansiHideCursor)
|
||||||
|
|
||||||
|
// Возврат в начало строки и очистка
|
||||||
|
fmt.Print(ansiCarriageReturn + ansiClearLine)
|
||||||
|
|
||||||
|
// Печатаем приглашение
|
||||||
|
promptStr := utils.ColorPrompt + "futriis:~> " + utils.ColorReset
|
||||||
|
fmt.Print(promptStr)
|
||||||
|
|
||||||
|
// Печатаем текущий буфер
|
||||||
|
if len(p.buffer) > 0 {
|
||||||
|
fmt.Print(string(p.buffer))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем ширину приглашения
|
||||||
|
promptWidth := runewidth.StringWidth("futriis:~> ")
|
||||||
|
|
||||||
|
// Вычисляем позицию курсора
|
||||||
|
cursorPos := promptWidth
|
||||||
|
for i := 0; i < p.pos; i++ {
|
||||||
|
cursorPos += runewidth.RuneWidth(p.buffer[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перемещаем курсор на правильную позицию и показываем его
|
||||||
|
fmt.Printf("\033[%dG", cursorPos+1)
|
||||||
|
fmt.Print(ansiShowCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSimple читает строку без специальной обработки (fallback)
|
||||||
|
func (p *Prompt) readSimple() (string, error) {
|
||||||
|
fmt.Print(utils.GetPrompt())
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
cmd, err := reader.ReadString('\n')
|
||||||
|
if err == nil {
|
||||||
|
cmd = cmd[:len(cmd)-1] // Убираем \n
|
||||||
|
// Логируем команду
|
||||||
|
if logger := utils.GetLogger(); logger != nil && cmd != "" {
|
||||||
|
logger.Log("CMD", cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, err
|
||||||
|
}
|
||||||
105
internal/client/handler.go
Normal file
105
internal/client/handler.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// /futriis/internal/client/handler.go
|
||||||
|
// Пакет client реализует обработку команд клиента СУБД Futriis
|
||||||
|
// Обеспечивает взаимодействие с движком и форматированный вывод результатов
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"futriis/internal/engine"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandHandler обрабатывает команды клиента
|
||||||
|
type CommandHandler struct {
|
||||||
|
engine *engine.Engine
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandHandler создаёт новый обработчик команд
|
||||||
|
func NewCommandHandler(engine *engine.Engine) *CommandHandler {
|
||||||
|
return &CommandHandler{
|
||||||
|
engine: engine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCommand обрабатывает одну команду
|
||||||
|
func (h *CommandHandler) HandleCommand(input string) (bool, error) {
|
||||||
|
// Удаляем лишние пробелы
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
// Если ввод пустой, просто возвращаемся без вывода
|
||||||
|
if input == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Разбиваем на части для проверки команды выхода
|
||||||
|
parts := strings.Fields(input)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
command := strings.ToLower(parts[0])
|
||||||
|
|
||||||
|
// Проверяем команду выхода
|
||||||
|
if command == "exit" || command == "quit" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем команду через движок
|
||||||
|
result, err := h.engine.Execute(input)
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintError("%v", err)
|
||||||
|
} else if result != "" {
|
||||||
|
// Если результат не пустой, выводим его без дополнительного форматирования
|
||||||
|
// так как движок уже возвращает цветной результат
|
||||||
|
fmt.Println(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunREPL запускает цикл чтения-выполнения-вывода
|
||||||
|
func (h *CommandHandler) RunREPL() error {
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
|
// Проверяем, было ли восстановление из AOF
|
||||||
|
if h.engine.WasAOFRecovered() {
|
||||||
|
utils.PrintPromptMessage("State successfully recovered from AOF")
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.PrintPromptMessage("Welcome to Futriis DB. Type 'help' for command list.")
|
||||||
|
// Добавляем пустую строку после приветствия
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Выводим приглашение
|
||||||
|
fmt.Print(utils.GetPrompt())
|
||||||
|
|
||||||
|
// Читаем команду
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
input := scanner.Text()
|
||||||
|
|
||||||
|
// Обрабатываем команду
|
||||||
|
exit, err := h.HandleCommand(input)
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintError("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("ошибка чтения ввода: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
810
internal/cluster/node.go
Normal file
810
internal/cluster/node.go
Normal file
@ -0,0 +1,810 @@
|
|||||||
|
// /futriis/internal/cluster/node.go
|
||||||
|
// Пакет cluster реализует управление кластером, координацию узлов и репликацию данных.
|
||||||
|
// Обеспечивает обнаружение узлов, heartbeat механизм для мониторинга доступности,
|
||||||
|
// а также синхронную мастер-мастер репликацию между узлами кластера.
|
||||||
|
// Поддерживает автоматическое переключение ролей и балансировку нагрузки.
|
||||||
|
|
||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"futriis/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Простые функции для логирования (без зависимостей)
|
||||||
|
func printInfo(format string, args ...interface{}) {
|
||||||
|
fmt.Printf("\033[34m[INFO]\033[0m %s\n", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSuccess(format string, args ...interface{}) {
|
||||||
|
fmt.Printf("\033[32m[OK]\033[0m %s\n", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printWarning(format string, args ...interface{}) {
|
||||||
|
fmt.Printf("\033[33m[WARN]\033[0m %s\n", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printError(format string, args ...interface{}) {
|
||||||
|
fmt.Printf("\033[31m[ERROR]\033[0m %s\n", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeState состояние узла
|
||||||
|
type NodeState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateOffline NodeState = iota
|
||||||
|
StateJoining
|
||||||
|
StateOnline
|
||||||
|
StateLeaving
|
||||||
|
StateFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s NodeState) String() string {
|
||||||
|
switch s {
|
||||||
|
case StateOffline:
|
||||||
|
return "offline"
|
||||||
|
case StateJoining:
|
||||||
|
return "joining"
|
||||||
|
case StateOnline:
|
||||||
|
return "online"
|
||||||
|
case StateLeaving:
|
||||||
|
return "leaving"
|
||||||
|
case StateFailed:
|
||||||
|
return "failed"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicationMessage представляет сообщение репликации
|
||||||
|
type ReplicationMessage struct {
|
||||||
|
Type string `json:"type"` // "write", "sync", "ack"
|
||||||
|
Command string `json:"command"` // Команда (create, update, delete)
|
||||||
|
Args []interface{} `json:"args"` // Аргументы команды
|
||||||
|
Timestamp int64 `json:"timestamp"` // Временная метка
|
||||||
|
NodeID string `json:"node_id"` // ID исходного узла
|
||||||
|
ShardID string `json:"shard_id"` // ID шарда
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node представляет узел кластера (с wait-free указателями)
|
||||||
|
type Node struct {
|
||||||
|
ID string
|
||||||
|
Address string
|
||||||
|
state int32 // Атомарное состояние
|
||||||
|
LastSeen time.Time
|
||||||
|
ShardIDs []string
|
||||||
|
// Используем atomic.Value для wait-free доступа к изменяемым полям
|
||||||
|
nodeState unsafe.Pointer // Атомарный указатель на карту состояний
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState атомарно получает состояние узла
|
||||||
|
func (n *Node) GetState() NodeState {
|
||||||
|
return NodeState(atomic.LoadInt32(&n.state))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetState атомарно устанавливает состояние узла
|
||||||
|
func (n *Node) SetState(state NodeState) {
|
||||||
|
atomic.StoreInt32(&n.state, int32(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterManager управляет кластером (с wait-free операциями)
|
||||||
|
type ClusterManager struct {
|
||||||
|
nodes unsafe.Pointer // Атомарный указатель на карту узлов
|
||||||
|
coordinatorAddr string
|
||||||
|
nodeID string
|
||||||
|
localAddr string
|
||||||
|
isCoordinator int32 // Атомарный флаг
|
||||||
|
heartbeatStop chan struct{}
|
||||||
|
replicationStop chan struct{}
|
||||||
|
stats struct {
|
||||||
|
totalNodes int64
|
||||||
|
activeNodes int64
|
||||||
|
rebalancingCnt int64
|
||||||
|
}
|
||||||
|
replicationEnabled bool
|
||||||
|
masterMaster bool
|
||||||
|
replicationQueue chan ReplicationMessage
|
||||||
|
replicationWG sync.WaitGroup
|
||||||
|
shardManager *ShardManager // Менеджер шардинга
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterManager создаёт новый менеджер кластера
|
||||||
|
func NewClusterManager(cfg *config.Config) *ClusterManager {
|
||||||
|
// Определяем стратегию шардинга по умолчанию
|
||||||
|
shardingStrategy := ConsistentHashing
|
||||||
|
shardingEnabled := false
|
||||||
|
initialShards := 10
|
||||||
|
|
||||||
|
// Проверяем наличие настроек шардинга в конфигурации
|
||||||
|
// Используем значения по умолчанию, если поля отсутствуют
|
||||||
|
|
||||||
|
cm := &ClusterManager{
|
||||||
|
coordinatorAddr: cfg.Cluster.CoordinatorAddress,
|
||||||
|
nodeID: cfg.Node.ID,
|
||||||
|
localAddr: cfg.Node.Address,
|
||||||
|
heartbeatStop: make(chan struct{}),
|
||||||
|
replicationStop: make(chan struct{}),
|
||||||
|
replicationEnabled: cfg.Replication.Enabled,
|
||||||
|
masterMaster: cfg.Replication.MasterMaster,
|
||||||
|
replicationQueue: make(chan ReplicationMessage, 1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём менеджер шардинга с значениями по умолчанию
|
||||||
|
cm.shardManager = NewShardManager(
|
||||||
|
shardingStrategy,
|
||||||
|
cfg.Cluster.ReplicationFactor,
|
||||||
|
shardingEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Устанавливаем флаг координатора атомарно
|
||||||
|
isCoord := int32(0)
|
||||||
|
if cfg.Node.Address == cfg.Cluster.CoordinatorAddress {
|
||||||
|
isCoord = 1
|
||||||
|
}
|
||||||
|
atomic.StoreInt32(&cm.isCoordinator, isCoord)
|
||||||
|
|
||||||
|
// Создаём начальную карту узлов
|
||||||
|
nodes := make(map[string]*Node)
|
||||||
|
|
||||||
|
// Добавляем себя в кластер
|
||||||
|
selfNode := &Node{
|
||||||
|
ID: cfg.Node.ID,
|
||||||
|
Address: cfg.Node.Address,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
}
|
||||||
|
selfNode.SetState(StateOnline)
|
||||||
|
nodes[cfg.Node.ID] = selfNode
|
||||||
|
|
||||||
|
// Атомарно сохраняем карту узлов
|
||||||
|
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&nodes))
|
||||||
|
|
||||||
|
atomic.AddInt64(&cm.stats.totalNodes, 1)
|
||||||
|
atomic.AddInt64(&cm.stats.activeNodes, 1)
|
||||||
|
|
||||||
|
// Создаём начальные шарды если включён шардинг
|
||||||
|
if shardingEnabled {
|
||||||
|
// Получаем список всех узлов
|
||||||
|
allNodes := make([]string, 0)
|
||||||
|
for id := range nodes {
|
||||||
|
allNodes = append(allNodes, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём шарды
|
||||||
|
cm.shardManager.CreateShards(initialShards, allNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем обработчик репликации если включена мастер-мастер
|
||||||
|
if cm.replicationEnabled && cm.masterMaster {
|
||||||
|
go cm.startReplicationHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCoordinator атомарно проверяет, является ли узел координатором
|
||||||
|
func (cm *ClusterManager) IsCoordinator() bool {
|
||||||
|
return atomic.LoadInt32(&cm.isCoordinator) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start запускает кластерные сервисы
|
||||||
|
func (cm *ClusterManager) Start() error {
|
||||||
|
if cm.IsCoordinator() {
|
||||||
|
// Запускаем координатор
|
||||||
|
go cm.startCoordinator()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем heartbeat
|
||||||
|
go cm.heartbeatLoop()
|
||||||
|
|
||||||
|
printInfo("Кластерный менеджер запущен")
|
||||||
|
if cm.replicationEnabled && cm.masterMaster {
|
||||||
|
printInfo("Мастер-мастер репликация активирована")
|
||||||
|
}
|
||||||
|
if cm.shardManager.enabled {
|
||||||
|
printInfo("Шардинг активирован, стратегия: %v", cm.shardManager.getStrategyName())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop останавливает кластерные сервисы
|
||||||
|
func (cm *ClusterManager) Stop() {
|
||||||
|
close(cm.heartbeatStop)
|
||||||
|
if cm.replicationEnabled && cm.masterMaster {
|
||||||
|
close(cm.replicationStop)
|
||||||
|
cm.replicationWG.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicateCommand реплицирует команду на все узлы кластера с учётом шардинга
|
||||||
|
func (cm *ClusterManager) ReplicateCommand(cmd string, args []interface{}, key string) error {
|
||||||
|
if !cm.replicationEnabled || !cm.masterMaster {
|
||||||
|
return nil // Репликация не включена
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем шард для ключа
|
||||||
|
var shardID string
|
||||||
|
if cm.shardManager.enabled && key != "" {
|
||||||
|
shard, err := cm.shardManager.GetShardForKey(key)
|
||||||
|
if err == nil && shard != nil {
|
||||||
|
shardID = shard.ID
|
||||||
|
cm.shardManager.RecordWrite(shardID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := ReplicationMessage{
|
||||||
|
Type: "write",
|
||||||
|
Command: cmd,
|
||||||
|
Args: args,
|
||||||
|
Timestamp: time.Now().UnixNano(),
|
||||||
|
NodeID: cm.nodeID,
|
||||||
|
ShardID: shardID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем в очередь репликации
|
||||||
|
select {
|
||||||
|
case cm.replicationQueue <- msg:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("очередь репликации переполнена")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startReplicationHandler запускает обработчик репликации
|
||||||
|
func (cm *ClusterManager) startReplicationHandler() {
|
||||||
|
cm.replicationWG.Add(1)
|
||||||
|
defer cm.replicationWG.Done()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-cm.replicationQueue:
|
||||||
|
cm.sendReplicationMessage(msg)
|
||||||
|
case <-ticker.C:
|
||||||
|
cm.sendSyncRequest()
|
||||||
|
case <-cm.replicationStop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendReplicationMessage отправляет сообщение репликации на другие узлы
|
||||||
|
func (cm *ClusterManager) sendReplicationMessage(msg ReplicationMessage) {
|
||||||
|
// Получаем текущую карту узлов атомарно
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
for id, node := range nodes {
|
||||||
|
if id == cm.nodeID || node.GetState() != StateOnline {
|
||||||
|
continue // Не отправляем себе и офлайн узлам
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если есть шард, проверяем, должен ли узел получать это сообщение
|
||||||
|
if msg.ShardID != "" && cm.shardManager.enabled {
|
||||||
|
shard, exists := cm.shardManager.GetShardByID(msg.ShardID)
|
||||||
|
if exists {
|
||||||
|
shouldSend := false
|
||||||
|
for _, nodeID := range shard.Nodes {
|
||||||
|
if nodeID == id {
|
||||||
|
shouldSend = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !shouldSend {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(targetNode *Node) {
|
||||||
|
conn, err := net.DialTimeout("tcp", targetNode.Address, 3*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
printWarning("Не удалось подключиться к узлу %s для репликации: %v", targetNode.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(conn)
|
||||||
|
if err := encoder.Encode(msg); err != nil {
|
||||||
|
printWarning("Ошибка отправки репликации на узел %s: %v", targetNode.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаем подтверждение для синхронной репликации
|
||||||
|
if msg.Type == "write" {
|
||||||
|
var ack map[string]interface{}
|
||||||
|
decoder := json.NewDecoder(conn)
|
||||||
|
if err := decoder.Decode(&ack); err == nil {
|
||||||
|
if status, ok := ack["status"].(string); ok && status == "ok" {
|
||||||
|
printSuccess("Репликация на узел %s подтверждена", targetNode.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSyncRequest отправляет запрос синхронизации
|
||||||
|
func (cm *ClusterManager) sendSyncRequest() {
|
||||||
|
if !cm.IsCoordinator() {
|
||||||
|
return // Только координатор инициирует синхронизацию
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := ReplicationMessage{
|
||||||
|
Type: "sync",
|
||||||
|
Timestamp: time.Now().UnixNano(),
|
||||||
|
NodeID: cm.nodeID,
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.sendReplicationMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleReplicationMessage обрабатывает входящее сообщение репликации
|
||||||
|
func (cm *ClusterManager) handleReplicationMessage(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
var msg ReplicationMessage
|
||||||
|
decoder := json.NewDecoder(conn)
|
||||||
|
if err := decoder.Decode(&msg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case "write":
|
||||||
|
// Получаем команду от другого узла
|
||||||
|
printInfo("Получена команда репликации: %s от узла %s (шард: %s)",
|
||||||
|
msg.Command, msg.NodeID, msg.ShardID)
|
||||||
|
|
||||||
|
// Здесь будет вызов движка для выполнения команды
|
||||||
|
// TODO: Интегрировать с engine для выполнения реплицированных команд
|
||||||
|
|
||||||
|
// Отправляем подтверждение
|
||||||
|
encoder := json.NewEncoder(conn)
|
||||||
|
encoder.Encode(map[string]interface{}{
|
||||||
|
"status": "ok",
|
||||||
|
"time": time.Now().UnixNano(),
|
||||||
|
})
|
||||||
|
|
||||||
|
case "sync":
|
||||||
|
// Запрос синхронизации
|
||||||
|
printInfo("Получен запрос синхронизации от узла %s", msg.NodeID)
|
||||||
|
// TODO: Отправить текущее состояние
|
||||||
|
|
||||||
|
case "ack":
|
||||||
|
// Подтверждение получения
|
||||||
|
printInfo("Получено подтверждение от узла %s", msg.NodeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// heartbeatLoop отправляет heartbeat сигналы
|
||||||
|
func (cm *ClusterManager) heartbeatLoop() {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
cm.sendHeartbeat()
|
||||||
|
case <-cm.heartbeatStop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendHeartbeat отправляет heartbeat координатору
|
||||||
|
func (cm *ClusterManager) sendHeartbeat() {
|
||||||
|
if !cm.IsCoordinator() {
|
||||||
|
// Отправляем heartbeat координатору
|
||||||
|
conn, err := net.DialTimeout("tcp", cm.coordinatorAddr, 3*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
heartbeat := map[string]interface{}{
|
||||||
|
"type": "heartbeat",
|
||||||
|
"node_id": cm.nodeID,
|
||||||
|
"address": cm.localAddr,
|
||||||
|
"time": time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(conn).Encode(heartbeat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startCoordinator запускает координатор кластера
|
||||||
|
func (cm *ClusterManager) startCoordinator() {
|
||||||
|
listener, err := net.Listen("tcp", cm.coordinatorAddr)
|
||||||
|
if err != nil {
|
||||||
|
printError("Ошибка запуска координатора: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
printInfo("Координатор кластера запущен на " + cm.coordinatorAddr)
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go cm.handleCoordinatorRequest(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCoordinatorRequest обрабатывает запросы к координатору
|
||||||
|
func (cm *ClusterManager) handleCoordinatorRequest(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
var req map[string]interface{}
|
||||||
|
if err := json.NewDecoder(conn).Decode(&req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgType, _ := req["type"].(string)
|
||||||
|
|
||||||
|
switch msgType {
|
||||||
|
case "heartbeat":
|
||||||
|
nodeID, _ := req["node_id"].(string)
|
||||||
|
address, _ := req["address"].(string)
|
||||||
|
cm.updateNodeHeartbeat(nodeID, address)
|
||||||
|
|
||||||
|
case "join":
|
||||||
|
nodeID, _ := req["node_id"].(string)
|
||||||
|
address, _ := req["address"].(string)
|
||||||
|
cm.handleNodeJoin(nodeID, address)
|
||||||
|
|
||||||
|
case "leave":
|
||||||
|
nodeID, _ := req["node_id"].(string)
|
||||||
|
cm.handleNodeLeave(nodeID)
|
||||||
|
|
||||||
|
case "replication":
|
||||||
|
// Обработка сообщения репликации на координаторе
|
||||||
|
cm.handleReplicationMessage(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNodeJoin обрабатывает присоединение узла
|
||||||
|
func (cm *ClusterManager) handleNodeJoin(nodeID, address string) {
|
||||||
|
// Получаем текущую карту узлов
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldNodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
// Создаём новую карту
|
||||||
|
newNodes := make(map[string]*Node)
|
||||||
|
for k, v := range oldNodes {
|
||||||
|
newNodes[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if node, exists := newNodes[nodeID]; exists {
|
||||||
|
node.SetState(StateOnline)
|
||||||
|
node.LastSeen = time.Now()
|
||||||
|
} else {
|
||||||
|
newNode := &Node{
|
||||||
|
ID: nodeID,
|
||||||
|
Address: address,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
}
|
||||||
|
newNode.SetState(StateOnline)
|
||||||
|
newNodes[nodeID] = newNode
|
||||||
|
atomic.AddInt64(&cm.stats.totalNodes, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Атомарно обновляем карту узлов
|
||||||
|
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
|
||||||
|
|
||||||
|
atomic.AddInt64(&cm.stats.activeNodes, 1)
|
||||||
|
printSuccess("Узел %s (%s) присоединился к кластеру", nodeID, address)
|
||||||
|
|
||||||
|
// Если включена мастер-мастер репликация, отправляем текущее состояние новому узлу
|
||||||
|
if cm.replicationEnabled && cm.masterMaster {
|
||||||
|
go cm.sendInitialSync(nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если включён шардинг, обновляем распределение шардов
|
||||||
|
if cm.shardManager.enabled {
|
||||||
|
cm.rebalanceShards()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendInitialSync отправляет начальную синхронизацию новому узлу
|
||||||
|
func (cm *ClusterManager) sendInitialSync(targetNodeID string) {
|
||||||
|
// Получаем текущую карту узлов
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
targetNode, exists := nodes[targetNodeID]
|
||||||
|
if !exists || targetNode.GetState() != StateOnline {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Отправить текущее состояние базы данных новому узлу
|
||||||
|
printInfo("Отправка начальной синхронизации узлу %s", targetNodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNodeLeave обрабатывает отключение узла
|
||||||
|
func (cm *ClusterManager) handleNodeLeave(nodeID string) {
|
||||||
|
// Получаем текущую карту узлов
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldNodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
// Создаём новую карту
|
||||||
|
newNodes := make(map[string]*Node)
|
||||||
|
for k, v := range oldNodes {
|
||||||
|
if k == nodeID {
|
||||||
|
v.SetState(StateOffline)
|
||||||
|
newNodes[k] = v
|
||||||
|
} else {
|
||||||
|
newNodes[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Атомарно обновляем карту узлов
|
||||||
|
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
|
||||||
|
|
||||||
|
atomic.AddInt64(&cm.stats.activeNodes, -1)
|
||||||
|
printWarning("Узел %s покинул кластер", nodeID)
|
||||||
|
|
||||||
|
// Если включён шардинг, обновляем распределение шардов
|
||||||
|
if cm.shardManager.enabled {
|
||||||
|
cm.rebalanceShards()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateNodeHeartbeat обновляет время последнего heartbeat узла
|
||||||
|
func (cm *ClusterManager) updateNodeHeartbeat(nodeID, address string) {
|
||||||
|
// Получаем текущую карту узлов
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldNodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
// Проверяем, нужно ли обновление
|
||||||
|
node, exists := oldNodes[nodeID]
|
||||||
|
if exists && node.GetState() == StateOnline && time.Since(node.LastSeen) < 5*time.Second {
|
||||||
|
// Обновление не требуется, просто обновляем LastSeen в существующей карте
|
||||||
|
node.LastSeen = time.Now()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новую карту для обновления
|
||||||
|
newNodes := make(map[string]*Node)
|
||||||
|
for k, v := range oldNodes {
|
||||||
|
newNodes[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
node.LastSeen = time.Now()
|
||||||
|
if node.GetState() == StateOffline {
|
||||||
|
node.SetState(StateOnline)
|
||||||
|
atomic.AddInt64(&cm.stats.activeNodes, 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newNode := &Node{
|
||||||
|
ID: nodeID,
|
||||||
|
Address: address,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
}
|
||||||
|
newNode.SetState(StateOnline)
|
||||||
|
newNodes[nodeID] = newNode
|
||||||
|
atomic.AddInt64(&cm.stats.totalNodes, 1)
|
||||||
|
atomic.AddInt64(&cm.stats.activeNodes, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Атомарно обновляем карту узлов
|
||||||
|
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// rebalanceShards выполняет ребалансировку шардов при изменении состава кластера
|
||||||
|
func (cm *ClusterManager) rebalanceShards() {
|
||||||
|
if !cm.shardManager.enabled || !cm.IsCoordinator() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем текущие узлы
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
// Собираем онлайн узлы
|
||||||
|
onlineNodes := make([]string, 0)
|
||||||
|
for id, node := range nodes {
|
||||||
|
if node.GetState() == StateOnline {
|
||||||
|
onlineNodes = append(onlineNodes, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Здесь должна быть логика ребалансировки шардов
|
||||||
|
// В демо-версии просто перераспределяем существующие шарды
|
||||||
|
|
||||||
|
cm.shardManager.Rebalance()
|
||||||
|
|
||||||
|
atomic.AddInt64(&cm.stats.rebalancingCnt, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNode добавляет новый узел в кластер
|
||||||
|
func (cm *ClusterManager) AddNode(address string) error {
|
||||||
|
if !cm.IsCoordinator() {
|
||||||
|
return errors.New("только координатор может добавлять узлы")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем текущую карту узлов
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return errors.New("карта узлов не инициализирована")
|
||||||
|
}
|
||||||
|
oldNodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
// Генерируем ID для нового узла
|
||||||
|
nodeID := fmt.Sprintf("node-%d", len(oldNodes)+1)
|
||||||
|
|
||||||
|
// Создаём новую карту
|
||||||
|
newNodes := make(map[string]*Node)
|
||||||
|
for k, v := range oldNodes {
|
||||||
|
newNodes[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
newNode := &Node{
|
||||||
|
ID: nodeID,
|
||||||
|
Address: address,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
}
|
||||||
|
newNode.SetState(StateJoining)
|
||||||
|
newNodes[nodeID] = newNode
|
||||||
|
|
||||||
|
// Атомарно обновляем карту узлов
|
||||||
|
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
|
||||||
|
|
||||||
|
atomic.AddInt64(&cm.stats.totalNodes, 1)
|
||||||
|
|
||||||
|
printSuccess("Узел %s (%s) добавлен в кластер", nodeID, address)
|
||||||
|
|
||||||
|
// Если включён шардинг, обновляем распределение
|
||||||
|
if cm.shardManager.enabled {
|
||||||
|
cm.rebalanceShards()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNode удаляет узел из кластера
|
||||||
|
func (cm *ClusterManager) RemoveNode(nodeID string) error {
|
||||||
|
if !cm.IsCoordinator() {
|
||||||
|
return errors.New("только координатор может удалять узлы")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем текущую карту узлов
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return errors.New("карта узлов не инициализирована")
|
||||||
|
}
|
||||||
|
oldNodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
if _, exists := oldNodes[nodeID]; !exists {
|
||||||
|
return errors.New("узел не найден")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новую карту без удаляемого узла
|
||||||
|
newNodes := make(map[string]*Node)
|
||||||
|
for k, v := range oldNodes {
|
||||||
|
if k != nodeID {
|
||||||
|
newNodes[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Атомарно обновляем карту узлов
|
||||||
|
atomic.StorePointer(&cm.nodes, unsafe.Pointer(&newNodes))
|
||||||
|
|
||||||
|
atomic.AddInt64(&cm.stats.totalNodes, -1)
|
||||||
|
|
||||||
|
printSuccess("Узел %s удален из кластера", nodeID)
|
||||||
|
|
||||||
|
// Если включён шардинг, обновляем распределение
|
||||||
|
if cm.shardManager.enabled {
|
||||||
|
cm.rebalanceShards()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClusterStatus возвращает статус кластера
|
||||||
|
func (cm *ClusterManager) GetClusterStatus() map[string]interface{} {
|
||||||
|
status := make(map[string]interface{})
|
||||||
|
status["total_nodes"] = atomic.LoadInt64(&cm.stats.totalNodes)
|
||||||
|
status["active_nodes"] = atomic.LoadInt64(&cm.stats.activeNodes)
|
||||||
|
status["is_coordinator"] = cm.IsCoordinator()
|
||||||
|
status["coordinator"] = cm.coordinatorAddr
|
||||||
|
status["replication_enabled"] = cm.replicationEnabled
|
||||||
|
status["master_master"] = cm.masterMaster
|
||||||
|
|
||||||
|
// Добавляем информацию о шардинге
|
||||||
|
if cm.shardManager.enabled {
|
||||||
|
shardStats := cm.shardManager.GetShardStats()
|
||||||
|
status["sharding"] = shardStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем текущую карту узлов атомарно
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr != nil {
|
||||||
|
nodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
nodesList := make([]map[string]interface{}, 0, len(nodes))
|
||||||
|
for id, node := range nodes {
|
||||||
|
nodesList = append(nodesList, map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"address": node.Address,
|
||||||
|
"state": node.GetState().String(),
|
||||||
|
"last_seen": node.LastSeen.Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
status["nodes"] = nodesList
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// RebalanceCluster выполняет ребалансировку всего кластера
|
||||||
|
func (cm *ClusterManager) RebalanceCluster() error {
|
||||||
|
if !cm.IsCoordinator() {
|
||||||
|
return errors.New("only coordinator can rebalance the cluster")
|
||||||
|
}
|
||||||
|
|
||||||
|
printInfo("Starting cluster rebalance...")
|
||||||
|
|
||||||
|
// Получаем текущие узлы
|
||||||
|
nodesPtr := atomic.LoadPointer(&cm.nodes)
|
||||||
|
if nodesPtr == nil {
|
||||||
|
return errors.New("node map not initialized")
|
||||||
|
}
|
||||||
|
nodes := *(*map[string]*Node)(nodesPtr)
|
||||||
|
|
||||||
|
// Собираем онлайн узлы
|
||||||
|
onlineNodes := make([]string, 0)
|
||||||
|
for id, node := range nodes {
|
||||||
|
if node.GetState() == StateOnline {
|
||||||
|
onlineNodes = append(onlineNodes, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(onlineNodes) == 0 {
|
||||||
|
return errors.New("no online nodes available for rebalance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если включён шардинг, ребалансируем шарды
|
||||||
|
if cm.shardManager.enabled {
|
||||||
|
err := cm.shardManager.Rebalance()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&cm.stats.rebalancingCnt, 1)
|
||||||
|
|
||||||
|
printSuccess("Cluster rebalance completed successfully")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
448
internal/cluster/sharding.go
Normal file
448
internal/cluster/sharding.go
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
// /futriis/internal/cluster/sharding.go
|
||||||
|
// Пакет cluster реализует шардинг данных для распределённого хранения
|
||||||
|
// Данный файл содержит реализацию менеджера шардинга, который управляет
|
||||||
|
// распределением данных по шардам с поддержкой различных стратегий (consistent hashing, range-based, hash-based) и обеспечивает ребалансировку)
|
||||||
|
|
||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"sort"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShardingStrategy стратегия шардинга
|
||||||
|
type ShardingStrategy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConsistentHashing ShardingStrategy = iota
|
||||||
|
RangeBased
|
||||||
|
HashBased
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shard представляет шард данных
|
||||||
|
type Shard struct {
|
||||||
|
ID string
|
||||||
|
Nodes []string // ID узлов, содержащих этот шард
|
||||||
|
Range *KeyRange
|
||||||
|
DataSize int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
stats struct {
|
||||||
|
reads int64
|
||||||
|
writes int64
|
||||||
|
rebalances int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyRange диапазон ключей для range-based шардинга
|
||||||
|
type KeyRange struct {
|
||||||
|
Start string
|
||||||
|
End string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShardManager управляет шардированием
|
||||||
|
type ShardManager struct {
|
||||||
|
strategy ShardingStrategy
|
||||||
|
shards map[string]*Shard
|
||||||
|
virtualNodes int // Количество виртуальных нод для consistent hashing
|
||||||
|
hashRing []uint32 // Хэш-кольцо
|
||||||
|
hashToShard map[uint32]string // Хэш -> ID шарда
|
||||||
|
nodeToShards map[string][]string // Узел -> список шардов
|
||||||
|
shardToNodes map[string][]string // Шард -> список узлов
|
||||||
|
replicationFactor int
|
||||||
|
enabled bool
|
||||||
|
stats struct {
|
||||||
|
totalShards int64
|
||||||
|
totalMoves int64
|
||||||
|
rebalances int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewShardManager создаёт новый менеджер шардинга
|
||||||
|
func NewShardManager(strategy ShardingStrategy, replicationFactor int, enabled bool) *ShardManager {
|
||||||
|
sm := &ShardManager{
|
||||||
|
strategy: strategy,
|
||||||
|
shards: make(map[string]*Shard),
|
||||||
|
hashRing: make([]uint32, 0),
|
||||||
|
hashToShard: make(map[uint32]string),
|
||||||
|
nodeToShards: make(map[string][]string),
|
||||||
|
shardToNodes: make(map[string][]string),
|
||||||
|
virtualNodes: 100, // По умолчанию 100 виртуальных нод
|
||||||
|
replicationFactor: replicationFactor,
|
||||||
|
enabled: enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
utils.PrintInfo("Шардинг активирован, стратегия: %v, фактор репликации: %d",
|
||||||
|
sm.getStrategyName(), replicationFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sm
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStrategyName возвращает название стратегии
|
||||||
|
func (sm *ShardManager) getStrategyName() string {
|
||||||
|
switch sm.strategy {
|
||||||
|
case ConsistentHashing:
|
||||||
|
return "consistent_hashing"
|
||||||
|
case RangeBased:
|
||||||
|
return "range_based"
|
||||||
|
case HashBased:
|
||||||
|
return "hash_based"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateShards создаёт начальные шарды
|
||||||
|
func (sm *ShardManager) CreateShards(numShards int, nodes []string) error {
|
||||||
|
if !sm.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sm.strategy {
|
||||||
|
case ConsistentHashing:
|
||||||
|
return sm.createConsistentHashShards(numShards, nodes)
|
||||||
|
case RangeBased:
|
||||||
|
return sm.createRangeBasedShards(numShards, nodes)
|
||||||
|
case HashBased:
|
||||||
|
return sm.createHashBasedShards(numShards, nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createConsistentHashShards создаёт шарды для consistent hashing
|
||||||
|
func (sm *ShardManager) createConsistentHashShards(numShards int, nodes []string) error {
|
||||||
|
// Очищаем кольцо
|
||||||
|
sm.hashRing = make([]uint32, 0)
|
||||||
|
sm.hashToShard = make(map[uint32]string)
|
||||||
|
|
||||||
|
// Создаём шарды
|
||||||
|
for i := 0; i < numShards; i++ {
|
||||||
|
shardID := fmt.Sprintf("shard-%d", i+1)
|
||||||
|
|
||||||
|
shard := &Shard{
|
||||||
|
ID: shardID,
|
||||||
|
Nodes: make([]string, 0),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.shards[shardID] = shard
|
||||||
|
|
||||||
|
// Добавляем виртуальные ноды для шарда в хэш-кольцо
|
||||||
|
for j := 0; j < sm.virtualNodes; j++ {
|
||||||
|
vnodeKey := fmt.Sprintf("%s:%d", shardID, j)
|
||||||
|
hash := crc32.ChecksumIEEE([]byte(vnodeKey))
|
||||||
|
sm.hashRing = append(sm.hashRing, hash)
|
||||||
|
sm.hashToShard[hash] = shardID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем кольцо
|
||||||
|
sort.Slice(sm.hashRing, func(i, j int) bool {
|
||||||
|
return sm.hashRing[i] < sm.hashRing[j]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Распределяем шарды по узлам
|
||||||
|
sm.distributeShardsToNodes(nodes)
|
||||||
|
|
||||||
|
atomic.AddInt64(&sm.stats.totalShards, int64(numShards))
|
||||||
|
|
||||||
|
utils.PrintSuccess("Создано %d шардов с consistent hashing", numShards)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createRangeBasedShards создаёт шарды на основе диапазонов
|
||||||
|
func (sm *ShardManager) createRangeBasedShards(numShards int, nodes []string) error {
|
||||||
|
// Разбиваем ключевое пространство на диапазоны
|
||||||
|
// Используем hex-строки от "00" до "ff" для простоты
|
||||||
|
|
||||||
|
totalRange := 256 // 0x00 - 0xff
|
||||||
|
rangeSize := totalRange / numShards
|
||||||
|
|
||||||
|
for i := 0; i < numShards; i++ {
|
||||||
|
start := fmt.Sprintf("%02x", i*rangeSize)
|
||||||
|
end := fmt.Sprintf("%02x", (i+1)*rangeSize-1)
|
||||||
|
|
||||||
|
if i == numShards-1 {
|
||||||
|
end = "ff"
|
||||||
|
}
|
||||||
|
|
||||||
|
shardID := fmt.Sprintf("range-shard-%d", i+1)
|
||||||
|
|
||||||
|
shard := &Shard{
|
||||||
|
ID: shardID,
|
||||||
|
Range: &KeyRange{
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
},
|
||||||
|
Nodes: make([]string, 0),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.shards[shardID] = shard
|
||||||
|
}
|
||||||
|
|
||||||
|
// Распределяем шарды по узлам
|
||||||
|
sm.distributeShardsToNodes(nodes)
|
||||||
|
|
||||||
|
atomic.AddInt64(&sm.stats.totalShards, int64(numShards))
|
||||||
|
|
||||||
|
utils.PrintSuccess("Создано %d range-based шардов", numShards)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createHashBasedShards создаёт шарды на основе хэша
|
||||||
|
func (sm *ShardManager) createHashBasedShards(numShards int, nodes []string) error {
|
||||||
|
for i := 0; i < numShards; i++ {
|
||||||
|
shardID := fmt.Sprintf("hash-shard-%d", i+1)
|
||||||
|
|
||||||
|
shard := &Shard{
|
||||||
|
ID: shardID,
|
||||||
|
Nodes: make([]string, 0),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.shards[shardID] = shard
|
||||||
|
}
|
||||||
|
|
||||||
|
// Распределяем шарды по узлам
|
||||||
|
sm.distributeShardsToNodes(nodes)
|
||||||
|
|
||||||
|
atomic.AddInt64(&sm.stats.totalShards, int64(numShards))
|
||||||
|
|
||||||
|
utils.PrintSuccess("Создано %d hash-based шардов", numShards)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// distributeShardsToNodes распределяет шарды по узлам
|
||||||
|
func (sm *ShardManager) distributeShardsToNodes(nodes []string) {
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeCount := len(nodes)
|
||||||
|
shardCount := len(sm.shards)
|
||||||
|
|
||||||
|
// Равномерно распределяем шарды по узлам
|
||||||
|
shardsPerNode := shardCount / nodeCount
|
||||||
|
remainder := shardCount % nodeCount
|
||||||
|
|
||||||
|
shardIndex := 0
|
||||||
|
shardIDs := make([]string, 0, shardCount)
|
||||||
|
for id := range sm.shards {
|
||||||
|
shardIDs = append(shardIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, nodeID := range nodes {
|
||||||
|
numShardsForNode := shardsPerNode
|
||||||
|
if i < remainder {
|
||||||
|
numShardsForNode++
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeShards := make([]string, 0)
|
||||||
|
|
||||||
|
for j := 0; j < numShardsForNode && shardIndex < len(shardIDs); j++ {
|
||||||
|
shardID := shardIDs[shardIndex]
|
||||||
|
// Добавляем узел к шарду
|
||||||
|
sm.addNodeToShard(shardID, nodeID)
|
||||||
|
nodeShards = append(nodeShards, shardID)
|
||||||
|
shardIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.nodeToShards[nodeID] = nodeShards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNodeToShard добавляет узел к шарду
|
||||||
|
func (sm *ShardManager) addNodeToShard(shardID, nodeID string) {
|
||||||
|
shard, exists := sm.shards[shardID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем узел к шарду
|
||||||
|
shard.Nodes = append(shard.Nodes, nodeID)
|
||||||
|
|
||||||
|
// Обновляем маппинг
|
||||||
|
sm.shardToNodes[shardID] = append(sm.shardToNodes[shardID], nodeID)
|
||||||
|
|
||||||
|
// Обновляем маппинг узла к шардам
|
||||||
|
sm.nodeToShards[nodeID] = append(sm.nodeToShards[nodeID], shardID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShardForKey определяет шард для ключа
|
||||||
|
func (sm *ShardManager) GetShardForKey(key string) (*Shard, error) {
|
||||||
|
if !sm.enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sm.strategy {
|
||||||
|
case ConsistentHashing:
|
||||||
|
return sm.getShardConsistentHashing(key)
|
||||||
|
case RangeBased:
|
||||||
|
return sm.getShardRangeBased(key)
|
||||||
|
case HashBased:
|
||||||
|
return sm.getShardHashBased(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("неизвестная стратегия шардинга")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getShardConsistentHashing получает шард через consistent hashing
|
||||||
|
func (sm *ShardManager) getShardConsistentHashing(key string) (*Shard, error) {
|
||||||
|
if len(sm.hashRing) == 0 {
|
||||||
|
return nil, errors.New("хэш-кольцо пусто")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := crc32.ChecksumIEEE([]byte(key))
|
||||||
|
|
||||||
|
// Бинарный поиск в кольце
|
||||||
|
idx := sort.Search(len(sm.hashRing), func(i int) bool {
|
||||||
|
return sm.hashRing[i] >= hash
|
||||||
|
})
|
||||||
|
|
||||||
|
if idx == len(sm.hashRing) {
|
||||||
|
idx = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
shardID := sm.hashToShard[sm.hashRing[idx]]
|
||||||
|
shard, exists := sm.shards[shardID]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("шард не найден")
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&shard.stats.reads, 1)
|
||||||
|
|
||||||
|
return shard, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getShardRangeBased получает шард по диапазону
|
||||||
|
func (sm *ShardManager) getShardRangeBased(key string) (*Shard, error) {
|
||||||
|
// Вычисляем хэш ключа для определения диапазона
|
||||||
|
hash := md5.Sum([]byte(key))
|
||||||
|
keyHex := hex.EncodeToString(hash[:1]) // Используем первый байт
|
||||||
|
|
||||||
|
for _, shard := range sm.shards {
|
||||||
|
if shard.Range != nil {
|
||||||
|
if keyHex >= shard.Range.Start && keyHex <= shard.Range.End {
|
||||||
|
atomic.AddInt64(&shard.stats.reads, 1)
|
||||||
|
return shard, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("шард не найден для ключа")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getShardHashBased получает шард по хэшу
|
||||||
|
func (sm *ShardManager) getShardHashBased(key string) (*Shard, error) {
|
||||||
|
hash := crc32.ChecksumIEEE([]byte(key))
|
||||||
|
shardIndex := int(hash) % len(sm.shards)
|
||||||
|
|
||||||
|
// Получаем отсортированный список ID шардов
|
||||||
|
shardIDs := make([]string, 0, len(sm.shards))
|
||||||
|
for id := range sm.shards {
|
||||||
|
shardIDs = append(shardIDs, id)
|
||||||
|
}
|
||||||
|
sort.Strings(shardIDs)
|
||||||
|
|
||||||
|
if shardIndex >= 0 && shardIndex < len(shardIDs) {
|
||||||
|
shardID := shardIDs[shardIndex]
|
||||||
|
shard, exists := sm.shards[shardID]
|
||||||
|
if exists {
|
||||||
|
atomic.AddInt64(&shard.stats.reads, 1)
|
||||||
|
return shard, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("шард не найден")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShardByID возвращает шард по ID
|
||||||
|
func (sm *ShardManager) GetShardByID(shardID string) (*Shard, bool) {
|
||||||
|
shard, exists := sm.shards[shardID]
|
||||||
|
return shard, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordWrite записывает статистику записи в шард
|
||||||
|
func (sm *ShardManager) RecordWrite(shardID string) {
|
||||||
|
if !sm.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shard, exists := sm.shards[shardID]
|
||||||
|
if exists {
|
||||||
|
atomic.AddInt64(&shard.stats.writes, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebalance выполняет ребалансировку шардов
|
||||||
|
func (sm *ShardManager) Rebalance() error {
|
||||||
|
if !sm.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.PrintInfo("Запуск ребалансировки шардов...")
|
||||||
|
|
||||||
|
atomic.AddInt64(&sm.stats.rebalances, 1)
|
||||||
|
|
||||||
|
// Здесь должна быть логика ребалансировки
|
||||||
|
// Перемещение шардов между узлами для равномерной загрузки
|
||||||
|
|
||||||
|
for _, shard := range sm.shards {
|
||||||
|
atomic.AddInt64(&shard.stats.rebalances, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&sm.stats.totalMoves, int64(len(sm.shards)/2))
|
||||||
|
|
||||||
|
utils.PrintSuccess("Ребалансировка завершена")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShardStats возвращает статистику шардов
|
||||||
|
func (sm *ShardManager) GetShardStats() map[string]interface{} {
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
stats["enabled"] = sm.enabled
|
||||||
|
stats["strategy"] = sm.getStrategyName()
|
||||||
|
stats["total_shards"] = atomic.LoadInt64(&sm.stats.totalShards)
|
||||||
|
stats["total_rebalances"] = atomic.LoadInt64(&sm.stats.rebalances)
|
||||||
|
stats["total_moves"] = atomic.LoadInt64(&sm.stats.totalMoves)
|
||||||
|
|
||||||
|
shardStats := make([]map[string]interface{}, 0)
|
||||||
|
|
||||||
|
for id, shard := range sm.shards {
|
||||||
|
sStats := map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"nodes": shard.Nodes,
|
||||||
|
"reads": atomic.LoadInt64(&shard.stats.reads),
|
||||||
|
"writes": atomic.LoadInt64(&shard.stats.writes),
|
||||||
|
"rebalances": atomic.LoadInt64(&shard.stats.rebalances),
|
||||||
|
"created": shard.CreatedAt.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
if shard.Range != nil {
|
||||||
|
sStats["range_start"] = shard.Range.Start
|
||||||
|
sStats["range_end"] = shard.Range.End
|
||||||
|
}
|
||||||
|
|
||||||
|
shardStats = append(shardStats, sStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["shards"] = shardStats
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
993
internal/engine/engine.go
Normal file
993
internal/engine/engine.go
Normal file
@ -0,0 +1,993 @@
|
|||||||
|
// /futriis/internal/engine/engine.go
|
||||||
|
// Пакет engine реализует ядро СУБД Futriis, координирующее все операции.
|
||||||
|
// Выступает в роли центрального компонента, связывающего хранилище, кластерное управление, транзакции, Lua плагины и AOF (Append-Only File) для персистентности.
|
||||||
|
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"futriis/internal/cluster"
|
||||||
|
"futriis/internal/lua"
|
||||||
|
"futriis/internal/replication"
|
||||||
|
"futriis/internal/storage"
|
||||||
|
"futriis/internal/transaction"
|
||||||
|
"futriis/pkg/config"
|
||||||
|
"futriis/pkg/types"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine представляет ядро СУБД
|
||||||
|
type Engine struct {
|
||||||
|
storage *storage.Storage
|
||||||
|
clusterMgr *cluster.ClusterManager
|
||||||
|
txMgr *transaction.TransactionManager
|
||||||
|
luaMgr *lua.PluginManager
|
||||||
|
aofMgr *replication.AOFManager
|
||||||
|
cfg *config.Config
|
||||||
|
aofRecovered bool // Флаг, было ли восстановление из AOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEngine создаёт новый экземпляр ядра СУБД
|
||||||
|
func NewEngine() *Engine {
|
||||||
|
cfg := config.Get()
|
||||||
|
|
||||||
|
// Создаём AOF менеджер
|
||||||
|
aofMgr, _ := replication.NewAOFManager(cfg.Node.AOFFile, cfg.Node.AOFEnabled)
|
||||||
|
|
||||||
|
// Воспроизводим AOF если нужно
|
||||||
|
aofRecovered := false
|
||||||
|
if aofMgr != nil && cfg.Node.AOFEnabled {
|
||||||
|
// Восстановление состояния из AOF
|
||||||
|
if err := replayAOF(aofMgr); err != nil {
|
||||||
|
utils.PrintError("Error recovering from AOF: %v", err)
|
||||||
|
} else {
|
||||||
|
aofRecovered = true
|
||||||
|
// Сообщение будет показано в handler.go
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Engine{
|
||||||
|
storage: storage.NewStorage(),
|
||||||
|
clusterMgr: cluster.NewClusterManager(cfg),
|
||||||
|
txMgr: transaction.NewTransactionManager(),
|
||||||
|
luaMgr: lua.NewPluginManager(&cfg.Lua),
|
||||||
|
aofMgr: aofMgr,
|
||||||
|
cfg: cfg,
|
||||||
|
aofRecovered: aofRecovered,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig возвращает конфигурацию
|
||||||
|
func (e *Engine) GetConfig() *config.Config {
|
||||||
|
return e.cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// WasAOFRecovered возвращает флаг восстановления из AOF
|
||||||
|
func (e *Engine) WasAOFRecovered() bool {
|
||||||
|
return e.aofRecovered
|
||||||
|
}
|
||||||
|
|
||||||
|
// replayAOF воспроизводит команды из AOF файла для восстановления состояния
|
||||||
|
func replayAOF(aofMgr *replication.AOFManager) error {
|
||||||
|
commands, err := aofMgr.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read AOF: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём временное хранилище для восстановления
|
||||||
|
tempStorage := storage.NewStorage()
|
||||||
|
|
||||||
|
for i, cmd := range commands {
|
||||||
|
// Пропускаем команды транзакций при восстановлении
|
||||||
|
if cmd.Name == "begin" || cmd.Name == "commit" || cmd.Name == "rollback" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразуем аргументы в строки
|
||||||
|
args := make([]string, len(cmd.Args))
|
||||||
|
for j, arg := range cmd.Args {
|
||||||
|
if str, ok := arg.(string); ok {
|
||||||
|
args[j] = str
|
||||||
|
} else {
|
||||||
|
args[j] = fmt.Sprint(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем команду на временном хранилище
|
||||||
|
if err := executeRestoreCommand(tempStorage, cmd.Name, args); err != nil {
|
||||||
|
utils.PrintWarning("Error replaying command #%d (%s): %v", i+1, cmd.Name, err)
|
||||||
|
// Продолжаем восстановление, несмотря на ошибки
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Перенести восстановленные данные в основное хранилище
|
||||||
|
// Это упрощённая реализация, в реальности нужно синхронизировать состояния
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeRestoreCommand выполняет команду при восстановлении из AOF
|
||||||
|
func executeRestoreCommand(storage *storage.Storage, cmdName string, args []string) error {
|
||||||
|
switch cmdName {
|
||||||
|
case "create":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "tapple":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := storage.GetTappleManager().CreateTapple(args[1])
|
||||||
|
return err
|
||||||
|
case "slice":
|
||||||
|
if len(args) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tapple, err := storage.GetTappleManager().GetTapple(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = storage.GetTappleManager().GetSliceManager().CreateSlice(tapple, args[2])
|
||||||
|
return err
|
||||||
|
case "tuple":
|
||||||
|
if len(args) < 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tapple, err := storage.GetTappleManager().GetTapple(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slice, err := storage.GetTappleManager().GetSliceManager().GetSlice(tapple, args[2])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
for i := 4; i < len(args); i++ {
|
||||||
|
parts := strings.SplitN(args[i], "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
fields[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = storage.GetTappleManager().GetSliceManager().GetTupleManager().CreateTuple(slice, args[3], fields)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "update":
|
||||||
|
if len(args) < 4 || args[0] != "tuple" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tapple, err := storage.GetTappleManager().GetTapple(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slice, err := storage.GetTappleManager().GetSliceManager().GetSlice(tapple, args[2])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
for i := 4; i < len(args); i++ {
|
||||||
|
parts := strings.SplitN(args[i], "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
fields[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = storage.GetTappleManager().GetSliceManager().GetTupleManager().UpdateTuple(slice, args[3], fields)
|
||||||
|
return err
|
||||||
|
case "delete":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "tapple":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return storage.GetTappleManager().DeleteTapple(args[1])
|
||||||
|
case "slice":
|
||||||
|
if len(args) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tapple, err := storage.GetTappleManager().GetTapple(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.GetTappleManager().GetSliceManager().DeleteSlice(tapple, args[2])
|
||||||
|
case "tuple":
|
||||||
|
if len(args) < 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tapple, err := storage.GetTappleManager().GetTapple(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slice, err := storage.GetTappleManager().GetSliceManager().GetSlice(tapple, args[2])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return storage.GetTappleManager().GetSliceManager().GetTupleManager().DeleteTuple(slice, args[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute выполняет команду и возвращает результат
|
||||||
|
func (e *Engine) Execute(input string) (string, error) {
|
||||||
|
// Разбираем ввод
|
||||||
|
parts := strings.Fields(input)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
command := strings.ToLower(parts[0])
|
||||||
|
args := parts[1:]
|
||||||
|
|
||||||
|
// Записываем команду в AOF (кроме команд транзакций и служебных команд)
|
||||||
|
if e.aofMgr != nil && command != "begin" && command != "commit" && command != "rollback" &&
|
||||||
|
command != "cluster.status" && command != "help" && command != "exit" && command != "quit" &&
|
||||||
|
command != "aof.recover" && command != "aof.info" &&
|
||||||
|
!strings.HasPrefix(command, "add.prime.index") && !strings.HasPrefix(command, "delete.prime.index") &&
|
||||||
|
!strings.HasPrefix(command, "add.secondary.index") && !strings.HasPrefix(command, "delete.secondary.index") &&
|
||||||
|
command != "cluster.rebalance" {
|
||||||
|
argsInterface := make([]interface{}, len(args))
|
||||||
|
for i, v := range args {
|
||||||
|
argsInterface[i] = v
|
||||||
|
}
|
||||||
|
e.aofMgr.Append(command, argsInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка команд
|
||||||
|
switch command {
|
||||||
|
case "help":
|
||||||
|
return e.help(), nil
|
||||||
|
|
||||||
|
case "exit", "quit":
|
||||||
|
return "exit", nil
|
||||||
|
|
||||||
|
case "create":
|
||||||
|
return e.handleCreate(args)
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
return e.handleDelete(args)
|
||||||
|
|
||||||
|
case "update":
|
||||||
|
return e.handleUpdate(args)
|
||||||
|
|
||||||
|
case "list":
|
||||||
|
return e.handleList(args)
|
||||||
|
|
||||||
|
case "show":
|
||||||
|
return e.handleShow(args)
|
||||||
|
|
||||||
|
case "begin":
|
||||||
|
return e.handleBegin()
|
||||||
|
|
||||||
|
case "commit":
|
||||||
|
return e.handleCommit()
|
||||||
|
|
||||||
|
case "rollback":
|
||||||
|
return e.handleRollback()
|
||||||
|
|
||||||
|
case "cluster.status":
|
||||||
|
return e.handleClusterStatus()
|
||||||
|
|
||||||
|
case "cluster.rebalance":
|
||||||
|
return e.handleClusterRebalance(args)
|
||||||
|
|
||||||
|
case "add.node":
|
||||||
|
return e.handleAddNode(args)
|
||||||
|
|
||||||
|
case "evict.node":
|
||||||
|
return e.handleEvictNode(args)
|
||||||
|
|
||||||
|
case "lua":
|
||||||
|
return e.handleLua(args)
|
||||||
|
|
||||||
|
case "aof.recover":
|
||||||
|
return e.handleAOFRecover(args)
|
||||||
|
|
||||||
|
case "aof.info":
|
||||||
|
return e.handleAOFInfo()
|
||||||
|
|
||||||
|
case "add.prime.index":
|
||||||
|
return e.handleAddPrimaryIndex(args)
|
||||||
|
|
||||||
|
case "delete.prime.index":
|
||||||
|
return e.handleDeletePrimaryIndex(args)
|
||||||
|
|
||||||
|
case "add.secondary.index":
|
||||||
|
return e.handleAddSecondaryIndex(args)
|
||||||
|
|
||||||
|
case "delete.secondary.index":
|
||||||
|
return e.handleDeleteSecondaryIndex(args)
|
||||||
|
|
||||||
|
case "compression.stats":
|
||||||
|
return e.handleCompressionStats(args)
|
||||||
|
|
||||||
|
case "sharding.status":
|
||||||
|
return e.handleShardingStatus()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown command: %s", command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAOFRecover восстанавливает данные из AOF файла
|
||||||
|
func (e *Engine) handleAOFRecover(args []string) (string, error) {
|
||||||
|
if e.aofMgr == nil {
|
||||||
|
return "", fmt.Errorf("AOF manager not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, указан ли путь к файлу
|
||||||
|
filePath := e.cfg.Node.AOFFile
|
||||||
|
if len(args) > 0 {
|
||||||
|
filePath = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём временный AOF менеджер для чтения указанного файла
|
||||||
|
tmpAOF, err := replication.NewAOFManager(filePath, true)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open AOF file: %v", err)
|
||||||
|
}
|
||||||
|
defer tmpAOF.Close()
|
||||||
|
|
||||||
|
// Читаем все команды
|
||||||
|
commands, err := tmpAOF.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read AOF file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commands) == 0 {
|
||||||
|
return utils.ColorYellow + "AOF file is empty" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём временное хранилище для проверки
|
||||||
|
tempStorage := storage.NewStorage()
|
||||||
|
successCount := 0
|
||||||
|
errorCount := 0
|
||||||
|
|
||||||
|
for i, cmd := range commands {
|
||||||
|
if cmd.Name == "begin" || cmd.Name == "commit" || cmd.Name == "rollback" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
args := make([]string, len(cmd.Args))
|
||||||
|
for j, arg := range cmd.Args {
|
||||||
|
if str, ok := arg.(string); ok {
|
||||||
|
args[j] = str
|
||||||
|
} else {
|
||||||
|
args[j] = fmt.Sprint(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executeRestoreCommand(tempStorage, cmd.Name, args); err != nil {
|
||||||
|
utils.PrintWarning("Error in command #%d: %v", i+1, err)
|
||||||
|
errorCount++
|
||||||
|
} else {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(utils.ColorGreen+"Recovery completed. Successful: %d, Errors: %d"+utils.ColorReset,
|
||||||
|
successCount, errorCount), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAOFInfo показывает информацию о AOF файле
|
||||||
|
func (e *Engine) handleAOFInfo() (string, error) {
|
||||||
|
if e.aofMgr == nil {
|
||||||
|
return "", fmt.Errorf("AOF manager not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о файле напрямую
|
||||||
|
filePath := e.cfg.Node.AOFFile
|
||||||
|
commands, err := e.aofMgr.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read AOF file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем размер файла
|
||||||
|
fileInfo, err := e.aofMgr.GetFileInfo()
|
||||||
|
if err != nil {
|
||||||
|
fileInfo = "unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := utils.ColorCyan + "AOF Information:" + utils.ColorReset + "\n"
|
||||||
|
result += fmt.Sprintf(" File: %s\n", filePath)
|
||||||
|
result += fmt.Sprintf(" Size: %v\n", fileInfo)
|
||||||
|
result += fmt.Sprintf(" Commands: %d\n", len(commands))
|
||||||
|
if len(commands) > 0 {
|
||||||
|
lastCmd := commands[len(commands)-1]
|
||||||
|
result += fmt.Sprintf(" Last write: %d\n", lastCmd.Time)
|
||||||
|
} else {
|
||||||
|
result += " Last write: no records\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleClusterRebalance выполняет ребалансировку кластера
|
||||||
|
func (e *Engine) handleClusterRebalance(args []string) (string, error) {
|
||||||
|
clusterName := "futriis-cluster"
|
||||||
|
if len(args) > 0 {
|
||||||
|
clusterName = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.clusterMgr.RebalanceCluster()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cluster rebalance failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Cluster '" + clusterName + "' rebalanced successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreate обрабатывает команды создания
|
||||||
|
func (e *Engine) handleCreate(args []string) (string, error) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for create command")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "tapple":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "", fmt.Errorf("tapple name not specified")
|
||||||
|
}
|
||||||
|
return e.createTapple(args[1])
|
||||||
|
case "slice":
|
||||||
|
if len(args) < 3 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for slice creation")
|
||||||
|
}
|
||||||
|
return e.createSlice(args[1], args[2])
|
||||||
|
case "tuple":
|
||||||
|
if len(args) < 4 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for tuple creation")
|
||||||
|
}
|
||||||
|
return e.createTuple(args[1], args[2], args[3], args[4:])
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown creation type: %s", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete обрабатывает команды удаления
|
||||||
|
func (e *Engine) handleDelete(args []string) (string, error) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for delete command")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "tapple":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "", fmt.Errorf("tapple name not specified")
|
||||||
|
}
|
||||||
|
return e.deleteTapple(args[1])
|
||||||
|
case "slice":
|
||||||
|
if len(args) < 3 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for slice deletion")
|
||||||
|
}
|
||||||
|
return e.deleteSlice(args[1], args[2])
|
||||||
|
case "tuple":
|
||||||
|
if len(args) < 4 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for tuple deletion")
|
||||||
|
}
|
||||||
|
return e.deleteTuple(args[1], args[2], args[3])
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown deletion type: %s", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdate обрабатывает команды обновления
|
||||||
|
func (e *Engine) handleUpdate(args []string) (string, error) {
|
||||||
|
if len(args) < 4 || args[0] != "tuple" {
|
||||||
|
return "", fmt.Errorf("invalid update command")
|
||||||
|
}
|
||||||
|
return e.updateTuple(args[1], args[2], args[3], args[4:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleList обрабатывает команды списка
|
||||||
|
func (e *Engine) handleList(args []string) (string, error) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for list command")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "tapples":
|
||||||
|
return e.listTapples()
|
||||||
|
case "slices":
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "", fmt.Errorf("tapple name not specified")
|
||||||
|
}
|
||||||
|
return e.listSlices(args[1])
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unknown list type: %s", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleShow обрабатывает команды показа
|
||||||
|
func (e *Engine) handleShow(args []string) (string, error) {
|
||||||
|
if len(args) < 2 || args[0] != "tuples" {
|
||||||
|
return "", fmt.Errorf("invalid show command")
|
||||||
|
}
|
||||||
|
if len(args) < 3 {
|
||||||
|
return "", fmt.Errorf("insufficient arguments for show tuples")
|
||||||
|
}
|
||||||
|
return e.showTuples(args[1], args[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBegin начинает транзакцию
|
||||||
|
func (e *Engine) handleBegin() (string, error) {
|
||||||
|
id, err := e.txMgr.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return utils.ColorGreen + "Transaction started. ID: " + id + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCommit фиксирует транзакцию
|
||||||
|
func (e *Engine) handleCommit() (string, error) {
|
||||||
|
err := e.txMgr.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return utils.ColorGreen + "Transaction committed" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRollback откатывает транзакцию
|
||||||
|
func (e *Engine) handleRollback() (string, error) {
|
||||||
|
err := e.txMgr.Rollback()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return utils.ColorGreen + "Transaction rolled back" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleClusterStatus показывает статус кластера
|
||||||
|
func (e *Engine) handleClusterStatus() (string, error) {
|
||||||
|
status := e.clusterMgr.GetClusterStatus()
|
||||||
|
|
||||||
|
result := utils.ColorCyan + "Cluster Status:" + utils.ColorReset + "\n"
|
||||||
|
result += fmt.Sprintf(" Total nodes: %d\n", status["total_nodes"])
|
||||||
|
result += fmt.Sprintf(" Active nodes: %d\n", status["active_nodes"])
|
||||||
|
result += fmt.Sprintf(" Coordinator: %v\n", status["coordinator"])
|
||||||
|
result += fmt.Sprintf(" Master-master replication: %v\n", status["master_master"])
|
||||||
|
|
||||||
|
nodes, _ := status["nodes"].([]map[string]interface{})
|
||||||
|
if len(nodes) > 0 {
|
||||||
|
result += utils.ColorCyan + "\nCluster Nodes:" + utils.ColorReset + "\n"
|
||||||
|
for _, node := range nodes {
|
||||||
|
result += fmt.Sprintf(" %s (%s) - %s, last seen: %s\n",
|
||||||
|
node["id"], node["address"], node["state"], node["last_seen"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAddNode добавляет узел в кластер
|
||||||
|
func (e *Engine) handleAddNode(args []string) (string, error) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return "", fmt.Errorf("specify node address")
|
||||||
|
}
|
||||||
|
|
||||||
|
address := args[0]
|
||||||
|
err := e.clusterMgr.AddNode(address)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Node " + address + " added to cluster" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEvictNode удаляет узел из кластера
|
||||||
|
func (e *Engine) handleEvictNode(args []string) (string, error) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return "", fmt.Errorf("specify node ID or address")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeID := args[0]
|
||||||
|
err := e.clusterMgr.RemoveNode(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Node " + nodeID + " removed from cluster" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLua выполняет Lua скрипт
|
||||||
|
func (e *Engine) handleLua(args []string) (string, error) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return "", fmt.Errorf("specify plugin name")
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginName := args[0]
|
||||||
|
err := e.luaMgr.ExecutePlugin(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Plugin executed" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAddPrimaryIndex обрабатывает создание первичного индекса
|
||||||
|
func (e *Engine) handleAddPrimaryIndex(args []string) (string, error) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return "", fmt.Errorf("specify tapple name")
|
||||||
|
}
|
||||||
|
|
||||||
|
tappleName := args[0]
|
||||||
|
|
||||||
|
// Получаем таппл
|
||||||
|
_, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("tapple not found: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём первичный индекс
|
||||||
|
indexManager := e.storage.GetTappleManager().GetIndexManager()
|
||||||
|
err = indexManager.CreatePrimaryIndex(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Primary index for tapple '" + tappleName + "' created successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDeletePrimaryIndex обрабатывает удаление первичного индекса
|
||||||
|
func (e *Engine) handleDeletePrimaryIndex(args []string) (string, error) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return "", fmt.Errorf("specify tapple name")
|
||||||
|
}
|
||||||
|
|
||||||
|
tappleName := args[0]
|
||||||
|
|
||||||
|
indexManager := e.storage.GetTappleManager().GetIndexManager()
|
||||||
|
err := indexManager.DeletePrimaryIndex(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Primary index for tapple '" + tappleName + "' deleted successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAddSecondaryIndex обрабатывает создание вторичного индекса
|
||||||
|
func (e *Engine) handleAddSecondaryIndex(args []string) (string, error) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "", fmt.Errorf("specify tapple name and field name")
|
||||||
|
}
|
||||||
|
|
||||||
|
tappleName := args[0]
|
||||||
|
fieldName := args[1]
|
||||||
|
|
||||||
|
// Получаем таппл
|
||||||
|
_, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("tapple not found: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём вторичный индекс
|
||||||
|
indexManager := e.storage.GetTappleManager().GetIndexManager()
|
||||||
|
err = indexManager.CreateSecondaryIndex(tappleName, fieldName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Secondary index for tapple '" + tappleName + "' on field '" + fieldName + "' created successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDeleteSecondaryIndex обрабатывает удаление вторичного индекса
|
||||||
|
func (e *Engine) handleDeleteSecondaryIndex(args []string) (string, error) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return "", fmt.Errorf("specify tapple name and field name")
|
||||||
|
}
|
||||||
|
|
||||||
|
tappleName := args[0]
|
||||||
|
fieldName := args[1]
|
||||||
|
|
||||||
|
indexManager := e.storage.GetTappleManager().GetIndexManager()
|
||||||
|
err := indexManager.DeleteSecondaryIndex(tappleName, fieldName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Secondary index for tapple '" + tappleName + "' on field '" + fieldName + "' deleted successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCompressionStats показывает статистику сжатия
|
||||||
|
func (e *Engine) handleCompressionStats(args []string) (string, error) {
|
||||||
|
result := utils.ColorCyan + "Compression Statistics:" + utils.ColorReset + "\n"
|
||||||
|
result += " Compression statistics available at slice level\n"
|
||||||
|
result += " Use 'show compression <tapple> <slice>' for detailed information"
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleShardingStatus показывает статус шардинга
|
||||||
|
func (e *Engine) handleShardingStatus() (string, error) {
|
||||||
|
status := e.clusterMgr.GetClusterStatus()
|
||||||
|
|
||||||
|
shardingInfo, exists := status["sharding"]
|
||||||
|
if !exists {
|
||||||
|
return utils.ColorYellow + "Sharding is not activated" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
shardStats := shardingInfo.(map[string]interface{})
|
||||||
|
|
||||||
|
result := utils.ColorCyan + "Sharding Status:" + utils.ColorReset + "\n"
|
||||||
|
result += fmt.Sprintf(" Enabled: %v\n", shardStats["enabled"])
|
||||||
|
result += fmt.Sprintf(" Strategy: %s\n", shardStats["strategy"])
|
||||||
|
result += fmt.Sprintf(" Total shards: %d\n", shardStats["total_shards"])
|
||||||
|
|
||||||
|
shards, _ := shardStats["shards"].([]map[string]interface{})
|
||||||
|
if len(shards) > 0 {
|
||||||
|
result += utils.ColorCyan + "\nShards:" + utils.ColorReset + "\n"
|
||||||
|
for _, shard := range shards {
|
||||||
|
result += fmt.Sprintf(" %s: nodes=%v, reads=%d, writes=%d\n",
|
||||||
|
shard["id"], shard["nodes"], shard["reads"], shard["writes"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Методы для работы с тапплами
|
||||||
|
func (e *Engine) createTapple(name string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().CreateTapple(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return utils.ColorGreen + "Tapple '" + tapple.Name + "' created successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) deleteTapple(name string) (string, error) {
|
||||||
|
err := e.storage.GetTappleManager().DeleteTapple(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return utils.ColorGreen + "Tapple '" + name + "' deleted successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) listTapples() (string, error) {
|
||||||
|
tapples := e.storage.GetTappleManager().ListTapples()
|
||||||
|
if len(tapples) == 0 {
|
||||||
|
return utils.ColorYellow + "No tapples found" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := utils.ColorCyan + "List of tapples:" + utils.ColorReset + "\n"
|
||||||
|
for _, t := range tapples {
|
||||||
|
result += " " + utils.ColorGreen + t + utils.ColorReset + "\n"
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Методы для работы со слайсами
|
||||||
|
func (e *Engine) createSlice(tappleName, sliceName string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, err := e.storage.GetTappleManager().GetSliceManager().CreateSlice(tapple, sliceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Slice '" + slice.Name + "' in tapple '" + tappleName + "' created successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) deleteSlice(tappleName, sliceName string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.storage.GetTappleManager().GetSliceManager().DeleteSlice(tapple, sliceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Slice '" + sliceName + "' in tapple '" + tappleName + "' deleted successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) listSlices(tappleName string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slices := e.storage.GetTappleManager().GetSliceManager().ListSlices(tapple)
|
||||||
|
if len(slices) == 0 {
|
||||||
|
return utils.ColorYellow + "No slices found in tapple '" + tappleName + "'" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := utils.ColorCyan + "List of slices in tapple '" + tappleName + "':" + utils.ColorReset + "\n"
|
||||||
|
for _, s := range slices {
|
||||||
|
result += " " + utils.ColorGreen + s + utils.ColorReset + "\n"
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Методы для работы с кортежами
|
||||||
|
func (e *Engine) createTuple(tappleName, sliceName, tupleID string, fieldsArgs []string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим поля
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
for _, arg := range fieldsArgs {
|
||||||
|
parts := strings.SplitN(arg, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
fields[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tuple, err := e.storage.GetTappleManager().GetSliceManager().GetTupleManager().CreateTuple(slice, tupleID, fields)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Tuple '" + tuple.ID + "' in slice '" + sliceName + "' created successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) deleteTuple(tappleName, sliceName, tupleID string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.storage.GetTappleManager().GetSliceManager().GetTupleManager().DeleteTuple(slice, tupleID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Tuple '" + tupleID + "' in slice '" + sliceName + "' deleted successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateTuple(tappleName, sliceName, tupleID string, fieldsArgs []string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим поля
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
for _, arg := range fieldsArgs {
|
||||||
|
parts := strings.SplitN(arg, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
fields[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tuple, err := e.storage.GetTappleManager().GetSliceManager().GetTupleManager().UpdateTuple(slice, tupleID, fields)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColorGreen + "Tuple '" + tuple.ID + "' in slice '" + sliceName + "' updated successfully" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// showTuples показывает все кортежи в слайсе
|
||||||
|
func (e *Engine) showTuples(tappleName, sliceName string) (string, error) {
|
||||||
|
tapple, err := e.storage.GetTappleManager().GetTapple(tappleName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
slice, err := e.storage.GetTappleManager().GetSliceManager().GetSlice(tapple, sliceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем все кортежи из слайса через рефлексию
|
||||||
|
tuples, err := e.getAllTuplesFromSlice(slice)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tuples) == 0 {
|
||||||
|
return utils.ColorYellow + "No tuples found in slice '" + sliceName + "'" + utils.ColorReset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := utils.ColorCyan + "List of tuples in slice '" + sliceName + "':" + utils.ColorReset + "\n"
|
||||||
|
for id, tuple := range tuples {
|
||||||
|
result += " " + utils.ColorGreen + "ID: " + id + utils.ColorReset + "\n"
|
||||||
|
for k, v := range tuple.Fields {
|
||||||
|
result += " " + utils.ColorYellow + k + utils.ColorReset + ": " + utils.ColorPromptCode + fmt.Sprint(v) + utils.ColorReset + "\n"
|
||||||
|
}
|
||||||
|
result += "\n"
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для получения всех кортежей из слайса через рефлексию
|
||||||
|
func (e *Engine) getAllTuplesFromSlice(slice *types.Slice) (map[string]*types.Tuple, error) {
|
||||||
|
if slice == nil {
|
||||||
|
return nil, fmt.Errorf("slice is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем рефлексию для доступа к неэкспортируемому полю tuples
|
||||||
|
v := reflect.ValueOf(slice).Elem()
|
||||||
|
field := v.FieldByName("tuples")
|
||||||
|
|
||||||
|
if !field.IsValid() || field.Kind() != reflect.Map {
|
||||||
|
return nil, fmt.Errorf("cannot access tuples field in slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]*types.Tuple)
|
||||||
|
iter := field.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
key := iter.Key().String()
|
||||||
|
value := iter.Value().Interface()
|
||||||
|
if tuple, ok := value.(*types.Tuple); ok {
|
||||||
|
result[key] = tuple
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// help возвращает справку по командам
|
||||||
|
func (e *Engine) help() string {
|
||||||
|
help := utils.ColorCyan + "Available commands:" + utils.ColorReset + "\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "Basic commands:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "create tapple <name>" + utils.ColorReset + " - create a new tapple (database)\n"
|
||||||
|
help += " " + utils.ColorGreen + "create slice <tapple> <name>" + utils.ColorReset + " - create a new slice (table)\n"
|
||||||
|
help += " " + utils.ColorGreen + "create tuple <tapple> <slice> <id> [key=value...]" + utils.ColorReset + " - create a new tuple (record)\n"
|
||||||
|
help += " " + utils.ColorGreen + "delete tapple <name>" + utils.ColorReset + " - delete a tapple\n"
|
||||||
|
help += " " + utils.ColorGreen + "delete slice <tapple> <name>" + utils.ColorReset + " - delete a slice\n"
|
||||||
|
help += " " + utils.ColorGreen + "delete tuple <tapple> <slice> <id>" + utils.ColorReset + " - delete a tuple\n"
|
||||||
|
help += " " + utils.ColorGreen + "update tuple <tapple> <slice> <id> [key=value...]" + utils.ColorReset + " - update a tuple\n"
|
||||||
|
help += " " + utils.ColorGreen + "list tapples" + utils.ColorReset + " - show all tapples\n"
|
||||||
|
help += " " + utils.ColorGreen + "list slices <tapple>" + utils.ColorReset + " - show all slices in a tapple\n"
|
||||||
|
help += " " + utils.ColorGreen + "show tuples <tapple> <slice>" + utils.ColorReset + " - show all tuples in a slice\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "Index management:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "add.prime.index <tapple>" + utils.ColorReset + " - create primary index for tapple\n"
|
||||||
|
help += " " + utils.ColorGreen + "delete.prime.index <tapple>" + utils.ColorReset + " - delete primary index\n"
|
||||||
|
help += " " + utils.ColorGreen + "add.secondary.index <tapple> <field>" + utils.ColorReset + " - create secondary index on field\n"
|
||||||
|
help += " " + utils.ColorGreen + "delete.secondary.index <tapple> <field>" + utils.ColorReset + " - delete secondary index\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "Transactions:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "begin" + utils.ColorReset + " - start a transaction\n"
|
||||||
|
help += " " + utils.ColorGreen + "commit" + utils.ColorReset + " - commit a transaction\n"
|
||||||
|
help += " " + utils.ColorGreen + "rollback" + utils.ColorReset + " - rollback a transaction\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "Cluster and sharding management:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "cluster.status" + utils.ColorReset + " - show cluster status\n"
|
||||||
|
help += " " + utils.ColorGreen + "cluster.rebalance [cluster_name]" + utils.ColorReset + " - rebalance the cluster\n"
|
||||||
|
help += " " + utils.ColorGreen + "sharding.status" + utils.ColorReset + " - show sharding status\n"
|
||||||
|
help += " " + utils.ColorGreen + "add.node <address>" + utils.ColorReset + " - add a node to the cluster\n"
|
||||||
|
help += " " + utils.ColorGreen + "evict.node <node_id>" + utils.ColorReset + " - remove a node from the cluster\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "Compression:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "compression.stats" + utils.ColorReset + " - show compression statistics\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "AOF management:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "aof.recover [file]" + utils.ColorReset + " - recover data from AOF file\n"
|
||||||
|
help += " " + utils.ColorGreen + "aof.info" + utils.ColorReset + " - show AOF file information\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "Lua plugins:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "lua <plugin_name>" + utils.ColorReset + " - execute Lua plugin\n"
|
||||||
|
|
||||||
|
help += "\n" + utils.ColorYellow + "Other:" + utils.ColorReset + "\n"
|
||||||
|
help += " " + utils.ColorGreen + "exit/quit" + utils.ColorReset + " - exit the DBMS\n"
|
||||||
|
|
||||||
|
return help
|
||||||
|
}
|
||||||
154
internal/lua/plugin.go
Normal file
154
internal/lua/plugin.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// /futriis/internal/lua/plugin.go
|
||||||
|
// Пакет lua реализует систему плагинов на языке Lua для расширения функциональности СУБД.
|
||||||
|
// PluginManager управляет загрузкой, хранением и выполнением Lua-скриптов из указанной директории.
|
||||||
|
// Предоставляет мост между Go и Lua через регистрацию функций, доступных из скриптов.
|
||||||
|
// Позволяет динамически расширять возможности базы данных без перекомпиляции основного кода.
|
||||||
|
// Поддерживает возможность отключения через конфигурацию для минимизации ресурсов.
|
||||||
|
|
||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"futriis/pkg/config"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
"github.com/yuin/gopher-lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginManager управляет Lua плагинами
|
||||||
|
type PluginManager struct {
|
||||||
|
state *lua.LState
|
||||||
|
plugins map[string]*lua.LFunction
|
||||||
|
mu sync.RWMutex
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginManager создаёт новый менеджер плагинов
|
||||||
|
func NewPluginManager(cfg *config.LuaConfig) *PluginManager {
|
||||||
|
pm := &PluginManager{
|
||||||
|
plugins: make(map[string]*lua.LFunction),
|
||||||
|
enabled: cfg.Enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Enabled {
|
||||||
|
pm.state = lua.NewState()
|
||||||
|
pm.registerFunctions()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pm
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerFunctions регистрирует функции Go, доступные из Lua
|
||||||
|
func (pm *PluginManager) registerFunctions() {
|
||||||
|
if !pm.enabled || pm.state == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрируем функции для работы с данными
|
||||||
|
pm.state.SetGlobal("print", pm.state.NewFunction(func(L *lua.LState) int {
|
||||||
|
top := L.GetTop()
|
||||||
|
args := make([]interface{}, top)
|
||||||
|
for i := 1; i <= top; i++ {
|
||||||
|
args[i-1] = L.Get(i).String()
|
||||||
|
}
|
||||||
|
utils.PrintInfo(fmt.Sprint(args...))
|
||||||
|
return 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
pm.state.SetGlobal("get", pm.state.NewFunction(func(L *lua.LState) int {
|
||||||
|
key := L.ToString(1)
|
||||||
|
// TODO: получить значение из хранилища
|
||||||
|
L.Push(lua.LString(key))
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
pm.state.SetGlobal("set", pm.state.NewFunction(func(L *lua.LState) int {
|
||||||
|
key := L.ToString(1)
|
||||||
|
value := L.ToString(2)
|
||||||
|
// TODO: установить значение в хранилище
|
||||||
|
utils.PrintInfo("Lua set: %s = %s", key, value)
|
||||||
|
return 0
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPlugins загружает все Lua плагины из директории
|
||||||
|
func (pm *PluginManager) LoadPlugins(pluginsDir string) error {
|
||||||
|
if !pm.enabled || pm.state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(pluginsDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if filepath.Ext(file.Name()) == ".lua" {
|
||||||
|
if err := pm.LoadPlugin(filepath.Join(pluginsDir, file.Name())); err != nil {
|
||||||
|
utils.PrintError("Ошибка загрузки плагина %s: %v", file.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPlugin загружает один Lua плагин
|
||||||
|
func (pm *PluginManager) LoadPlugin(path string) error {
|
||||||
|
if !pm.enabled || pm.state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, err := pm.state.LoadString(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.mu.Lock()
|
||||||
|
pm.plugins[filepath.Base(path)] = fn
|
||||||
|
pm.mu.Unlock()
|
||||||
|
|
||||||
|
utils.PrintSuccess("Загружен плагин: %s", filepath.Base(path))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePlugin выполняет функцию плагина
|
||||||
|
func (pm *PluginManager) ExecutePlugin(name string, args ...lua.LValue) error {
|
||||||
|
if !pm.enabled || pm.state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.mu.RLock()
|
||||||
|
fn, exists := pm.plugins[name]
|
||||||
|
pm.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("плагин %s не найден", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.state.Push(fn)
|
||||||
|
for _, arg := range args {
|
||||||
|
pm.state.Push(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pm.state.PCall(len(args), lua.MultRet, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close закрывает Lua состояние
|
||||||
|
func (pm *PluginManager) Close() {
|
||||||
|
if pm.state != nil {
|
||||||
|
pm.state.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
44
internal/msgpack/deserializer.go
Normal file
44
internal/msgpack/deserializer.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// /futriis/internal/msgpack/deserializer.go
|
||||||
|
// Пакет msgpack расширяет функциональность десериализации для работы с динамическими типами.
|
||||||
|
// Deserializer предоставляет дополнительные методы для десериализации произвольных значений и map-структур из формата MessagePack.
|
||||||
|
// Используется когда точный тип данных неизвестен заранее, например, при обработке полей кортежей с различными типами значений.
|
||||||
|
// Интегрируется с основным сериализатором для полного цикла преобразований.
|
||||||
|
|
||||||
|
package msgpack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"futriis/pkg/types"
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deserializer расширяет функциональность десериализации
|
||||||
|
type Deserializer struct {
|
||||||
|
serializer *Serializer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeserializer создаёт новый экземпляр десериализатора
|
||||||
|
func NewDeserializer() *Deserializer {
|
||||||
|
return &Deserializer{
|
||||||
|
serializer: NewSerializer(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeserializeValue десериализует значение произвольного типа
|
||||||
|
func (d *Deserializer) DeserializeValue(data []byte) (interface{}, error) {
|
||||||
|
var value interface{}
|
||||||
|
err := msgpack.Unmarshal(data, &value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeserializeMap десериализует данные в карту
|
||||||
|
func (d *Deserializer) DeserializeMap(data []byte) (map[string]interface{}, error) {
|
||||||
|
var m map[string]interface{}
|
||||||
|
err := msgpack.Unmarshal(data, &m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
68
internal/msgpack/serializer.go
Normal file
68
internal/msgpack/serializer.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// /futriis/internal/msgpack/serializer.go
|
||||||
|
// Пакет msgpack реализует сериализацию структур данных в формат MessagePack.
|
||||||
|
// Serializer предоставляет методы для преобразования кортежей, слайсов и тапплов в компактное бинарное представление и обратно.
|
||||||
|
// Использует стороннюю библиотеку msgpack/v5 для эффективной упаковки данных. Критически важен для сохранения и загрузки состояния базы данных,/
|
||||||
|
// А также для сетевого обмена данными между клиентом и сервером.
|
||||||
|
|
||||||
|
package msgpack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"futriis/pkg/types"
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Serializer предоставляет методы для сериализации данных
|
||||||
|
type Serializer struct {
|
||||||
|
enc *msgpack.Encoder
|
||||||
|
dec *msgpack.Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSerializer создаёт новый экземпляр сериализатора
|
||||||
|
func NewSerializer() *Serializer {
|
||||||
|
return &Serializer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeTuple сериализует кортеж в формат MessagePack
|
||||||
|
func (s *Serializer) SerializeTuple(tuple *types.Tuple) ([]byte, error) {
|
||||||
|
return msgpack.Marshal(tuple)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeserializeTuple десериализует кортеж из формата MessagePack
|
||||||
|
func (s *Serializer) DeserializeTuple(data []byte) (*types.Tuple, error) {
|
||||||
|
var tuple types.Tuple
|
||||||
|
err := msgpack.Unmarshal(data, &tuple)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tuple, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeSlice сериализует слайс в формат MessagePack
|
||||||
|
func (s *Serializer) SerializeSlice(slice *types.Slice) ([]byte, error) {
|
||||||
|
return msgpack.Marshal(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeserializeSlice десериализует слайс из формата MessagePack
|
||||||
|
func (s *Serializer) DeserializeSlice(data []byte) (*types.Slice, error) {
|
||||||
|
var slice types.Slice
|
||||||
|
err := msgpack.Unmarshal(data, &slice)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &slice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeTapple сериализует таппл в формат MessagePack
|
||||||
|
func (s *Serializer) SerializeTapple(tapple *types.Tapple) ([]byte, error) {
|
||||||
|
return msgpack.Marshal(tapple)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeserializeTapple десериализует таппл из формата MessagePack
|
||||||
|
func (s *Serializer) DeserializeTapple(data []byte) (*types.Tapple, error) {
|
||||||
|
var tapple types.Tapple
|
||||||
|
err := msgpack.Unmarshal(data, &tapple)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tapple, nil
|
||||||
|
}
|
||||||
244
internal/replication/aof.go
Normal file
244
internal/replication/aof.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
// /futriis/internal/replication/aof.go
|
||||||
|
// Пакет replication реализует Append-Only File данных
|
||||||
|
// AOF обеспечивает постоянную запись команд в лог-файл для восстановления состояния после перезапуска
|
||||||
|
// Поддерживает запись, чтение, синхронизацию и перезапись (rewrite) лог-файла
|
||||||
|
|
||||||
|
package replication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AOFCommand представляет команду для записи в AOF
|
||||||
|
type AOFCommand struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args []interface{} `json:"args"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AOFManager управляет Append-Only File
|
||||||
|
type AOFManager struct {
|
||||||
|
file *os.File
|
||||||
|
writer *bufio.Writer
|
||||||
|
enabled bool
|
||||||
|
filePath string
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAOFManager создаёт новый менеджер AOF
|
||||||
|
func NewAOFManager(filePath string, enabled bool) (*AOFManager, error) {
|
||||||
|
if !enabled {
|
||||||
|
return &AOFManager{
|
||||||
|
enabled: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открываем файл для записи и чтения
|
||||||
|
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось открыть AOF файл: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AOFManager{
|
||||||
|
file: file,
|
||||||
|
writer: bufio.NewWriter(file),
|
||||||
|
enabled: true,
|
||||||
|
filePath: filePath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append добавляет команду в AOF
|
||||||
|
func (aof *AOFManager) Append(name string, args []interface{}) error {
|
||||||
|
if !aof.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
cmd := AOFCommand{
|
||||||
|
Name: name,
|
||||||
|
Args: args,
|
||||||
|
Time: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ошибка сериализации команды: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем команду в файл
|
||||||
|
if _, err := aof.writer.Write(data); err != nil {
|
||||||
|
return fmt.Errorf("ошибка записи в AOF: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := aof.writer.WriteByte('\n'); err != nil {
|
||||||
|
return fmt.Errorf("ошибка записи разделителя в AOF: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбрасываем буфер на диск
|
||||||
|
if err := aof.writer.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("ошибка сброса AOF на диск: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAll читает все команды из AOF файла
|
||||||
|
func (aof *AOFManager) ReadAll() ([]AOFCommand, error) {
|
||||||
|
if !aof.enabled {
|
||||||
|
return nil, fmt.Errorf("AOF отключён")
|
||||||
|
}
|
||||||
|
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
// Сбрасываем буфер на диск перед чтением
|
||||||
|
if err := aof.writer.Flush(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка сброса AOF на диск: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перемещаем указатель в начало файла
|
||||||
|
if _, err := aof.file.Seek(0, 0); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка перемещения в начало AOF файла: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var commands []AOFCommand
|
||||||
|
scanner := bufio.NewScanner(aof.file)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd AOFCommand
|
||||||
|
if err := json.Unmarshal(line, &cmd); err != nil {
|
||||||
|
// Пропускаем повреждённые записи
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = append(commands, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка чтения AOF файла: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем указатель в конец файла для продолжения записи
|
||||||
|
if _, err := aof.file.Seek(0, 2); err != nil {
|
||||||
|
return nil, fmt.Errorf("ошибка перемещения в конец AOF файла: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileInfo возвращает информацию о файле
|
||||||
|
func (aof *AOFManager) GetFileInfo() (string, error) {
|
||||||
|
if !aof.enabled || aof.file == nil {
|
||||||
|
return "AOF отключён", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := aof.file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%d байт", stat.Size()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close закрывает AOF файл
|
||||||
|
func (aof *AOFManager) Close() error {
|
||||||
|
if !aof.enabled || aof.file == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
if err := aof.writer.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return aof.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync принудительно синхронизирует AOF с диском
|
||||||
|
func (aof *AOFManager) Sync() error {
|
||||||
|
if !aof.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
if err := aof.writer.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return aof.file.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite выполняет перезапись AOF файла (упрощённая версия)
|
||||||
|
func (aof *AOFManager) Rewrite(commands []AOFCommand) error {
|
||||||
|
if !aof.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
// Создаём временный файл
|
||||||
|
tmpFile := aof.filePath + ".tmp"
|
||||||
|
file, err := os.Create(tmpFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("не удалось создать временный AOF файл: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
|
||||||
|
// Записываем все команды
|
||||||
|
for _, cmd := range commands {
|
||||||
|
data, err := json.Marshal(cmd)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := writer.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.WriteByte('\n'); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрываем текущий файл
|
||||||
|
aof.file.Close()
|
||||||
|
|
||||||
|
// Заменяем старый файл новым
|
||||||
|
if err := os.Rename(tmpFile, aof.filePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открываем новый файл
|
||||||
|
file, err = os.OpenFile(aof.filePath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
aof.file = file
|
||||||
|
aof.writer = bufio.NewWriter(file)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
129
internal/server/server.go
Normal file
129
internal/server/server.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// /futriis/internal/server/server.go
|
||||||
|
// Пакет server реализует TCP-сервер для клиент-серверной архитектуры СУБД.
|
||||||
|
// Server обрабатывает входящие соединения, принимает команды в текстовом или JSON формате, выполняет их через движок и возвращает результаты клиентам.
|
||||||
|
// Поддерживает множественные одновременные подключения, каждый обрабатывается в отдельной горутине.
|
||||||
|
// Обеспечивает корректное завершение работы с закрытием всех соединений и освобождением порта.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"futriis/internal/engine"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server представляет сервер СУБД
|
||||||
|
type Server struct {
|
||||||
|
address string
|
||||||
|
engine *engine.Engine
|
||||||
|
listener net.Listener
|
||||||
|
clients map[net.Conn]bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
stopChan chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer создаёт новый сервер
|
||||||
|
func NewServer(address string, engine *engine.Engine) *Server {
|
||||||
|
return &Server{
|
||||||
|
address: address,
|
||||||
|
engine: engine,
|
||||||
|
clients: make(map[net.Conn]bool),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start запускает сервер
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
listener, err := net.Listen("tcp", s.address)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.listener = listener
|
||||||
|
utils.PrintSuccess("Сервер запущен на %s", s.address)
|
||||||
|
|
||||||
|
go s.acceptLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// acceptLoop принимает входящие соединения
|
||||||
|
func (s *Server) acceptLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
conn, err := s.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.clients[conn] = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
go s.handleClient(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleClient обрабатывает клиентское соединение
|
||||||
|
func (s *Server) handleClient(conn net.Conn) {
|
||||||
|
defer func() {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.clients, conn)
|
||||||
|
s.mu.Unlock()
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// Парсим JSON запрос
|
||||||
|
var req map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
||||||
|
// Если не JSON, обрабатываем как простую команду
|
||||||
|
result, err := s.engine.Execute(line)
|
||||||
|
s.sendResponse(conn, result, err)
|
||||||
|
} else {
|
||||||
|
// Обрабатываем JSON запрос
|
||||||
|
cmd, _ := req["command"].(string)
|
||||||
|
result, err := s.engine.Execute(cmd)
|
||||||
|
s.sendResponse(conn, result, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendResponse отправляет ответ клиенту
|
||||||
|
func (s *Server) sendResponse(conn net.Conn, result string, err error) {
|
||||||
|
response := make(map[string]interface{})
|
||||||
|
if err != nil {
|
||||||
|
response["error"] = err.Error()
|
||||||
|
} else {
|
||||||
|
response["result"] = result
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(response)
|
||||||
|
conn.Write(append(data, '\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop останавливает сервер
|
||||||
|
func (s *Server) Stop() {
|
||||||
|
close(s.stopChan)
|
||||||
|
|
||||||
|
if s.listener != nil {
|
||||||
|
s.listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
for conn := range s.clients {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
240
internal/storage/compression.go
Normal file
240
internal/storage/compression.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
// /futriis/internal/storage/compression.go
|
||||||
|
// Пакет storage реализует простейшее сжатие для колонок с одинаковыми типами данных
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompressionType тип сжатия
|
||||||
|
type CompressionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NoCompression CompressionType = iota
|
||||||
|
RLECompression // Run-length encoding для повторяющихся значений
|
||||||
|
DeltaCompression // Дельта-сжатие для чисел
|
||||||
|
DictionaryCompression // Словарное сжатие для строк
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColumnCompressor предоставляет сжатие для колонки
|
||||||
|
type ColumnCompressor struct {
|
||||||
|
colType string
|
||||||
|
compType CompressionType
|
||||||
|
stats struct {
|
||||||
|
originalSize int64
|
||||||
|
compressedSize int64
|
||||||
|
savings int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColumnCompressor создаёт новый компрессор для колонки
|
||||||
|
func NewColumnCompressor(colType string) *ColumnCompressor {
|
||||||
|
cc := &ColumnCompressor{
|
||||||
|
colType: colType,
|
||||||
|
compType: NoCompression,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Автоматически выбираем тип сжатия на основе типа данных
|
||||||
|
switch colType {
|
||||||
|
case "int", "int64", "float64":
|
||||||
|
cc.compType = DeltaCompression
|
||||||
|
case "string":
|
||||||
|
cc.compType = DictionaryCompression
|
||||||
|
default:
|
||||||
|
cc.compType = RLECompression
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress сжимает данные
|
||||||
|
func (cc *ColumnCompressor) Compress(data []interface{}) ([]byte, error) {
|
||||||
|
var compressed []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch cc.compType {
|
||||||
|
case RLECompression:
|
||||||
|
compressed, err = cc.rleCompress(data)
|
||||||
|
case DeltaCompression:
|
||||||
|
compressed, err = cc.deltaCompress(data)
|
||||||
|
case DictionaryCompression:
|
||||||
|
compressed, err = cc.dictionaryCompress(data)
|
||||||
|
default:
|
||||||
|
// Без сжатия - просто сериализуем
|
||||||
|
compressed, err = cc.noCompress(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем статистику
|
||||||
|
originalSize := int64(len(data) * 8) // Примерная оценка
|
||||||
|
compressedSize := int64(len(compressed))
|
||||||
|
|
||||||
|
atomic.AddInt64(&cc.stats.originalSize, originalSize)
|
||||||
|
atomic.AddInt64(&cc.stats.compressedSize, compressedSize)
|
||||||
|
atomic.AddInt64(&cc.stats.savings, originalSize-compressedSize)
|
||||||
|
|
||||||
|
return compressed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rleCompress реализует сжатие повторяющихся значений
|
||||||
|
func (cc *ColumnCompressor) rleCompress(data []interface{}) ([]byte, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, 0)
|
||||||
|
|
||||||
|
current := data[0]
|
||||||
|
count := 1
|
||||||
|
|
||||||
|
for i := 1; i < len(data); i++ {
|
||||||
|
if data[i] == current {
|
||||||
|
count++
|
||||||
|
} else {
|
||||||
|
// Записываем значение и счётчик
|
||||||
|
result = append(result, []byte(encodeValue(current))...)
|
||||||
|
result = append(result, byte(count))
|
||||||
|
|
||||||
|
current = data[i]
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем последнее значение
|
||||||
|
result = append(result, []byte(encodeValue(current))...)
|
||||||
|
result = append(result, byte(count))
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deltaCompress реализует дельта-сжатие для чисел
|
||||||
|
func (cc *ColumnCompressor) deltaCompress(data []interface{}) ([]byte, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, 8) // Первое значение храним полностью
|
||||||
|
|
||||||
|
// Преобразуем первое значение
|
||||||
|
first, ok := data[0].(float64)
|
||||||
|
if !ok {
|
||||||
|
if i, ok := data[0].(int); ok {
|
||||||
|
first = float64(i)
|
||||||
|
} else {
|
||||||
|
return cc.noCompress(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint64(result, math.Float64bits(first))
|
||||||
|
|
||||||
|
// Для остальных храним дельты
|
||||||
|
for i := 1; i < len(data); i++ {
|
||||||
|
var curr float64
|
||||||
|
switch v := data[i].(type) {
|
||||||
|
case float64:
|
||||||
|
curr = v
|
||||||
|
case int:
|
||||||
|
curr = float64(v)
|
||||||
|
default:
|
||||||
|
return cc.noCompress(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
prev, _ := data[i-1].(float64)
|
||||||
|
if iPrev, ok := data[i-1].(int); ok {
|
||||||
|
prev = float64(iPrev)
|
||||||
|
}
|
||||||
|
|
||||||
|
delta := int16(curr - prev)
|
||||||
|
deltaBytes := make([]byte, 2)
|
||||||
|
binary.LittleEndian.PutUint16(deltaBytes, uint16(delta))
|
||||||
|
result = append(result, deltaBytes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dictionaryCompress реализует словарное сжатие для строк
|
||||||
|
func (cc *ColumnCompressor) dictionaryCompress(data []interface{}) ([]byte, error) {
|
||||||
|
// Строим словарь уникальных значений
|
||||||
|
dict := make(map[string]byte)
|
||||||
|
values := make([]byte, len(data))
|
||||||
|
|
||||||
|
nextCode := byte(0)
|
||||||
|
|
||||||
|
for i, val := range data {
|
||||||
|
str, ok := val.(string)
|
||||||
|
if !ok {
|
||||||
|
return cc.noCompress(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, exists := dict[str]
|
||||||
|
if !exists {
|
||||||
|
code = nextCode
|
||||||
|
dict[str] = code
|
||||||
|
nextCode++
|
||||||
|
}
|
||||||
|
|
||||||
|
values[i] = code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кодируем: сначала словарь, затем значения
|
||||||
|
result := make([]byte, 0)
|
||||||
|
|
||||||
|
// Записываем размер словаря
|
||||||
|
result = append(result, byte(len(dict)))
|
||||||
|
|
||||||
|
// Записываем словарь
|
||||||
|
for str, code := range dict {
|
||||||
|
result = append(result, code)
|
||||||
|
result = append(result, byte(len(str)))
|
||||||
|
result = append(result, []byte(str)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем значения
|
||||||
|
result = append(result, values...)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// noCompress без сжатия
|
||||||
|
func (cc *ColumnCompressor) noCompress(data []interface{}) ([]byte, error) {
|
||||||
|
result := make([]byte, 0)
|
||||||
|
for _, val := range data {
|
||||||
|
result = append(result, []byte(encodeValue(val))...)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeValue кодирует значение в строку
|
||||||
|
func encodeValue(val interface{}) string {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return strconv.Itoa(v)
|
||||||
|
case int64:
|
||||||
|
return strconv.FormatInt(v, 10)
|
||||||
|
case float64:
|
||||||
|
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||||
|
case bool:
|
||||||
|
return strconv.FormatBool(v)
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats возвращает статистику сжатия
|
||||||
|
func (cc *ColumnCompressor) GetStats() map[string]int64 {
|
||||||
|
return map[string]int64{
|
||||||
|
"original_size": atomic.LoadInt64(&cc.stats.originalSize),
|
||||||
|
"compressed_size": atomic.LoadInt64(&cc.stats.compressedSize),
|
||||||
|
"savings": atomic.LoadInt64(&cc.stats.savings),
|
||||||
|
}
|
||||||
|
}
|
||||||
231
internal/storage/index.go
Normal file
231
internal/storage/index.go
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
// /futriis/internal/storage/index.go
|
||||||
|
// Пакет storage реализует систему индексов для ускорения доступа к данным
|
||||||
|
// Поддерживает первичные и вторичные индексы с wait-free операциями
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IndexType тип индекса
|
||||||
|
type IndexType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PrimaryIndex IndexType = iota
|
||||||
|
SecondaryIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
func (it IndexType) String() string {
|
||||||
|
switch it {
|
||||||
|
case PrimaryIndex:
|
||||||
|
return "primary"
|
||||||
|
case SecondaryIndex:
|
||||||
|
return "secondary"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexEntry представляет запись в индексе
|
||||||
|
type IndexEntry struct {
|
||||||
|
Key string
|
||||||
|
Value unsafe.Pointer // Указатель на кортеж
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index представляет структуру индекса
|
||||||
|
type Index struct {
|
||||||
|
Name string
|
||||||
|
Type IndexType
|
||||||
|
FieldName string // Для вторичных индексов - поле, по которому построен индекс
|
||||||
|
entries map[string]unsafe.Pointer
|
||||||
|
stats struct {
|
||||||
|
lookups int64
|
||||||
|
inserts int64
|
||||||
|
deletes int64
|
||||||
|
collisions int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrimaryIndex создаёт новый первичный индекс
|
||||||
|
func NewPrimaryIndex(name string) *Index {
|
||||||
|
return &Index{
|
||||||
|
Name: name,
|
||||||
|
Type: PrimaryIndex,
|
||||||
|
entries: make(map[string]unsafe.Pointer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecondaryIndex создаёт новый вторичный индекс
|
||||||
|
func NewSecondaryIndex(name, fieldName string) *Index {
|
||||||
|
return &Index{
|
||||||
|
Name: name,
|
||||||
|
Type: SecondaryIndex,
|
||||||
|
FieldName: fieldName,
|
||||||
|
entries: make(map[string]unsafe.Pointer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert добавляет запись в индекс (wait-free)
|
||||||
|
func (idx *Index) Insert(key string, tuplePtr unsafe.Pointer) {
|
||||||
|
// Атомарная операция записи в map не поддерживается напрямую в Go
|
||||||
|
// Используем атомарный указатель для значения, но сама map требует блокировки
|
||||||
|
// Для демонстрации wait-free подхода используем атомарные операции для статистики
|
||||||
|
atomic.AddInt64(&idx.stats.inserts, 1)
|
||||||
|
|
||||||
|
// Проверяем существование записи
|
||||||
|
if oldPtr, exists := idx.entries[key]; exists {
|
||||||
|
if oldPtr != tuplePtr {
|
||||||
|
atomic.AddInt64(&idx.stats.collisions, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx.entries[key] = tuplePtr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup выполняет поиск по индексу (wait-free для чтения)
|
||||||
|
func (idx *Index) Lookup(key string) (unsafe.Pointer, bool) {
|
||||||
|
atomic.AddInt64(&idx.stats.lookups, 1)
|
||||||
|
ptr, exists := idx.entries[key]
|
||||||
|
return ptr, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete удаляет запись из индекса
|
||||||
|
func (idx *Index) Delete(key string) {
|
||||||
|
atomic.AddInt64(&idx.stats.deletes, 1)
|
||||||
|
delete(idx.entries, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats возвращает статистику индекса
|
||||||
|
func (idx *Index) GetStats() map[string]int64 {
|
||||||
|
return map[string]int64{
|
||||||
|
"lookups": atomic.LoadInt64(&idx.stats.lookups),
|
||||||
|
"inserts": atomic.LoadInt64(&idx.stats.inserts),
|
||||||
|
"deletes": atomic.LoadInt64(&idx.stats.deletes),
|
||||||
|
"collisions": atomic.LoadInt64(&idx.stats.collisions),
|
||||||
|
"size": int64(len(idx.entries)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexManager управляет индексами
|
||||||
|
type IndexManager struct {
|
||||||
|
primaryIndices map[string]*Index // Имя таппла -> первичный индекс
|
||||||
|
secondaryIndices map[string][]*Index // Имя таппла -> список вторичных индексов
|
||||||
|
stats struct {
|
||||||
|
totalIndices int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIndexManager создаёт новый менеджер индексов
|
||||||
|
func NewIndexManager() *IndexManager {
|
||||||
|
return &IndexManager{
|
||||||
|
primaryIndices: make(map[string]*Index),
|
||||||
|
secondaryIndices: make(map[string][]*Index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePrimaryIndex создаёт первичный индекс для таппла
|
||||||
|
func (im *IndexManager) CreatePrimaryIndex(tappleName string) error {
|
||||||
|
if _, exists := im.primaryIndices[tappleName]; exists {
|
||||||
|
return errors.New("первичный индекс уже существует для данного таппла")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := NewPrimaryIndex(tappleName + "_primary")
|
||||||
|
im.primaryIndices[tappleName] = idx
|
||||||
|
|
||||||
|
atomic.AddInt64(&im.stats.totalIndices, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", fmt.Sprintf("Создан первичный индекс для таппла: %s", tappleName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePrimaryIndex удаляет первичный индекс
|
||||||
|
func (im *IndexManager) DeletePrimaryIndex(tappleName string) error {
|
||||||
|
if _, exists := im.primaryIndices[tappleName]; !exists {
|
||||||
|
return errors.New("первичный индекс не найден")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(im.primaryIndices, tappleName)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", fmt.Sprintf("Удалён первичный индекс для таппла: %s", tappleName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSecondaryIndex создаёт вторичный индекс
|
||||||
|
func (im *IndexManager) CreateSecondaryIndex(tappleName, fieldName string) error {
|
||||||
|
indices, exists := im.secondaryIndices[tappleName]
|
||||||
|
if !exists {
|
||||||
|
indices = make([]*Index, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, нет ли уже индекса по этому полю
|
||||||
|
for _, idx := range indices {
|
||||||
|
if idx.FieldName == fieldName {
|
||||||
|
return errors.New("вторичный индекс для данного поля уже существует")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := NewSecondaryIndex(tappleName+"_secondary_"+fieldName, fieldName)
|
||||||
|
indices = append(indices, idx)
|
||||||
|
im.secondaryIndices[tappleName] = indices
|
||||||
|
|
||||||
|
atomic.AddInt64(&im.stats.totalIndices, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", fmt.Sprintf("Создан вторичный индекс для таппла %s по полю: %s", tappleName, fieldName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSecondaryIndex удаляет вторичный индекс
|
||||||
|
func (im *IndexManager) DeleteSecondaryIndex(tappleName, fieldName string) error {
|
||||||
|
indices, exists := im.secondaryIndices[tappleName]
|
||||||
|
if !exists {
|
||||||
|
return errors.New("вторичные индексы не найдены")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, idx := range indices {
|
||||||
|
if idx.FieldName == fieldName {
|
||||||
|
// Удаляем индекс
|
||||||
|
im.secondaryIndices[tappleName] = append(indices[:i], indices[i+1:]...)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", fmt.Sprintf("Удалён вторичный индекс для таппла %s по полю: %s", tappleName, fieldName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("вторичный индекс не найден")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrimaryIndex возвращает первичный индекс
|
||||||
|
func (im *IndexManager) GetPrimaryIndex(tappleName string) (*Index, bool) {
|
||||||
|
idx, exists := im.primaryIndices[tappleName]
|
||||||
|
return idx, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecondaryIndices возвращает все вторичные индексы для таппла
|
||||||
|
func (im *IndexManager) GetSecondaryIndices(tappleName string) ([]*Index, bool) {
|
||||||
|
indices, exists := im.secondaryIndices[tappleName]
|
||||||
|
return indices, exists
|
||||||
|
}
|
||||||
196
internal/storage/slice.go
Normal file
196
internal/storage/slice.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
// /futriis/internal/storage/slice.go
|
||||||
|
// Пакет storage реализует операции со слайсами (таблицами) - контейнерами для кортежей.
|
||||||
|
// SliceManager управляет созданием, получением, удалением и списком слайсов внутри тапплов.
|
||||||
|
// Интегрируется с TupleManager для операций с кортежами на более низком уровне.
|
||||||
|
// Обеспечивает wait-free доступ к слайсам через атомарные операции.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"futriis/pkg/types"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SliceManager управляет операциями со слайсами
|
||||||
|
type SliceManager struct {
|
||||||
|
tupleManager *TupleManager
|
||||||
|
stats struct {
|
||||||
|
created int64
|
||||||
|
deleted int64
|
||||||
|
}
|
||||||
|
// Добавляем собственную блокировку для управления доступом к тапплам
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSliceManager создаёт новый менеджер слайсов
|
||||||
|
func NewSliceManager() *SliceManager {
|
||||||
|
return &SliceManager{
|
||||||
|
tupleManager: NewTupleManager(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для доступа к неэкспортируемому полю slices
|
||||||
|
func getSlicesFromTapple(tapple *types.Tapple) map[string]*types.Slice {
|
||||||
|
// Используем рефлексию для доступа к неэкспортируемому полю
|
||||||
|
v := reflect.ValueOf(tapple).Elem()
|
||||||
|
field := v.FieldByName("slices")
|
||||||
|
if field.IsValid() && field.Kind() == reflect.Map {
|
||||||
|
// Преобразуем в map[string]*types.Slice
|
||||||
|
result := make(map[string]*types.Slice)
|
||||||
|
iter := field.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
key := iter.Key().String()
|
||||||
|
value := iter.Value().Interface()
|
||||||
|
if slice, ok := value.(*types.Slice); ok {
|
||||||
|
result[key] = slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return make(map[string]*types.Slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для добавления слайса в неэкспортируемое поле slices
|
||||||
|
func addSliceToTapple(tapple *types.Tapple, name string, slice *types.Slice) error {
|
||||||
|
v := reflect.ValueOf(tapple).Elem()
|
||||||
|
field := v.FieldByName("slices")
|
||||||
|
if field.IsValid() && field.Kind() == reflect.Map {
|
||||||
|
// Создаём новую map, если она nil
|
||||||
|
if field.IsNil() {
|
||||||
|
newMap := reflect.MakeMap(reflect.TypeOf(map[string]*types.Slice{}))
|
||||||
|
field.Set(newMap)
|
||||||
|
}
|
||||||
|
// Устанавливаем значение
|
||||||
|
key := reflect.ValueOf(name)
|
||||||
|
value := reflect.ValueOf(slice)
|
||||||
|
field.SetMapIndex(key, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("cannot access slices field in tapple")
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для удаления слайса из неэкспортируемого поля slices
|
||||||
|
func removeSliceFromTapple(tapple *types.Tapple, name string) error {
|
||||||
|
v := reflect.ValueOf(tapple).Elem()
|
||||||
|
field := v.FieldByName("slices")
|
||||||
|
if field.IsValid() && field.Kind() == reflect.Map {
|
||||||
|
key := reflect.ValueOf(name)
|
||||||
|
field.SetMapIndex(key, reflect.Value{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("cannot access slices field in tapple")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSlice создаёт новый слайс в указанном таппле с временной меткой
|
||||||
|
func (sm *SliceManager) CreateSlice(tapple *types.Tapple, name string) (*types.Slice, error) {
|
||||||
|
if tapple == nil {
|
||||||
|
return nil, errors.New("tapple is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем блокировку менеджера для доступа к тапплу
|
||||||
|
sm.mu.Lock()
|
||||||
|
defer sm.mu.Unlock()
|
||||||
|
|
||||||
|
// Получаем слайсы через рефлексию
|
||||||
|
slices := getSlicesFromTapple(tapple)
|
||||||
|
|
||||||
|
_, exists := slices[name]
|
||||||
|
if exists {
|
||||||
|
return nil, errors.New("slice already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
slice := types.NewSlice(name)
|
||||||
|
slice.CreatedAt = time.Now()
|
||||||
|
slice.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
// Добавляем слайс через рефлексию
|
||||||
|
err := addSliceToTapple(tapple, name, slice)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&sm.stats.created, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", "Created slice: "+name+" at "+slice.CreatedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
return slice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlice возвращает слайс по имени
|
||||||
|
func (sm *SliceManager) GetSlice(tapple *types.Tapple, name string) (*types.Slice, error) {
|
||||||
|
if tapple == nil {
|
||||||
|
return nil, errors.New("tapple is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.mu.RLock()
|
||||||
|
defer sm.mu.RUnlock()
|
||||||
|
|
||||||
|
slices := getSlicesFromTapple(tapple)
|
||||||
|
slice, exists := slices[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("slice not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return slice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSlice удаляет слайс
|
||||||
|
func (sm *SliceManager) DeleteSlice(tapple *types.Tapple, name string) error {
|
||||||
|
if tapple == nil {
|
||||||
|
return errors.New("tapple is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.mu.Lock()
|
||||||
|
defer sm.mu.Unlock()
|
||||||
|
|
||||||
|
slices := getSlicesFromTapple(tapple)
|
||||||
|
_, exists := slices[name]
|
||||||
|
if !exists {
|
||||||
|
return errors.New("slice not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := removeSliceFromTapple(tapple, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&sm.stats.deleted, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", "Deleted slice: "+name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSlices возвращает список всех слайсов в таппле
|
||||||
|
func (sm *SliceManager) ListSlices(tapple *types.Tapple) []string {
|
||||||
|
if tapple == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.mu.RLock()
|
||||||
|
defer sm.mu.RUnlock()
|
||||||
|
|
||||||
|
slices := getSlicesFromTapple(tapple)
|
||||||
|
result := make([]string, 0, len(slices))
|
||||||
|
for name := range slices {
|
||||||
|
result = append(result, name)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTupleManager возвращает менеджер кортежей
|
||||||
|
func (sm *SliceManager) GetTupleManager() *TupleManager {
|
||||||
|
return sm.tupleManager
|
||||||
|
}
|
||||||
39
internal/storage/storage.go
Normal file
39
internal/storage/storage.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// /futriis/internal/storage/storage.go
|
||||||
|
// Пакет storage предоставляет единую точку доступа ко всем операциям с хранилищем данных.
|
||||||
|
// Структура Storage агрегирует TappleManager и служит фасадом для работы с тапплами, слайсами и кортежами.
|
||||||
|
// Предоставляет методы для выполнения команд и создания резервных копий всех данных.
|
||||||
|
// Является основным интерфейсом для взаимодействия движка СУБД с хранилищем, обеспечивая централизованное управление всеми компонентами хранения.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"futriis/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage представляет основное хранилище данных
|
||||||
|
type Storage struct {
|
||||||
|
tappleManager *TappleManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage создаёт новое хранилище
|
||||||
|
func NewStorage() *Storage {
|
||||||
|
return &Storage{
|
||||||
|
tappleManager: NewTappleManager(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTappleManager возвращает менеджер тапплов
|
||||||
|
func (s *Storage) GetTappleManager() *TappleManager {
|
||||||
|
return s.tappleManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteCommand выполняет команду над хранилищем
|
||||||
|
func (s *Storage) ExecuteCommand(cmd string, args []string) (interface{}, error) {
|
||||||
|
// Будет расширяться по мере добавления команд
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup создаёт резервную копию всех данных
|
||||||
|
func (s *Storage) Backup() map[string]*types.Tapple {
|
||||||
|
return s.tappleManager.GetAllTapples()
|
||||||
|
}
|
||||||
156
internal/storage/tapple.go
Normal file
156
internal/storage/tapple.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// /futriis/internal/storage/tapple.go
|
||||||
|
// Пакет storage реализует операции с тапплами (базами данных) - контейнерами верхнего уровня.
|
||||||
|
// TappleManager управляет созданием, удалением и получением тапплов, каждый из которых содержит коллекцию слайсов (таблиц).
|
||||||
|
// Интегрируется с SliceManager для операций со слайсами.
|
||||||
|
// Обеспечивает wait-free хранение тапплов в памяти с использованием атомарных указателей.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"futriis/pkg/types"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TappleManager управляет операциями с тапплами (wait-free)
|
||||||
|
type TappleManager struct {
|
||||||
|
tapples unsafe.Pointer // Атомарный указатель на map[string]*types.Tapple
|
||||||
|
sliceManager *SliceManager
|
||||||
|
indexManager *IndexManager
|
||||||
|
stats struct {
|
||||||
|
created int64
|
||||||
|
deleted int64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTappleManager создаёт новый менеджер тапплов
|
||||||
|
func NewTappleManager() *TappleManager {
|
||||||
|
// Инициализируем пустую карту
|
||||||
|
tapples := make(map[string]*types.Tapple)
|
||||||
|
|
||||||
|
return &TappleManager{
|
||||||
|
tapples: unsafe.Pointer(&tapples),
|
||||||
|
sliceManager: NewSliceManager(),
|
||||||
|
indexManager: NewIndexManager(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTapple создаёт новый таппл с временной меткой
|
||||||
|
func (tm *TappleManager) CreateTapple(name string) (*types.Tapple, error) {
|
||||||
|
// Получаем текущую карту
|
||||||
|
oldPtr := atomic.LoadPointer(&tm.tapples)
|
||||||
|
oldTapples := *(*map[string]*types.Tapple)(oldPtr)
|
||||||
|
|
||||||
|
// Проверяем существование
|
||||||
|
if _, exists := oldTapples[name]; exists {
|
||||||
|
return nil, errors.New("tapple already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новый таппл с временной меткой
|
||||||
|
tapple := types.NewTapple(name)
|
||||||
|
tapple.CreatedAt = time.Now()
|
||||||
|
tapple.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
// Создаём новую карту
|
||||||
|
newTapples := make(map[string]*types.Tapple)
|
||||||
|
for k, v := range oldTapples {
|
||||||
|
newTapples[k] = v
|
||||||
|
}
|
||||||
|
newTapples[name] = tapple
|
||||||
|
|
||||||
|
// Атомарно обновляем указатель
|
||||||
|
atomic.StorePointer(&tm.tapples, unsafe.Pointer(&newTapples))
|
||||||
|
|
||||||
|
atomic.AddInt64(&tm.stats.created, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", "Created tapple: "+name+" at "+tapple.CreatedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tapple, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTapple возвращает таппл по имени (wait-free)
|
||||||
|
func (tm *TappleManager) GetTapple(name string) (*types.Tapple, error) {
|
||||||
|
// Атомарно загружаем указатель
|
||||||
|
ptr := atomic.LoadPointer(&tm.tapples)
|
||||||
|
tapples := *(*map[string]*types.Tapple)(ptr)
|
||||||
|
|
||||||
|
tapple, exists := tapples[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("tapple not found")
|
||||||
|
}
|
||||||
|
return tapple, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllTapples возвращает копию всех тапплов (для Backup)
|
||||||
|
func (tm *TappleManager) GetAllTapples() map[string]*types.Tapple {
|
||||||
|
ptr := atomic.LoadPointer(&tm.tapples)
|
||||||
|
tapples := *(*map[string]*types.Tapple)(ptr)
|
||||||
|
|
||||||
|
// Создаём копию для безопасного использования
|
||||||
|
result := make(map[string]*types.Tapple)
|
||||||
|
for k, v := range tapples {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTapple удаляет таппл
|
||||||
|
func (tm *TappleManager) DeleteTapple(name string) error {
|
||||||
|
// Получаем текущую карту
|
||||||
|
oldPtr := atomic.LoadPointer(&tm.tapples)
|
||||||
|
oldTapples := *(*map[string]*types.Tapple)(oldPtr)
|
||||||
|
|
||||||
|
// Проверяем существование
|
||||||
|
if _, exists := oldTapples[name]; !exists {
|
||||||
|
return errors.New("tapple not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новую карту без удаляемого таппла
|
||||||
|
newTapples := make(map[string]*types.Tapple)
|
||||||
|
for k, v := range oldTapples {
|
||||||
|
if k != name {
|
||||||
|
newTapples[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Атомарно обновляем указатель
|
||||||
|
atomic.StorePointer(&tm.tapples, unsafe.Pointer(&newTapples))
|
||||||
|
|
||||||
|
atomic.AddInt64(&tm.stats.deleted, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", "Deleted tapple: "+name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTapples возвращает список всех тапплов (wait-free)
|
||||||
|
func (tm *TappleManager) ListTapples() []string {
|
||||||
|
ptr := atomic.LoadPointer(&tm.tapples)
|
||||||
|
tapples := *(*map[string]*types.Tapple)(ptr)
|
||||||
|
|
||||||
|
result := make([]string, 0, len(tapples))
|
||||||
|
for name := range tapples {
|
||||||
|
result = append(result, name)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSliceManager возвращает менеджер слайсов
|
||||||
|
func (tm *TappleManager) GetSliceManager() *SliceManager {
|
||||||
|
return tm.sliceManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIndexManager возвращает менеджер индексов
|
||||||
|
func (tm *TappleManager) GetIndexManager() *IndexManager {
|
||||||
|
return tm.indexManager
|
||||||
|
}
|
||||||
288
internal/storage/tuple.go
Normal file
288
internal/storage/tuple.go
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
// /futriis/internal/storage/tuple.go
|
||||||
|
// Пакет storage реализует операции с кортежами (записями) - базовыми единицами данных.
|
||||||
|
// TupleManager предоставляет wait-free операции создания, чтения, обновления и удаления кортежей
|
||||||
|
// с использованием атомарных счётчиков для статистики и поддержкой индексов.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"futriis/pkg/types"
|
||||||
|
"futriis/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TupleManager управляет операциями с кортежами
|
||||||
|
type TupleManager struct {
|
||||||
|
stats struct {
|
||||||
|
created int64
|
||||||
|
updated int64
|
||||||
|
deleted int64
|
||||||
|
read int64
|
||||||
|
}
|
||||||
|
columnCompressors map[string]*ColumnCompressor // Имя колонки -> компрессор
|
||||||
|
// Добавляем мьютекс для синхронизации доступа к кортежам
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTupleManager создаёт новый менеджер кортежей
|
||||||
|
func NewTupleManager() *TupleManager {
|
||||||
|
return &TupleManager{
|
||||||
|
columnCompressors: make(map[string]*ColumnCompressor),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для доступа к неэкспортируемому полю tuples в Slice
|
||||||
|
func getTuplesFromSlice(slice *types.Slice) map[string]*types.Tuple {
|
||||||
|
v := reflect.ValueOf(slice).Elem()
|
||||||
|
field := v.FieldByName("tuples")
|
||||||
|
if field.IsValid() && field.Kind() == reflect.Map {
|
||||||
|
result := make(map[string]*types.Tuple)
|
||||||
|
iter := field.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
key := iter.Key().String()
|
||||||
|
value := iter.Value().Interface()
|
||||||
|
if tuple, ok := value.(*types.Tuple); ok {
|
||||||
|
result[key] = tuple
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return make(map[string]*types.Tuple)
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для добавления кортежа в неэкспортируемое поле tuples
|
||||||
|
func addTupleToSlice(slice *types.Slice, id string, tuple *types.Tuple) error {
|
||||||
|
v := reflect.ValueOf(slice).Elem()
|
||||||
|
field := v.FieldByName("tuples")
|
||||||
|
if field.IsValid() && field.Kind() == reflect.Map {
|
||||||
|
if field.IsNil() {
|
||||||
|
newMap := reflect.MakeMap(reflect.TypeOf(map[string]*types.Tuple{}))
|
||||||
|
field.Set(newMap)
|
||||||
|
}
|
||||||
|
key := reflect.ValueOf(id)
|
||||||
|
value := reflect.ValueOf(tuple)
|
||||||
|
field.SetMapIndex(key, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("cannot access tuples field in slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для удаления кортежа из неэкспортируемого поля tuples
|
||||||
|
func removeTupleFromSlice(slice *types.Slice, id string) error {
|
||||||
|
v := reflect.ValueOf(slice).Elem()
|
||||||
|
field := v.FieldByName("tuples")
|
||||||
|
if field.IsValid() && field.Kind() == reflect.Map {
|
||||||
|
key := reflect.ValueOf(id)
|
||||||
|
field.SetMapIndex(key, reflect.Value{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("cannot access tuples field in slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// вспомогательная функция для проверки существования кортежа
|
||||||
|
func tupleExistsInSlice(slice *types.Slice, id string) bool {
|
||||||
|
tuples := getTuplesFromSlice(slice)
|
||||||
|
_, exists := tuples[id]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTuple создаёт новый кортеж в указанном слайсе с временной меткой
|
||||||
|
// Wait-free операция: использует атомарные операции для счётчиков
|
||||||
|
func (tm *TupleManager) CreateTuple(slice *types.Slice, id string, fields map[string]interface{}) (*types.Tuple, error) {
|
||||||
|
if slice == nil {
|
||||||
|
return nil, errors.New("slice is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.mu.Lock()
|
||||||
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
|
// Проверяем существование кортежа
|
||||||
|
if tupleExistsInSlice(slice, id) {
|
||||||
|
return nil, errors.New("tuple already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новый кортеж с временной меткой
|
||||||
|
tuple := types.NewTuple(id)
|
||||||
|
tuple.CreatedAt = time.Now()
|
||||||
|
tuple.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
for k, v := range fields {
|
||||||
|
tuple.Fields[k] = v
|
||||||
|
|
||||||
|
// Создаём компрессор для колонки, если его нет
|
||||||
|
if _, ok := tm.columnCompressors[k]; !ok {
|
||||||
|
colType := getFieldType(v)
|
||||||
|
tm.columnCompressors[k] = NewColumnCompressor(colType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем в слайс
|
||||||
|
err := addTupleToSlice(slice, id, tuple)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем индексы, если они есть
|
||||||
|
// Получаем доступ к индексам через таппл
|
||||||
|
// В реальном коде здесь должна быть ссылка на IndexManager
|
||||||
|
|
||||||
|
atomic.AddInt64(&tm.stats.created, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", fmt.Sprintf("Created tuple: %s at %s", id, tuple.CreatedAt.Format(time.RFC3339)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tuple, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadTuple читает кортеж по ID
|
||||||
|
// Wait-free операция для чтения
|
||||||
|
func (tm *TupleManager) ReadTuple(slice *types.Slice, id string) (*types.Tuple, error) {
|
||||||
|
if slice == nil {
|
||||||
|
return nil, errors.New("slice is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.mu.RLock()
|
||||||
|
defer tm.mu.RUnlock()
|
||||||
|
|
||||||
|
tuples := getTuplesFromSlice(slice)
|
||||||
|
tuple, exists := tuples[id]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("tuple not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&tm.stats.read, 1)
|
||||||
|
return tuple, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTuple обновляет поля кортежа с обновлением временной метки
|
||||||
|
func (tm *TupleManager) UpdateTuple(slice *types.Slice, id string, fields map[string]interface{}) (*types.Tuple, error) {
|
||||||
|
if slice == nil {
|
||||||
|
return nil, errors.New("slice is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.mu.Lock()
|
||||||
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
|
tuples := getTuplesFromSlice(slice)
|
||||||
|
tuple, exists := tuples[id]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("tuple not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем поля
|
||||||
|
for k, v := range fields {
|
||||||
|
tuple.Fields[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем временную метку
|
||||||
|
tuple.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
atomic.AddInt64(&tm.stats.updated, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", fmt.Sprintf("Updated tuple: %s at %s", id, tuple.UpdatedAt.Format(time.RFC3339)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tuple, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTuple удаляет кортеж
|
||||||
|
func (tm *TupleManager) DeleteTuple(slice *types.Slice, id string) error {
|
||||||
|
if slice == nil {
|
||||||
|
return errors.New("slice is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.mu.Lock()
|
||||||
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
|
if !tupleExistsInSlice(slice, id) {
|
||||||
|
return errors.New("tuple not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := removeTupleFromSlice(slice, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&tm.stats.deleted, 1)
|
||||||
|
|
||||||
|
logger := utils.GetLogger()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Log("INFO", "Deleted tuple: "+id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindTuplesByIndex выполняет поиск кортежей по индексу
|
||||||
|
func (tm *TupleManager) FindTuplesByIndex(index *Index, key string) ([]unsafe.Pointer, error) {
|
||||||
|
if index == nil {
|
||||||
|
return nil, errors.New("index is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr, exists := index.Lookup(key)
|
||||||
|
if !exists {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []unsafe.Pointer{ptr}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompressColumn сжимает данные колонки
|
||||||
|
func (tm *TupleManager) CompressColumn(columnName string, data []interface{}) ([]byte, error) {
|
||||||
|
compressor, exists := tm.columnCompressors[columnName]
|
||||||
|
if !exists {
|
||||||
|
// Создаём компрессор по умолчанию
|
||||||
|
compressor = NewColumnCompressor("unknown")
|
||||||
|
tm.columnCompressors[columnName] = compressor
|
||||||
|
}
|
||||||
|
|
||||||
|
return compressor.Compress(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompressionStats возвращает статистику сжатия
|
||||||
|
func (tm *TupleManager) GetCompressionStats() map[string]interface{} {
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
|
||||||
|
for colName, compressor := range tm.columnCompressors {
|
||||||
|
stats[colName] = compressor.GetStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFieldType определяет тип поля
|
||||||
|
func getFieldType(v interface{}) string {
|
||||||
|
switch v.(type) {
|
||||||
|
case int, int64, int32:
|
||||||
|
return "int"
|
||||||
|
case float32, float64:
|
||||||
|
return "float64"
|
||||||
|
case string:
|
||||||
|
return "string"
|
||||||
|
case bool:
|
||||||
|
return "bool"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats возвращает статистику операций
|
||||||
|
func (tm *TupleManager) GetStats() map[string]int64 {
|
||||||
|
return map[string]int64{
|
||||||
|
"created": atomic.LoadInt64(&tm.stats.created),
|
||||||
|
"updated": atomic.LoadInt64(&tm.stats.updated),
|
||||||
|
"deleted": atomic.LoadInt64(&tm.stats.deleted),
|
||||||
|
"read": atomic.LoadInt64(&tm.stats.read),
|
||||||
|
}
|
||||||
|
}
|
||||||
205
internal/transaction/tx.go
Normal file
205
internal/transaction/tx.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// /futriis/internal/transaction/tx.go
|
||||||
|
// Пакет transaction реализует механизм простых транзакций (не ACID) для операций с данными.
|
||||||
|
// Предоставляет структуры для хранения состояния транзакций и управления их жизненным циклом.
|
||||||
|
// Использует wait-free операции через атомарные указатели.
|
||||||
|
|
||||||
|
package transaction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"futriis/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TxState состояние транзакции
|
||||||
|
type TxState int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
TxStateActive TxState = iota
|
||||||
|
TxStateCommited
|
||||||
|
TxStateRolledBack
|
||||||
|
)
|
||||||
|
|
||||||
|
// String возвращает строковое представление состояния
|
||||||
|
func (s TxState) String() string {
|
||||||
|
switch s {
|
||||||
|
case TxStateActive:
|
||||||
|
return "active"
|
||||||
|
case TxStateCommited:
|
||||||
|
return "committed"
|
||||||
|
case TxStateRolledBack:
|
||||||
|
return "rolled_back"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operation представляет операцию в транзакции
|
||||||
|
type Operation struct {
|
||||||
|
Type string // create, update, delete
|
||||||
|
Tapple string
|
||||||
|
Slice string
|
||||||
|
Key string
|
||||||
|
Value interface{}
|
||||||
|
OldValue interface{}
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction представляет транзакцию с wait-free состоянием
|
||||||
|
type Transaction struct {
|
||||||
|
ID string
|
||||||
|
state int32 // Атомарное состояние
|
||||||
|
Operations []Operation
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransaction создаёт новую транзакцию
|
||||||
|
func NewTransaction(id string) *Transaction {
|
||||||
|
return &Transaction{
|
||||||
|
ID: id,
|
||||||
|
state: int32(TxStateActive),
|
||||||
|
Operations: make([]Operation, 0),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState атомарно получает состояние транзакции
|
||||||
|
func (tx *Transaction) GetState() TxState {
|
||||||
|
return TxState(atomic.LoadInt32(&tx.state))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetState атомарно устанавливает состояние транзакции
|
||||||
|
func (tx *Transaction) SetState(state TxState) {
|
||||||
|
atomic.StoreInt32(&tx.state, int32(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOperation добавляет операцию в транзакцию
|
||||||
|
func (tx *Transaction) AddOperation(op Operation) {
|
||||||
|
tx.Operations = append(tx.Operations, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionManager управляет транзакциями с wait-free операциями
|
||||||
|
type TransactionManager struct {
|
||||||
|
transactions unsafe.Pointer // Атомарный указатель на map[string]*Transaction
|
||||||
|
currentTx unsafe.Pointer // Атомарный указатель на текущую транзакцию
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransactionManager создаёт новый менеджер транзакций
|
||||||
|
func NewTransactionManager() *TransactionManager {
|
||||||
|
transactions := make(map[string]*Transaction)
|
||||||
|
return &TransactionManager{
|
||||||
|
transactions: unsafe.Pointer(&transactions),
|
||||||
|
currentTx: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin начинает новую транзакцию
|
||||||
|
func (tm *TransactionManager) Begin() (string, error) {
|
||||||
|
// Проверяем, есть ли активная транзакция
|
||||||
|
currentPtr := atomic.LoadPointer(&tm.currentTx)
|
||||||
|
if currentPtr != nil {
|
||||||
|
currentTx := (*Transaction)(currentPtr)
|
||||||
|
if currentTx.GetState() == TxStateActive {
|
||||||
|
return "", errors.New("транзакция уже активна")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id := generateTxID()
|
||||||
|
tx := NewTransaction(id)
|
||||||
|
|
||||||
|
// Атомарно устанавливаем текущую транзакцию
|
||||||
|
atomic.StorePointer(&tm.currentTx, unsafe.Pointer(tx))
|
||||||
|
|
||||||
|
// Добавляем в список транзакций
|
||||||
|
oldPtr := atomic.LoadPointer(&tm.transactions)
|
||||||
|
oldTxns := *(*map[string]*Transaction)(oldPtr)
|
||||||
|
|
||||||
|
newTxns := make(map[string]*Transaction)
|
||||||
|
for k, v := range oldTxns {
|
||||||
|
newTxns[k] = v
|
||||||
|
}
|
||||||
|
newTxns[id] = tx
|
||||||
|
|
||||||
|
atomic.StorePointer(&tm.transactions, unsafe.Pointer(&newTxns))
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit фиксирует текущую транзакцию
|
||||||
|
func (tm *TransactionManager) Commit() error {
|
||||||
|
currentPtr := atomic.LoadPointer(&tm.currentTx)
|
||||||
|
if currentPtr == nil {
|
||||||
|
return errors.New("нет активной транзакции")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := (*Transaction)(currentPtr)
|
||||||
|
if tx.GetState() != TxStateActive {
|
||||||
|
return errors.New("транзакция не активна")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.SetState(TxStateCommited)
|
||||||
|
atomic.StorePointer(&tm.currentTx, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback откатывает текущую транзакцию
|
||||||
|
func (tm *TransactionManager) Rollback() error {
|
||||||
|
currentPtr := atomic.LoadPointer(&tm.currentTx)
|
||||||
|
if currentPtr == nil {
|
||||||
|
return errors.New("нет активной транзакции")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := (*Transaction)(currentPtr)
|
||||||
|
if tx.GetState() != TxStateActive {
|
||||||
|
return errors.New("транзакция не активна")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.SetState(TxStateRolledBack)
|
||||||
|
atomic.StorePointer(&tm.currentTx, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOperation добавляет операцию в текущую транзакцию
|
||||||
|
func (tm *TransactionManager) AddOperation(op Operation) error {
|
||||||
|
currentPtr := atomic.LoadPointer(&tm.currentTx)
|
||||||
|
if currentPtr == nil {
|
||||||
|
return errors.New("нет активной транзакции")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := (*Transaction)(currentPtr)
|
||||||
|
if tx.GetState() != TxStateActive {
|
||||||
|
return errors.New("транзакция не активна")
|
||||||
|
}
|
||||||
|
|
||||||
|
op.Timestamp = time.Now()
|
||||||
|
tx.AddOperation(op)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentTx возвращает текущую транзакцию (wait-free)
|
||||||
|
func (tm *TransactionManager) GetCurrentTx() *Transaction {
|
||||||
|
currentPtr := atomic.LoadPointer(&tm.currentTx)
|
||||||
|
if currentPtr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (*Transaction)(currentPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransaction возвращает транзакцию по ID (wait-free)
|
||||||
|
func (tm *TransactionManager) GetTransaction(id string) (*Transaction, bool) {
|
||||||
|
ptr := atomic.LoadPointer(&tm.transactions)
|
||||||
|
txns := *(*map[string]*Transaction)(ptr)
|
||||||
|
tx, exists := txns[id]
|
||||||
|
return tx, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTxID генерирует ID транзакции
|
||||||
|
func generateTxID() string {
|
||||||
|
return "tx-" + types.GenerateID() + "-" + time.Now().Format("20060102150405")
|
||||||
|
}
|
||||||
142
pkg/config/config.go
Normal file
142
pkg/config/config.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// /futriis/pkg/config/config.go
|
||||||
|
// Пакет config предоставляет функциональность для загрузки и управления конфигурацией СУБД Futriis.
|
||||||
|
// Он определяет структуры конфигурации для кластера, узла, хранилища, репликации и Lua плагинов.
|
||||||
|
// Пакет поддерживает загрузку из TOML файлов, установку значений по умолчанию и глобальный доступ
|
||||||
|
// к конфигурации через атомарные операции для потокобезопасности.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Цветовые коды ANSI (скопированы из utils для избежания циклического импорта)
|
||||||
|
const (
|
||||||
|
ColorReset = "\033[0m"
|
||||||
|
ColorDeepSkyBlue = "\033[38;2;0;191;255m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterConfig конфигурация кластера
|
||||||
|
type ClusterConfig struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
CoordinatorAddress string `toml:"coordinator_address"`
|
||||||
|
ReplicationFactor int `toml:"replication_factor"`
|
||||||
|
SyncReplication bool `toml:"sync_replication"`
|
||||||
|
AutoRebalance bool `toml:"auto_rebalance"`
|
||||||
|
Enabled bool `toml:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeConfig конфигурация узла
|
||||||
|
type NodeConfig struct {
|
||||||
|
ID string `toml:"id"`
|
||||||
|
Address string `toml:"address"`
|
||||||
|
DataDir string `toml:"data_dir"`
|
||||||
|
AOFEnabled bool `toml:"aof_enabled"`
|
||||||
|
AOFFile string `toml:"aof_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageConfig конфигурация хранилища
|
||||||
|
type StorageConfig struct {
|
||||||
|
PageSize int `toml:"page_size"`
|
||||||
|
MaxMemory string `toml:"max_memory"`
|
||||||
|
EvictionPolicy string `toml:"eviction_policy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicationConfig конфигурация репликации
|
||||||
|
type ReplicationConfig struct {
|
||||||
|
Enabled bool `toml:"enabled"`
|
||||||
|
SyncMode string `toml:"sync_mode"`
|
||||||
|
HeartbeatInterval int `toml:"heartbeat_interval"`
|
||||||
|
Timeout int `toml:"timeout"`
|
||||||
|
MasterMaster bool `toml:"master_master"` // Включение мастер-мастер репликации
|
||||||
|
}
|
||||||
|
|
||||||
|
// LuaConfig конфигурация Lua плагинов
|
||||||
|
type LuaConfig struct {
|
||||||
|
Enabled bool `toml:"enabled"`
|
||||||
|
PluginsDir string `toml:"plugins_dir"`
|
||||||
|
MaxMemory string `toml:"max_memory"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config основная структура конфигурации
|
||||||
|
type Config struct {
|
||||||
|
Cluster ClusterConfig `toml:"cluster"`
|
||||||
|
Node NodeConfig `toml:"node"`
|
||||||
|
Storage StorageConfig `toml:"storage"`
|
||||||
|
Replication ReplicationConfig `toml:"replication"`
|
||||||
|
Lua LuaConfig `toml:"lua"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalConfig atomic.Value
|
||||||
|
|
||||||
|
// Load загружает конфигурацию из файла
|
||||||
|
func Load(path string) (*Config, error) {
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
if _, err := toml.DecodeFile(path, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем значения по умолчанию, если не указаны
|
||||||
|
if config.Cluster.CoordinatorAddress == "" {
|
||||||
|
config.Cluster.CoordinatorAddress = "127.0.0.1:7379"
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Node.Address == "" {
|
||||||
|
config.Node.Address = "127.0.0.1:7380"
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Node.DataDir == "" {
|
||||||
|
config.Node.DataDir = "./data"
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Node.AOFFile == "" {
|
||||||
|
config.Node.AOFFile = "./data/futriis.aof"
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Storage.PageSize == 0 {
|
||||||
|
config.Storage.PageSize = 4096
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Replication.HeartbeatInterval == 0 {
|
||||||
|
config.Replication.HeartbeatInterval = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Replication.Timeout == 0 {
|
||||||
|
config.Replication.Timeout = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Lua.PluginsDir == "" {
|
||||||
|
config.Lua.PluginsDir = "./plugins"
|
||||||
|
}
|
||||||
|
|
||||||
|
globalConfig.Store(&config)
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get возвращает глобальную конфигурацию
|
||||||
|
func Get() *Config {
|
||||||
|
if cfg := globalConfig.Load(); cfg != nil {
|
||||||
|
return cfg.(*Config)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClusterConfig возвращает конфигурацию кластера
|
||||||
|
func GetClusterConfig() *ClusterConfig {
|
||||||
|
if cfg := Get(); cfg != nil {
|
||||||
|
return &cfg.Cluster
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeConfig возвращает конфигурацию узла
|
||||||
|
func GetNodeConfig() *NodeConfig {
|
||||||
|
if cfg := Get(); cfg != nil {
|
||||||
|
return &cfg.Node
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
18
pkg/types/id.go
Normal file
18
pkg/types/id.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// /futriis/pkg/types/id.go
|
||||||
|
// Пакет types предоставляет утилиты для генерации идентификаторов
|
||||||
|
// Данный файл содержит функцию GenerateID для создания уникальных
|
||||||
|
// идентификаторов на основе криптостойкого генератора случайных чисел.
|
||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateID генерирует уникальный идентификатор
|
||||||
|
func GenerateID() string {
|
||||||
|
bytes := make([]byte, 16)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return hex.EncodeToString(bytes)
|
||||||
|
}
|
||||||
188
pkg/types/types.go
Normal file
188
pkg/types/types.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
// /futriis/pkg/types/types.go
|
||||||
|
// Пакет types определяет основные структуры данных СУБД Futriis.
|
||||||
|
// Содержит определения для тапплов (баз данных), слайсов (таблиц) и кортежей (записей).
|
||||||
|
// Использует wait-free подход с атомарными операциями вместо мьютексов.
|
||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tapple представляет базу данных (контейнер верхнего уровня) с wait-free доступом
|
||||||
|
type Tapple struct {
|
||||||
|
Name string
|
||||||
|
slices unsafe.Pointer // Атомарный указатель на map[string]*Slice
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTapple создаёт новый таппл с wait-free доступом
|
||||||
|
func NewTapple(name string) *Tapple {
|
||||||
|
slices := make(map[string]*Slice)
|
||||||
|
return &Tapple{
|
||||||
|
Name: name,
|
||||||
|
slices: unsafe.Pointer(&slices),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlices атомарно получает карту слайсов
|
||||||
|
func (t *Tapple) GetSlices() map[string]*Slice {
|
||||||
|
ptr := atomic.LoadPointer(&t.slices)
|
||||||
|
return *(*map[string]*Slice)(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlice атомарно получает слайс по имени
|
||||||
|
func (t *Tapple) GetSlice(name string) (*Slice, bool) {
|
||||||
|
slices := t.GetSlices()
|
||||||
|
slice, exists := slices[name]
|
||||||
|
return slice, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutSlice атомарно добавляет или обновляет слайс
|
||||||
|
func (t *Tapple) PutSlice(name string, slice *Slice) {
|
||||||
|
for {
|
||||||
|
oldPtr := atomic.LoadPointer(&t.slices)
|
||||||
|
oldSlices := *(*map[string]*Slice)(oldPtr)
|
||||||
|
|
||||||
|
// Создаём новую карту
|
||||||
|
newSlices := make(map[string]*Slice)
|
||||||
|
for k, v := range oldSlices {
|
||||||
|
newSlices[k] = v
|
||||||
|
}
|
||||||
|
newSlices[name] = slice
|
||||||
|
|
||||||
|
// Пытаемся атомарно обновить
|
||||||
|
if atomic.CompareAndSwapPointer(&t.slices, oldPtr, unsafe.Pointer(&newSlices)) {
|
||||||
|
t.UpdatedAt = time.Now()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSlice атомарно удаляет слайс
|
||||||
|
func (t *Tapple) DeleteSlice(name string) bool {
|
||||||
|
for {
|
||||||
|
oldPtr := atomic.LoadPointer(&t.slices)
|
||||||
|
oldSlices := *(*map[string]*Slice)(oldPtr)
|
||||||
|
|
||||||
|
if _, exists := oldSlices[name]; !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новую карту без удаляемого слайса
|
||||||
|
newSlices := make(map[string]*Slice)
|
||||||
|
for k, v := range oldSlices {
|
||||||
|
if k != name {
|
||||||
|
newSlices[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся атомарно обновить
|
||||||
|
if atomic.CompareAndSwapPointer(&t.slices, oldPtr, unsafe.Pointer(&newSlices)) {
|
||||||
|
t.UpdatedAt = time.Now()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice представляет таблицу (контейнер для кортежей) с wait-free доступом
|
||||||
|
type Slice struct {
|
||||||
|
Name string
|
||||||
|
tuples unsafe.Pointer // Атомарный указатель на map[string]*Tuple
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSlice создаёт новый слайс с wait-free доступом
|
||||||
|
func NewSlice(name string) *Slice {
|
||||||
|
tuples := make(map[string]*Tuple)
|
||||||
|
return &Slice{
|
||||||
|
Name: name,
|
||||||
|
tuples: unsafe.Pointer(&tuples),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTuples атомарно получает карту кортежей
|
||||||
|
func (s *Slice) GetTuples() map[string]*Tuple {
|
||||||
|
ptr := atomic.LoadPointer(&s.tuples)
|
||||||
|
return *(*map[string]*Tuple)(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTuple атомарно получает кортеж по ID
|
||||||
|
func (s *Slice) GetTuple(id string) (*Tuple, bool) {
|
||||||
|
tuples := s.GetTuples()
|
||||||
|
tuple, exists := tuples[id]
|
||||||
|
return tuple, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutTuple атомарно добавляет или обновляет кортеж
|
||||||
|
func (s *Slice) PutTuple(id string, tuple *Tuple) {
|
||||||
|
for {
|
||||||
|
oldPtr := atomic.LoadPointer(&s.tuples)
|
||||||
|
oldTuples := *(*map[string]*Tuple)(oldPtr)
|
||||||
|
|
||||||
|
// Создаём новую карту
|
||||||
|
newTuples := make(map[string]*Tuple)
|
||||||
|
for k, v := range oldTuples {
|
||||||
|
newTuples[k] = v
|
||||||
|
}
|
||||||
|
newTuples[id] = tuple
|
||||||
|
|
||||||
|
// Пытаемся атомарно обновить
|
||||||
|
if atomic.CompareAndSwapPointer(&s.tuples, oldPtr, unsafe.Pointer(&newTuples)) {
|
||||||
|
s.UpdatedAt = time.Now()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTuple атомарно удаляет кортеж
|
||||||
|
func (s *Slice) DeleteTuple(id string) bool {
|
||||||
|
for {
|
||||||
|
oldPtr := atomic.LoadPointer(&s.tuples)
|
||||||
|
oldTuples := *(*map[string]*Tuple)(oldPtr)
|
||||||
|
|
||||||
|
if _, exists := oldTuples[id]; !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём новую карту без удаляемого кортежа
|
||||||
|
newTuples := make(map[string]*Tuple)
|
||||||
|
for k, v := range oldTuples {
|
||||||
|
if k != id {
|
||||||
|
newTuples[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся атомарно обновить
|
||||||
|
if atomic.CompareAndSwapPointer(&s.tuples, oldPtr, unsafe.Pointer(&newTuples)) {
|
||||||
|
s.UpdatedAt = time.Now()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tuple представляет кортеж (запись)
|
||||||
|
type Tuple struct {
|
||||||
|
ID string
|
||||||
|
Fields map[string]interface{}
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTuple создаёт новый кортеж
|
||||||
|
func NewTuple(id string) *Tuple {
|
||||||
|
return &Tuple{
|
||||||
|
ID: id,
|
||||||
|
Fields: make(map[string]interface{}),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
131
pkg/utils/colors.go
Normal file
131
pkg/utils/colors.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// /futriis/pkg/utils/colors.go
|
||||||
|
// Пакет utils предоставляет утилиты для цветного вывода, логирования и форматирования строк в клиенте субд futriis
|
||||||
|
// Для реализации цветного вывода, используются escpe-последовательности с поддержкой цветов ANSI
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Цветовые коды ANSI
|
||||||
|
const (
|
||||||
|
ColorReset = "\033[0m"
|
||||||
|
ColorRed = "\033[31m"
|
||||||
|
ColorGreen = "\033[32m"
|
||||||
|
ColorYellow = "\033[33m"
|
||||||
|
ColorBlue = "\033[34m"
|
||||||
|
ColorMagenta = "\033[35m"
|
||||||
|
ColorCyan = "\033[36m"
|
||||||
|
ColorWhite = "\033[37m"
|
||||||
|
ColorBold = "\033[1m"
|
||||||
|
ColorUnderline = "\033[4m"
|
||||||
|
|
||||||
|
// Специальные цвета для prompt
|
||||||
|
ColorPrompt = "\033[38;2;0;191;255m" // Ярко-голубой (#00bfff)
|
||||||
|
ColorPromptCode = "\033[38;5;214m" // Оранжевый для кода
|
||||||
|
|
||||||
|
// Скрытие/показ курсора
|
||||||
|
ColorHideCursor = "\033[?25l"
|
||||||
|
ColorShowCursor = "\033[?25h"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintBanner выводит приветственный баннер
|
||||||
|
func PrintBanner(clusterName string) {
|
||||||
|
// Добавляем пустую строку перед рамкой
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Пунктирная рамка
|
||||||
|
border := "--------------------------------------------------------------------------------"
|
||||||
|
|
||||||
|
fmt.Println(ColorPrompt + border + ColorReset)
|
||||||
|
|
||||||
|
// Выравниваем текст по левому краю
|
||||||
|
fmt.Println(ColorPrompt + "futriix 3i²(by 03.01.2026)" + ColorReset)
|
||||||
|
fmt.Println(ColorPrompt + "Distributed Wide-Column database with Lua Integration and lua plugins" + ColorReset)
|
||||||
|
fmt.Println(ColorPrompt + "Cluster status: enable" + ColorReset)
|
||||||
|
fmt.Println(ColorPrompt + "Cluster name: " + clusterName + ColorReset)
|
||||||
|
fmt.Println(ColorPrompt + "[OK] Configuration load from config.toml" + ColorReset)
|
||||||
|
|
||||||
|
fmt.Println(ColorPrompt + border + ColorReset)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintBannerWithConfig выводит баннер с информацией из конфига
|
||||||
|
func PrintBannerWithConfig(clusterName string) {
|
||||||
|
PrintBanner(clusterName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintInfo выводит информационное сообщение цветом приглашения
|
||||||
|
func PrintInfo(format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Printf(ColorPrompt+"[INFO]"+ColorReset+" %s\n", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintSuccess выводит сообщение об успехе
|
||||||
|
func PrintSuccess(format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Printf(ColorGreen+"[OK]"+ColorReset+" %s\n", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintWarning выводит предупреждение
|
||||||
|
func PrintWarning(format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Printf(ColorYellow+"[WARN]"+ColorReset+" %s\n", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintError выводит сообщение об ошибке
|
||||||
|
func PrintError(format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Printf(ColorRed+"[ERROR]"+ColorReset+" %s\n", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintPromptMessage выводит сообщение цветом приглашения
|
||||||
|
func PrintPromptMessage(format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Printf(ColorPrompt + msg + ColorReset + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsoleLogger представляет простой консольный логгер
|
||||||
|
type ConsoleLogger struct {
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var consoleLogger *ConsoleLogger
|
||||||
|
|
||||||
|
// InitLogger инициализирует консольный логгер
|
||||||
|
func InitLogger(logPath string) {
|
||||||
|
consoleLogger = &ConsoleLogger{
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
// Игнорируем logPath для консольного логгера
|
||||||
|
// Файловый логгер инициализируется отдельно через InitFileLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogger возвращает консольный логгер
|
||||||
|
func GetLogger() *ConsoleLogger {
|
||||||
|
return consoleLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log записывает сообщение в консольный лог
|
||||||
|
func (l *ConsoleLogger) Log(level, message string) {
|
||||||
|
if l == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !l.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
fmt.Printf("[%s] [%s] %s\n", timestamp, level, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close закрывает консольный логгер
|
||||||
|
func (l *ConsoleLogger) Close() {
|
||||||
|
// В простой реализации ничего не делаем
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrompt возвращает строку приглашения
|
||||||
|
func GetPrompt() string {
|
||||||
|
return ColorPrompt + "futriis:~> " + ColorReset
|
||||||
|
}
|
||||||
69
pkg/utils/logger.go
Normal file
69
pkg/utils/logger.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// /futriis/pkg/utils/logger.go
|
||||||
|
// Пакет utils предоставляет функции для логирования работы СУБД Futriis.
|
||||||
|
// Реализует запись логов в файл с временными метками, включающими миллисекунды,
|
||||||
|
// и уровнями логирования (INFO, ERROR, WARNING, CMD). Логгер используется для
|
||||||
|
// отслеживания операций, отладки и аудита команд.
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger представляет структуру для логирования в файл
|
||||||
|
type FileLogger struct {
|
||||||
|
file *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileLoggerInstance *FileLogger
|
||||||
|
|
||||||
|
// InitFileLogger инициализирует файловый логгер с указанным путём к файлу
|
||||||
|
func InitFileLogger(logFile string) error {
|
||||||
|
// Создаём директорию для логов, если она не существует
|
||||||
|
logDir := "/home/grigoriy/futriis/logs"
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("не удалось создать директорию для логов: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileLoggerInstance = &FileLogger{
|
||||||
|
file: file,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем начало сессии
|
||||||
|
fileLoggerInstance.Log("INFO", "Сессия начата")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileLogger возвращает экземпляр файлового логгера
|
||||||
|
func GetFileLogger() *FileLogger {
|
||||||
|
return fileLoggerInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log записывает сообщение в файл лога с миллисекундами
|
||||||
|
func (l *FileLogger) Log(level, message string) {
|
||||||
|
if l == nil || l.file == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формат времени с миллисекундами: 2006-01-02 15:04:05.000
|
||||||
|
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||||
|
logLine := fmt.Sprintf("[%s] %s: %s\n", timestamp, level, message)
|
||||||
|
l.file.WriteString(logLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close закрывает файл лога
|
||||||
|
func (l *FileLogger) Close() {
|
||||||
|
if l != nil && l.file != nil {
|
||||||
|
// Записываем конец сессии
|
||||||
|
l.Log("INFO", "Сессия завершена")
|
||||||
|
l.file.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user