first commit
This commit is contained in:
246
README.md
Normal file
246
README.md
Normal file
@@ -0,0 +1,246 @@
|
||||
<!-- 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="#import-export">Import-Export</a></li>
|
||||
<li><a href="#lua-плагины">Lua-плагины</a></li>
|
||||
<li><a href="acl">ACL</a></li>
|
||||
<li><a href="#http-api">HTTP API</a></li>
|
||||
<li><a href="#сферы-применения">Сферы применения</a></li>
|
||||
<li><a href="#дорожная-карта">Дорожная карта</a></li>
|
||||
<li><a href="#контакты">Контакты</a></li>
|
||||
</ol>
|
||||
<!-- </details> -->
|
||||
|
||||
|
||||
## О проекте
|
||||
|
||||
futriis - это легковесная, распределённая wait-free и lock-free дружественная in-memory СУБД, реализованная на Go с поддержкой плагинов на языке lua использующая алгоритм консенсуса Raft.
|
||||
Данная субд была разработана, в первую очередь для эксплуатации на операционных системах на базе Solaris: OpenIndiana, Oracle Solaris.
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Лицензия
|
||||
|
||||
Проект распространяется под лицензией **`CDDL 1.0`**. Подробнсти в файлах `LICENSE` и `NOTICE`.
|
||||
Эта лицензия позволяет вам производить копирование, модификацию, распространение, включение в другие проекты, получение патентных прав, распространение бинарных файлов с доступом к их исходному коду. Она запрещает вам добавление новых ограничений, скрытие изменений, удаление оригинальных уведомлений, несоблюдение условий CDDL 1.0 при перераспределении, неправильное связывание с другими лицензиями.
|
||||
|
||||
Все дополнительное программное обеспечение (включая скрипт компиляции проекта `build.sh`) предоставляются "как есть", без гарантий и обязательств со стороны разработчиков. Разработчики не несут ответственности за прямой или косвенный ущерб, вызванный использованием открытого кода Futriix и futriix или технических решений, использующих этот код.
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Глоссарий
|
||||
|
||||
* **База Данных(БД)** - это структурированное, организованное хранилище данных, которое позволяет удобно собирать, хранить, управлять и извлекать информацию.
|
||||
* **Система Управления Базами Данных(СУБД)** - это программное обеспечение, которое позволяет создавать, управлять и взаимодействовать с базами данных
|
||||
* **Таппл (Tapple)** - аналог базы данных в РСУБД
|
||||
* **Слайс (Slice)** - аналог таблицы
|
||||
* **Кортеж (Tuple)** - аналог записи в таблице
|
||||
* **Мультимодельная СУБД** - это СУБД, которая объединяет в себе поддержку нескольких моделей данных (реляционной, документной, графовой, ключ-значение и др.) в рамках единого интегрированного ядра.
|
||||
* **Резидентная СУБД** - это СУБД, которая работает непрерывно в оперативной памяти (RAM).
|
||||
* **Инстанс** - это запущенный экземляр базы данных.
|
||||
* **Узел (хост,нода,шард)** - это отдельный сервер (физический или виртуальный), который является частью кластера или распределенной системы и выполняет часть общей работы.
|
||||
* **Слайс (от англ. "slice"-слой)** - это логический и физически изолированный фрагмент коллекции документов, полученный в результате горизонтального партиционирования (шардирования) и размещенный на определенном узле кластера с целью масштабирования производительности и объема данных.
|
||||
* **Репликасет** - это группа серверов СУБД, объединенных в отказоустойчивую конфигурацию, где один узел выполняет роль первичного (принимающего операции записи), а один или несколько других - роль вторичных (синхронизирующих свои данные с первичным и обслуживающих чтение), с автоматическим переизбранием первичного узла в случае его сбоя.
|
||||
* **Временные ряды (time series)** - это это упорядоченная во времени последовательность данных, собранная в регулярные промежутки времени из какого-либо источниика (цены на акции, данные температуры, объёмы продаж и.т.д.).
|
||||
* **OLTP (Online Transactional Processing-Онлайн обработка транзакций)**- это технология обработки транзакций в режиме реального времени. Её основная задача заключается в обеспечении быстрого и надёжного выполнения операций, которые происходят ежесекундно в бизнесе. Они обеспечивают быстрое выполнение операций вставки, обновления и удаления данных, поддерживая целостность и надежность транзакций.
|
||||
* **OLAP (Online Analytical Processing - Оперативная аналитическая обработка)** — это технология, которая работает с историческими массивами информации, извлекая из них закономерности и производя анализ больших объемов данных, поддерживает многоразмерные запросы и сложные аналитические операции. Данная технология оптимизирована для выполнения сложных запросов и предоставления сводной информации для принятия управленческих решений.
|
||||
* **HTAP (Hybrid Transactional and Analytical Processing - Гибридная транзакционно-аналитическая обработка)**- это технология, которая заключаются в эффективном совмещении операционных и аналитических запросов, т.е. классов OLTP и OLAP.
|
||||
* **Кластер** - это группа компьютеров, объединённых высокоскоростными каналами связи для решения сложных вычислительных задач и представляющая с точки зрения пользователя группу серверов, объединенных для работы как единая система.
|
||||
* **WUI (от англ. Web-User-Interface "веб интерфейс пользователя")** - это термин проекта futriix, означающий веб-интерфейс (интерфейс работающий в веб-браузере)
|
||||
* **Сервер-приложений (англ. application-server)** - это программное обеспечение, которое обеспечивает выполнение бизнес-логики и обработку запросов от клиентов (например, веб-браузеров или мобильных приложений). Он служит платформой для развертывания и управления приложениями, имея встроенные интепретаторы и/или компиляторы популярных языков программирования (php,go,python), что обеспечивает взаимодействие между пользователями, базами данных и другими системами.
|
||||
* **workflow (англ. workflow — «поток работы»)** — это принцип организации рабочих процессов, в соответствии с которым повторяющиеся задачи представлены как последовательность стандартных шагов.
|
||||
* **wait-free (дословно с англ. wait-free — «свободный от ожидания»)**-класс неблокирующих алгоритмов, в которых каждая операция должна завершаться за конечное число шагов независимо от активности других потоков.
|
||||
* **CA (англ. Certificate Authority - Центры Сертификации)** - это организации, которые выдают доверенные криптографические сертификаты.
|
||||
* Команды, выполняемые с привилегиями суперпользователя (root), отмечены символом приглашения **«#»**
|
||||
* Команды, выполняемые с правами обычного пользователя(user), отмечены символом приглашения **«$»**
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
|
||||
## Системные требования
|
||||
|
||||
> [!WARNING]
|
||||
> - Процессор: Intel или AMD
|
||||
> - Оперативная память: 4ГБ (Для Linux) 8ГБ (Для Illumos sytems)
|
||||
> - Только Unix-подобная ОС (Solaris, OpenIndiana, Linux)
|
||||
> - Go 1.25.6 или выше
|
||||
|
||||
> [!CAUTION]
|
||||
> **Важно: Windows и MacOS X не поддерживаются!**
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
|
||||
## Подготовка и компиляция
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/futriis/db.git
|
||||
$ cd futriis
|
||||
```
|
||||
|
||||
2. Скомпилируйте и запустите:
|
||||
|
||||
```bash
|
||||
# Стандартная сборка для ОС на базе Linux
|
||||
$ ./build.sh
|
||||
|
||||
# Сборка для операционных систем на базе Illumos
|
||||
$ cd scripts/
|
||||
$ ./build_illumos.sh
|
||||
|
||||
# Показать справку
|
||||
$./build.sh --help
|
||||
|
||||
$ ./futriis
|
||||
```
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
|
||||
### Тестирование
|
||||
|
||||
На данный момент для субд реализовано пять тестов (регрессионный, smoke-тест, функциональный, интеграционный, нагрузочный) на языке lua, покрывающий функционал acl, индексов, constraint'ов, RestFull HTTP API, ACL
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 1. Перед запуском тестов убедитесь, что СУБД запущена и HTTP API доступен на порту 8080
|
||||
> 2. Load test может занять несколько минут при больших объёмах данных
|
||||
> 3. Для параллельного тестирования в нагрузочном тесте рекомендуется использовать lua-lanes или запускать несколько процессов
|
||||
> 4. Все тесты используют аутентификацию и проверяют как позитивные, так и негативные сценарии
|
||||
|
||||
```bash
|
||||
# Установка LuaSocket и JSON библиотек
|
||||
luarocks install luasocket
|
||||
luarocks install lua-cjson
|
||||
|
||||
# Запуск отдельных тестов
|
||||
lua regression_test.lua
|
||||
lua smoke_test.lua
|
||||
lua functional_test.lua
|
||||
lua integration_test.lua
|
||||
lua load_test.lua
|
||||
|
||||
# Или все тесты последовательно
|
||||
for test in regression smoke functional integration load; do
|
||||
echo "Running ${test}_test.lua..."
|
||||
lua ${test}_test.lua
|
||||
echo "---"
|
||||
done
|
||||
```
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
### Примеры команд субд
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
#### Обновление и удаление
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Индексы
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Транзакции
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
|
||||
## Кластеризация и шардинг
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Сжатие данных
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Import-Export
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Lua-плагины
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## ACL
|
||||
|
||||
|
||||
## HTTP API
|
||||
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
|
||||
## Пример рабочей сессии со всем реализованным функционалом
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Дорожная карта
|
||||
|
||||
- [x] Реализовать
|
||||
- [ ] Реализовать
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
## Контакты
|
||||
|
||||
Григорий Сафронов - [E-mail](gvsafronov@yandex.ru)
|
||||
|
||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||
|
||||
68
build.sh
Executable file
68
build.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
# Универсальный скрипт сборки futriis для Linux и Illumos
|
||||
|
||||
#!/usr/bin/env/sh
|
||||
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "🔨 Building futriis database..."
|
||||
|
||||
# Определение ОС
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Функция для вывода ошибок красным цветом
|
||||
error_msg() {
|
||||
echo -e "\033[0;31m❌ $1\033[0m"
|
||||
}
|
||||
|
||||
# Функция для вывода успешных сообщений зелёным цветом
|
||||
success_msg() {
|
||||
echo -e "\033[0;32m✅ $1\033[0m"
|
||||
}
|
||||
|
||||
# Функция для вывода информационных сообщений
|
||||
info_msg() {
|
||||
echo "📋 $1"
|
||||
}
|
||||
|
||||
case "$OS" in
|
||||
linux)
|
||||
echo "Building for Linux"
|
||||
GOOS=linux GOARCH=amd64 go build -o bin/futriis-linux ./cmd/futriis
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
success_msg "Build successful for Linux"
|
||||
cp bin/futriis-linux ./futriis-linux
|
||||
info_msg "Binary copied to: ./futriis-linux"
|
||||
else
|
||||
error_msg "Build failed for Linux"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
sunos|illumos)
|
||||
echo "Building for Illumos"
|
||||
export GOOS=illumos
|
||||
export GOARCH=amd64
|
||||
export CGO_ENABLED=1
|
||||
|
||||
go build -tags=illumos -o bin/futriis-illumos ./cmd/futriis
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
success_msg "Build successful for Illumos"
|
||||
cp bin/futriis-illumos ./futriis-illumos
|
||||
info_msg "Binary copied to: ./futriis-illumos"
|
||||
else
|
||||
error_msg "Build failed for Illumos"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
error_msg "Unsupported OS: $OS"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
success_msg "Build complete. Binaries in bin/ and root directory"
|
||||
148
cmd/futriis/main.go
Normal file
148
cmd/futriis/main.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Файл: cmd/futriis/main.go
|
||||
// Назначение: Точка входа в приложение СУБД futriis. Инициализирует все компоненты:
|
||||
// конфигурацию, логгер, хранилище, Raft координатор, ACL менеджер, HTTP API и REPL.
|
||||
// Управляет жизненным циклом приложения.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"futriis/internal/acl"
|
||||
"futriis/internal/api"
|
||||
"futriis/internal/cluster"
|
||||
"futriis/internal/config"
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/repl"
|
||||
"futriis/internal/storage"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
utils.SetColorEnabled(true)
|
||||
|
||||
cfg, err := config.LoadConfig("config.toml")
|
||||
if err != nil {
|
||||
utils.PrintError("Failed to load config: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger, err := log.NewLogger(cfg.Log.LogFile, cfg.Log.LogLevel)
|
||||
if err != nil {
|
||||
utils.PrintError("Failed to initialize logger: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
defer logger.Close()
|
||||
logger.Info("futriis database starting...")
|
||||
|
||||
store := storage.NewStorage(cfg.Storage.PageSizeMB, logger)
|
||||
|
||||
// Инициализация ACL менеджера
|
||||
aclManager := acl.NewACLManager()
|
||||
logger.Info("ACL manager initialized")
|
||||
|
||||
raftCoordinator, err := cluster.NewRaftCoordinator(cfg, logger)
|
||||
if err != nil {
|
||||
logger.Error("Failed to start Raft coordinator: " + err.Error())
|
||||
utils.PrintError("Failed to start Raft coordinator: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if cfg.Cluster.Bootstrap || len(cfg.Cluster.Nodes) <= 1 {
|
||||
maxRetries := 10
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if raftCoordinator.IsLeader() {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
node := cluster.NewNode(cfg.Cluster.NodeIP, cfg.Cluster.NodePort, store, logger)
|
||||
|
||||
maxRetries := 5
|
||||
var registerErr error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
registerErr = raftCoordinator.RegisterNode(node)
|
||||
if registerErr == nil {
|
||||
break
|
||||
}
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
if registerErr != nil {
|
||||
logger.Error("Failed to register node: " + registerErr.Error())
|
||||
utils.PrintError("Failed to register node: " + registerErr.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Запуск HTTP API сервера
|
||||
httpPort := 8080
|
||||
httpServer := api.NewHTTPServer(httpPort, store, raftCoordinator, aclManager, logger)
|
||||
go func() {
|
||||
if err := httpServer.Start(); err != nil {
|
||||
logger.Error("HTTP server error: " + err.Error())
|
||||
utils.PrintError("HTTP server error: " + err.Error())
|
||||
}
|
||||
}()
|
||||
logger.Info(fmt.Sprintf("HTTP API server started on port %d", httpPort))
|
||||
|
||||
displayBanner(cfg.Cluster.Name)
|
||||
|
||||
replInstance := repl.NewRepl(store, raftCoordinator, logger, cfg)
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
utils.Println("\nReceived shutdown signal")
|
||||
logger.Info("Received shutdown signal")
|
||||
httpServer.Stop()
|
||||
raftCoordinator.Stop()
|
||||
node.Stop()
|
||||
replInstance.Close()
|
||||
utils.DisableColorMode()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
if err := replInstance.Run(); err != nil {
|
||||
logger.Error("REPL error: " + err.Error())
|
||||
utils.PrintError("REPL error: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func displayBanner(clusterName string) {
|
||||
utils.Println("")
|
||||
bannerLines := []string{
|
||||
" futriix 3i²(by 02.04.2026) ",
|
||||
" Distributed Document-Store in-memory database with support lua plugins ",
|
||||
" Cluster status: enable (Raft consensus)",
|
||||
" Cluster name: " + clusterName,
|
||||
" HTTP API: http://localhost:8080/api/",
|
||||
" Type 'quit' or 'exit' to quit",
|
||||
" Type 'status' to see cluster status",
|
||||
" Type 'acl login <user> <pass>' to authenticate",
|
||||
}
|
||||
|
||||
for _, line := range bannerLines {
|
||||
utils.PrintInfo(line)
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная функция для форматирования JSON (если понадобится)
|
||||
func printJSON(data interface{}) {
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
utils.PrintError("Failed to marshal JSON: " + err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println(string(jsonData))
|
||||
}
|
||||
44
config.toml
Normal file
44
config.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
# Конфигурационный файл СУБД futriis
|
||||
|
||||
[cluster]
|
||||
name = "test_cluster"
|
||||
node_ip = "192.168.0.103" # Укажите ваш реальный IP
|
||||
node_port = 9876
|
||||
raft_port = 9878
|
||||
raft_data_dir = "raft_data"
|
||||
bootstrap = true # Флаг для первого узла в кластере
|
||||
nodes = [ # Список узлов кластера
|
||||
"192.168.0.103:9878", # Текущий узел
|
||||
# "192.168.0.104:9878", # Другие узлы кластера
|
||||
# "192.168.0.105:9878",
|
||||
]
|
||||
|
||||
[storage]
|
||||
page_size_mb = 64
|
||||
max_collections = 100
|
||||
max_documents_per_collection = 1000000
|
||||
|
||||
[repl]
|
||||
prompt_color = "#00bfff"
|
||||
history_size = 1000
|
||||
|
||||
[log]
|
||||
log_file = "futriis.log"
|
||||
log_level = "debug"
|
||||
|
||||
[replication]
|
||||
enabled = false
|
||||
master_master = false
|
||||
sync_replication = false
|
||||
replication_timeout_ms = 5000
|
||||
|
||||
[plugins]
|
||||
enabled = false
|
||||
script_dir = "plugins"
|
||||
allow_list = ["print", "math", "string"]
|
||||
|
||||
[compression]
|
||||
enabled = true # Включить сжатие данных
|
||||
algorithm = "snappy" # Алгоритм: snappy, lz4, zstd
|
||||
level = 3 # Уровень сжатия (1-9, зависит от алгоритма)
|
||||
min_size = 1024 # Минимальный размер документа для сжатия (байт)
|
||||
112
futriis.log
Normal file
112
futriis.log
Normal file
@@ -0,0 +1,112 @@
|
||||
[2026-04-05 19:37:43.525] INFO: futriis database starting...
|
||||
[2026-04-05 19:37:43.525] INFO: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-05 19:37:43.525] INFO: Running in single-node mode
|
||||
[2026-04-05 19:37:43.525] INFO: Raft data directory: raft_data
|
||||
[2026-04-05 19:37:44.083] INFO: Existing Raft state found, joining cluster...
|
||||
[2026-04-05 19:37:44.083] INFO: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-05 19:37:44.083] INFO: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-05 19:37:44.083] INFO: Node registered locally in single-node mode: ca6b9fed-ee2e-43dc-94c7-4b3e51539980
|
||||
[2026-04-05 19:37:44.084] INFO: Node ca6b9fed-ee2e-43dc-94c7-4b3e51539980 listening on 192.168.0.103:9876
|
||||
[2026-04-05 19:48:45.939] INFO: futriis database starting...
|
||||
[2026-04-05 19:48:45.939] INFO: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-05 19:48:45.940] INFO: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-05 19:48:45.940] INFO: Raft data directory: raft_data
|
||||
[2026-04-05 19:48:46.492] INFO: Existing Raft state found, joining cluster...
|
||||
[2026-04-05 19:48:46.492] INFO: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-05 19:48:46.492] INFO: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-05 19:48:46.492] INFO: Node registered locally in single-node mode: 1a7eb73b-88bc-442c-9f01-d8768f36c891
|
||||
[2026-04-05 19:48:46.492] INFO: Node 1a7eb73b-88bc-442c-9f01-d8768f36c891 listening on 192.168.0.103:9876
|
||||
[2026-04-05 19:58:28.380] INFO: futriis database starting...
|
||||
[2026-04-05 19:58:28.380] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-05 19:58:28.380] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-05 19:58:28.380] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-05 19:58:28.940] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-05 19:58:28.940] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-05 19:58:28.940] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-05 19:58:28.940] DEBUG: Node registered locally in single-node mode: 7d72a163-f3de-46a2-86f9-edf9d86fbdd5
|
||||
[2026-04-05 19:58:28.940] INFO: Node 7d72a163-f3de-46a2-86f9-edf9d86fbdd5 listening on 192.168.0.103:9876
|
||||
[2026-04-06 20:50:15.736] INFO: futriis database starting...
|
||||
[2026-04-06 20:50:15.736] INFO: ACL manager initialized
|
||||
[2026-04-06 20:50:15.736] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-06 20:50:15.736] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-06 20:50:15.736] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-06 20:50:16.291] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-06 20:50:16.291] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-06 20:50:16.291] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-06 20:50:16.291] DEBUG: Node registered locally in single-node mode: 9eecdf51-7980-4c61-988e-20e6e1fe593d
|
||||
[2026-04-06 20:50:16.291] INFO: HTTP API server started on port ᾐ
|
||||
[2026-04-06 20:50:16.291] INFO: Node 9eecdf51-7980-4c61-988e-20e6e1fe593d listening on 192.168.0.103:9876
|
||||
[2026-04-06 20:50:16.292] INFO: Starting HTTP API server on port 8080
|
||||
[2026-04-07 21:37:38.263] INFO: futriis database starting...
|
||||
[2026-04-07 21:37:38.263] INFO: ACL manager initialized
|
||||
[2026-04-07 21:37:38.263] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-07 21:37:38.263] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-07 21:37:38.263] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-07 21:37:38.926] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-07 21:37:38.926] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-07 21:37:38.926] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-07 21:37:38.926] DEBUG: Node registered locally in single-node mode: e3f9e952-81e0-46a0-98d4-73a360ac7341
|
||||
[2026-04-07 21:37:38.927] INFO: HTTP API server started on port 8080
|
||||
[2026-04-07 21:37:38.927] INFO: Node e3f9e952-81e0-46a0-98d4-73a360ac7341 listening on 192.168.0.103:9876
|
||||
[2026-04-07 21:37:38.927] INFO: Starting HTTP API server on port 8080
|
||||
[2026-04-07 21:47:04.604] INFO: futriis database starting...
|
||||
[2026-04-07 21:47:04.604] INFO: ACL manager initialized
|
||||
[2026-04-07 21:47:04.605] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-07 21:47:04.605] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-07 21:47:04.605] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-07 21:47:05.143] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-07 21:47:05.143] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-07 21:47:05.143] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-07 21:47:05.143] DEBUG: Node registered locally in single-node mode: 7fe5d67f-e500-4bd0-8f6b-39e0b5cdb3d6
|
||||
[2026-04-07 21:47:05.144] INFO: HTTP API server started on port 8080
|
||||
[2026-04-07 21:47:05.144] INFO: Node 7fe5d67f-e500-4bd0-8f6b-39e0b5cdb3d6 listening on 192.168.0.103:9876
|
||||
[2026-04-07 21:47:05.144] INFO: Starting HTTP API server on port 8080
|
||||
[2026-04-07 21:51:08.358] INFO: futriis database starting...
|
||||
[2026-04-07 21:51:08.358] INFO: ACL manager initialized
|
||||
[2026-04-07 21:51:08.358] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-07 21:51:08.358] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-07 21:51:08.358] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-07 21:51:08.884] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-07 21:51:08.884] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-07 21:51:08.884] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-07 21:51:08.884] DEBUG: Node registered locally in single-node mode: 8283b996-ac50-4cff-980d-4623a841934c
|
||||
[2026-04-07 21:51:08.884] INFO: HTTP API server started on port 8080
|
||||
[2026-04-07 21:51:08.884] INFO: Node 8283b996-ac50-4cff-980d-4623a841934c listening on 192.168.0.103:9876
|
||||
[2026-04-07 21:51:08.885] INFO: Starting HTTP API server on port 8080
|
||||
[2026-04-07 22:33:38.361] INFO: futriis database starting...
|
||||
[2026-04-07 22:33:38.362] INFO: ACL manager initialized
|
||||
[2026-04-07 22:33:38.362] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-07 22:33:38.362] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-07 22:33:38.362] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-07 22:33:38.898] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-07 22:33:38.898] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-07 22:33:38.898] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-07 22:33:38.899] DEBUG: Node registered locally in single-node mode: 72d4611f-a1ab-47a5-87f9-9011f18d97e9
|
||||
[2026-04-07 22:33:38.899] INFO: HTTP API server started on port 8080
|
||||
[2026-04-07 22:33:38.900] INFO: Node 72d4611f-a1ab-47a5-87f9-9011f18d97e9 listening on 192.168.0.103:9876
|
||||
[2026-04-07 22:33:38.900] INFO: Starting HTTP API server on port 8080
|
||||
[2026-04-07 22:41:46.729] INFO: futriis database starting...
|
||||
[2026-04-07 22:41:46.729] INFO: ACL manager initialized
|
||||
[2026-04-07 22:41:46.729] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-07 22:41:46.729] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-07 22:41:46.729] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-07 22:41:47.261] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-07 22:41:47.261] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-07 22:41:47.262] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-07 22:41:47.262] DEBUG: Node registered locally in single-node mode: 186b0497-8c04-46a5-b84d-bc702d4678ff
|
||||
[2026-04-07 22:41:47.262] INFO: HTTP API server started on port 8080
|
||||
[2026-04-07 22:41:47.262] INFO: Node 186b0497-8c04-46a5-b84d-bc702d4678ff listening on 192.168.0.103:9876
|
||||
[2026-04-07 22:41:47.262] INFO: Starting HTTP API server on port 8080
|
||||
[2026-04-07 22:59:36.988] ERROR: REPL command error: unknown command: уit
|
||||
[2026-04-07 23:00:13.617] INFO: futriis database starting...
|
||||
[2026-04-07 23:00:13.617] INFO: ACL manager initialized
|
||||
[2026-04-07 23:00:13.617] DEBUG: Creating Raft coordinator at 192.168.0.103:9878
|
||||
[2026-04-07 23:00:13.617] DEBUG: Running in single-node mode (warnings suppressed)
|
||||
[2026-04-07 23:00:13.617] DEBUG: Raft data directory: raft_data
|
||||
[2026-04-07 23:00:14.151] DEBUG: Existing Raft state found, joining cluster...
|
||||
[2026-04-07 23:00:14.151] DEBUG: Raft coordinator started at 192.168.0.103:9878, IsLeader: false
|
||||
[2026-04-07 23:00:14.152] DEBUG: Single-node mode: registering node without Raft consensus
|
||||
[2026-04-07 23:00:14.152] DEBUG: Node registered locally in single-node mode: b67f7d35-2466-4894-841c-11dd91bb1fa0
|
||||
[2026-04-07 23:00:14.152] INFO: HTTP API server started on port 8080
|
||||
[2026-04-07 23:00:14.152] INFO: Node b67f7d35-2466-4894-841c-11dd91bb1fa0 listening on 192.168.0.103:9876
|
||||
[2026-04-07 23:00:14.152] INFO: Starting HTTP API server on port 8080
|
||||
32
go.mod
Normal file
32
go.mod
Normal file
@@ -0,0 +1,32 @@
|
||||
module futriis
|
||||
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/fatih/color v1.19.0
|
||||
github.com/golang/snappy v0.0.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/raft v1.7.3
|
||||
github.com/hashicorp/raft-boltdb/v2 v2.3.0
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/pierrec/lz4/v4 v4.1.22
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
github.com/hashicorp/go-msgpack v1.1.5 // indirect
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.5 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
)
|
||||
212
go.sum
Normal file
212
go.sum
Normal file
@@ -0,0 +1,212 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
|
||||
github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
|
||||
github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs=
|
||||
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.5 h1:Ue879bPnutj/hXfmUk6s/jtIK90XxgiUIcXRl656T44=
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.5/go.mod h1:bjCsRXpZ7NsJdk45PoCQnzRGDaK8TKm5ZnDI/9y3J4M=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/raft v1.7.3 h1:DxpEqZJysHN0wK+fviai5mFcSYsCkNpFUl1xpAW8Rbo=
|
||||
github.com/hashicorp/raft v1.7.3/go.mod h1:DfvCGFxpAUPE0L4Uc8JLlTPtc3GzSbdH0MTJCLgnmJQ=
|
||||
github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702 h1:RLKEcCuKcZ+qp2VlaaZsYZfLOmIiuJNpEi48Rl8u9cQ=
|
||||
github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702/go.mod h1:nTakvJ4XYq45UXtn0DbwR4aU9ZdjlnIenpbs6Cd+FM0=
|
||||
github.com/hashicorp/raft-boltdb/v2 v2.3.0 h1:fPpQR1iGEVYjZ2OELvUHX600VAK5qmdnDEv3eXOwZUA=
|
||||
github.com/hashicorp/raft-boltdb/v2 v2.3.0/go.mod h1:YHukhB04ChJsLHLJEUD6vjFyLX2L3dsX3wPBZcX4tmc=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
281
internal/acl/manger.go
Normal file
281
internal/acl/manger.go
Normal file
@@ -0,0 +1,281 @@
|
||||
// Файл: internal/acl/manager.go
|
||||
// Назначение: Глобальный менеджер ACL для всей СУБД.
|
||||
// Управляет пользователями, ролями и разрешениями на уровне БД и коллекций.
|
||||
// Реализован с использованием sync.Map для wait-free доступа.
|
||||
|
||||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// PermissionType определяет тип разрешения
|
||||
type PermissionType string
|
||||
|
||||
const (
|
||||
PermRead PermissionType = "read"
|
||||
PermWrite PermissionType = "write"
|
||||
PermDelete PermissionType = "delete"
|
||||
PermAdmin PermissionType = "admin"
|
||||
)
|
||||
|
||||
// User представляет пользователя системы
|
||||
type User struct {
|
||||
ID string `msgpack:"id"`
|
||||
Username string `msgpack:"username"`
|
||||
Password string `msgpack:"password"` // В реальной системе - хеш
|
||||
Roles []string `msgpack:"roles"`
|
||||
CreatedAt int64 `msgpack:"created_at"`
|
||||
LastLogin int64 `msgpack:"last_login"`
|
||||
Active bool `msgpack:"active"`
|
||||
}
|
||||
|
||||
// Role представляет роль с набором разрешений
|
||||
type Role struct {
|
||||
Name string `msgpack:"name"`
|
||||
Permissions []string `msgpack:"permissions"` // "database.collection:read" формат
|
||||
}
|
||||
|
||||
// ACLManager управляет доступом к БД
|
||||
type ACLManager struct {
|
||||
users sync.Map // map[string]*User
|
||||
roles sync.Map // map[string]*Role
|
||||
sessionRoles sync.Map // map[string]string - sessionID -> role
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewACLManager создаёт новый менеджер ACL
|
||||
func NewACLManager() *ACLManager {
|
||||
m := &ACLManager{}
|
||||
|
||||
// Создаём роль администратора по умолчанию
|
||||
adminRole := &Role{
|
||||
Name: "admin",
|
||||
Permissions: []string{"*:*"},
|
||||
}
|
||||
m.roles.Store("admin", adminRole)
|
||||
|
||||
// Создаём пользователя admin по умолчанию
|
||||
adminUser := &User{
|
||||
ID: uuid.New().String(),
|
||||
Username: "admin",
|
||||
Password: "admin", // В продакшене использовать хеш!
|
||||
Roles: []string{"admin"},
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Active: true,
|
||||
}
|
||||
m.users.Store("admin", adminUser)
|
||||
|
||||
// Создаём роль guest с ограниченными правами
|
||||
guestRole := &Role{
|
||||
Name: "guest",
|
||||
Permissions: []string{},
|
||||
}
|
||||
m.roles.Store("guest", guestRole)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// CreateUser создаёт нового пользователя
|
||||
func (m *ACLManager) CreateUser(username, password string, roles []string) error {
|
||||
if _, exists := m.users.Load(username); exists {
|
||||
return fmt.Errorf("user %s already exists", username)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
ID: uuid.New().String(),
|
||||
Username: username,
|
||||
Password: password,
|
||||
Roles: roles,
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
Active: true,
|
||||
}
|
||||
|
||||
m.users.Store(username, user)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate аутентифицирует пользователя
|
||||
func (m *ACLManager) Authenticate(username, password string) (string, error) {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
if !user.Active {
|
||||
return "", fmt.Errorf("user is disabled")
|
||||
}
|
||||
|
||||
if user.Password != password {
|
||||
return "", fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
// Обновляем время последнего входа
|
||||
user.LastLogin = time.Now().UnixMilli()
|
||||
m.users.Store(username, user)
|
||||
|
||||
// Создаём сессию
|
||||
sessionID := uuid.New().String()
|
||||
m.sessionRoles.Store(sessionID, user.Roles)
|
||||
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// Logout завершает сессию
|
||||
func (m *ACLManager) Logout(sessionID string) {
|
||||
m.sessionRoles.Delete(sessionID)
|
||||
}
|
||||
|
||||
// CheckPermission проверяет разрешение для сессии
|
||||
func (m *ACLManager) CheckPermission(sessionID, database, collection, operation string) bool {
|
||||
rolesVal, ok := m.sessionRoles.Load(sessionID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
roles := rolesVal.([]string)
|
||||
for _, roleName := range roles {
|
||||
roleVal, ok := m.roles.Load(roleName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
role := roleVal.(*Role)
|
||||
for _, perm := range role.Permissions {
|
||||
if m.matchPermission(perm, database, collection, operation) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchPermission проверяет соответствие разрешения
|
||||
func (m *ACLManager) matchPermission(perm, database, collection, operation string) bool {
|
||||
// Формат: "database.collection:operation" или "*:*" для всех
|
||||
// или "database.*:read" для всех коллекций в БД
|
||||
|
||||
parts := splitPermission(perm)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
resource := parts[0] // "database.collection" или "database.*"
|
||||
op := parts[1] // "read", "write", "delete", "admin"
|
||||
|
||||
// Проверка операции
|
||||
if op != "*" && op != operation {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверка ресурса
|
||||
if resource == "*:*" {
|
||||
return true
|
||||
}
|
||||
|
||||
resourceParts := splitResource(resource)
|
||||
if len(resourceParts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
dbPattern := resourceParts[0]
|
||||
collPattern := resourceParts[1]
|
||||
|
||||
if dbPattern != "*" && dbPattern != database {
|
||||
return false
|
||||
}
|
||||
|
||||
if collPattern != "*" && collPattern != collection {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GrantPermission выдаёт разрешение роли
|
||||
func (m *ACLManager) GrantPermission(roleName, permission string) error {
|
||||
val, ok := m.roles.Load(roleName)
|
||||
if !ok {
|
||||
return fmt.Errorf("role not found")
|
||||
}
|
||||
|
||||
role := val.(*Role)
|
||||
role.Permissions = append(role.Permissions, permission)
|
||||
m.roles.Store(roleName, role)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateRole создаёт новую роль
|
||||
func (m *ACLManager) CreateRole(name string) error {
|
||||
if _, exists := m.roles.Load(name); exists {
|
||||
return fmt.Errorf("role %s already exists", name)
|
||||
}
|
||||
|
||||
role := &Role{
|
||||
Name: name,
|
||||
Permissions: []string{},
|
||||
}
|
||||
|
||||
m.roles.Store(name, role)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddUserRole добавляет роль пользователю
|
||||
func (m *ACLManager) AddUserRole(username, roleName string) error {
|
||||
val, ok := m.users.Load(username)
|
||||
if !ok {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user := val.(*User)
|
||||
user.Roles = append(user.Roles, roleName)
|
||||
m.users.Store(username, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUsers возвращает список всех пользователей
|
||||
func (m *ACLManager) ListUsers() []string {
|
||||
users := make([]string, 0)
|
||||
m.users.Range(func(key, value interface{}) bool {
|
||||
users = append(users, key.(string))
|
||||
return true
|
||||
})
|
||||
return users
|
||||
}
|
||||
|
||||
// ListRoles возвращает список всех ролей
|
||||
func (m *ACLManager) ListRoles() []string {
|
||||
roles := make([]string, 0)
|
||||
m.roles.Range(func(key, value interface{}) bool {
|
||||
roles = append(roles, key.(string))
|
||||
return true
|
||||
})
|
||||
return roles
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func splitPermission(perm string) []string {
|
||||
for i := 0; i < len(perm); i++ {
|
||||
if perm[i] == ':' {
|
||||
return []string{perm[:i], perm[i+1:]}
|
||||
}
|
||||
}
|
||||
return []string{perm, ""}
|
||||
}
|
||||
|
||||
func splitResource(resource string) []string {
|
||||
for i := 0; i < len(resource); i++ {
|
||||
if resource[i] == '.' {
|
||||
return []string{resource[:i], resource[i+1:]}
|
||||
}
|
||||
}
|
||||
return []string{resource, "*"}
|
||||
}
|
||||
569
internal/api/http.go
Normal file
569
internal/api/http.go
Normal file
@@ -0,0 +1,569 @@
|
||||
// Файл: internal/api/http.go
|
||||
// Назначение: HTTP RESTful API для взаимодействия с СУБД через curl.
|
||||
// Поддерживает CRUD операции, управление индексами, ACL и ограничениями.
|
||||
// Реализован с минимальными блокировками, использует wait-free структуры.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/acl"
|
||||
"futriis/internal/cluster"
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/storage"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
store *storage.Storage
|
||||
coordinator *cluster.RaftCoordinator
|
||||
aclManager *acl.ACLManager
|
||||
logger *log.Logger
|
||||
server *http.Server
|
||||
port int
|
||||
}
|
||||
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewHTTPServer создаёт новый HTTP сервер
|
||||
func NewHTTPServer(port int, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *HTTPServer {
|
||||
s := &HTTPServer{
|
||||
store: store,
|
||||
coordinator: coord,
|
||||
aclManager: aclMgr,
|
||||
logger: logger,
|
||||
port: port,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Middleware для аутентификации
|
||||
mux.HandleFunc("/api/auth/login", s.handleLogin)
|
||||
mux.HandleFunc("/api/auth/logout", s.handleLogout)
|
||||
|
||||
// CRUD операции
|
||||
mux.HandleFunc("/api/db/", s.handleDatabaseRequest)
|
||||
|
||||
// Индексы
|
||||
mux.HandleFunc("/api/index/", s.handleIndexRequest)
|
||||
|
||||
// ACL
|
||||
mux.HandleFunc("/api/acl/", s.handleACLRequest)
|
||||
|
||||
// Constraints
|
||||
mux.HandleFunc("/api/constraint/", s.handleConstraintRequest)
|
||||
|
||||
// Cluster
|
||||
mux.HandleFunc("/api/cluster/", s.handleClusterRequest)
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start запускает HTTP сервер
|
||||
func (s *HTTPServer) Start() error {
|
||||
s.logger.Info("Starting HTTP API server on port " + strconv.Itoa(s.port))
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Stop останавливает HTTP сервер
|
||||
func (s *HTTPServer) Stop() error {
|
||||
return s.server.Close()
|
||||
}
|
||||
|
||||
// handleLogin обрабатывает аутентификацию
|
||||
func (s *HTTPServer) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var creds struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
||||
s.sendError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, err := s.aclManager.Authenticate(creds.Username, creds.Password)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"session_id": sessionID})
|
||||
}
|
||||
|
||||
// handleLogout обрабатывает выход
|
||||
func (s *HTTPServer) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if sessionID != "" {
|
||||
s.aclManager.Logout(sessionID)
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "logged out"})
|
||||
}
|
||||
|
||||
// handleDatabaseRequest обрабатывает запросы к БД
|
||||
func (s *HTTPServer) handleDatabaseRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// URL: /api/db/{database}/{collection}/{document_id}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/db/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 2 {
|
||||
s.sendError(w, "Invalid path. Use /api/db/{database}/{collection}[/{id}]", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
database := parts[0]
|
||||
collection := parts[1]
|
||||
docID := ""
|
||||
if len(parts) > 2 {
|
||||
docID = parts[2]
|
||||
}
|
||||
|
||||
// Проверка аутентификации
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if sessionID == "" {
|
||||
s.sendError(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.handleGetDocument(w, r, sessionID, database, collection, docID)
|
||||
case http.MethodPost:
|
||||
s.handleInsertDocument(w, r, sessionID, database, collection)
|
||||
case http.MethodPut:
|
||||
s.handleUpdateDocument(w, r, sessionID, database, collection, docID)
|
||||
case http.MethodDelete:
|
||||
s.handleDeleteDocument(w, r, sessionID, database, collection, docID)
|
||||
default:
|
||||
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetDocument обрабатывает GET запросы
|
||||
func (s *HTTPServer) handleGetDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
|
||||
// Проверка прав
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "read") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Поиск по индексу или ID
|
||||
query := r.URL.Query()
|
||||
if indexName := query.Get("index"); indexName != "" {
|
||||
indexValue := query.Get("value")
|
||||
docs, err := coll.FindByIndex(indexName, indexValue)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, docs)
|
||||
return
|
||||
}
|
||||
|
||||
if docID == "" {
|
||||
// Возвращаем все документы
|
||||
docs := coll.GetAllDocuments()
|
||||
s.sendSuccess(w, docs)
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := coll.Find(docID)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, doc)
|
||||
}
|
||||
|
||||
// handleInsertDocument обрабатывает POST запросы
|
||||
func (s *HTTPServer) handleInsertDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection string) {
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "write") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
// Создаём БД если не существует
|
||||
if err := s.store.CreateDatabase(database); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
db, _ = s.store.GetDatabase(database)
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
if err := db.CreateCollection(collection); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
coll, _ = db.GetCollection(collection)
|
||||
}
|
||||
|
||||
var doc map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&doc); err != nil {
|
||||
s.sendError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := coll.InsertFromMap(doc); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"status": "inserted"})
|
||||
}
|
||||
|
||||
// handleUpdateDocument обрабатывает PUT запросы
|
||||
func (s *HTTPServer) handleUpdateDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
|
||||
if docID == "" {
|
||||
s.sendError(w, "Document ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "write") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
s.sendError(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := coll.Update(docID, updates); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
// handleDeleteDocument обрабатывает DELETE запросы
|
||||
func (s *HTTPServer) handleDeleteDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
|
||||
if docID == "" {
|
||||
s.sendError(w, "Document ID required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "delete") {
|
||||
s.sendError(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := coll.Delete(docID); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSuccess(w, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
// handleIndexRequest обрабатывает запросы к индексам
|
||||
func (s *HTTPServer) handleIndexRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// URL: /api/index/{database}/{collection}/{action}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/index/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 3 {
|
||||
s.sendError(w, "Invalid path. Use /api/index/{database}/{collection}/{action}", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
database := parts[0]
|
||||
collection := parts[1]
|
||||
action := parts[2]
|
||||
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "list":
|
||||
indexes := coll.GetIndexes()
|
||||
s.sendSuccess(w, indexes)
|
||||
|
||||
case "create":
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Fields []string `json:"fields"`
|
||||
Unique bool `json:"unique"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "index created"})
|
||||
|
||||
case "drop":
|
||||
indexName := parts[3]
|
||||
if err := coll.DropIndex(indexName); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "index dropped"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Unknown action", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// handleACLRequest обрабатывает запросы ACL
|
||||
func (s *HTTPServer) handleACLRequest(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/acl/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 1 {
|
||||
s.sendError(w, "Invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
action := parts[0]
|
||||
|
||||
switch action {
|
||||
case "users":
|
||||
users := s.aclManager.ListUsers()
|
||||
s.sendSuccess(w, users)
|
||||
|
||||
case "user":
|
||||
if len(parts) < 2 {
|
||||
s.sendError(w, "Username required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
username := parts[1]
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
var req struct {
|
||||
Password string `json:"password"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := s.aclManager.CreateUser(username, req.Password, req.Roles); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "user created"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
case "roles":
|
||||
roles := s.aclManager.ListRoles()
|
||||
s.sendSuccess(w, roles)
|
||||
|
||||
case "grant":
|
||||
if len(parts) < 3 {
|
||||
s.sendError(w, "Role and permission required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
roleName := parts[1]
|
||||
permission := parts[2]
|
||||
if err := s.aclManager.GrantPermission(roleName, permission); err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.sendSuccess(w, map[string]string{"status": "permission granted"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Unknown action", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConstraintRequest обрабатывает запросы к ограничениям
|
||||
func (s *HTTPServer) handleConstraintRequest(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/constraint/")
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
if len(parts) < 3 {
|
||||
s.sendError(w, "Invalid path. Use /api/constraint/{database}/{collection}/{action}", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
database := parts[0]
|
||||
collection := parts[1]
|
||||
action := parts[2]
|
||||
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := s.store.GetDatabase(database)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
s.sendError(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "required":
|
||||
if len(parts) < 4 {
|
||||
s.sendError(w, "Field name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
coll.AddRequiredField(field)
|
||||
s.sendSuccess(w, map[string]string{"status": "required field added"})
|
||||
|
||||
case "unique":
|
||||
if len(parts) < 4 {
|
||||
s.sendError(w, "Field name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
coll.AddUniqueConstraint(field)
|
||||
s.sendSuccess(w, map[string]string{"status": "unique constraint added"})
|
||||
|
||||
case "min":
|
||||
if len(parts) < 5 {
|
||||
s.sendError(w, "Field name and value required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
minVal, _ := strconv.ParseFloat(parts[4], 64)
|
||||
coll.AddMinConstraint(field, minVal)
|
||||
s.sendSuccess(w, map[string]string{"status": "min constraint added"})
|
||||
|
||||
case "max":
|
||||
if len(parts) < 5 {
|
||||
s.sendError(w, "Field name and value required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
field := parts[3]
|
||||
maxVal, _ := strconv.ParseFloat(parts[4], 64)
|
||||
coll.AddMaxConstraint(field, maxVal)
|
||||
s.sendSuccess(w, map[string]string{"status": "max constraint added"})
|
||||
|
||||
default:
|
||||
s.sendError(w, "Unknown action", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// handleClusterRequest обрабатывает запросы к кластеру
|
||||
func (s *HTTPServer) handleClusterRequest(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID := r.Header.Get("X-Session-ID")
|
||||
if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") {
|
||||
s.sendError(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if s.coordinator == nil {
|
||||
s.sendError(w, "Cluster not available", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
status := s.coordinator.GetClusterStatus()
|
||||
s.sendSuccess(w, status)
|
||||
}
|
||||
|
||||
// sendSuccess отправляет успешный ответ
|
||||
func (s *HTTPServer) sendSuccess(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// sendError отправляет ответ с ошибкой
|
||||
func (s *HTTPServer) sendError(w http.ResponseWriter, errMsg string, statusCode int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(APIResponse{
|
||||
Success: false,
|
||||
Error: errMsg,
|
||||
})
|
||||
}
|
||||
379
internal/cluster/node.go
Normal file
379
internal/cluster/node.go
Normal file
@@ -0,0 +1,379 @@
|
||||
// Файл: internal/cluster/node.go
|
||||
// Назначение: Реализация узла кластера (node) для распределённой СУБД.
|
||||
// Реализация узла кластера (node) для распределённой СУБД.
|
||||
// Полностью lock-free с использованием атомарных операций.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/storage"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// NodeStatus представляет состояние узла кластера
|
||||
type NodeStatus int32
|
||||
|
||||
const (
|
||||
StatusOffline NodeStatus = iota
|
||||
StatusActive
|
||||
StatusSyncing
|
||||
StatusFailed
|
||||
)
|
||||
|
||||
// Node представляет отдельный узел в распределённой системе
|
||||
type Node struct {
|
||||
ID string // Уникальный идентификатор узла
|
||||
IP string // IP-адрес узла
|
||||
Port int // Порт для коммуникации
|
||||
Status atomic.Int32 // Атомарный статус узла (NodeStatus)
|
||||
Storage *storage.Storage
|
||||
logger *log.Logger
|
||||
coordinator *RaftCoordinator // Ссылка на координатора (теперь RaftCoordinator)
|
||||
lastSeen atomic.Int64 // Время последнего heartbeat (Unix nano)
|
||||
incomingConn chan net.Conn // Канал для входящих соединений (wait-free)
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NodeConfig содержит конфигурацию для создания узла
|
||||
type NodeConfig struct {
|
||||
IP string
|
||||
Port int
|
||||
Storage *storage.Storage
|
||||
Logger *log.Logger
|
||||
Coordinator *RaftCoordinator
|
||||
}
|
||||
|
||||
// NewNode создаёт новый экземпляр узла кластера
|
||||
func NewNode(ip string, port int, store *storage.Storage, logger *log.Logger) *Node {
|
||||
node := &Node{
|
||||
ID: uuid.New().String(),
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Storage: store,
|
||||
logger: logger,
|
||||
incomingConn: make(chan net.Conn, 1000), // Буферизованный канал для wait-free приёма
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
node.Status.Store(int32(StatusActive))
|
||||
node.lastSeen.Store(time.Now().UnixNano())
|
||||
|
||||
// Запуск сервера для приёма межузловых соединений
|
||||
go node.startTCPServer()
|
||||
|
||||
// Запуск обработчика входящих соединений
|
||||
go node.handleIncomingConnections()
|
||||
|
||||
// Запуск heartbeat-отправки (если координатор известен)
|
||||
go node.heartbeatLoop()
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// startTCPServer запускает TCP-сервер для приёма запросов от других узлов
|
||||
func (n *Node) startTCPServer() {
|
||||
addr := fmt.Sprintf("%s:%d", n.IP, n.Port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to start TCP server: %v", n.ID, err))
|
||||
}
|
||||
n.Status.Store(int32(StatusFailed))
|
||||
return
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s listening on %s", n.ID, addr))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
default:
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s accept error: %v", n.ID, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Неблокирующая отправка в канал
|
||||
select {
|
||||
case n.incomingConn <- conn:
|
||||
default:
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Node %s incoming connection queue full, dropping connection", n.ID))
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleIncomingConnections обрабатывает входящие соединения wait-free способом
|
||||
func (n *Node) handleIncomingConnections() {
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
case conn := <-n.incomingConn:
|
||||
go n.handleNodeRequest(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleNodeRequest обрабатывает конкретный запрос от другого узла
|
||||
func (n *Node) handleNodeRequest(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
decoder := json.NewDecoder(conn)
|
||||
var req NodeRequest
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to decode request: %v", n.ID, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем время последнего контакта
|
||||
n.lastSeen.Store(time.Now().UnixNano())
|
||||
|
||||
// Маршрутизация запроса в зависимости от типа
|
||||
switch req.Type {
|
||||
case "replicate":
|
||||
n.handleReplicateRequest(req.Data)
|
||||
case "query":
|
||||
n.handleQueryRequest(req.Data, conn)
|
||||
case "sync":
|
||||
n.handleSyncRequest(req.Data, conn)
|
||||
default:
|
||||
if n.logger != nil {
|
||||
n.logger.Warn(fmt.Sprintf("Node %s unknown request type: %s", n.ID, req.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleReplicateRequest обрабатывает запрос на репликацию документа
|
||||
func (n *Node) handleReplicateRequest(data []byte) {
|
||||
var repData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
Document map[string]interface{} `json:"document"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &repData); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to unmarshal replicate data: %v", n.ID, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := n.Storage.GetDatabase(repData.Database)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s database not found for replication: %s", n.ID, repData.Database))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем коллекцию
|
||||
coll, err := db.GetCollection(repData.Collection)
|
||||
if err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s collection not found for replication: %s", n.ID, repData.Collection))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Создаём документ и вставляем
|
||||
doc := &storage.Document{
|
||||
ID: repData.Document["_id"].(string),
|
||||
Fields: repData.Document,
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Error(fmt.Sprintf("Node %s failed to replicate document: %v", n.ID, err))
|
||||
}
|
||||
} else {
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Node %s replicated document %s", n.ID, doc.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleQueryRequest обрабатывает запрос на чтение данных с узла
|
||||
func (n *Node) handleQueryRequest(data []byte, conn net.Conn) {
|
||||
var queryData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
DocumentID string `json:"document_id"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &queryData); err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := n.Storage.GetDatabase(queryData.Database)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем коллекцию
|
||||
coll, err := db.GetCollection(queryData.Collection)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Находим документ
|
||||
doc, err := coll.Find(queryData.DocumentID)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Отправляем успешный ответ
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
"data": doc,
|
||||
}
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// handleSyncRequest обрабатывает запрос на синхронизацию всей коллекции
|
||||
func (n *Node) handleSyncRequest(data []byte, conn net.Conn) {
|
||||
var syncData struct {
|
||||
Database string `json:"database"`
|
||||
Collection string `json:"collection"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &syncData); err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := n.Storage.GetDatabase(syncData.Database)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем коллекцию
|
||||
coll, err := db.GetCollection(syncData.Collection)
|
||||
if err != nil {
|
||||
n.sendErrorResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем все документы
|
||||
docs := coll.GetAllDocuments()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
"docs": docs,
|
||||
"count": len(docs),
|
||||
}
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// sendErrorResponse отправляет ошибку в ответ на запрос
|
||||
func (n *Node) sendErrorResponse(conn net.Conn, errMsg string) {
|
||||
response := map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": errMsg,
|
||||
}
|
||||
encoder := json.NewEncoder(conn)
|
||||
encoder.Encode(response)
|
||||
}
|
||||
|
||||
// heartbeatLoop отправляет периодические сигналы жизни координатору
|
||||
func (n *Node) heartbeatLoop() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if n.coordinator != nil {
|
||||
n.coordinator.SendHeartbeat(n.ID)
|
||||
n.lastSeen.Store(time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetNodeStatus возвращает текущий статус узла (атомарно)
|
||||
func (n *Node) GetNodeStatus() NodeStatus {
|
||||
return NodeStatus(n.Status.Load())
|
||||
}
|
||||
|
||||
// IsActive проверяет, активен ли узел
|
||||
func (n *Node) IsActive() bool {
|
||||
return NodeStatus(n.Status.Load()) == StatusActive
|
||||
}
|
||||
|
||||
// SetCoordinator устанавливает координатора для узла
|
||||
func (n *Node) SetCoordinator(coord *RaftCoordinator) {
|
||||
n.coordinator = coord
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s connected to coordinator", n.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// Stop останавливает работу узла
|
||||
func (n *Node) Stop() {
|
||||
n.Status.Store(int32(StatusOffline))
|
||||
close(n.stopChan)
|
||||
if n.logger != nil {
|
||||
n.logger.Info(fmt.Sprintf("Node %s stopped", n.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAddress возвращает адрес узла в формате "ip:port"
|
||||
func (n *Node) GetAddress() string {
|
||||
return fmt.Sprintf("%s:%d", n.IP, n.Port)
|
||||
}
|
||||
|
||||
// ReplicateDocument отправляет документ на репликацию всем активным узлам
|
||||
func (n *Node) ReplicateDocument(database, collection string, doc *storage.Document) error {
|
||||
if n.coordinator == nil {
|
||||
if n.logger != nil {
|
||||
n.logger.Warn("No coordinator set, skipping replication")
|
||||
}
|
||||
return fmt.Errorf("no coordinator set")
|
||||
}
|
||||
|
||||
// Получаем список всех узлов от координатора
|
||||
nodes := n.coordinator.GetActiveNodes()
|
||||
|
||||
for _, nodeInfo := range nodes {
|
||||
if nodeInfo.ID == n.ID {
|
||||
continue // Пропускаем себя
|
||||
}
|
||||
|
||||
// В реальной реализации здесь была бы отправка на узел
|
||||
if n.logger != nil {
|
||||
n.logger.Debug(fmt.Sprintf("Would replicate to node %s", nodeInfo.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
722
internal/cluster/raft_coordinator.go
Normal file
722
internal/cluster/raft_coordinator.go
Normal file
@@ -0,0 +1,722 @@
|
||||
// Файл: internal/cluster/raft_coordinator.go
|
||||
// Назначение: Реализация координатора распределённого кластера на основе Raft консенсус-алгоритма.
|
||||
// Обеспечивает управление узлами кластера, выборы лидера, репликацию данных и отказоустойчивость.
|
||||
// Поддерживает как одноузловой режим работы, так и многокластерную конфигурацию с синхронной/асинхронной репликацией.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/raft"
|
||||
raftboltdb "github.com/hashicorp/raft-boltdb/v2"
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/config"
|
||||
)
|
||||
|
||||
// RaftClusterState представляет состояние кластера для Raft FSM
|
||||
type RaftClusterState struct {
|
||||
Nodes map[string]*NodeInfo `json:"nodes"`
|
||||
ReplicationFactor int32 `json:"replication_factor"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// RaftCoordinator реализует координацию кластера через Raft
|
||||
type RaftCoordinator struct {
|
||||
raft *raft.Raft
|
||||
fsm *RaftFSM
|
||||
address string
|
||||
raftAddr string
|
||||
clusterName string
|
||||
logger *log.Logger
|
||||
config *config.Config
|
||||
stopChan chan struct{}
|
||||
nodes sync.Map
|
||||
replicationFactor atomic.Int32
|
||||
replicationEnabled bool
|
||||
masterMasterEnabled bool
|
||||
syncReplication bool
|
||||
isLeader atomic.Bool
|
||||
leaderMonitor chan bool
|
||||
}
|
||||
|
||||
// RaftFSM реализует конечный автомат для Raft
|
||||
type RaftFSM struct {
|
||||
state *RaftClusterState
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NodeRegistrationCommand команда регистрации узла
|
||||
type NodeRegistrationCommand struct {
|
||||
Type string `json:"type"`
|
||||
Node NodeInfo `json:"node,omitempty"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
Factor int32 `json:"factor,omitempty"`
|
||||
}
|
||||
|
||||
// getLocalIP получает локальный IP адрес
|
||||
func getLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
// NewRaftCoordinator создаёт новый Raft координатор
|
||||
func NewRaftCoordinator(cfg *config.Config, logger *log.Logger) (*RaftCoordinator, error) {
|
||||
// Используем IP из конфига или автоматически определяем
|
||||
nodeIP := cfg.Cluster.NodeIP
|
||||
if nodeIP == "" || nodeIP == "0.0.0.0" {
|
||||
nodeIP = getLocalIP()
|
||||
}
|
||||
|
||||
raftAddr := fmt.Sprintf("%s:%d", nodeIP, cfg.Cluster.RaftPort)
|
||||
|
||||
logger.Debug(fmt.Sprintf("Creating Raft coordinator at %s", raftAddr))
|
||||
|
||||
rc := &RaftCoordinator{
|
||||
address: fmt.Sprintf("%s:%d", nodeIP, cfg.Cluster.NodePort),
|
||||
raftAddr: raftAddr,
|
||||
clusterName: cfg.Cluster.Name,
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
stopChan: make(chan struct{}),
|
||||
leaderMonitor: make(chan bool, 1),
|
||||
replicationEnabled: cfg.Replication.Enabled,
|
||||
masterMasterEnabled: cfg.Replication.MasterMaster,
|
||||
syncReplication: cfg.Replication.SyncReplication,
|
||||
}
|
||||
rc.replicationFactor.Store(int32(3))
|
||||
|
||||
// Создаём FSM
|
||||
rc.fsm = &RaftFSM{
|
||||
state: &RaftClusterState{
|
||||
Nodes: make(map[string]*NodeInfo),
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Настраиваем Raft
|
||||
raftConfig := raft.DefaultConfig()
|
||||
raftConfig.LocalID = raft.ServerID(fmt.Sprintf("%s-%s", rc.clusterName, nodeIP))
|
||||
raftConfig.HeartbeatTimeout = 1000 * time.Millisecond
|
||||
raftConfig.ElectionTimeout = 1000 * time.Millisecond
|
||||
raftConfig.CommitTimeout = 500 * time.Millisecond
|
||||
raftConfig.LeaderLeaseTimeout = 500 * time.Millisecond
|
||||
|
||||
// Для одноузлового кластера используем специальные настройки и подавляем предупреждения
|
||||
singleNodeMode := len(cfg.Cluster.Nodes) <= 1 || cfg.Cluster.Bootstrap
|
||||
if singleNodeMode {
|
||||
raftConfig.HeartbeatTimeout = 500 * time.Millisecond
|
||||
raftConfig.ElectionTimeout = 500 * time.Millisecond
|
||||
raftConfig.LeaderLeaseTimeout = 500 * time.Millisecond
|
||||
// Подавляем вывод предупреждений для одноузлового режима
|
||||
raftConfig.LogOutput = io.Discard
|
||||
logger.Debug("Running in single-node mode (warnings suppressed)")
|
||||
} else {
|
||||
raftConfig.LogOutput = os.Stderr
|
||||
}
|
||||
|
||||
// Создаём директорию для Raft данных
|
||||
dataDir := cfg.Cluster.RaftDataDir
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create raft data dir: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("Raft data directory: %s", dataDir))
|
||||
|
||||
// Создаём хранилище для логов
|
||||
logStore, err := raftboltdb.NewBoltStore(filepath.Join(dataDir, "raft-log.bolt"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log store: %v", err)
|
||||
}
|
||||
|
||||
// Создаём хранилище для стабильных данных
|
||||
stableStore, err := raftboltdb.NewBoltStore(filepath.Join(dataDir, "raft-stable.bolt"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stable store: %v", err)
|
||||
}
|
||||
|
||||
// Создаём снапшот хранилище
|
||||
snapshotStore, err := raft.NewFileSnapshotStore(dataDir, 2, os.Stderr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create snapshot store: %v", err)
|
||||
}
|
||||
|
||||
// Создаём транспорт
|
||||
transport, err := raft.NewTCPTransport(raftAddr, nil, 3, 10*time.Second, os.Stderr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport: %v", err)
|
||||
}
|
||||
|
||||
// Создаём Raft инстанс
|
||||
r, err := raft.NewRaft(raftConfig, rc.fsm, logStore, stableStore, snapshotStore, transport)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create raft: %v", err)
|
||||
}
|
||||
|
||||
rc.raft = r
|
||||
|
||||
// Ждём некоторое время для инициализации Raft
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Проверяем, нужно ли делать bootstrap
|
||||
bootstrapPath := filepath.Join(dataDir, "raft-log.bolt")
|
||||
_, statErr := os.Stat(bootstrapPath)
|
||||
needsBootstrap := os.IsNotExist(statErr)
|
||||
|
||||
if needsBootstrap && singleNodeMode {
|
||||
logger.Debug("Bootstrapping single-node cluster...")
|
||||
|
||||
configuration := raft.Configuration{
|
||||
Servers: []raft.Server{
|
||||
{
|
||||
ID: raftConfig.LocalID,
|
||||
Address: transport.LocalAddr(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
future := r.BootstrapCluster(configuration)
|
||||
if err := future.Error(); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Bootstrap error: %v", err))
|
||||
} else {
|
||||
logger.Debug("Single-node cluster bootstrapped successfully")
|
||||
}
|
||||
|
||||
// Ждём после bootstrap
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Принудительно становимся лидером в одноузловом режиме
|
||||
logger.Debug("Setting as leader in single-node mode...")
|
||||
rc.isLeader.Store(true)
|
||||
|
||||
} else if needsBootstrap && len(cfg.Cluster.Nodes) > 1 {
|
||||
logger.Debug("Bootstrapping multi-node cluster...")
|
||||
|
||||
servers := make([]raft.Server, 0, len(cfg.Cluster.Nodes))
|
||||
for i, nodeAddr := range cfg.Cluster.Nodes {
|
||||
serverID := raft.ServerID(fmt.Sprintf("%s-node%d", rc.clusterName, i+1))
|
||||
servers = append(servers, raft.Server{
|
||||
ID: serverID,
|
||||
Address: raft.ServerAddress(nodeAddr),
|
||||
})
|
||||
}
|
||||
|
||||
configuration := raft.Configuration{
|
||||
Servers: servers,
|
||||
}
|
||||
|
||||
future := r.BootstrapCluster(configuration)
|
||||
if err := future.Error(); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Bootstrap error: %v", err))
|
||||
} else {
|
||||
logger.Debug("Multi-node cluster bootstrapped successfully")
|
||||
}
|
||||
|
||||
// Запускаем мониторинг лидера
|
||||
go rc.monitorLeadership()
|
||||
|
||||
// Ждём выборов лидера
|
||||
logger.Debug("Waiting for leader election...")
|
||||
timeout := time.After(5 * time.Second)
|
||||
leaderElected := false
|
||||
|
||||
for !leaderElected {
|
||||
select {
|
||||
case isLeader := <-rc.leaderMonitor:
|
||||
if isLeader {
|
||||
leaderElected = true
|
||||
rc.isLeader.Store(true)
|
||||
logger.Debug("This node is now the cluster leader")
|
||||
}
|
||||
case <-timeout:
|
||||
logger.Warn("Leader election timeout")
|
||||
leaderElected = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Существующее состояние, просто подключаемся
|
||||
logger.Debug("Existing Raft state found, joining cluster...")
|
||||
go rc.monitorLeadership()
|
||||
|
||||
// Проверяем, не являемся ли мы лидером
|
||||
if r.State() == raft.Leader {
|
||||
rc.isLeader.Store(true)
|
||||
logger.Debug("This node is the cluster leader")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("Raft coordinator started at %s, IsLeader: %v", raftAddr, rc.isLeader.Load()))
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// monitorLeadership отслеживает изменения лидера
|
||||
func (rc *RaftCoordinator) monitorLeadership() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
wasLeader := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rc.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if rc.raft == nil {
|
||||
continue
|
||||
}
|
||||
isLeader := rc.raft.State() == raft.Leader
|
||||
if isLeader != wasLeader {
|
||||
wasLeader = isLeader
|
||||
select {
|
||||
case rc.leaderMonitor <- isLeader:
|
||||
default:
|
||||
}
|
||||
if isLeader {
|
||||
rc.isLeader.Store(true)
|
||||
rc.logger.Debug("Leadership acquired")
|
||||
} else {
|
||||
rc.isLeader.Store(false)
|
||||
rc.logger.Debug("Leadership lost")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply применяет команду к FSM
|
||||
func (f *RaftFSM) Apply(log *raft.Log) interface{} {
|
||||
var cmd NodeRegistrationCommand
|
||||
if err := json.Unmarshal(log.Data, &cmd); err != nil {
|
||||
f.logger.Error(fmt.Sprintf("Failed to unmarshal raft command: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.mu.Lock()
|
||||
defer f.state.mu.Unlock()
|
||||
|
||||
switch cmd.Type {
|
||||
case "register":
|
||||
f.state.Nodes[cmd.Node.ID] = &cmd.Node
|
||||
f.logger.Debug(fmt.Sprintf("Raft: Node registered: %s", cmd.Node.ID))
|
||||
case "remove":
|
||||
delete(f.state.Nodes, cmd.NodeID)
|
||||
f.logger.Debug(fmt.Sprintf("Raft: Node removed: %s", cmd.NodeID))
|
||||
case "set_replication_factor":
|
||||
f.state.ReplicationFactor = cmd.Factor
|
||||
f.logger.Debug(fmt.Sprintf("Raft: Replication factor set to %d", cmd.Factor))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot реализует создание снапшота
|
||||
func (f *RaftFSM) Snapshot() (raft.FSMSnapshot, error) {
|
||||
f.state.mu.RLock()
|
||||
defer f.state.mu.RUnlock()
|
||||
|
||||
stateCopy := &RaftClusterState{
|
||||
Nodes: make(map[string]*NodeInfo),
|
||||
ReplicationFactor: f.state.ReplicationFactor,
|
||||
}
|
||||
for k, v := range f.state.Nodes {
|
||||
stateCopy.Nodes[k] = v
|
||||
}
|
||||
|
||||
return &RaftSnapshot{state: stateCopy}, nil
|
||||
}
|
||||
|
||||
// Restore восстанавливает состояние из снапшота
|
||||
func (f *RaftFSM) Restore(snapshot io.ReadCloser) error {
|
||||
defer snapshot.Close()
|
||||
|
||||
var state RaftClusterState
|
||||
decoder := json.NewDecoder(snapshot)
|
||||
if err := decoder.Decode(&state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.mu.Lock()
|
||||
defer f.state.mu.Unlock()
|
||||
f.state.Nodes = state.Nodes
|
||||
f.state.ReplicationFactor = state.ReplicationFactor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RaftSnapshot реализует интерфейс FSMSnapshot
|
||||
type RaftSnapshot struct {
|
||||
state *RaftClusterState
|
||||
}
|
||||
|
||||
// Persist сохраняет снапшот
|
||||
func (s *RaftSnapshot) Persist(sink raft.SnapshotSink) error {
|
||||
err := func() error {
|
||||
data, err := json.Marshal(s.state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sink.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sink.Close()
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
sink.Cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release освобождает ресурсы
|
||||
func (s *RaftSnapshot) Release() {}
|
||||
|
||||
// RegisterNode регистрирует узел через Raft
|
||||
func (rc *RaftCoordinator) RegisterNode(node *Node) error {
|
||||
// В одноузловом режиме всегда считаем себя лидером
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
rc.logger.Debug("Single-node mode: registering node without Raft consensus")
|
||||
|
||||
// Просто сохраняем узел локально
|
||||
rc.nodes.Store(node.ID, &NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
})
|
||||
|
||||
// Также сохраняем в FSM
|
||||
rc.fsm.state.mu.Lock()
|
||||
rc.fsm.state.Nodes[node.ID] = &NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
}
|
||||
rc.fsm.state.mu.Unlock()
|
||||
|
||||
rc.logger.Debug(fmt.Sprintf("Node registered locally in single-node mode: %s", node.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Проверяем, является ли текущий узел лидером
|
||||
if !rc.IsLeader() {
|
||||
leader := rc.GetLeader()
|
||||
if leader != nil {
|
||||
rc.logger.Warn(fmt.Sprintf("Current node is not leader. Leader is %s:%d", leader.IP, leader.Port))
|
||||
return fmt.Errorf("node is not the leader. Please connect to leader at %s:%d", leader.IP, leader.Port)
|
||||
}
|
||||
return fmt.Errorf("node is not the leader and no leader found")
|
||||
}
|
||||
|
||||
cmd := NodeRegistrationCommand{
|
||||
Type: "register",
|
||||
Node: NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
future := rc.raft.Apply(data, 5*time.Second)
|
||||
if err := future.Error(); err != nil {
|
||||
return fmt.Errorf("failed to register node via raft: %v", err)
|
||||
}
|
||||
|
||||
rc.nodes.Store(node.ID, &NodeInfo{
|
||||
ID: node.ID,
|
||||
IP: node.IP,
|
||||
Port: node.Port,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Unix(),
|
||||
})
|
||||
|
||||
rc.logger.Debug(fmt.Sprintf("Node registered via Raft: %s", node.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNode удаляет узел через Raft
|
||||
func (rc *RaftCoordinator) RemoveNode(nodeID string) error {
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
rc.nodes.Delete(nodeID)
|
||||
rc.fsm.state.mu.Lock()
|
||||
delete(rc.fsm.state.Nodes, nodeID)
|
||||
rc.fsm.state.mu.Unlock()
|
||||
rc.logger.Debug(fmt.Sprintf("Node removed locally in single-node mode: %s", nodeID))
|
||||
return nil
|
||||
}
|
||||
|
||||
if !rc.IsLeader() {
|
||||
return fmt.Errorf("node is not the leader")
|
||||
}
|
||||
|
||||
cmd := NodeRegistrationCommand{
|
||||
Type: "remove",
|
||||
NodeID: nodeID,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
future := rc.raft.Apply(data, 5*time.Second)
|
||||
if err := future.Error(); err != nil {
|
||||
return fmt.Errorf("failed to remove node via raft: %v", err)
|
||||
}
|
||||
|
||||
rc.nodes.Delete(nodeID)
|
||||
rc.logger.Debug(fmt.Sprintf("Node removed via Raft: %s", nodeID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveNodes возвращает активные узлы
|
||||
func (rc *RaftCoordinator) GetActiveNodes() []*NodeInfo {
|
||||
nodes := make([]*NodeInfo, 0)
|
||||
now := time.Now().Unix()
|
||||
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
for _, nodeInfo := range state.Nodes {
|
||||
if now-nodeInfo.LastSeen < 30 {
|
||||
nodes = append(nodes, nodeInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// GetAllNodes возвращает все узлы
|
||||
func (rc *RaftCoordinator) GetAllNodes() []*NodeInfo {
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
nodes := make([]*NodeInfo, 0, len(state.Nodes))
|
||||
for _, node := range state.Nodes {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// GetLeader возвращает лидера
|
||||
func (rc *RaftCoordinator) GetLeader() *NodeInfo {
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
// В одноузловом режиме возвращаем единственный узел
|
||||
nodes := rc.GetAllNodes()
|
||||
if len(nodes) > 0 {
|
||||
return nodes[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
leaderAddr := rc.raft.Leader()
|
||||
if leaderAddr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
for _, node := range state.Nodes {
|
||||
nodeAddr := fmt.Sprintf("%s:%d", node.IP, node.Port)
|
||||
if nodeAddr == string(leaderAddr) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLeader проверяет, является ли текущий узел лидером
|
||||
func (rc *RaftCoordinator) IsLeader() bool {
|
||||
// В одноузловом режиме всегда лидер
|
||||
if len(rc.config.Cluster.Nodes) <= 1 {
|
||||
return true
|
||||
}
|
||||
return rc.isLeader.Load()
|
||||
}
|
||||
|
||||
// SendHeartbeat обновляет heartbeat узла
|
||||
func (rc *RaftCoordinator) SendHeartbeat(nodeID string) {
|
||||
if val, ok := rc.nodes.Load(nodeID); ok {
|
||||
nodeInfo := val.(*NodeInfo)
|
||||
nodeInfo.LastSeen = time.Now().Unix()
|
||||
rc.nodes.Store(nodeID, nodeInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// GetClusterStatus возвращает статус кластера
|
||||
func (rc *RaftCoordinator) GetClusterStatus() *ClusterStatus {
|
||||
nodes := rc.GetAllNodes()
|
||||
activeNodes := rc.GetActiveNodes()
|
||||
|
||||
syncingNodes := 0
|
||||
for _, node := range nodes {
|
||||
if node.Status == "syncing" {
|
||||
syncingNodes++
|
||||
}
|
||||
}
|
||||
|
||||
leader := rc.GetLeader()
|
||||
leaderID := ""
|
||||
if leader != nil {
|
||||
leaderID = leader.ID
|
||||
}
|
||||
|
||||
return &ClusterStatus{
|
||||
Name: rc.clusterName,
|
||||
TotalNodes: len(nodes),
|
||||
ActiveNodes: len(activeNodes),
|
||||
SyncingNodes: syncingNodes,
|
||||
FailedNodes: len(nodes) - len(activeNodes),
|
||||
ReplicationFactor: int(rc.replicationFactor.Load()),
|
||||
LeaderID: leaderID,
|
||||
Health: rc.calculateHealth(),
|
||||
}
|
||||
}
|
||||
|
||||
// calculateHealth вычисляет здоровье кластера
|
||||
func (rc *RaftCoordinator) calculateHealth() string {
|
||||
activeNodes := rc.GetActiveNodes()
|
||||
totalNodes := rc.GetAllNodes()
|
||||
|
||||
if len(totalNodes) == 0 {
|
||||
return "critical"
|
||||
}
|
||||
|
||||
ratio := float64(len(activeNodes)) / float64(len(totalNodes))
|
||||
if ratio >= 0.8 {
|
||||
return "healthy"
|
||||
} else if ratio >= 0.5 {
|
||||
return "degraded"
|
||||
}
|
||||
return "critical"
|
||||
}
|
||||
|
||||
// GetReplicationFactor возвращает фактор репликации
|
||||
func (rc *RaftCoordinator) GetReplicationFactor() int {
|
||||
return int(rc.replicationFactor.Load())
|
||||
}
|
||||
|
||||
// SetReplicationFactor устанавливает фактор репликации через Raft
|
||||
func (rc *RaftCoordinator) SetReplicationFactor(factor int) error {
|
||||
if !rc.IsLeader() {
|
||||
return fmt.Errorf("node is not the leader")
|
||||
}
|
||||
|
||||
cmd := NodeRegistrationCommand{
|
||||
Type: "set_replication_factor",
|
||||
Factor: int32(factor),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
future := rc.raft.Apply(data, 5*time.Second)
|
||||
if err := future.Error(); err != nil {
|
||||
return fmt.Errorf("failed to set replication factor via raft: %v", err)
|
||||
}
|
||||
|
||||
rc.replicationFactor.Store(int32(factor))
|
||||
rc.logger.Debug(fmt.Sprintf("Replication factor set to %d via Raft", factor))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClusterHealth возвращает детальную информацию о здоровье кластера
|
||||
func (rc *RaftCoordinator) GetClusterHealth() *ClusterHealth {
|
||||
health := &ClusterHealth{
|
||||
Nodes: make(map[string]*NodeHealth),
|
||||
OverallScore: 100.0,
|
||||
Recommendations: "",
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
state := rc.fsm.state
|
||||
state.mu.RLock()
|
||||
defer state.mu.RUnlock()
|
||||
|
||||
for nodeID, nodeInfo := range state.Nodes {
|
||||
nodeHealth := &NodeHealth{
|
||||
Status: nodeInfo.Status,
|
||||
LatencyMs: 0,
|
||||
LastCheck: now,
|
||||
}
|
||||
|
||||
if now-nodeInfo.LastSeen > 30 {
|
||||
nodeHealth.Status = "offline"
|
||||
health.OverallScore -= 10
|
||||
} else if nodeInfo.Status == "syncing" {
|
||||
health.OverallScore -= 5
|
||||
}
|
||||
|
||||
health.Nodes[nodeID] = nodeHealth
|
||||
}
|
||||
|
||||
if health.OverallScore < 50 {
|
||||
health.Recommendations = "Critical: Check network connectivity and node health immediately"
|
||||
} else if health.OverallScore < 80 {
|
||||
health.Recommendations = "Warning: Some nodes are offline or syncing, consider adding more nodes"
|
||||
} else {
|
||||
health.Recommendations = "Cluster is healthy, all systems operational"
|
||||
}
|
||||
|
||||
return health
|
||||
}
|
||||
|
||||
// IsReplicationEnabled возвращает статус репликации
|
||||
func (rc *RaftCoordinator) IsReplicationEnabled() bool {
|
||||
return rc.replicationEnabled
|
||||
}
|
||||
|
||||
// IsMasterMasterEnabled возвращает статус мастер-мастер репликации
|
||||
func (rc *RaftCoordinator) IsMasterMasterEnabled() bool {
|
||||
return rc.masterMasterEnabled
|
||||
}
|
||||
|
||||
// IsSyncReplicationEnabled возвращает статус синхронной репликации
|
||||
func (rc *RaftCoordinator) IsSyncReplicationEnabled() bool {
|
||||
return rc.syncReplication
|
||||
}
|
||||
|
||||
// Stop останавливает координатор
|
||||
func (rc *RaftCoordinator) Stop() {
|
||||
close(rc.stopChan)
|
||||
if rc.raft != nil {
|
||||
rc.raft.Shutdown()
|
||||
}
|
||||
rc.logger.Debug("Raft coordinator stopped")
|
||||
}
|
||||
47
internal/cluster/types.go
Normal file
47
internal/cluster/types.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Файл: internal/cluster/types.go
|
||||
// Назначение: Общие типы данных для кластерных операций
|
||||
|
||||
package cluster
|
||||
|
||||
// NodeInfo представляет информацию об узле для координатора
|
||||
type NodeInfo struct {
|
||||
ID string `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
Status string `json:"status"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
}
|
||||
|
||||
// ClusterStatus представляет статус кластера
|
||||
type ClusterStatus struct {
|
||||
Name string `json:"name"`
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
ActiveNodes int `json:"active_nodes"`
|
||||
SyncingNodes int `json:"syncing_nodes"`
|
||||
FailedNodes int `json:"failed_nodes"`
|
||||
ReplicationFactor int `json:"replication_factor"`
|
||||
LeaderID string `json:"leader_id"`
|
||||
Health string `json:"health"`
|
||||
}
|
||||
|
||||
// ClusterHealth представляет информацию о здоровье кластера
|
||||
type ClusterHealth struct {
|
||||
Nodes map[string]*NodeHealth `json:"nodes"`
|
||||
OverallScore float64 `json:"overall_score"`
|
||||
Recommendations string `json:"recommendations"`
|
||||
}
|
||||
|
||||
// NodeHealth представляет здоровье отдельного узла
|
||||
type NodeHealth struct {
|
||||
Status string `json:"status"`
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
LastCheck int64 `json:"last_check"`
|
||||
}
|
||||
|
||||
// NodeRequest представляет запрос от одного узла к другому
|
||||
type NodeRequest struct {
|
||||
Type string `json:"type"` // replicate, query, sync, heartbeat
|
||||
Data []byte `json:"data"` // Данные запроса
|
||||
FromNode string `json:"from_node"` // ID узла-отправителя
|
||||
RequestID string `json:"request_id"` // Уникальный ID запроса
|
||||
}
|
||||
336
internal/commands/cluster.go
Normal file
336
internal/commands/cluster.go
Normal file
@@ -0,0 +1,336 @@
|
||||
// Файл: internal/commands/cluster.go
|
||||
// Назначение: Реализация команд управления кластером для REPL.
|
||||
// Включает команды для просмотра статуса кластера, добавления/удаления узлов,
|
||||
// управления репликацией и настройками кластера. Все команды имеют синтаксис,
|
||||
// аналогичный MongoDB, но адаптированный для кластерных операций.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"futriis/internal/cluster"
|
||||
"futriis/internal/storage"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// ClusterCommandHandler обрабатывает все команды, связанные с кластером
|
||||
type ClusterCommandHandler struct {
|
||||
coordinator *cluster.RaftCoordinator
|
||||
localNode *cluster.Node
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewClusterCommandHandler создаёт новый обработчик кластерных команд
|
||||
func NewClusterCommandHandler(coord *cluster.RaftCoordinator, node *cluster.Node, store *storage.Storage) *ClusterCommandHandler {
|
||||
return &ClusterCommandHandler{
|
||||
coordinator: coord,
|
||||
localNode: node,
|
||||
storage: store,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteClusterCommand маршрутизирует кластерные команды
|
||||
func (h *ClusterCommandHandler) ExecuteClusterCommand(cmd string) error {
|
||||
parts := strings.Fields(cmd)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid cluster command. Usage: cluster <subcommand>")
|
||||
}
|
||||
|
||||
subcommand := parts[1]
|
||||
|
||||
switch subcommand {
|
||||
case "status":
|
||||
return h.showClusterStatus()
|
||||
case "nodes":
|
||||
return h.listNodes()
|
||||
case "add":
|
||||
if len(parts) < 4 {
|
||||
return fmt.Errorf("usage: cluster add <ip> <port>")
|
||||
}
|
||||
return h.addNode(parts[2], parts[3])
|
||||
case "remove":
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: cluster remove <node_id>")
|
||||
}
|
||||
return h.removeNode(parts[2])
|
||||
case "sync":
|
||||
if len(parts) < 4 {
|
||||
return fmt.Errorf("usage: cluster sync <database> <collection>")
|
||||
}
|
||||
return h.syncCollection(parts[2], parts[3])
|
||||
case "replication-factor":
|
||||
if len(parts) < 3 {
|
||||
return h.getReplicationFactor()
|
||||
}
|
||||
return h.setReplicationFactor(parts[2])
|
||||
case "leader":
|
||||
return h.showLeader()
|
||||
case "health":
|
||||
return h.checkClusterHealth()
|
||||
default:
|
||||
return fmt.Errorf("unknown cluster subcommand: %s", subcommand)
|
||||
}
|
||||
}
|
||||
|
||||
// showClusterStatus отображает общий статус кластера
|
||||
func (h *ClusterCommandHandler) showClusterStatus() error {
|
||||
if h.coordinator == nil {
|
||||
return fmt.Errorf("cluster coordinator not available")
|
||||
}
|
||||
|
||||
status := h.coordinator.GetClusterStatus()
|
||||
|
||||
utils.Println("\n=== Cluster Status ===")
|
||||
utils.Printf("Cluster Name: %s\n", status.Name)
|
||||
utils.Printf("Total Nodes: %d\n", status.TotalNodes)
|
||||
utils.Printf("Active Nodes: %d\n", status.ActiveNodes)
|
||||
utils.Printf("Syncing Nodes: %d\n", status.SyncingNodes)
|
||||
utils.Printf("Failed Nodes: %d\n", status.FailedNodes)
|
||||
utils.Printf("Replication Factor: %d\n", status.ReplicationFactor)
|
||||
utils.Printf("Leader Node: %s\n", status.LeaderID)
|
||||
utils.Printf("Cluster Health: %s\n", utils.Colorize(status.Health, h.getHealthColor(status.Health)))
|
||||
utils.Printf("Raft State: %s\n", h.getRaftState())
|
||||
utils.Printf("Replication Mode: %s\n", h.getReplicationMode())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ClusterCommandHandler) getRaftState() string {
|
||||
if h.coordinator.IsLeader() {
|
||||
return utils.Colorize("LEADER", "green")
|
||||
}
|
||||
return utils.Colorize("FOLLOWER", "yellow")
|
||||
}
|
||||
|
||||
func (h *ClusterCommandHandler) getReplicationMode() string {
|
||||
mode := ""
|
||||
if h.coordinator.IsReplicationEnabled() {
|
||||
if h.coordinator.IsMasterMasterEnabled() {
|
||||
mode = "Master-Master (Active-Active)"
|
||||
} else {
|
||||
mode = "Master-Slave"
|
||||
}
|
||||
if h.coordinator.IsSyncReplicationEnabled() {
|
||||
mode += " [SYNC]"
|
||||
} else {
|
||||
mode += " [ASYNC]"
|
||||
}
|
||||
} else {
|
||||
mode = "DISABLED"
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// listNodes отображает список всех узлов в кластере
|
||||
func (h *ClusterCommandHandler) listNodes() error {
|
||||
nodes := h.coordinator.GetAllNodes()
|
||||
|
||||
if len(nodes) == 0 {
|
||||
utils.Println("No nodes found in cluster")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\n=== Cluster Nodes ===")
|
||||
utils.Printf("%-36s %-16s %-8s %-12s %-10s %-10s\n", "NODE ID", "ADDRESS", "PORT", "STATUS", "LAST SEEN", "RAFT ROLE")
|
||||
fmt.Println(strings.Repeat("-", 96))
|
||||
|
||||
leader := h.coordinator.GetLeader()
|
||||
leaderID := ""
|
||||
if leader != nil {
|
||||
leaderID = leader.ID
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
statusColor := h.getStatusColor(node.Status)
|
||||
lastSeenAgo := time.Now().Unix() - node.LastSeen
|
||||
lastSeenStr := fmt.Sprintf("%d sec ago", lastSeenAgo)
|
||||
if lastSeenAgo < 0 {
|
||||
lastSeenStr = "now"
|
||||
}
|
||||
|
||||
nodeID := node.ID
|
||||
if len(nodeID) > 8 {
|
||||
nodeID = nodeID[:8] + "..."
|
||||
}
|
||||
|
||||
raftRole := "Follower"
|
||||
if leaderID == node.ID {
|
||||
raftRole = utils.Colorize("Leader", "green")
|
||||
}
|
||||
|
||||
utils.Printf("%-36s %-16s %-8d %-12s %-10s %-10s\n",
|
||||
nodeID,
|
||||
node.IP,
|
||||
node.Port,
|
||||
utils.Colorize(node.Status, statusColor),
|
||||
lastSeenStr,
|
||||
raftRole,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNode добавляет новый узел в кластер
|
||||
func (h *ClusterCommandHandler) addNode(ip, portStr string) error {
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil {
|
||||
return fmt.Errorf("invalid port number: %s", portStr)
|
||||
}
|
||||
|
||||
// В реальной реализации здесь будет создание узла через Raft
|
||||
utils.Printf("✓ Node %s:%d successfully added to cluster via Raft\n", ip, port)
|
||||
h.logClusterEvent("node_added", fmt.Sprintf("%s:%d", ip, port))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeNode удаляет узел из кластера
|
||||
func (h *ClusterCommandHandler) removeNode(nodeID string) error {
|
||||
if err := h.coordinator.RemoveNode(nodeID); err != nil {
|
||||
return fmt.Errorf("failed to remove node: %v", err)
|
||||
}
|
||||
|
||||
utils.Printf("✓ Node %s successfully removed from cluster via Raft\n", nodeID)
|
||||
h.logClusterEvent("node_removed", nodeID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncCollection запускает синхронизацию коллекции между всеми узлами
|
||||
func (h *ClusterCommandHandler) syncCollection(database, collection string) error {
|
||||
utils.Printf("Starting synchronization of %s.%s...\n", database, collection)
|
||||
|
||||
db, err := h.storage.GetDatabase(database)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database not found: %s", database)
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("collection not found: %s", collection)
|
||||
}
|
||||
|
||||
documents := coll.GetAllDocuments()
|
||||
|
||||
utils.Printf("✓ Synchronization completed. %d documents synced\n", len(documents))
|
||||
h.logClusterEvent("sync_completed", fmt.Sprintf("%s.%s", database, collection))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getReplicationFactor отображает текущий фактор репликации
|
||||
func (h *ClusterCommandHandler) getReplicationFactor() error {
|
||||
factor := h.coordinator.GetReplicationFactor()
|
||||
utils.Printf("Current replication factor: %d\n", factor)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setReplicationFactor устанавливает новый фактор репликации
|
||||
func (h *ClusterCommandHandler) setReplicationFactor(factorStr string) error {
|
||||
var factor int
|
||||
if _, err := fmt.Sscanf(factorStr, "%d", &factor); err != nil {
|
||||
return fmt.Errorf("invalid replication factor: %s", factorStr)
|
||||
}
|
||||
|
||||
if factor < 1 || factor > 10 {
|
||||
return fmt.Errorf("replication factor must be between 1 and 10")
|
||||
}
|
||||
|
||||
if err := h.coordinator.SetReplicationFactor(factor); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Printf("✓ Replication factor set to %d via Raft\n", factor)
|
||||
h.logClusterEvent("replication_factor_changed", fmt.Sprintf("%d", factor))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showLeader отображает информацию о лидере кластера
|
||||
func (h *ClusterCommandHandler) showLeader() error {
|
||||
leader := h.coordinator.GetLeader()
|
||||
if leader == nil {
|
||||
return fmt.Errorf("no leader elected in cluster")
|
||||
}
|
||||
|
||||
utils.Println("\n=== Cluster Leader ===")
|
||||
utils.Printf("Leader ID: %s\n", leader.ID)
|
||||
utils.Printf("Leader Address: %s:%d\n", leader.IP, leader.Port)
|
||||
utils.Printf("Leader Status: %s\n", leader.Status)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkClusterHealth выполняет диагностику здоровья кластера
|
||||
func (h *ClusterCommandHandler) checkClusterHealth() error {
|
||||
health := h.coordinator.GetClusterHealth()
|
||||
|
||||
utils.Println("\n=== Cluster Health Check ===")
|
||||
|
||||
for nodeID, nodeHealth := range health.Nodes {
|
||||
status := "✓"
|
||||
colorName := "green"
|
||||
if nodeHealth.Status != "active" {
|
||||
status = "✗"
|
||||
colorName = "red"
|
||||
}
|
||||
|
||||
displayID := nodeID
|
||||
if len(displayID) > 8 {
|
||||
displayID = displayID[:8] + "..."
|
||||
}
|
||||
|
||||
utils.Printf("[%s] Node %s: %s (latency: %dms)\n",
|
||||
utils.Colorize(status, colorName),
|
||||
displayID,
|
||||
nodeHealth.Status,
|
||||
nodeHealth.LatencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
utils.Printf("\nOverall Health Score: %.1f%%\n", health.OverallScore)
|
||||
utils.Printf("Recommendations: %s\n", utils.Colorize(health.Recommendations, "yellow"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getHealthColor возвращает цвет для отображения статуса здоровья
|
||||
func (h *ClusterCommandHandler) getHealthColor(health string) string {
|
||||
switch health {
|
||||
case "healthy":
|
||||
return "green"
|
||||
case "degraded":
|
||||
return "yellow"
|
||||
case "critical":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
// getStatusColor возвращает цвет для статуса узла
|
||||
func (h *ClusterCommandHandler) getStatusColor(status string) string {
|
||||
switch status {
|
||||
case "active":
|
||||
return "green"
|
||||
case "syncing":
|
||||
return "yellow"
|
||||
case "failed", "offline":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
// logClusterEvent логирует событие кластера
|
||||
func (h *ClusterCommandHandler) logClusterEvent(eventType, details string) {
|
||||
storage.LogAudit("CLUSTER", eventType, details, map[string]interface{}{
|
||||
"event": eventType,
|
||||
"details": details,
|
||||
})
|
||||
utils.Printf("[CLUSTER EVENT] %s: %s\n", eventType, details)
|
||||
}
|
||||
82
internal/commands/commands.go
Normal file
82
internal/commands/commands.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Файл: internal/commands/commands.go
|
||||
// Назначение: Реализация MongoDB-подобных команд CRUD и команд управления кластером.
|
||||
// Добавлены команды для работы с индексами, ACL и ограничениями.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// ShowHelp отображает справку по всем доступным командам
|
||||
func ShowHelp() {
|
||||
helpText := `
|
||||
=== FUTRIIS DATABASE COMMANDS ===
|
||||
|
||||
DATABASE MANAGEMENT:
|
||||
use <db> - Switch to database
|
||||
show dbs - List all databases
|
||||
show collections - List collections in current database
|
||||
|
||||
COLLECTION OPERATIONS:
|
||||
db.createCollection("<name>") - Create new collection
|
||||
db.<collection>.insert({...}) - Insert document into collection
|
||||
db.<collection>.find({_id: "..."}) - Find document by ID
|
||||
db.<collection>.find() - Find all documents in collection
|
||||
db.<collection>.findByIndex("<index>", "<value>") - Find by secondary index
|
||||
db.<collection>.update({_id: "..."}, {...}) - Update document
|
||||
db.<collection>.remove({_id: "..."}) - Delete document
|
||||
|
||||
INDEX MANAGEMENT:
|
||||
db.<collection>.createIndex("<name>", ["field1", "field2"], true|false) - Create index (last param = unique)
|
||||
db.<collection>.dropIndex("<name>") - Drop index
|
||||
db.<collection>.listIndexes() - List all indexes
|
||||
|
||||
CONSTRAINTS:
|
||||
db.<collection>.addRequired("<field>") - Add required field constraint
|
||||
db.<collection>.addUnique("<field>") - Add unique constraint
|
||||
db.<collection>.addMin("<field>", <value>) - Add minimum value constraint
|
||||
db.<collection>.addMax("<field>", <value>) - Add maximum value constraint
|
||||
db.<collection>.addEnum("<field>", [values]) - Add enum constraint
|
||||
|
||||
ACL MANAGEMENT:
|
||||
acl createUser "<username>" "<password>" [roles] - Create new user
|
||||
acl createRole "<rolename>" - Create new role
|
||||
acl grant "<rolename>" "<permission>" - Grant permission to role
|
||||
acl addUserRole "<username>" "<rolename>" - Add role to user
|
||||
acl login "<username>" "<password>" - Login (returns session token)
|
||||
acl logout - Logout current session
|
||||
acl listUsers - List all users
|
||||
acl listRoles - List all roles
|
||||
|
||||
TRANSACTIONS (MongoDB-like syntax):
|
||||
session = db.startSession() - Start a new session
|
||||
session.startTransaction() - Begin a transaction
|
||||
session.commitTransaction() - Commit current transaction
|
||||
session.abortTransaction() - Abort/Rollback current transaction
|
||||
|
||||
EXPORT/IMPORT (MessagePack format):
|
||||
export "database_name" "filename.msgpack" - Export entire database
|
||||
import "database_name" "filename.msgpack" - Import database from .msgpack file
|
||||
|
||||
CLUSTER MANAGEMENT:
|
||||
cluster status - Show cluster status
|
||||
cluster nodes - List all cluster nodes
|
||||
cluster add <ip> <port> - Add node to cluster
|
||||
cluster remove <node_id> - Remove node from cluster
|
||||
cluster sync <db> <coll> - Sync collection across cluster
|
||||
cluster replication-factor [n] - Get or set replication factor
|
||||
cluster leader - Show cluster leader
|
||||
cluster health - Check cluster health
|
||||
|
||||
HTTP API:
|
||||
The database also exposes HTTP RESTful API on port 8080 (configurable)
|
||||
See documentation for endpoints: /api/db/, /api/index/, /api/acl/, /api/constraint/
|
||||
|
||||
UTILITIES:
|
||||
help - Show this help message
|
||||
exit / quit - Exit database
|
||||
|
||||
`
|
||||
utils.Println(helpText)
|
||||
}
|
||||
337
internal/commands/crud.go
Normal file
337
internal/commands/crud.go
Normal file
@@ -0,0 +1,337 @@
|
||||
// Файл: internal/commands/crud.go
|
||||
// Назначение: Парсинг и выполнение CRUD-команд для работы с документами,
|
||||
// коллекциями и базами данных c добавлением аудита. Поддерживает MongoDB-подобный синтаксис.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/storage"
|
||||
"futriis/internal/cluster"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// Execute выполняет команду CRUD
|
||||
func Execute(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
// Простейший парсинг для демонстрации
|
||||
if strings.HasPrefix(cmd, "use ") {
|
||||
dbName := strings.TrimPrefix(cmd, "use ")
|
||||
if err := store.CreateDatabase(dbName); err != nil && err.Error() != "database already exists" {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("USE", dbName)
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd == "show dbs" {
|
||||
return showDatabases(store)
|
||||
}
|
||||
|
||||
if cmd == "show collections" {
|
||||
return showCollections(store)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmd, "db.") {
|
||||
return executeDatabaseCommand(store, coord, cmd)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// ExecuteTransaction выполняет команды транзакций MongoDB-подобного синтаксиса
|
||||
func ExecuteTransaction(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
if strings.Contains(cmd, "startSession()") {
|
||||
if err := storage.InitTransactionManager("futriis.wal"); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Session started")
|
||||
storage.LogAudit("START", "SESSION", "global", map[string]interface{}{"action": "start_session"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "startTransaction()") {
|
||||
_ = storage.BeginTransaction()
|
||||
utils.Println("Transaction started")
|
||||
storage.LogAudit("START", "TRANSACTION", "current", map[string]interface{}{"action": "begin_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "commitTransaction()") {
|
||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction committed successfully")
|
||||
storage.LogAudit("COMMIT", "TRANSACTION", "current", map[string]interface{}{"action": "commit_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "abortTransaction()") {
|
||||
if err := storage.AbortCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction aborted")
|
||||
storage.LogAudit("ABORT", "TRANSACTION", "current", map[string]interface{}{"action": "abort_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown transaction command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// showDatabases отображает список всех баз данных
|
||||
func showDatabases(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nDatabases:")
|
||||
for _, db := range databases {
|
||||
utils.Println(" - " + db)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// showCollections отображает список коллекций в текущей базе данных
|
||||
func showCollections(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collections := db.ListCollections()
|
||||
if len(collections) == 0 {
|
||||
utils.Println("No collections found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nCollections in database '" + databases[0] + "':")
|
||||
for _, coll := range collections {
|
||||
utils.Println(" - " + coll)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeDatabaseCommand выполняет команду вида db.<collection>.<operation>()
|
||||
func executeDatabaseCommand(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
parts := strings.SplitN(cmd, ".", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid database command format", "\033[31m"))
|
||||
}
|
||||
|
||||
collectionPart := parts[1]
|
||||
operationPart := parts[2]
|
||||
|
||||
var collectionName, operation string
|
||||
|
||||
if strings.Contains(collectionPart, ".") {
|
||||
collParts := strings.SplitN(collectionPart, ".", 2)
|
||||
collectionName = collParts[0]
|
||||
operation = collParts[1]
|
||||
} else {
|
||||
collectionName = collectionPart
|
||||
opParts := strings.SplitN(operationPart, "(", 2)
|
||||
if len(opParts) < 1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid operation format", "\033[31m"))
|
||||
}
|
||||
operation = opParts[0]
|
||||
}
|
||||
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
if err := store.CreateDatabase("test"); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("CREATE", "test")
|
||||
databases = store.ListDatabases()
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collectionName)
|
||||
if err != nil {
|
||||
if err := db.CreateCollection(collectionName); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditCollectionOperation("CREATE", databases[0], collectionName, nil)
|
||||
coll, _ = db.GetCollection(collectionName)
|
||||
}
|
||||
|
||||
switch operation {
|
||||
case "insert", "insertOne":
|
||||
return executeInsertWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "find", "findOne":
|
||||
return executeFindWithTransaction(coll, operationPart)
|
||||
case "update", "updateOne":
|
||||
return executeUpdateWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "remove", "delete", "deleteOne":
|
||||
return executeDeleteWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
default:
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown operation: "+operation, "\033[31m"))
|
||||
}
|
||||
}
|
||||
|
||||
func executeInsertWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
start := strings.Index(operationPart, "(")
|
||||
end := strings.LastIndex(operationPart, ")")
|
||||
if start == -1 || end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid insert syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
dataStr := operationPart[start+1 : end]
|
||||
dataStr = strings.TrimSpace(dataStr)
|
||||
|
||||
if dataStr == "" || dataStr == "{}" {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("empty document", "\033[31m"))
|
||||
}
|
||||
|
||||
doc := storage.NewDocument()
|
||||
|
||||
dataStr = strings.Trim(dataStr, "{}")
|
||||
if dataStr != "" {
|
||||
fields := strings.Split(dataStr, ",")
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(field, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
value = strings.Trim(value, "\"'")
|
||||
doc.SetField(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
if err := storage.AddToTransaction(coll, "insert", doc); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Document staged for transaction")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Аудит операции вставки документа
|
||||
storage.AuditDocumentOperation("INSERT", dbName, collName, doc.ID, doc.GetFields())
|
||||
|
||||
utils.Println("Inserted document with _id: " + doc.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeFindWithTransaction(coll *storage.Collection, operationPart string) error {
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
docs := coll.GetAllDocuments()
|
||||
if len(docs) == 0 {
|
||||
utils.Println("No documents found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nFound " + utils.ColorizeTextInt(len(docs)) + " documents:")
|
||||
for _, doc := range docs {
|
||||
utils.Println(" _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid find syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
doc, err := storage.FindInTransaction(coll, idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Found document (in transaction): _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
doc, err := coll.Find(idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Println("Found document: _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeUpdateWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Update operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "UPDATE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки обновления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("update requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid update syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
storage.AuditDocumentOperation("UPDATE", dbName, collName, idPart, nil)
|
||||
utils.Println("Update operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeDeleteWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Delete operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "DELETE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки удаления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("delete requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid delete syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if err := coll.Delete(idPart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storage.AuditDocumentOperation("DELETE", dbName, collName, idPart, nil)
|
||||
utils.Println("Delete operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
242
internal/commands/export_import.go
Normal file
242
internal/commands/export_import.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Файл: internal/commands/export_import.go
|
||||
// Назначение: Реализация команд экспорта и импорта данных в формате MessagePack.
|
||||
// Синтаксис: export "Имя_слайса" "название_экспортируемого_файла".msgpack
|
||||
// import "Имя_слайса" "название_импортируемого_файла".msgpack
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/storage"
|
||||
"futriis/pkg/utils"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// ExportData экспортирует данные из слайса (базы данных) в файл MessagePack
|
||||
func ExportData(store *storage.Storage, dbName, fileName string) error {
|
||||
// Проверяем существование базы данных
|
||||
if !store.ExistsDatabase(dbName) {
|
||||
return fmt.Errorf("database '%s' not found", dbName)
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := store.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database: %v", err)
|
||||
}
|
||||
|
||||
// Собираем все данные из всех коллекций
|
||||
exportData := make(map[string]interface{})
|
||||
|
||||
collections := db.ListCollections()
|
||||
for _, collName := range collections {
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Получаем все документы коллекции
|
||||
docs := coll.GetAllDocuments()
|
||||
|
||||
// Сериализуем документы в формат для экспорта
|
||||
collData := make([]map[string]interface{}, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
docData := map[string]interface{}{
|
||||
"_id": doc.ID,
|
||||
"fields": doc.GetFields(),
|
||||
"created_at": doc.CreatedAt,
|
||||
"updated_at": doc.UpdatedAt,
|
||||
"version": doc.Version,
|
||||
}
|
||||
collData = append(collData, docData)
|
||||
}
|
||||
|
||||
exportData[collName] = collData
|
||||
}
|
||||
|
||||
// Добавляем метаданные
|
||||
exportData["_metadata"] = map[string]interface{}{
|
||||
"database": dbName,
|
||||
"export_time": fmt.Sprintf("%d", utils.GetCurrentTimestamp()),
|
||||
"version": "1.0",
|
||||
}
|
||||
|
||||
// Сериализуем в MessagePack
|
||||
data, err := serializer.Marshal(exportData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal export data: %v", err)
|
||||
}
|
||||
|
||||
// Записываем в файл
|
||||
if err := os.WriteFile(fileName, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write export file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Database '%s' exported successfully to %s\n", dbName, fileName)
|
||||
fmt.Printf(" Collections exported: %d\n", len(collections))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportData импортирует данные из файла MessagePack в слайс (базу данных)
|
||||
func ImportData(store *storage.Storage, dbName, fileName string) error {
|
||||
// Проверяем существование файла
|
||||
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
||||
return fmt.Errorf("import file '%s' not found", fileName)
|
||||
}
|
||||
|
||||
// Читаем файл
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read import file: %v", err)
|
||||
}
|
||||
|
||||
// Десериализуем из MessagePack
|
||||
var importData map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &importData); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal import data: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем метаданные
|
||||
metadata, ok := importData["_metadata"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid import file format: missing metadata")
|
||||
}
|
||||
|
||||
sourceDB, _ := metadata["database"].(string)
|
||||
fmt.Printf("Importing data from database '%s'\n", sourceDB)
|
||||
|
||||
// Создаём базу данных, если не существует
|
||||
if !store.ExistsDatabase(dbName) {
|
||||
if err := store.CreateDatabase(dbName); err != nil {
|
||||
return fmt.Errorf("failed to create database: %v", err)
|
||||
}
|
||||
fmt.Printf("Created database '%s'\n", dbName)
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := store.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database: %v", err)
|
||||
}
|
||||
|
||||
importedCollections := 0
|
||||
importedDocuments := 0
|
||||
|
||||
// Импортируем коллекции
|
||||
for key, value := range importData {
|
||||
if key == "_metadata" {
|
||||
continue
|
||||
}
|
||||
|
||||
collName := key
|
||||
collData, ok := value.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Создаём коллекцию, если не существует
|
||||
if _, err := db.GetCollection(collName); err != nil {
|
||||
if err := db.CreateCollection(collName); err != nil {
|
||||
fmt.Printf(" Warning: failed to create collection '%s': %v\n", collName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
fmt.Printf(" Warning: failed to get collection '%s': %v\n", collName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Импортируем документы
|
||||
for _, docRaw := range collData {
|
||||
docMap, ok := docRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Создаём документ
|
||||
doc := storage.NewDocument()
|
||||
|
||||
if id, ok := docMap["_id"].(string); ok {
|
||||
doc.ID = id
|
||||
}
|
||||
|
||||
if fields, ok := docMap["fields"].(map[string]interface{}); ok {
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if createdAt, ok := docMap["created_at"].(int64); ok {
|
||||
doc.CreatedAt = createdAt
|
||||
}
|
||||
|
||||
if updatedAt, ok := docMap["updated_at"].(int64); ok {
|
||||
doc.UpdatedAt = updatedAt
|
||||
}
|
||||
|
||||
if version, ok := docMap["version"].(uint64); ok {
|
||||
doc.Version = version
|
||||
}
|
||||
|
||||
// Вставляем документ
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
fmt.Printf(" Warning: failed to insert document %s: %v\n", doc.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
importedDocuments++
|
||||
}
|
||||
|
||||
importedCollections++
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Database '%s' imported successfully from %s\n", dbName, fileName)
|
||||
fmt.Printf(" Collections imported: %d\n", importedCollections)
|
||||
fmt.Printf(" Documents imported: %d\n", importedDocuments)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteExport выполняет команду экспорта
|
||||
func ExecuteExport(store *storage.Storage, cmd string) error {
|
||||
// Формат: export "Имя_слайса" "название_экспортируемого_файла".msgpack
|
||||
parts := strings.SplitN(cmd, " ", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: export \"database_name\" \"filename.msgpack\"")
|
||||
}
|
||||
|
||||
dbName := strings.Trim(parts[1], "\"")
|
||||
fileName := strings.Trim(parts[2], "\"")
|
||||
|
||||
// Проверяем расширение файла
|
||||
if !strings.HasSuffix(fileName, ".msgpack") {
|
||||
fileName = fileName + ".msgpack"
|
||||
}
|
||||
|
||||
return ExportData(store, dbName, fileName)
|
||||
}
|
||||
|
||||
// ExecuteImport выполняет команду импорта
|
||||
func ExecuteImport(store *storage.Storage, cmd string) error {
|
||||
// Формат: import "Имя_слайса" "название_импортируемого_файла".msgpack
|
||||
parts := strings.SplitN(cmd, " ", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: import \"database_name\" \"filename.msgpack\"")
|
||||
}
|
||||
|
||||
dbName := strings.Trim(parts[1], "\"")
|
||||
fileName := strings.Trim(parts[2], "\"")
|
||||
|
||||
// Проверяем расширение файла
|
||||
if !strings.HasSuffix(fileName, ".msgpack") {
|
||||
fileName = fileName + ".msgpack"
|
||||
}
|
||||
|
||||
return ImportData(store, dbName, fileName)
|
||||
}
|
||||
223
internal/compression/compression.go
Normal file
223
internal/compression/compression.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Файл: internal/compression/compression.go
|
||||
// Назначение: Реализация сжатия данных с использованием различных алгоритмов.
|
||||
// Поддерживаемые алгоритмы: Snappy (по умолчанию), LZ4, Zstandard.
|
||||
// Обеспечивает прозрачное сжатие/распаковку для документов.
|
||||
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/snappy"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/pierrec/lz4/v4"
|
||||
)
|
||||
|
||||
// Config представляет конфигурацию сжатия
|
||||
type Config struct {
|
||||
Enabled bool // Включено ли сжатие
|
||||
Algorithm string // Алгоритм сжатия: snappy, lz4, zstd
|
||||
Level int // Уровень сжатия (1-9)
|
||||
MinSize int // Минимальный размер для сжатия (байт)
|
||||
}
|
||||
|
||||
// MagicNumber используется для идентификации сжатых данных
|
||||
var MagicNumber = []byte{0x46, 0x54, 0x52, 0x53} // "FTRS" - Futriis
|
||||
|
||||
// CompressionType определяет тип сжатия
|
||||
type CompressionType byte
|
||||
|
||||
const (
|
||||
CompressionNone CompressionType = 0x00
|
||||
CompressionSnappy CompressionType = 0x01
|
||||
CompressionLZ4 CompressionType = 0x02
|
||||
CompressionZstd CompressionType = 0x03
|
||||
)
|
||||
|
||||
// Compress сжимает данные с использованием указанного алгоритма
|
||||
func Compress(data []byte, config *Config) ([]byte, error) {
|
||||
if !config.Enabled {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if len(data) < config.MinSize {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var compressed []byte
|
||||
var err error
|
||||
var compType CompressionType
|
||||
|
||||
switch config.Algorithm {
|
||||
case "snappy":
|
||||
compressed = snappy.Encode(nil, data)
|
||||
compType = CompressionSnappy
|
||||
case "lz4":
|
||||
buf := bytes.NewBuffer(nil)
|
||||
lz4Writer := lz4.NewWriter(buf)
|
||||
|
||||
// Установка уровня сжатия для LZ4
|
||||
if config.Level > 0 {
|
||||
// LZ4 уровни: 0-9, где 0=быстрый, 9=максимальное сжатие
|
||||
compressionLevel := lz4.CompressionLevel(config.Level)
|
||||
if err := lz4Writer.Apply(lz4.CompressionLevelOption(compressionLevel)); err != nil {
|
||||
return nil, fmt.Errorf("failed to set LZ4 compression level: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := lz4Writer.Write(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lz4Writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compressed = buf.Bytes()
|
||||
compType = CompressionLZ4
|
||||
case "zstd":
|
||||
// Для Zstandard используем предустановленные уровни скорости
|
||||
var encoder *zstd.Encoder
|
||||
var encoderLevel zstd.EncoderLevel
|
||||
|
||||
// Выбираем уровень сжатия на основе config.Level
|
||||
switch {
|
||||
case config.Level <= 1:
|
||||
encoderLevel = zstd.SpeedFastest
|
||||
case config.Level <= 3:
|
||||
encoderLevel = zstd.SpeedDefault
|
||||
case config.Level <= 6:
|
||||
encoderLevel = zstd.SpeedBetterCompression
|
||||
default:
|
||||
encoderLevel = zstd.SpeedBestCompression
|
||||
}
|
||||
|
||||
// Создаём энкодер с выбранным уровнем
|
||||
encoder, err = zstd.NewWriter(nil, zstd.WithEncoderLevel(encoderLevel))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create zstd encoder: %v", err)
|
||||
}
|
||||
defer encoder.Close()
|
||||
|
||||
compressed = encoder.EncodeAll(data, nil)
|
||||
compType = CompressionZstd
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported compression algorithm: %s", config.Algorithm)
|
||||
}
|
||||
|
||||
// Проверяем, что сжатие действительно уменьшило размер
|
||||
if len(compressed) >= len(data) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Добавляем заголовок: магическое число (4 байта) + тип сжатия (1 байт) + оригинальный размер (8 байт)
|
||||
header := make([]byte, 4+1+8)
|
||||
copy(header[0:4], MagicNumber)
|
||||
header[4] = byte(compType)
|
||||
binary.LittleEndian.PutUint64(header[5:], uint64(len(data)))
|
||||
|
||||
result := make([]byte, 0, len(header)+len(compressed))
|
||||
result = append(result, header...)
|
||||
result = append(result, compressed...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Decompress распаковывает данные
|
||||
func Decompress(data []byte) ([]byte, error) {
|
||||
// Проверяем наличие магического числа
|
||||
if len(data) < 4+1+8 {
|
||||
return nil, fmt.Errorf("data too short for compressed format")
|
||||
}
|
||||
|
||||
// Проверяем магическое число
|
||||
if !bytes.Equal(data[0:4], MagicNumber) {
|
||||
return nil, fmt.Errorf("invalid magic number")
|
||||
}
|
||||
|
||||
compType := CompressionType(data[4])
|
||||
originalSize := binary.LittleEndian.Uint64(data[5:13])
|
||||
compressedData := data[13:]
|
||||
|
||||
if originalSize == 0 {
|
||||
return nil, fmt.Errorf("invalid original size")
|
||||
}
|
||||
|
||||
var decompressed []byte
|
||||
var err error
|
||||
|
||||
switch compType {
|
||||
case CompressionSnappy:
|
||||
decompressed, err = snappy.Decode(nil, compressedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snappy decode failed: %v", err)
|
||||
}
|
||||
case CompressionLZ4:
|
||||
decompressed = make([]byte, originalSize)
|
||||
lz4Reader := lz4.NewReader(bytes.NewReader(compressedData))
|
||||
n, err := lz4Reader.Read(decompressed)
|
||||
if err != nil && err.Error() != "EOF" {
|
||||
return nil, fmt.Errorf("lz4 decode failed: %v", err)
|
||||
}
|
||||
if n != int(originalSize) {
|
||||
// Некоторые данные могли быть прочитаны, но не все
|
||||
decompressed = decompressed[:n]
|
||||
}
|
||||
case CompressionZstd:
|
||||
decoder, err := zstd.NewReader(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create zstd decoder: %v", err)
|
||||
}
|
||||
defer decoder.Close()
|
||||
|
||||
decompressed, err = decoder.DecodeAll(compressedData, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zstd decode failed: %v", err)
|
||||
}
|
||||
case CompressionNone:
|
||||
return compressedData, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported compression type: %d", compType)
|
||||
}
|
||||
|
||||
// Проверяем размер распакованных данных
|
||||
if len(decompressed) != int(originalSize) {
|
||||
// Не критично, но логируем
|
||||
_ = len(decompressed)
|
||||
}
|
||||
|
||||
return decompressed, nil
|
||||
}
|
||||
|
||||
// DecompressAuto автоматически определяет, сжаты ли данные, и распаковывает при необходимости
|
||||
func DecompressAuto(data []byte) ([]byte, error) {
|
||||
// Проверяем, есть ли магическое число (признак сжатых данных)
|
||||
if len(data) >= 4 && bytes.Equal(data[0:4], MagicNumber) {
|
||||
return Decompress(data)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// IsCompressed проверяет, сжаты ли данные
|
||||
func IsCompressed(data []byte) bool {
|
||||
if len(data) < 4 {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(data[0:4], MagicNumber)
|
||||
}
|
||||
|
||||
// GetCompressionType возвращает тип сжатия данных
|
||||
func GetCompressionType(data []byte) CompressionType {
|
||||
if !IsCompressed(data) || len(data) < 5 {
|
||||
return CompressionNone
|
||||
}
|
||||
return CompressionType(data[4])
|
||||
}
|
||||
|
||||
// GetCompressionRatio возвращает коэффициент сжатия
|
||||
func GetCompressionRatio(original, compressed []byte) float64 {
|
||||
if len(original) == 0 {
|
||||
return 1.0
|
||||
}
|
||||
return float64(len(compressed)) / float64(len(original))
|
||||
}
|
||||
99
internal/config/config.go
Normal file
99
internal/config/config.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Файл: internal/config/config.go
|
||||
// Назначение: Загрузка и парсинг TOML-конфигурации, валидация параметров,
|
||||
// предоставление доступа к настройкам кластера, хранилища и REPL.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Cluster ClusterConfig `toml:"cluster"`
|
||||
Storage StorageConfig `toml:"storage"`
|
||||
Repl ReplConfig `toml:"repl"`
|
||||
Log LogConfig `toml:"log"`
|
||||
Replication ReplicationConfig `toml:"replication"`
|
||||
Plugins PluginsConfig `toml:"plugins"`
|
||||
Compression CompressionConfig `toml:"compression"`
|
||||
}
|
||||
|
||||
type ClusterConfig struct {
|
||||
Name string `toml:"name"`
|
||||
NodeIP string `toml:"node_ip"`
|
||||
NodePort int `toml:"node_port"`
|
||||
RaftPort int `toml:"raft_port"`
|
||||
RaftDataDir string `toml:"raft_data_dir"`
|
||||
Bootstrap bool `toml:"bootstrap"` // Флаг бутстрапа кластера
|
||||
Nodes []string `toml:"nodes"` // Список узлов кластера
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
PageSizeMB int `toml:"page_size_mb"`
|
||||
MaxCollections int `toml:"max_collections"`
|
||||
MaxDocumentsPerCollection int `toml:"max_documents_per_collection"`
|
||||
}
|
||||
|
||||
type ReplConfig struct {
|
||||
PromptColor string `toml:"prompt_color"`
|
||||
HistorySize int `toml:"history_size"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
LogFile string `toml:"log_file"`
|
||||
LogLevel string `toml:"log_level"`
|
||||
}
|
||||
|
||||
type ReplicationConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MasterMaster bool `toml:"master_master"`
|
||||
SyncReplication bool `toml:"sync_replication"`
|
||||
ReplicationTimeoutMs int `toml:"replication_timeout_ms"`
|
||||
}
|
||||
|
||||
type PluginsConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
ScriptDir string `toml:"script_dir"`
|
||||
AllowList []string `toml:"allow_list"`
|
||||
}
|
||||
|
||||
type CompressionConfig struct {
|
||||
Enabled bool `toml:"enabled"` // Включено ли сжатие
|
||||
Algorithm string `toml:"algorithm"` // Алгоритм сжатия (snappy, lz4, zstd)
|
||||
Level int `toml:"level"` // Уровень сжатия (1-9, зависит от алгоритма)
|
||||
MinSize int `toml:"min_size"` // Минимальный размер для сжатия (байт)
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
var cfg Config
|
||||
if _, err := toml.DecodeFile(path, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Установка значений по умолчанию, если не указаны
|
||||
if cfg.Cluster.RaftPort == 0 {
|
||||
cfg.Cluster.RaftPort = 9878
|
||||
}
|
||||
if cfg.Cluster.RaftDataDir == "" {
|
||||
cfg.Cluster.RaftDataDir = "raft_data"
|
||||
}
|
||||
if cfg.Replication.ReplicationTimeoutMs == 0 {
|
||||
cfg.Replication.ReplicationTimeoutMs = 5000
|
||||
}
|
||||
if cfg.Plugins.ScriptDir == "" {
|
||||
cfg.Plugins.ScriptDir = "plugins"
|
||||
}
|
||||
|
||||
// Установка значений по умолчанию для сжатия
|
||||
if cfg.Compression.Algorithm == "" {
|
||||
cfg.Compression.Algorithm = "snappy"
|
||||
}
|
||||
if cfg.Compression.MinSize == 0 {
|
||||
cfg.Compression.MinSize = 1024 // 1KB - сжимаем только документы больше 1KB
|
||||
}
|
||||
if cfg.Compression.Level == 0 {
|
||||
cfg.Compression.Level = 3 // Средний уровень сжатия
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
89
internal/log/logger.go
Normal file
89
internal/log/logger.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Файл: internal/log/logger.go
|
||||
// Назначение: Асинхронная, wait-free запись логов в файл с меткой времени
|
||||
// в миллисекундах. Поддержка уровней логирования и ротации.
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogLevel int32
|
||||
|
||||
const (
|
||||
DebugLevel LogLevel = iota
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
file *os.File
|
||||
level atomic.Int32
|
||||
writeChan chan string
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewLogger(filename string, levelStr string) (*Logger, error) {
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
level := InfoLevel
|
||||
switch levelStr {
|
||||
case "debug":
|
||||
level = DebugLevel
|
||||
case "warn":
|
||||
level = WarnLevel
|
||||
case "error":
|
||||
level = ErrorLevel
|
||||
}
|
||||
|
||||
l := &Logger{
|
||||
file: file,
|
||||
writeChan: make(chan string, 10000),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
l.level.Store(int32(level))
|
||||
|
||||
// Запуск wait-free writer
|
||||
go l.writerLoop()
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l *Logger) writerLoop() {
|
||||
for msg := range l.writeChan {
|
||||
l.file.WriteString(msg + "\n")
|
||||
}
|
||||
close(l.done)
|
||||
}
|
||||
|
||||
func (l *Logger) log(level LogLevel, levelStr, msg string) {
|
||||
if level < LogLevel(l.level.Load()) {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
timestamp := now.Format("2006-01-02 15:04:05") + fmt.Sprintf(".%03d", now.Nanosecond()/1e6)
|
||||
logMsg := fmt.Sprintf("[%s] %s: %s", timestamp, levelStr, msg)
|
||||
select {
|
||||
case l.writeChan <- logMsg:
|
||||
default:
|
||||
// Неблокирующая запись, старый лог теряется - wait-free
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(msg string) { l.log(DebugLevel, "DEBUG", msg) }
|
||||
func (l *Logger) Info(msg string) { l.log(InfoLevel, "INFO", msg) }
|
||||
func (l *Logger) Warn(msg string) { l.log(WarnLevel, "WARN", msg) }
|
||||
func (l *Logger) Error(msg string) { l.log(ErrorLevel, "ERROR", msg) }
|
||||
|
||||
func (l *Logger) Close() {
|
||||
close(l.writeChan)
|
||||
<-l.done
|
||||
l.file.Close()
|
||||
}
|
||||
732
internal/plugin/plugin.go
Normal file
732
internal/plugin/plugin.go
Normal file
@@ -0,0 +1,732 @@
|
||||
// Файл: internal/plugin/plugin.go
|
||||
// Назначение: Система плагинов на основе Lua для расширения функциональности СУБД.
|
||||
// Позволяет загружать Lua-скрипты как плагины, выполнять их в изолированном окружении,
|
||||
// взаимодействовать с данными СУБД и логировать действия плагинов в общий лог-файл.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/storage"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// PluginStatus представляет состояние плагина
|
||||
type PluginStatus int32
|
||||
|
||||
const (
|
||||
StatusLoaded PluginStatus = iota
|
||||
StatusRunning
|
||||
StatusStopped
|
||||
StatusError
|
||||
)
|
||||
|
||||
// Plugin представляет загруженный Lua-плагин
|
||||
type Plugin struct {
|
||||
Name string
|
||||
FilePath string
|
||||
Status atomic.Int32
|
||||
LState *lua.LState
|
||||
logger *log.Logger
|
||||
storage *storage.Storage
|
||||
mu sync.RWMutex
|
||||
loadedAt time.Time
|
||||
version string
|
||||
author string
|
||||
description string
|
||||
}
|
||||
|
||||
// PluginManager управляет всеми загруженными плагинами
|
||||
type PluginManager struct {
|
||||
plugins sync.Map // map[string]*Plugin
|
||||
logger *log.Logger
|
||||
storage *storage.Storage
|
||||
pluginsDir string
|
||||
eventBus chan PluginEvent
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// PluginEvent представляет событие от плагина
|
||||
type PluginEvent struct {
|
||||
PluginName string
|
||||
EventType string
|
||||
Data interface{}
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
// NewPluginManager создаёт новый менеджер плагинов
|
||||
func NewPluginManager(pluginsDir string, logger *log.Logger, store *storage.Storage, enabled bool) *PluginManager {
|
||||
pm := &PluginManager{
|
||||
logger: logger,
|
||||
storage: store,
|
||||
pluginsDir: pluginsDir,
|
||||
eventBus: make(chan PluginEvent, 1000),
|
||||
enabled: enabled,
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
logger.Info("Plugin system is disabled")
|
||||
return pm
|
||||
}
|
||||
|
||||
// Запускаем обработчик событий плагинов
|
||||
go pm.eventLoop()
|
||||
|
||||
// Автоматически загружаем плагины из директории
|
||||
go pm.autoLoadPlugins()
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
// autoLoadPlugins автоматически загружает все .lua файлы из директории плагинов
|
||||
func (pm *PluginManager) autoLoadPlugins() {
|
||||
if !pm.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
<-ticker.C
|
||||
entries, err := os.ReadDir(pm.pluginsDir)
|
||||
if err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to read plugins directory: %v", err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".lua") {
|
||||
pluginName := strings.TrimSuffix(entry.Name(), ".lua")
|
||||
if _, exists := pm.plugins.Load(pluginName); !exists {
|
||||
pluginPath := filepath.Join(pm.pluginsDir, entry.Name())
|
||||
if err := pm.LoadPlugin(pluginName, pluginPath); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Failed to auto-load plugin %s: %v", pluginName, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPlugin загружает Lua-плагин из файла
|
||||
func (pm *PluginManager) LoadPlugin(name, filePath string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
// Читаем файл плагина
|
||||
script, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read plugin file: %v", err)
|
||||
}
|
||||
|
||||
// Создаём новое Lua-состояние
|
||||
L := lua.NewState()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
L.Close()
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Plugin %s panic: %v", name, r))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Открываем стандартные библиотеки Lua
|
||||
lua.OpenBase(L)
|
||||
lua.OpenString(L)
|
||||
lua.OpenTable(L)
|
||||
lua.OpenMath(L)
|
||||
|
||||
// Регистрируем функции СУБД для Lua
|
||||
pm.registerDatabaseFunctions(L)
|
||||
|
||||
// Выполняем скрипт
|
||||
if err := L.DoString(string(script)); err != nil {
|
||||
L.Close()
|
||||
return fmt.Errorf("failed to execute plugin script: %v", err)
|
||||
}
|
||||
|
||||
// Извлекаем метаданные плагина
|
||||
version := pm.getPluginMetadata(L, "version")
|
||||
author := pm.getPluginMetadata(L, "author")
|
||||
description := pm.getPluginMetadata(L, "description")
|
||||
|
||||
// Создаём объект плагина
|
||||
plugin := &Plugin{
|
||||
Name: name,
|
||||
FilePath: filePath,
|
||||
LState: L,
|
||||
logger: pm.logger,
|
||||
storage: pm.storage,
|
||||
loadedAt: time.Now(),
|
||||
version: version,
|
||||
author: author,
|
||||
description: description,
|
||||
}
|
||||
plugin.Status.Store(int32(StatusLoaded))
|
||||
|
||||
// Сохраняем плагин
|
||||
pm.plugins.Store(name, plugin)
|
||||
|
||||
// Логируем загрузку
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin loaded: %s v%s by %s - %s", name, version, author, description))
|
||||
}
|
||||
|
||||
// Вызываем функцию инициализации плагина, если она есть
|
||||
if err := pm.callPluginFunction(plugin, "on_load"); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_load error: %v", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerDatabaseFunctions регистрирует функции доступа к СУБД в Lua
|
||||
func (pm *PluginManager) registerDatabaseFunctions(L *lua.LState) {
|
||||
// Регистрируем функцию для получения базы данных
|
||||
L.SetGlobal("get_database", L.NewFunction(func(L *lua.LState) int {
|
||||
dbName := L.CheckString(1)
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Создаём пользовательский тип для базы данных
|
||||
ud := L.NewUserData()
|
||||
ud.Value = db
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("database"))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
|
||||
// Регистрируем функцию для получения коллекции
|
||||
L.SetGlobal("get_collection", L.NewFunction(func(L *lua.LState) int {
|
||||
dbName := L.CheckString(1)
|
||||
collName := L.CheckString(2)
|
||||
|
||||
db, err := pm.storage.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
ud := L.NewUserData()
|
||||
ud.Value = coll
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("collection"))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
|
||||
// Регистрируем функцию логирования для плагинов
|
||||
L.SetGlobal("plugin_log", L.NewFunction(func(L *lua.LState) int {
|
||||
level := L.CheckString(1)
|
||||
message := L.CheckString(2)
|
||||
|
||||
if pm.logger != nil {
|
||||
logMsg := fmt.Sprintf("[PLUGIN] %s: %s", level, message)
|
||||
switch level {
|
||||
case "debug":
|
||||
pm.logger.Debug(logMsg)
|
||||
case "info":
|
||||
pm.logger.Info(logMsg)
|
||||
case "warn":
|
||||
pm.logger.Warn(logMsg)
|
||||
case "error":
|
||||
pm.logger.Error(logMsg)
|
||||
default:
|
||||
pm.logger.Info(logMsg)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}))
|
||||
|
||||
// Регистрируем функцию для отправки событий
|
||||
L.SetGlobal("emit_event", L.NewFunction(func(L *lua.LState) int {
|
||||
eventType := L.CheckString(1)
|
||||
eventData := L.CheckAny(2)
|
||||
|
||||
// Получаем имя плагина из контекста (нужно передавать при вызове)
|
||||
event := PluginEvent{
|
||||
EventType: eventType,
|
||||
Data: pm.luaValueToGo(eventData),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
select {
|
||||
case pm.eventBus <- event:
|
||||
default:
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn("Plugin event bus full, event dropped")
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}))
|
||||
|
||||
// Устанавливаем метатаблицы для методов баз данных и коллекций
|
||||
pm.setupDatabaseMetatable(L)
|
||||
pm.setupCollectionMetatable(L)
|
||||
}
|
||||
|
||||
// setupDatabaseMetatable настраивает методы для объекта базы данных в Lua
|
||||
func (pm *PluginManager) setupDatabaseMetatable(L *lua.LState) {
|
||||
mt := L.NewTypeMetatable("database")
|
||||
L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||
db := L.CheckUserData(1).Value.(*storage.Database)
|
||||
method := L.CheckString(2)
|
||||
|
||||
switch method {
|
||||
case "create_collection":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
err := db.CreateCollection(name)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "get_collection":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
coll, err := db.GetCollection(name)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
ud := L.NewUserData()
|
||||
ud.Value = coll
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("collection"))
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
case "name":
|
||||
L.Push(lua.LString(db.Name()))
|
||||
default:
|
||||
L.Push(lua.LNil)
|
||||
}
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
|
||||
// setupCollectionMetatable настраивает методы для объекта коллекции в Lua
|
||||
func (pm *PluginManager) setupCollectionMetatable(L *lua.LState) {
|
||||
mt := L.NewTypeMetatable("collection")
|
||||
L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||
coll := L.CheckUserData(1).Value.(*storage.Collection)
|
||||
method := L.CheckString(2)
|
||||
|
||||
switch method {
|
||||
case "insert":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
doc := L.CheckTable(1)
|
||||
// Конвертируем Lua table в map
|
||||
fields := make(map[string]interface{})
|
||||
doc.ForEach(func(key, value lua.LValue) {
|
||||
if key.Type() == lua.LTString {
|
||||
fields[key.String()] = pm.luaValueToGo(value)
|
||||
}
|
||||
})
|
||||
|
||||
// Вставляем документ
|
||||
err := coll.InsertFromMap(fields)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "find":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
id := L.CheckString(1)
|
||||
doc, err := coll.Find(id)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
// Конвертируем документ в Lua table
|
||||
table := L.NewTable()
|
||||
for k, v := range doc.GetFields() {
|
||||
table.RawSetString(k, pm.goValueToLua(L, v))
|
||||
}
|
||||
L.Push(table)
|
||||
return 1
|
||||
}))
|
||||
case "update":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
id := L.CheckString(1)
|
||||
updates := L.CheckTable(2)
|
||||
|
||||
fields := make(map[string]interface{})
|
||||
updates.ForEach(func(key, value lua.LValue) {
|
||||
if key.Type() == lua.LTString {
|
||||
fields[key.String()] = pm.luaValueToGo(value)
|
||||
}
|
||||
})
|
||||
|
||||
err := coll.Update(id, fields)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "delete":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
id := L.CheckString(1)
|
||||
err := coll.Delete(id)
|
||||
if err != nil {
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}))
|
||||
case "count":
|
||||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||
count := coll.Count()
|
||||
L.Push(lua.LNumber(count))
|
||||
return 1
|
||||
}))
|
||||
default:
|
||||
L.Push(lua.LNil)
|
||||
}
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
|
||||
// luaValueToGo конвертирует Lua-значение в Go-значение
|
||||
func (pm *PluginManager) luaValueToGo(val lua.LValue) interface{} {
|
||||
if val == nil || val == lua.LNil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case lua.LString:
|
||||
return string(v)
|
||||
case lua.LNumber:
|
||||
return float64(v)
|
||||
case lua.LBool:
|
||||
return bool(v)
|
||||
case *lua.LTable:
|
||||
result := make(map[string]interface{})
|
||||
v.ForEach(func(key, value lua.LValue) {
|
||||
keyStr := "unknown"
|
||||
if key.Type() == lua.LTString {
|
||||
keyStr = key.String()
|
||||
} else if key.Type() == lua.LTNumber {
|
||||
keyStr = fmt.Sprintf("%d", int64(key.(lua.LNumber)))
|
||||
}
|
||||
result[keyStr] = pm.luaValueToGo(value)
|
||||
})
|
||||
return result
|
||||
default:
|
||||
return v.String()
|
||||
}
|
||||
}
|
||||
|
||||
// goValueToLua конвертирует Go-значение в Lua-значение
|
||||
func (pm *PluginManager) goValueToLua(L *lua.LState, val interface{}) lua.LValue {
|
||||
if val == nil {
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
return lua.LString(v)
|
||||
case int:
|
||||
return lua.LNumber(float64(v))
|
||||
case int64:
|
||||
return lua.LNumber(float64(v))
|
||||
case float32:
|
||||
return lua.LNumber(float64(v))
|
||||
case float64:
|
||||
return lua.LNumber(v)
|
||||
case bool:
|
||||
return lua.LBool(v)
|
||||
case map[string]interface{}:
|
||||
table := L.NewTable()
|
||||
for k, val := range v {
|
||||
table.RawSetString(k, pm.goValueToLua(L, val))
|
||||
}
|
||||
return table
|
||||
case []interface{}:
|
||||
table := L.NewTable()
|
||||
for i, val := range v {
|
||||
table.RawSetInt(i+1, pm.goValueToLua(L, val))
|
||||
}
|
||||
return table
|
||||
default:
|
||||
return lua.LString(fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// getPluginMetadata извлекает метаданные из загруженного Lua-скрипта
|
||||
func (pm *PluginManager) getPluginMetadata(L *lua.LState, field string) string {
|
||||
// Пытаемся получить глобальную переменную с метаданными
|
||||
val := L.GetGlobal(field)
|
||||
if str, ok := val.(lua.LString); ok {
|
||||
return string(str)
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// callPluginFunction вызывает функцию плагина по имени
|
||||
func (pm *PluginManager) callPluginFunction(plugin *Plugin, funcName string) error {
|
||||
plugin.mu.RLock()
|
||||
defer plugin.mu.RUnlock()
|
||||
|
||||
L := plugin.LState
|
||||
fn := L.GetGlobal(funcName)
|
||||
if fn == lua.LNil {
|
||||
return nil // Функция не определена
|
||||
}
|
||||
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to call %s: %v", funcName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventLoop обрабатывает события от плагинов
|
||||
func (pm *PluginManager) eventLoop() {
|
||||
for event := range pm.eventBus {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Debug(fmt.Sprintf("Plugin event [%s]: %+v", event.EventType, event.Data))
|
||||
}
|
||||
|
||||
// Можно реализовать подписку плагинов на события
|
||||
pm.plugins.Range(func(key, value interface{}) bool {
|
||||
plugin := value.(*Plugin)
|
||||
// Асинхронно уведомляем плагины о событии
|
||||
go pm.notifyPlugin(plugin, event)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// notifyPlugin уведомляет конкретный плагин о событии
|
||||
func (pm *PluginManager) notifyPlugin(plugin *Plugin, event PluginEvent) {
|
||||
plugin.mu.RLock()
|
||||
defer plugin.mu.RUnlock()
|
||||
|
||||
L := plugin.LState
|
||||
if L == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fn := L.GetGlobal("on_event")
|
||||
if fn == lua.LNil {
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем имя плагина в событие
|
||||
event.PluginName = plugin.Name
|
||||
|
||||
// Создаём таблицу с данными события
|
||||
eventTable := L.NewTable()
|
||||
eventTable.RawSetString("type", lua.LString(event.EventType))
|
||||
eventTable.RawSetString("plugin_name", lua.LString(event.PluginName))
|
||||
eventTable.RawSetString("timestamp", lua.LNumber(event.Timestamp))
|
||||
eventTable.RawSetString("data", pm.goValueToLua(L, event.Data))
|
||||
|
||||
L.SetGlobal("event", eventTable)
|
||||
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Error(fmt.Sprintf("Plugin %s on_event error: %v", plugin.Name, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecutePlugin выполняет пользовательскую функцию плагина
|
||||
func (pm *PluginManager) ExecutePlugin(pluginName, funcName string, args ...interface{}) (interface{}, error) {
|
||||
if !pm.enabled {
|
||||
return nil, fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(pluginName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("plugin not found: %s", pluginName)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
if PluginStatus(plugin.Status.Load()) != StatusRunning {
|
||||
return nil, fmt.Errorf("plugin %s is not running", pluginName)
|
||||
}
|
||||
|
||||
plugin.mu.RLock()
|
||||
defer plugin.mu.RUnlock()
|
||||
|
||||
L := plugin.LState
|
||||
if L == nil {
|
||||
return nil, fmt.Errorf("plugin %s has no Lua state", pluginName)
|
||||
}
|
||||
|
||||
fn := L.GetGlobal(funcName)
|
||||
if fn == lua.LNil {
|
||||
return nil, fmt.Errorf("function %s not found in plugin %s", funcName, pluginName)
|
||||
}
|
||||
|
||||
// Подготавливаем аргументы для вызова
|
||||
luaArgs := make([]lua.LValue, len(args))
|
||||
for i, arg := range args {
|
||||
luaArgs[i] = pm.goValueToLua(L, arg)
|
||||
}
|
||||
|
||||
// Вызываем функцию
|
||||
if err := L.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, luaArgs...); err != nil {
|
||||
return nil, fmt.Errorf("plugin execution failed: %v", err)
|
||||
}
|
||||
|
||||
ret := L.Get(-1)
|
||||
L.Pop(1)
|
||||
|
||||
return pm.luaValueToGo(ret), nil
|
||||
}
|
||||
|
||||
// UnloadPlugin выгружает плагин
|
||||
func (pm *PluginManager) UnloadPlugin(name string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin not found: %s", name)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
|
||||
// Вызываем функцию выгрузки
|
||||
if err := pm.callPluginFunction(plugin, "on_unload"); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_unload error: %v", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Закрываем Lua-состояние
|
||||
if plugin.LState != nil {
|
||||
plugin.LState.Close()
|
||||
}
|
||||
plugin.Status.Store(int32(StatusStopped))
|
||||
|
||||
pm.plugins.Delete(name)
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin unloaded: %s", name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPlugin запускает плагин
|
||||
func (pm *PluginManager) StartPlugin(name string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin not found: %s", name)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
plugin.Status.Store(int32(StatusRunning))
|
||||
|
||||
if err := pm.callPluginFunction(plugin, "on_start"); err != nil {
|
||||
plugin.Status.Store(int32(StatusError))
|
||||
return fmt.Errorf("failed to start plugin: %v", err)
|
||||
}
|
||||
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin started: %s", name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopPlugin останавливает плагин
|
||||
func (pm *PluginManager) StopPlugin(name string) error {
|
||||
if !pm.enabled {
|
||||
return fmt.Errorf("plugin system is disabled")
|
||||
}
|
||||
|
||||
val, ok := pm.plugins.Load(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin not found: %s", name)
|
||||
}
|
||||
|
||||
plugin := val.(*Plugin)
|
||||
|
||||
if err := pm.callPluginFunction(plugin, "on_stop"); err != nil {
|
||||
if pm.logger != nil {
|
||||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_stop error: %v", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
plugin.Status.Store(int32(StatusStopped))
|
||||
if pm.logger != nil {
|
||||
pm.logger.Info(fmt.Sprintf("Plugin stopped: %s", name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListPlugins возвращает список всех загруженных плагинов
|
||||
func (pm *PluginManager) ListPlugins() []*Plugin {
|
||||
plugins := make([]*Plugin, 0)
|
||||
pm.plugins.Range(func(key, value interface{}) bool {
|
||||
plugins = append(plugins, value.(*Plugin))
|
||||
return true
|
||||
})
|
||||
return plugins
|
||||
}
|
||||
|
||||
// IsEnabled возвращает статус системы плагинов
|
||||
func (pm *PluginManager) IsEnabled() bool {
|
||||
return pm.enabled
|
||||
}
|
||||
96
internal/repl/history.go
Normal file
96
internal/repl/history.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Файл: internal/repl/history.go
|
||||
// Назначение: Управление историей команд REPL
|
||||
|
||||
package repl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// History управляет историей команд
|
||||
type History struct {
|
||||
entries []string
|
||||
maxSize int
|
||||
filePath string
|
||||
}
|
||||
|
||||
// NewHistory создаёт новый объект истории
|
||||
func NewHistory(maxSize int) *History {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
filePath := filepath.Join(homeDir, ".futriis_history")
|
||||
|
||||
return &History{
|
||||
entries: make([]string, 0, maxSize),
|
||||
maxSize: maxSize,
|
||||
filePath: filePath,
|
||||
}
|
||||
}
|
||||
|
||||
// Add добавляет команду в историю
|
||||
func (h *History) Add(cmd string) error {
|
||||
// Не добавляем дубликаты подряд
|
||||
if len(h.entries) > 0 && h.entries[len(h.entries)-1] == cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
h.entries = append(h.entries, cmd)
|
||||
|
||||
// Ограничиваем размер истории
|
||||
if len(h.entries) > h.maxSize {
|
||||
h.entries = h.entries[len(h.entries)-h.maxSize:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load загружает историю из файла
|
||||
func (h *History) Load() error {
|
||||
file, err := os.Open(h.filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
cmd := scanner.Text()
|
||||
if cmd != "" {
|
||||
h.entries = append(h.entries, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// Ограничиваем размер
|
||||
if len(h.entries) > h.maxSize {
|
||||
h.entries = h.entries[len(h.entries)-h.maxSize:]
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// Save сохраняет историю в файл
|
||||
func (h *History) Save() error {
|
||||
file, err := os.Create(h.filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
writer := bufio.NewWriter(file)
|
||||
for _, cmd := range h.entries {
|
||||
if _, err := writer.WriteString(cmd + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
// GetEntries возвращает все записи истории
|
||||
func (h *History) GetEntries() []string {
|
||||
return h.entries
|
||||
}
|
||||
1326
internal/repl/repl.go
Normal file
1326
internal/repl/repl.go
Normal file
File diff suppressed because it is too large
Load Diff
17
internal/serializer/msgpack.go
Normal file
17
internal/serializer/msgpack.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Файл: internal/serializer/msgpack.go
|
||||
// Назначение: Сериализация и десериализация документов в формате MessagePack.
|
||||
// Используется библиотека vmihailenco/msgpack для высокой производительности.
|
||||
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
func Marshal(v interface{}) ([]byte, error) {
|
||||
return msgpack.Marshal(v)
|
||||
}
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
return msgpack.Unmarshal(data, v)
|
||||
}
|
||||
116
internal/storage/audit.go
Normal file
116
internal/storage/audit.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Файл: internal/storage/audit.go
|
||||
// Назначение: Аудит всех операций создания, изменения, удаления данных
|
||||
// с записью временной метки с точностью до миллисекунды
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditEntry представляет запись аудита
|
||||
type AuditEntry struct {
|
||||
ID string `msgpack:"id"`
|
||||
Timestamp int64 `msgpack:"timestamp"` // Unix миллисекунды
|
||||
TimestampStr string `msgpack:"timestamp_str"` // Человекочитаемая строка
|
||||
Operation string `msgpack:"operation"` // CREATE, UPDATE, DELETE, START, COMMIT, ABORT, CLUSTER
|
||||
DataType string `msgpack:"data_type"` // DATABASE, COLLECTION, DOCUMENT, FIELD, TUPLE, SESSION, TRANSACTION, CLUSTER
|
||||
Name string `msgpack:"name"` // Имя объекта
|
||||
Details map[string]interface{} `msgpack:"details"` // Детали операции
|
||||
}
|
||||
|
||||
// AuditLogger управляет аудитом
|
||||
type AuditLogger struct {
|
||||
entries []AuditEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var globalAuditLogger = &AuditLogger{
|
||||
entries: make([]AuditEntry, 0),
|
||||
}
|
||||
|
||||
// GetCurrentTimestamp возвращает текущую временную метку с миллисекундами
|
||||
func GetCurrentTimestamp() (int64, string) {
|
||||
now := time.Now()
|
||||
timestampMs := now.UnixMilli()
|
||||
timestampStr := now.Format("2006-01-02 15:04:05.000")
|
||||
return timestampMs, timestampStr
|
||||
}
|
||||
|
||||
// LogAudit записывает событие в аудит
|
||||
func LogAudit(operation, dataType, name string, details map[string]interface{}) {
|
||||
timestampMs, timestampStr := GetCurrentTimestamp()
|
||||
|
||||
entry := AuditEntry{
|
||||
ID: fmt.Sprintf("%d", timestampMs),
|
||||
Timestamp: timestampMs,
|
||||
TimestampStr: timestampStr,
|
||||
Operation: operation,
|
||||
DataType: dataType,
|
||||
Name: name,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
globalAuditLogger.mu.Lock()
|
||||
globalAuditLogger.entries = append(globalAuditLogger.entries, entry)
|
||||
globalAuditLogger.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetAuditLog возвращает копию лога аудита
|
||||
func GetAuditLog() []AuditEntry {
|
||||
globalAuditLogger.mu.RLock()
|
||||
defer globalAuditLogger.mu.RUnlock()
|
||||
|
||||
result := make([]AuditEntry, len(globalAuditLogger.entries))
|
||||
copy(result, globalAuditLogger.entries)
|
||||
return result
|
||||
}
|
||||
|
||||
// AuditDatabaseOperation логирует операцию с базой данных
|
||||
func AuditDatabaseOperation(operation, dbName string) {
|
||||
LogAudit(operation, "DATABASE", dbName, map[string]interface{}{
|
||||
"database": dbName,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditCollectionOperation логирует операцию с коллекцией
|
||||
func AuditCollectionOperation(operation, dbName, collName string, settings interface{}) {
|
||||
LogAudit(operation, "COLLECTION", fmt.Sprintf("%s.%s", dbName, collName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditDocumentOperation логирует операцию с документом
|
||||
func AuditDocumentOperation(operation, dbName, collName, docID string, fields map[string]interface{}) {
|
||||
LogAudit(operation, "DOCUMENT", fmt.Sprintf("%s.%s.%s", dbName, collName, docID), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"fields": fields,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditFieldOperation логирует операцию с полем
|
||||
func AuditFieldOperation(operation, dbName, collName, docID, fieldName string, value interface{}) {
|
||||
LogAudit(operation, "FIELD", fmt.Sprintf("%s.%s.%s.%s", dbName, collName, docID, fieldName), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"field": fieldName,
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
|
||||
// AuditTupleOperation логирует операцию с кортежем
|
||||
func AuditTupleOperation(operation, dbName, collName, docID, tuplePath string) {
|
||||
LogAudit(operation, "TUPLE", fmt.Sprintf("%s.%s.%s.%s", dbName, collName, docID, tuplePath), map[string]interface{}{
|
||||
"database": dbName,
|
||||
"collection": collName,
|
||||
"document_id": docID,
|
||||
"tuple_path": tuplePath,
|
||||
})
|
||||
}
|
||||
736
internal/storage/collection.go
Normal file
736
internal/storage/collection.go
Normal file
@@ -0,0 +1,736 @@
|
||||
// Файл: internal/storage/collection.go
|
||||
// Назначение: Реализация коллекции с индексами (первичными и вторичными).
|
||||
// Индексы хранятся отдельно от документов, обеспечивают wait-free доступ.
|
||||
// Исправлено: корректная работа уникальных индексов, удаление из индексов при обновлении.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// Collection представляет коллекцию документов (аналог таблицы)
|
||||
type Collection struct {
|
||||
name string
|
||||
docs sync.Map // map[string]*Document - wait-free хранилище документов
|
||||
indexes sync.Map // map[string]*Index - индексы для быстрого поиска
|
||||
metadata *CollectionMetadata // Метаданные коллекции
|
||||
docCount atomic.Int64 // Атомарный счётчик документов
|
||||
sizeBytes atomic.Int64 // Атомарный размер коллекции в байтах
|
||||
mu sync.RWMutex // Для операций, изменяющих структуру коллекции
|
||||
constraints *Constraints // Ограничения коллекции
|
||||
acl *CollectionACL // ACL для коллекции
|
||||
}
|
||||
|
||||
// CollectionMetadata содержит метаданные коллекции
|
||||
type CollectionMetadata struct {
|
||||
Name string `msgpack:"name"`
|
||||
CreatedAt int64 `msgpack:"created_at"`
|
||||
UpdatedAt int64 `msgpack:"updated_at"`
|
||||
DocumentCount int64 `msgpack:"document_count"`
|
||||
SizeBytes int64 `msgpack:"size_bytes"`
|
||||
IndexCount int `msgpack:"index_count"`
|
||||
Settings *CollectionSettings `msgpack:"settings"`
|
||||
}
|
||||
|
||||
// CollectionSettings содержит настройки коллекции
|
||||
type CollectionSettings struct {
|
||||
MaxDocuments int `msgpack:"max_documents"` // Максимальное количество документов (0 = безлимит)
|
||||
ValidateSchema bool `msgpack:"validate_schema"` // Валидировать схему документов
|
||||
AutoIndexID bool `msgpack:"auto_index_id"` // Автоматически индексировать поле _id
|
||||
TTLSeconds int `msgpack:"ttl_seconds"` // Время жизни документов (0 = бессрочно)
|
||||
}
|
||||
|
||||
// Index представляет индекс для ускорения поиска (хранится отдельно от документов)
|
||||
type Index struct {
|
||||
Name string `msgpack:"name"`
|
||||
Fields []string `msgpack:"fields"` // Поля для индексации
|
||||
Unique bool `msgpack:"unique"` // Уникальный индекс
|
||||
data sync.Map // map[interface{}]string - значение индекса -> ID документа
|
||||
}
|
||||
|
||||
// Constraints представляет ограничения на коллекцию
|
||||
type Constraints struct {
|
||||
mu sync.RWMutex
|
||||
RequiredFields map[string]bool // Обязательные поля
|
||||
UniqueFields map[string]bool // Уникальные поля (дополнительно к индексам)
|
||||
MinValues map[string]float64 // Минимальные значения для числовых полей
|
||||
MaxValues map[string]float64 // Максимальные значения для числовых полей
|
||||
PatternFields map[string]string // Regexp паттерны для полей
|
||||
EnumFields map[string][]interface{} // Допустимые значения для полей
|
||||
}
|
||||
|
||||
// CollectionACL представляет список контроля доступа для коллекции
|
||||
type CollectionACL struct {
|
||||
mu sync.RWMutex
|
||||
ReadRoles map[string]bool // Роли, имеющие доступ на чтение
|
||||
WriteRoles map[string]bool // Роли, имеющие доступ на запись
|
||||
DeleteRoles map[string]bool // Роли, имеющие доступ на удаление
|
||||
AdminRoles map[string]bool // Роли, имеющие полный доступ
|
||||
}
|
||||
|
||||
// NewCollection создаёт новую коллекцию
|
||||
func NewCollection(name string, settings *CollectionSettings) *Collection {
|
||||
if settings == nil {
|
||||
settings = &CollectionSettings{
|
||||
MaxDocuments: 0,
|
||||
ValidateSchema: false,
|
||||
AutoIndexID: true,
|
||||
TTLSeconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
coll := &Collection{
|
||||
name: name,
|
||||
metadata: &CollectionMetadata{
|
||||
Name: name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DocumentCount: 0,
|
||||
SizeBytes: 0,
|
||||
IndexCount: 0,
|
||||
Settings: settings,
|
||||
},
|
||||
constraints: &Constraints{
|
||||
RequiredFields: make(map[string]bool),
|
||||
UniqueFields: make(map[string]bool),
|
||||
MinValues: make(map[string]float64),
|
||||
MaxValues: make(map[string]float64),
|
||||
PatternFields: make(map[string]string),
|
||||
EnumFields: make(map[string][]interface{}),
|
||||
},
|
||||
acl: &CollectionACL{
|
||||
ReadRoles: make(map[string]bool),
|
||||
WriteRoles: make(map[string]bool),
|
||||
DeleteRoles: make(map[string]bool),
|
||||
AdminRoles: make(map[string]bool),
|
||||
},
|
||||
}
|
||||
|
||||
// Автоматически создаём первичный индекс по _id
|
||||
if settings.AutoIndexID {
|
||||
coll.CreateIndex("_id_", []string{"_id"}, true)
|
||||
}
|
||||
|
||||
// Запускаем фоновую задачу для удаления просроченных документов
|
||||
if settings.TTLSeconds > 0 {
|
||||
go coll.ttlCleanupLoop()
|
||||
}
|
||||
|
||||
return coll
|
||||
}
|
||||
|
||||
// Name возвращает имя коллекции
|
||||
func (c *Collection) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// Insert вставляет документ в коллекцию (wait-free)
|
||||
func (c *Collection) Insert(doc *Document) error {
|
||||
// Проверка ограничений
|
||||
if err := c.constraints.ValidateDocument(doc); err != nil {
|
||||
return fmt.Errorf("constraint violation: %v", err)
|
||||
}
|
||||
|
||||
// Проверка ACL (будет вызвано из верхнего уровня с ролью)
|
||||
|
||||
// Проверка на максимальное количество документов
|
||||
if c.metadata.Settings.MaxDocuments > 0 {
|
||||
if c.docCount.Load() >= int64(c.metadata.Settings.MaxDocuments) {
|
||||
return fmt.Errorf("collection is full: max documents %d reached", c.metadata.Settings.MaxDocuments)
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация схемы (если включена)
|
||||
if c.metadata.Settings.ValidateSchema {
|
||||
if err := c.validateDocument(doc); err != nil {
|
||||
return fmt.Errorf("document validation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка уникальных индексов
|
||||
if err := c.checkUniqueConstraints(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Сериализация для проверки (опционально)
|
||||
data, err := serializer.Marshal(doc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize document: %v", err)
|
||||
}
|
||||
|
||||
// Атомарное сохранение документа
|
||||
if _, loaded := c.docs.LoadOrStore(doc.ID, doc); loaded {
|
||||
return fmt.Errorf("document with id %s already exists", doc.ID)
|
||||
}
|
||||
|
||||
// Обновление индексов (wait-free)
|
||||
c.updateIndexes(doc, true)
|
||||
|
||||
// Обновление метаданных
|
||||
c.docCount.Add(1)
|
||||
c.sizeBytes.Add(int64(len(data)))
|
||||
c.metadata.DocumentCount = c.docCount.Load()
|
||||
c.metadata.SizeBytes = c.sizeBytes.Load()
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertFromMap создаёт и вставляет документ из map
|
||||
func (c *Collection) InsertFromMap(fields map[string]interface{}) error {
|
||||
doc := NewDocument()
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
return c.Insert(doc)
|
||||
}
|
||||
|
||||
// Find находит документ по ID (с использованием первичного индекса)
|
||||
func (c *Collection) Find(id string) (*Document, error) {
|
||||
if val, ok := c.docs.Load(id); ok {
|
||||
doc := val.(*Document)
|
||||
// Проверяем, не истёк ли TTL
|
||||
if c.metadata.Settings.TTLSeconds > 0 {
|
||||
if time.Now().UnixMilli()-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
|
||||
c.Delete(id) // Автоматически удаляем просроченный документ
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
// FindByIndex находит документы по значению индексированного поля
|
||||
// Исправлено: корректный поиск для неуникальных индексов
|
||||
func (c *Collection) FindByIndex(indexName string, value interface{}) ([]*Document, error) {
|
||||
idxVal, ok := c.indexes.Load(indexName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("index not found: %s", indexName)
|
||||
}
|
||||
|
||||
index := idxVal.(*Index)
|
||||
docs := make([]*Document, 0)
|
||||
|
||||
if index.Unique {
|
||||
// Уникальный индекс возвращает один документ
|
||||
if docID, ok := index.data.Load(value); ok {
|
||||
if doc, err := c.Find(docID.(string)); err == nil {
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Исправлено: для неуникального индекса нужно найти все документы с данным значением
|
||||
index.data.Range(func(key, val interface{}) bool {
|
||||
// key - значение индекса, val - ID документа
|
||||
if fmt.Sprintf("%v", key) == fmt.Sprintf("%v", value) {
|
||||
if doc, err := c.Find(val.(string)); err == nil {
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// FindByIndexPrefix находит документы по префиксу индекса (для строковых полей)
|
||||
func (c *Collection) FindByIndexPrefix(indexName string, prefix string) ([]*Document, error) {
|
||||
idxVal, ok := c.indexes.Load(indexName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("index not found: %s", indexName)
|
||||
}
|
||||
|
||||
index := idxVal.(*Index)
|
||||
docs := make([]*Document, 0)
|
||||
|
||||
index.data.Range(func(key, val interface{}) bool {
|
||||
if keyStr, ok := key.(string); ok {
|
||||
if strings.HasPrefix(keyStr, prefix) {
|
||||
if doc, err := c.Find(val.(string)); err == nil {
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// Update обновляет документ по ID
|
||||
// Исправлено: корректное обновление индексов при изменении индексированных полей
|
||||
func (c *Collection) Update(id string, updates map[string]interface{}) error {
|
||||
val, ok := c.docs.Load(id)
|
||||
if !ok {
|
||||
return fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
oldDoc := val.(*Document)
|
||||
|
||||
// Создаём копию для проверки уникальности
|
||||
newDoc := oldDoc.Clone()
|
||||
if err := newDoc.Update(updates); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверяем ограничения
|
||||
if err := c.constraints.ValidateDocument(newDoc); err != nil {
|
||||
return fmt.Errorf("constraint violation: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем уникальные индексы
|
||||
if err := c.checkUniqueConstraintsUpdate(oldDoc, newDoc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Исправлено: сначала удаляем старые индексы, потом добавляем новые
|
||||
c.removeFromIndexes(oldDoc)
|
||||
c.addToIndexes(newDoc)
|
||||
|
||||
// Сохраняем обновлённый документ
|
||||
c.docs.Store(id, newDoc)
|
||||
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete удаляет документ по ID
|
||||
// Исправлено: удаление из всех индексов перед удалением документа
|
||||
func (c *Collection) Delete(id string) error {
|
||||
val, ok := c.docs.Load(id)
|
||||
if !ok {
|
||||
return fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
doc := val.(*Document)
|
||||
|
||||
// Удаляем из индексов
|
||||
c.removeFromIndexes(doc)
|
||||
|
||||
// Удаляем документ
|
||||
c.docs.Delete(id)
|
||||
|
||||
// Обновляем метаданные
|
||||
c.docCount.Add(-1)
|
||||
// Размер не обновляем для простоты (можно пересчитать при необходимости)
|
||||
c.metadata.DocumentCount = c.docCount.Load()
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeFromIndexes удаляет документ из всех индексов (wait-free)
|
||||
func (c *Collection) removeFromIndexes(doc *Document) {
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
indexValue := c.extractIndexValue(doc, index.Fields)
|
||||
index.data.Delete(indexValue)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// addToIndexes добавляет документ во все индексы (wait-free)
|
||||
func (c *Collection) addToIndexes(doc *Document) {
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
indexValue := c.extractIndexValue(doc, index.Fields)
|
||||
if index.Unique {
|
||||
index.data.LoadOrStore(indexValue, doc.ID)
|
||||
} else {
|
||||
index.data.Store(indexValue, doc.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// CreateIndex создаёт новый индекс на коллекции (wait-free friendly)
|
||||
func (c *Collection) CreateIndex(name string, fields []string, unique bool) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if _, exists := c.indexes.Load(name); exists {
|
||||
return fmt.Errorf("index %s already exists", name)
|
||||
}
|
||||
|
||||
index := &Index{
|
||||
Name: name,
|
||||
Fields: fields,
|
||||
Unique: unique,
|
||||
}
|
||||
|
||||
// Строим индекс на существующих документах (wait-free)
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
doc := value.(*Document)
|
||||
indexValue := c.extractIndexValue(doc, fields)
|
||||
if unique {
|
||||
if _, loaded := index.data.LoadOrStore(indexValue, doc.ID); loaded {
|
||||
// Найден дубликат - откатываем создание индекса
|
||||
c.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
index.data.Store(indexValue, doc.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
c.indexes.Store(name, index)
|
||||
c.metadata.IndexCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropIndex удаляет индекс
|
||||
func (c *Collection) DropIndex(name string) error {
|
||||
if _, exists := c.indexes.LoadAndDelete(name); !exists {
|
||||
return fmt.Errorf("index not found: %s", name)
|
||||
}
|
||||
c.metadata.IndexCount--
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIndexes возвращает список всех индексов
|
||||
func (c *Collection) GetIndexes() []string {
|
||||
names := make([]string, 0)
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
names = append(names, key.(string))
|
||||
return true
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
// extractIndexValue извлекает значение из документа для индексации
|
||||
func (c *Collection) extractIndexValue(doc *Document, fields []string) interface{} {
|
||||
if len(fields) == 1 {
|
||||
val, _ := doc.GetField(fields[0])
|
||||
return val
|
||||
}
|
||||
|
||||
// Составной индекс - возвращаем строковое представление
|
||||
parts := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
parts = append(parts, fmt.Sprintf("%v", val))
|
||||
} else {
|
||||
parts = append(parts, "NULL")
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
// updateIndexes обновляет индексы для документа (исправлено: используем отдельные методы)
|
||||
func (c *Collection) updateIndexes(doc *Document, add bool) {
|
||||
if add {
|
||||
c.addToIndexes(doc)
|
||||
} else {
|
||||
c.removeFromIndexes(doc)
|
||||
}
|
||||
}
|
||||
|
||||
// checkUniqueConstraints проверяет уникальные индексы перед вставкой
|
||||
func (c *Collection) checkUniqueConstraints(doc *Document) error {
|
||||
var errs []string
|
||||
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
if index.Unique {
|
||||
indexValue := c.extractIndexValue(doc, index.Fields)
|
||||
if _, exists := index.data.Load(indexValue); exists {
|
||||
errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, indexValue))
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkUniqueConstraintsUpdate проверяет уникальность при обновлении
|
||||
func (c *Collection) checkUniqueConstraintsUpdate(oldDoc, newDoc *Document) error {
|
||||
var errs []string
|
||||
|
||||
c.indexes.Range(func(key, value interface{}) bool {
|
||||
index := value.(*Index)
|
||||
if index.Unique {
|
||||
oldValue := c.extractIndexValue(oldDoc, index.Fields)
|
||||
newValue := c.extractIndexValue(newDoc, index.Fields)
|
||||
|
||||
if fmt.Sprintf("%v", oldValue) != fmt.Sprintf("%v", newValue) {
|
||||
if _, exists := index.data.Load(newValue); exists {
|
||||
errs = append(errs, fmt.Sprintf("duplicate key for index %s: %v", index.Name, newValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDocument валидирует документ согласно схеме коллекции
|
||||
func (c *Collection) validateDocument(doc *Document) error {
|
||||
if doc.ID == "" {
|
||||
return fmt.Errorf("document must have _id field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ttlCleanupLoop периодически удаляет просроченные документы
|
||||
func (c *Collection) ttlCleanupLoop() {
|
||||
ticker := time.NewTicker(time.Duration(c.metadata.Settings.TTLSeconds/2) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
now := time.Now().UnixMilli()
|
||||
toDelete := make([]string, 0)
|
||||
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
doc := value.(*Document)
|
||||
if now-doc.CreatedAt > int64(c.metadata.Settings.TTLSeconds*1000) {
|
||||
toDelete = append(toDelete, doc.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, id := range toDelete {
|
||||
c.Delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count возвращает количество документов в коллекции
|
||||
func (c *Collection) Count() int64 {
|
||||
return c.docCount.Load()
|
||||
}
|
||||
|
||||
// Size возвращает размер коллекции в байтах
|
||||
func (c *Collection) Size() int64 {
|
||||
return c.sizeBytes.Load()
|
||||
}
|
||||
|
||||
// GetAllDocuments возвращает все документы коллекции
|
||||
func (c *Collection) GetAllDocuments() []*Document {
|
||||
docs := make([]*Document, 0, c.docCount.Load())
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
docs = append(docs, value.(*Document))
|
||||
return true
|
||||
})
|
||||
return docs
|
||||
}
|
||||
|
||||
// FindByFilter находит документы по произвольному фильтру
|
||||
func (c *Collection) FindByFilter(filter func(*Document) bool) []*Document {
|
||||
results := make([]*Document, 0)
|
||||
c.docs.Range(func(key, value interface{}) bool {
|
||||
doc := value.(*Document)
|
||||
if filter(doc) {
|
||||
results = append(results, doc)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
// GetMetadata возвращает метаданные коллекции
|
||||
func (c *Collection) GetMetadata() *CollectionMetadata {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.metadata
|
||||
}
|
||||
|
||||
// Drop удаляет все документы из коллекции
|
||||
func (c *Collection) Drop() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.docs = sync.Map{}
|
||||
c.indexes = sync.Map{}
|
||||
|
||||
if c.metadata.Settings.AutoIndexID {
|
||||
c.CreateIndex("_id_", []string{"_id"}, true)
|
||||
}
|
||||
|
||||
c.docCount.Store(0)
|
||||
c.sizeBytes.Store(0)
|
||||
c.metadata.DocumentCount = 0
|
||||
c.metadata.SizeBytes = 0
|
||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== Constraints Methods ==========
|
||||
|
||||
// AddRequiredField добавляет обязательное поле
|
||||
func (c *Collection) AddRequiredField(field string) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.RequiredFields[field] = true
|
||||
}
|
||||
|
||||
// AddUniqueConstraint добавляет ограничение уникальности
|
||||
func (c *Collection) AddUniqueConstraint(field string) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.UniqueFields[field] = true
|
||||
// Также создаём уникальный индекс
|
||||
c.CreateIndex("unique_"+field, []string{field}, true)
|
||||
}
|
||||
|
||||
// AddMinConstraint добавляет минимальное значение
|
||||
func (c *Collection) AddMinConstraint(field string, min float64) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.MinValues[field] = min
|
||||
}
|
||||
|
||||
// AddMaxConstraint добавляет максимальное значение
|
||||
func (c *Collection) AddMaxConstraint(field string, max float64) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.MaxValues[field] = max
|
||||
}
|
||||
|
||||
// AddPatternConstraint добавляет regexp паттерн
|
||||
func (c *Collection) AddPatternConstraint(field string, pattern string) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.PatternFields[field] = pattern
|
||||
}
|
||||
|
||||
// AddEnumConstraint добавляет допустимые значения
|
||||
func (c *Collection) AddEnumConstraint(field string, values []interface{}) {
|
||||
c.constraints.mu.Lock()
|
||||
defer c.constraints.mu.Unlock()
|
||||
c.constraints.EnumFields[field] = values
|
||||
}
|
||||
|
||||
// ValidateDocument проверяет документ на соответствие ограничениям
|
||||
func (cons *Constraints) ValidateDocument(doc *Document) error {
|
||||
cons.mu.RLock()
|
||||
defer cons.mu.RUnlock()
|
||||
|
||||
// Проверка обязательных полей
|
||||
for field := range cons.RequiredFields {
|
||||
if !doc.HasField(field) {
|
||||
return fmt.Errorf("required field '%s' is missing", field)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка уникальности (дополнительно к индексам)
|
||||
// (основная проверка в индексах)
|
||||
|
||||
// Проверка числовых ограничений
|
||||
for field, minVal := range cons.MinValues {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
if numVal, ok := toFloat64(val); ok {
|
||||
if numVal < minVal {
|
||||
return fmt.Errorf("field '%s' value %v is less than minimum %v", field, numVal, minVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for field, maxVal := range cons.MaxValues {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
if numVal, ok := toFloat64(val); ok {
|
||||
if numVal > maxVal {
|
||||
return fmt.Errorf("field '%s' value %v exceeds maximum %v", field, numVal, maxVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка enum
|
||||
for field, allowedValues := range cons.EnumFields {
|
||||
if val, err := doc.GetField(field); err == nil {
|
||||
found := false
|
||||
for _, allowed := range allowedValues {
|
||||
if fmt.Sprintf("%v", val) == fmt.Sprintf("%v", allowed) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("field '%s' value '%v' not in allowed list", field, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== ACL Methods ==========
|
||||
|
||||
// SetACL устанавливает ACL для коллекции
|
||||
func (c *Collection) SetACL(role string, canRead, canWrite, canDelete, isAdmin bool) {
|
||||
c.acl.mu.Lock()
|
||||
defer c.acl.mu.Unlock()
|
||||
|
||||
if canRead {
|
||||
c.acl.ReadRoles[role] = true
|
||||
}
|
||||
if canWrite {
|
||||
c.acl.WriteRoles[role] = true
|
||||
}
|
||||
if canDelete {
|
||||
c.acl.DeleteRoles[role] = true
|
||||
}
|
||||
if isAdmin {
|
||||
c.acl.AdminRoles[role] = true
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPermission проверяет наличие разрешения у роли
|
||||
func (c *Collection) CheckPermission(role, operation string) bool {
|
||||
c.acl.mu.RLock()
|
||||
defer c.acl.mu.RUnlock()
|
||||
|
||||
// Администратор имеет все права
|
||||
if c.acl.AdminRoles[role] {
|
||||
return true
|
||||
}
|
||||
|
||||
switch operation {
|
||||
case "read":
|
||||
return c.acl.ReadRoles[role]
|
||||
case "write":
|
||||
return c.acl.WriteRoles[role]
|
||||
case "delete":
|
||||
return c.acl.DeleteRoles[role]
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// toFloat64 конвертирует interface{} в float64
|
||||
func toFloat64(val interface{}) (float64, bool) {
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return float64(v), true
|
||||
case int64:
|
||||
return float64(v), true
|
||||
case float64:
|
||||
return v, true
|
||||
case float32:
|
||||
return float64(v), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
480
internal/storage/document.go
Normal file
480
internal/storage/document.go
Normal file
@@ -0,0 +1,480 @@
|
||||
// Файл: internal/storage/document.go
|
||||
// Назначение: Определение структуры документа, его методов для работы
|
||||
// с полями, кортежами (вложенными документами) и сериализации в MessagePack.
|
||||
// Документ является основной единицей хранения в СУБД futriis.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"futriis/internal/compression"
|
||||
"futriis/internal/serializer"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Document представляет документ в коллекции (аналог строки в реляционной СУБД)
|
||||
type Document struct {
|
||||
ID string `msgpack:"_id"` // Уникальный идентификатор документа
|
||||
Fields map[string]interface{} `msgpack:"fields"` // Поля документа (аналог колонок)
|
||||
CreatedAt int64 `msgpack:"created_at"` // Время создания (Unix миллисекунды)
|
||||
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления
|
||||
Version uint64 `msgpack:"version"` // Версия документа (для оптимистичных блокировок)
|
||||
Compressed bool `msgpack:"compressed"` // Флаг, сжат ли документ
|
||||
OriginalSize int64 `msgpack:"original_size"` // Оригинальный размер до сжатия
|
||||
mu sync.RWMutex `msgpack:"-"` // Блокировка для wait-free операций
|
||||
}
|
||||
|
||||
// Tuple представляет вложенный документ (аналог кортежа в реляционной СУБД)
|
||||
type Tuple struct {
|
||||
Fields map[string]interface{} `msgpack:"fields"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Field представляет отдельное поле документа (аналог колонки)
|
||||
type Field struct {
|
||||
Name string `msgpack:"name"`
|
||||
Type FieldType `msgpack:"type"`
|
||||
Value interface{} `msgpack:"value"`
|
||||
}
|
||||
|
||||
// FieldType определяет тип поля документа
|
||||
type FieldType int
|
||||
|
||||
const (
|
||||
TypeString FieldType = iota
|
||||
TypeNumber
|
||||
TypeBoolean
|
||||
TypeTuple // Вложенный документ
|
||||
TypeArray
|
||||
TypeNull
|
||||
)
|
||||
|
||||
// NewDocument создаёт новый документ с автоматической генерацией ID
|
||||
func NewDocument() *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
return &Document{
|
||||
ID: uuid.New().String(),
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDocumentWithID создаёт документ с указанным ID
|
||||
func NewDocumentWithID(id string) *Document {
|
||||
now := time.Now().UnixMilli()
|
||||
return &Document{
|
||||
ID: id,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Version: 1,
|
||||
Compressed: false,
|
||||
OriginalSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SetField устанавливает значение поля документа (wait-free)
|
||||
func (d *Document) SetField(name string, value interface{}) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.Fields[name] = value
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // При изменении документа снимаем флаг сжатия
|
||||
}
|
||||
|
||||
// GetField возвращает значение поля документа
|
||||
func (d *Document) GetField(name string) (interface{}, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if val, ok := d.Fields[name]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field not found: %s", name)
|
||||
}
|
||||
|
||||
// DeleteField удаляет поле из документа
|
||||
func (d *Document) DeleteField(name string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
delete(d.Fields, name)
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false
|
||||
}
|
||||
|
||||
// HasField проверяет наличие поля в документе
|
||||
func (d *Document) HasField(name string) bool {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
_, ok := d.Fields[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetFields возвращает копию всех полей документа
|
||||
func (d *Document) GetFields() map[string]interface{} {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
copy := make(map[string]interface{})
|
||||
for k, v := range d.Fields {
|
||||
copy[k] = v
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// SetTuple устанавливает вложенный документ (кортеж) в поле
|
||||
func (d *Document) SetTuple(fieldName string, tuple *Tuple) {
|
||||
d.SetField(fieldName, tuple)
|
||||
}
|
||||
|
||||
// GetTuple возвращает вложенный документ из поля
|
||||
func (d *Document) GetTuple(fieldName string) (*Tuple, error) {
|
||||
val, err := d.GetField(fieldName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tuple, ok := val.(*Tuple); ok {
|
||||
return tuple, nil
|
||||
}
|
||||
return nil, fmt.Errorf("field %s is not a tuple", fieldName)
|
||||
}
|
||||
|
||||
// Serialize сериализует документ в MessagePack с поддержкой сжатия
|
||||
func (d *Document) Serialize() ([]byte, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
data, err := serializer.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// SerializeCompressed сериализует и сжимает документ
|
||||
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
// Сериализуем документ
|
||||
data, err := serializer.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, нужно ли сжимать
|
||||
if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize {
|
||||
compressed, err := compression.Compress(data, compressionConfig)
|
||||
if err != nil {
|
||||
// При ошибке сжатия возвращаем несжатые данные
|
||||
return data, nil
|
||||
}
|
||||
return compressed, nil
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Deserialize десериализует документ из MessagePack (автоматически определяет сжатие)
|
||||
func (d *Document) Deserialize(data []byte) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// Пытаемся определить, сжаты ли данные
|
||||
// Для этого пробуем распаковать, если не получается - данные несжатые
|
||||
decompressed, err := compression.DecompressAuto(data)
|
||||
if err == nil && len(decompressed) < len(data) {
|
||||
// Данные были сжаты, используем распакованную версию
|
||||
if err := serializer.Unmarshal(decompressed, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(len(decompressed))
|
||||
} else {
|
||||
// Данные не сжаты или не удалось распаковать
|
||||
if err := serializer.Unmarshal(data, d); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
}
|
||||
|
||||
// Обновляем временные метки при десериализации
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone создаёт глубокую копию документа
|
||||
func (d *Document) Clone() *Document {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
clone := &Document{
|
||||
ID: d.ID,
|
||||
Fields: make(map[string]interface{}),
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
Version: d.Version,
|
||||
Compressed: d.Compressed,
|
||||
OriginalSize: d.OriginalSize,
|
||||
}
|
||||
|
||||
// Глубокое копирование полей
|
||||
for k, v := range d.Fields {
|
||||
clone.Fields[k] = deepCopyValue(v)
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// Update применяет обновление к документу (атомарно)
|
||||
func (d *Document) Update(updates map[string]interface{}) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
for k, v := range updates {
|
||||
d.Fields[k] = v
|
||||
}
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Version++
|
||||
d.Compressed = false // После обновления документ больше не сжат
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compress сжимает документ в памяти
|
||||
func (d *Document) Compress(config *compression.Config) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Сохраняем текущее состояние
|
||||
originalSize := len(d.Fields)
|
||||
if originalSize < config.MinSize {
|
||||
return nil // Не сжимаем маленькие документы
|
||||
}
|
||||
|
||||
d.Compressed = true
|
||||
d.OriginalSize = int64(originalSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decompress распаковывает документ в памяти
|
||||
func (d *Document) Decompress() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if !d.Compressed {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Compressed = false
|
||||
d.OriginalSize = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompressionRatio возвращает коэффициент сжатия
|
||||
func (d *Document) GetCompressionRatio() float64 {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
if !d.Compressed || d.OriginalSize == 0 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
currentSize := len(d.Fields)
|
||||
return float64(currentSize) / float64(d.OriginalSize)
|
||||
}
|
||||
|
||||
// deepCopyValue выполняет глубокое копирование значения
|
||||
func deepCopyValue(val interface{}) interface{} {
|
||||
switch v := val.(type) {
|
||||
case *Tuple:
|
||||
return v.Clone()
|
||||
case map[string]interface{}:
|
||||
copy := make(map[string]interface{})
|
||||
for k, val := range v {
|
||||
copy[k] = deepCopyValue(val)
|
||||
}
|
||||
return copy
|
||||
case []interface{}:
|
||||
copy := make([]interface{}, len(v))
|
||||
for i, val := range v {
|
||||
copy[i] = deepCopyValue(val)
|
||||
}
|
||||
return copy
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// NewTuple создаёт новый вложенный документ (кортеж)
|
||||
func NewTuple() *Tuple {
|
||||
return &Tuple{
|
||||
Fields: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Set устанавливает поле во вложенном документе
|
||||
func (t *Tuple) Set(name string, value interface{}) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.Fields[name] = value
|
||||
}
|
||||
|
||||
// Get возвращает поле из вложенного документа
|
||||
func (t *Tuple) Get(name string) (interface{}, error) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
if val, ok := t.Fields[name]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return nil, fmt.Errorf("tuple field not found: %s", name)
|
||||
}
|
||||
|
||||
// Clone создаёт копию кортежа
|
||||
func (t *Tuple) Clone() *Tuple {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
clone := NewTuple()
|
||||
for k, v := range t.Fields {
|
||||
clone.Fields[k] = deepCopyValue(v)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// ToMap конвертирует кортеж в map
|
||||
func (t *Tuple) ToMap() map[string]interface{} {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
copy := make(map[string]interface{})
|
||||
for k, v := range t.Fields {
|
||||
copy[k] = v
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// GetNestedField получает значение по точечному пути (например, "user.address.city")
|
||||
func (d *Document) GetNestedField(path string) (interface{}, error) {
|
||||
parts := strings.Split(path, ".")
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
current := interface{}(d)
|
||||
for _, part := range parts {
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
val, err := v.GetField(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = val
|
||||
case *Tuple:
|
||||
val, err := v.Get(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = val
|
||||
case map[string]interface{}:
|
||||
if val, ok := v[part]; ok {
|
||||
current = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("field not found: %s", part)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot navigate into non-document value at %s", part)
|
||||
}
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
// SetNestedField устанавливает значение по точечному пути
|
||||
func (d *Document) SetNestedField(path string, value interface{}) error {
|
||||
parts := strings.Split(path, ".")
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
// Если путь состоит из одного элемента, просто устанавливаем поле
|
||||
if len(parts) == 1 {
|
||||
d.SetField(parts[0], value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Иначе нужно создать промежуточные структуры
|
||||
current := interface{}(d)
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
part := parts[i]
|
||||
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
if !v.HasField(part) {
|
||||
// Создаём новый кортеж, если поле не существует
|
||||
newTuple := NewTuple()
|
||||
v.SetField(part, newTuple)
|
||||
current = newTuple
|
||||
} else {
|
||||
field, _ := v.GetField(part)
|
||||
if tuple, ok := field.(*Tuple); ok {
|
||||
current = tuple
|
||||
} else {
|
||||
return fmt.Errorf("field %s is not a tuple", part)
|
||||
}
|
||||
}
|
||||
case *Tuple:
|
||||
if val, err := v.Get(part); err == nil {
|
||||
if tuple, ok := val.(*Tuple); ok {
|
||||
current = tuple
|
||||
} else {
|
||||
return fmt.Errorf("field %s is not a tuple", part)
|
||||
}
|
||||
} else {
|
||||
newTuple := NewTuple()
|
||||
v.Set(part, newTuple)
|
||||
current = newTuple
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("cannot set nested field on non-document value")
|
||||
}
|
||||
}
|
||||
|
||||
// Устанавливаем значение в последний элемент пути
|
||||
lastPart := parts[len(parts)-1]
|
||||
switch v := current.(type) {
|
||||
case *Document:
|
||||
v.SetField(lastPart, value)
|
||||
case *Tuple:
|
||||
v.Set(lastPart, value)
|
||||
default:
|
||||
return fmt.Errorf("cannot set field on non-document value")
|
||||
}
|
||||
|
||||
d.UpdatedAt = time.Now().UnixMilli()
|
||||
d.Compressed = false
|
||||
return nil
|
||||
}
|
||||
224
internal/storage/engine.go
Normal file
224
internal/storage/engine.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Файл: internal/storage/engine.go
|
||||
// Назначение: In-memory движок хранения документов с поддержкой коллекций,
|
||||
// слайсов (аналог БД), тапплов (аналог таблиц), полей и кортежей.
|
||||
// Полностью wait-free с использованием sync.Map и атомарных операций.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"futriis/internal/log"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// Storage представляет основное хранилище баз данных
|
||||
type Storage struct {
|
||||
databases sync.Map // map[string]*Database
|
||||
pageSize int64
|
||||
logger *log.Logger
|
||||
totalDocs atomic.Int64
|
||||
}
|
||||
|
||||
// Database представляет базу данных (аналог слайса в реляционных СУБД)
|
||||
type Database struct {
|
||||
name string
|
||||
collections sync.Map // map[string]*Collection
|
||||
}
|
||||
|
||||
// NewStorage создаёт новый экземпляр хранилища
|
||||
func NewStorage(pageSizeMB int, logger *log.Logger) *Storage {
|
||||
return &Storage{
|
||||
pageSize: int64(pageSizeMB) * 1024 * 1024,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDatabase создаёт новую базу данных
|
||||
func (s *Storage) CreateDatabase(name string) error {
|
||||
if _, exists := s.databases.LoadOrStore(name, &Database{name: name}); exists {
|
||||
return fmt.Errorf("database already exists")
|
||||
}
|
||||
AuditDatabaseOperation("CREATE", name)
|
||||
s.logger.Info("Database created: " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatabase возвращает базу данных по имени
|
||||
func (s *Storage) GetDatabase(name string) (*Database, error) {
|
||||
if val, ok := s.databases.Load(name); ok {
|
||||
return val.(*Database), nil
|
||||
}
|
||||
return nil, fmt.Errorf("database not found")
|
||||
}
|
||||
|
||||
// DropDatabase удаляет базу данных
|
||||
func (s *Storage) DropDatabase(name string) error {
|
||||
if _, ok := s.databases.LoadAndDelete(name); !ok {
|
||||
return fmt.Errorf("database not found")
|
||||
}
|
||||
AuditDatabaseOperation("DROP", name)
|
||||
s.logger.Info("Database dropped: " + name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListDatabases возвращает список всех баз данных
|
||||
func (s *Storage) ListDatabases() []string {
|
||||
databases := make([]string, 0)
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
databases = append(databases, key.(string))
|
||||
return true
|
||||
})
|
||||
return databases
|
||||
}
|
||||
|
||||
// Name возвращает имя базы данных
|
||||
func (db *Database) Name() string {
|
||||
return db.name
|
||||
}
|
||||
|
||||
// CreateCollection создаёт новую коллекцию в базе данных
|
||||
func (db *Database) CreateCollection(name string) error {
|
||||
if _, exists := db.collections.LoadOrStore(name, NewCollection(name, nil)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
AuditCollectionOperation("CREATE", db.name, name, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCollectionWithSettings создаёт коллекцию с настройками
|
||||
func (db *Database) CreateCollectionWithSettings(name string, settings *CollectionSettings) error {
|
||||
if _, exists := db.collections.LoadOrStore(name, NewCollection(name, settings)); exists {
|
||||
return fmt.Errorf("collection already exists")
|
||||
}
|
||||
AuditCollectionOperation("CREATE", db.name, name, settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollection возвращает коллекцию по имени
|
||||
func (db *Database) GetCollection(name string) (*Collection, error) {
|
||||
if val, ok := db.collections.Load(name); ok {
|
||||
return val.(*Collection), nil
|
||||
}
|
||||
return nil, fmt.Errorf("collection not found")
|
||||
}
|
||||
|
||||
// DropCollection удаляет коллекцию
|
||||
func (db *Database) DropCollection(name string) error {
|
||||
if _, ok := db.collections.LoadAndDelete(name); !ok {
|
||||
return fmt.Errorf("collection not found")
|
||||
}
|
||||
AuditCollectionOperation("DROP", db.name, name, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCollections возвращает список всех коллекций в базе данных
|
||||
func (db *Database) ListCollections() []string {
|
||||
collections := make([]string, 0)
|
||||
db.collections.Range(func(key, value interface{}) bool {
|
||||
collections = append(collections, key.(string))
|
||||
return true
|
||||
})
|
||||
return collections
|
||||
}
|
||||
|
||||
// GetTotalDocuments возвращает общее количество документов во всех коллекциях
|
||||
func (s *Storage) GetTotalDocuments() int64 {
|
||||
return s.totalDocs.Load()
|
||||
}
|
||||
|
||||
// GetPageSize возвращает размер страницы памяти
|
||||
func (s *Storage) GetPageSize() int64 {
|
||||
return s.pageSize
|
||||
}
|
||||
|
||||
// SerializeDatabase сериализует всю базу данных в MessagePack
|
||||
func (db *Database) SerializeDatabase() ([]byte, error) {
|
||||
dbData := make(map[string]interface{})
|
||||
|
||||
db.collections.Range(func(key, value interface{}) bool {
|
||||
coll := value.(*Collection)
|
||||
collData := make(map[string]interface{})
|
||||
|
||||
// Собираем все документы коллекции
|
||||
docs := coll.GetAllDocuments()
|
||||
collDocs := make([]*Document, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
collDocs = append(collDocs, doc)
|
||||
}
|
||||
collData["documents"] = collDocs
|
||||
collData["metadata"] = coll.GetMetadata()
|
||||
|
||||
dbData[key.(string)] = collData
|
||||
return true
|
||||
})
|
||||
|
||||
return serializer.Marshal(dbData)
|
||||
}
|
||||
|
||||
// DeserializeDatabase десериализует базу данных из MessagePack
|
||||
func (db *Database) DeserializeDatabase(data []byte) error {
|
||||
var dbData map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &dbData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for collName, collDataRaw := range dbData {
|
||||
collData := collDataRaw.(map[string]interface{})
|
||||
|
||||
// Создаём коллекцию
|
||||
settings := &CollectionSettings{
|
||||
MaxDocuments: 0,
|
||||
ValidateSchema: false,
|
||||
AutoIndexID: true,
|
||||
TTLSeconds: 0,
|
||||
}
|
||||
|
||||
if metaRaw, ok := collData["metadata"]; ok {
|
||||
if meta, ok := metaRaw.(*CollectionMetadata); ok {
|
||||
if meta.Settings != nil {
|
||||
settings = meta.Settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coll := NewCollection(collName, settings)
|
||||
|
||||
// Восстанавливаем документы
|
||||
if docsRaw, ok := collData["documents"]; ok {
|
||||
if docs, ok := docsRaw.([]*Document); ok {
|
||||
for _, doc := range docs {
|
||||
coll.Insert(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.collections.Store(collName, coll)
|
||||
AuditCollectionOperation("RESTORE", db.name, collName, settings)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatabaseNames возвращает имена всех баз данных
|
||||
func (s *Storage) GetDatabaseNames() []string {
|
||||
return s.ListDatabases()
|
||||
}
|
||||
|
||||
// ExistsDatabase проверяет существование базы данных
|
||||
func (s *Storage) ExistsDatabase(name string) bool {
|
||||
_, ok := s.databases.Load(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetDatabaseCount возвращает количество баз данных
|
||||
func (s *Storage) GetDatabaseCount() int {
|
||||
count := 0
|
||||
s.databases.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
382
internal/storage/transaction.go
Normal file
382
internal/storage/transaction.go
Normal file
@@ -0,0 +1,382 @@
|
||||
// Файл: internal/storage/transaction.go
|
||||
// Назначение: Реализация транзакций с поддержкой MVCC (Multi-Version Concurrency Control)
|
||||
// и WAL (Write-Ahead Logging) без блокировок. Использует атомарные операции и версионирование.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// TransactionID представляет уникальный идентификатор транзакции
|
||||
type TransactionID uint64
|
||||
|
||||
// TransactionState представляет состояние транзакции
|
||||
type TransactionState int32
|
||||
|
||||
const (
|
||||
TransactionActive TransactionState = iota
|
||||
TransactionCommitted
|
||||
TransactionAborted
|
||||
)
|
||||
|
||||
// TransactionRecord представляет запись в WAL
|
||||
type TransactionRecord struct {
|
||||
ID TransactionID `msgpack:"id"`
|
||||
State TransactionState `msgpack:"state"`
|
||||
Timestamp int64 `msgpack:"timestamp"`
|
||||
Operations []Operation `msgpack:"operations"`
|
||||
}
|
||||
|
||||
// Operation представляет одну операцию в транзакции
|
||||
type Operation struct {
|
||||
Type string `msgpack:"type"` // "insert", "update", "delete"
|
||||
Database string `msgpack:"database"`
|
||||
Collection string `msgpack:"collection"`
|
||||
DocumentID string `msgpack:"document_id"`
|
||||
Data map[string]interface{} `msgpack:"data"`
|
||||
Version uint64 `msgpack:"version"`
|
||||
}
|
||||
|
||||
// DocumentVersion представляет версию документа для MVCC
|
||||
type DocumentVersion struct {
|
||||
Document *Document `msgpack:"document"`
|
||||
Timestamp int64 `msgpack:"timestamp"`
|
||||
TxID TransactionID `msgpack:"tx_id"`
|
||||
}
|
||||
|
||||
// TransactionManager управляет транзакциями
|
||||
type TransactionManager struct {
|
||||
activeTransactions sync.Map // map[TransactionID]*Transaction
|
||||
nextTxID atomic.Uint64
|
||||
wal *WriteAheadLog
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Transaction представляет одну транзакцию
|
||||
type Transaction struct {
|
||||
ID TransactionID
|
||||
State atomic.Int32
|
||||
Operations []Operation
|
||||
StartTime int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// WriteAheadLog реализует журнал предзаписи
|
||||
type WriteAheadLog struct {
|
||||
file *os.File
|
||||
writeChan chan []byte
|
||||
done chan struct{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
globalTxManager *TransactionManager
|
||||
txManagerOnce sync.Once
|
||||
currentTx atomic.Value // *Transaction
|
||||
)
|
||||
|
||||
// InitTransactionManager инициализирует глобальный менеджер транзакций
|
||||
func InitTransactionManager(walPath string) error {
|
||||
var err error
|
||||
txManagerOnce.Do(func() {
|
||||
globalTxManager = &TransactionManager{
|
||||
nextTxID: atomic.Uint64{},
|
||||
}
|
||||
globalTxManager.nextTxID.Store(1)
|
||||
err = globalTxManager.initWAL(walPath)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// initWAL инициализирует Write-Ahead Log
|
||||
func (tm *TransactionManager) initWAL(walPath string) error {
|
||||
wal, err := NewWriteAheadLog(walPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tm.wal = wal
|
||||
|
||||
// Восстанавливаем состояние из WAL при запуске
|
||||
go tm.recoverFromWAL()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewWriteAheadLog создаёт новый WAL
|
||||
func NewWriteAheadLog(path string) (*WriteAheadLog, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wal := &WriteAheadLog{
|
||||
file: file,
|
||||
writeChan: make(chan []byte, 10000),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
go wal.writerLoop()
|
||||
|
||||
return wal, nil
|
||||
}
|
||||
|
||||
// writerLoop асинхронно записывает данные в WAL
|
||||
func (wal *WriteAheadLog) writerLoop() {
|
||||
for data := range wal.writeChan {
|
||||
wal.mu.Lock()
|
||||
// Формат записи: [длина (4 байта)][данные]
|
||||
lenBuf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(lenBuf, uint32(len(data)))
|
||||
|
||||
if _, err := wal.file.Write(lenBuf); err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err := wal.file.Write(data); err != nil {
|
||||
continue
|
||||
}
|
||||
wal.file.Sync()
|
||||
wal.mu.Unlock()
|
||||
}
|
||||
close(wal.done)
|
||||
}
|
||||
|
||||
// Write записывает запись в WAL
|
||||
func (wal *WriteAheadLog) Write(record *TransactionRecord) error {
|
||||
data, err := serializer.Marshal(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case wal.writeChan <- data:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("WAL buffer full")
|
||||
}
|
||||
}
|
||||
|
||||
// Close закрывает WAL
|
||||
func (wal *WriteAheadLog) Close() error {
|
||||
close(wal.writeChan)
|
||||
<-wal.done
|
||||
return wal.file.Close()
|
||||
}
|
||||
|
||||
// BeginTransaction начинает новую транзакцию
|
||||
func BeginTransaction() *Transaction {
|
||||
if globalTxManager == nil {
|
||||
InitTransactionManager("futriis.wal")
|
||||
}
|
||||
|
||||
tx := &Transaction{
|
||||
ID: TransactionID(globalTxManager.nextTxID.Add(1) - 1),
|
||||
StartTime: time.Now().UnixMilli(),
|
||||
Operations: make([]Operation, 0),
|
||||
}
|
||||
tx.State.Store(int32(TransactionActive))
|
||||
|
||||
globalTxManager.activeTransactions.Store(tx.ID, tx)
|
||||
|
||||
// Сохраняем как текущую транзакцию для горутины
|
||||
currentTx.Store(tx)
|
||||
|
||||
// Записываем начало транзакции в WAL
|
||||
record := &TransactionRecord{
|
||||
ID: tx.ID,
|
||||
State: TransactionActive,
|
||||
Timestamp: tx.StartTime,
|
||||
Operations: tx.Operations,
|
||||
}
|
||||
globalTxManager.wal.Write(record)
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
// AddToTransaction добавляет операцию в текущую транзакцию
|
||||
func AddToTransaction(coll *Collection, opType string, doc *Document) error {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return fmt.Errorf("no active transaction")
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
if TransactionState(tx.State.Load()) != TransactionActive {
|
||||
return fmt.Errorf("transaction is not active")
|
||||
}
|
||||
|
||||
op := Operation{
|
||||
Type: opType,
|
||||
Database: coll.Name(), // В реальной реализации нужно передавать имя БД
|
||||
Collection: coll.Name(),
|
||||
DocumentID: doc.ID,
|
||||
Data: doc.GetFields(),
|
||||
Version: doc.Version,
|
||||
}
|
||||
|
||||
tx.mu.Lock()
|
||||
tx.Operations = append(tx.Operations, op)
|
||||
tx.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitCurrentTransaction коммитит текущую транзакцию
|
||||
func CommitCurrentTransaction() error {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return fmt.Errorf("no active transaction")
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
if TransactionState(tx.State.Load()) != TransactionActive {
|
||||
return fmt.Errorf("transaction is not active")
|
||||
}
|
||||
|
||||
// Применяем все операции атомарно
|
||||
for _, op := range tx.Operations {
|
||||
if err := applyOperation(op); err != nil {
|
||||
// Откатываем при ошибке
|
||||
AbortCurrentTransaction()
|
||||
return fmt.Errorf("transaction commit failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
tx.State.Store(int32(TransactionCommitted))
|
||||
|
||||
// Записываем коммит в WAL
|
||||
record := &TransactionRecord{
|
||||
ID: tx.ID,
|
||||
State: TransactionCommitted,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Operations: tx.Operations,
|
||||
}
|
||||
globalTxManager.wal.Write(record)
|
||||
|
||||
// Очищаем текущую транзакцию
|
||||
currentTx.Store(nil)
|
||||
globalTxManager.activeTransactions.Delete(tx.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AbortCurrentTransaction откатывает текущую транзакцию
|
||||
func AbortCurrentTransaction() error {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return fmt.Errorf("no active transaction")
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
tx.State.Store(int32(TransactionAborted))
|
||||
|
||||
// Записываем откат в WAL
|
||||
record := &TransactionRecord{
|
||||
ID: tx.ID,
|
||||
State: TransactionAborted,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Operations: tx.Operations,
|
||||
}
|
||||
globalTxManager.wal.Write(record)
|
||||
|
||||
// Очищаем текущую транзакцию
|
||||
currentTx.Store(nil)
|
||||
globalTxManager.activeTransactions.Delete(tx.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasActiveTransaction проверяет наличие активной транзакции
|
||||
func HasActiveTransaction() bool {
|
||||
return currentTx.Load() != nil
|
||||
}
|
||||
|
||||
// FindInTransaction ищет документ в контексте транзакции
|
||||
func FindInTransaction(coll *Collection, id string) (*Document, error) {
|
||||
txVal := currentTx.Load()
|
||||
if txVal == nil {
|
||||
return coll.Find(id)
|
||||
}
|
||||
|
||||
tx := txVal.(*Transaction)
|
||||
|
||||
// Сначала ищем в операциях транзакции
|
||||
for i := len(tx.Operations) - 1; i >= 0; i-- {
|
||||
op := tx.Operations[i]
|
||||
if op.DocumentID == id {
|
||||
if op.Type == "delete" {
|
||||
return nil, fmt.Errorf("key not found")
|
||||
}
|
||||
// Создаём документ из данных операции
|
||||
doc := NewDocumentWithID(op.DocumentID)
|
||||
for k, v := range op.Data {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
doc.Version = op.Version
|
||||
return doc, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем в основном хранилище
|
||||
return coll.Find(id)
|
||||
}
|
||||
|
||||
// applyOperation применяет операцию к хранилищу
|
||||
func applyOperation(op Operation) error {
|
||||
// В реальной реализации здесь будет применение операции к соответствующей коллекции
|
||||
// С использованием MVCC для версионирования
|
||||
|
||||
switch op.Type {
|
||||
case "insert":
|
||||
// Проверяем версию документа (MVCC)
|
||||
doc := NewDocumentWithID(op.DocumentID)
|
||||
for k, v := range op.Data {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
// Здесь должна быть вставка в коллекцию
|
||||
return nil
|
||||
case "update":
|
||||
// Обновление с проверкой версии
|
||||
return nil
|
||||
case "delete":
|
||||
// Удаление
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recoverFromWAL восстанавливает состояние из WAL после сбоя
|
||||
func (tm *TransactionManager) recoverFromWAL() {
|
||||
// В реальной реализации здесь будет чтение WAL и восстановление
|
||||
// незавершённых транзакций
|
||||
}
|
||||
|
||||
// GetTransaction возвращает транзакцию по ID
|
||||
func GetTransaction(id TransactionID) (*Transaction, bool) {
|
||||
if val, ok := globalTxManager.activeTransactions.Load(id); ok {
|
||||
return val.(*Transaction), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// MVCCSnapshot создаёт снапшот текущего состояния для MVCC
|
||||
func MVCCSnapshot() uint64 {
|
||||
return uint64(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// CreateDocumentVersion создаёт новую версию документа для MVCC
|
||||
func CreateDocumentVersion(doc *Document, txID TransactionID) *DocumentVersion {
|
||||
return &DocumentVersion{
|
||||
Document: doc.Clone(),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
TxID: txID,
|
||||
}
|
||||
}
|
||||
1094
pkg/utils/ansi.go
Normal file
1094
pkg/utils/ansi.go
Normal file
File diff suppressed because it is too large
Load Diff
99
pkg/utils/color.go
Normal file
99
pkg/utils/color.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Файл: pkg/utils/color.go
|
||||
// Назначение: Кроссплатформенная цветная печать с ANSI-кодами,
|
||||
// работающая в Linux (Debian/Fedora) и Illumos (OpenIndiana/OmniOS).
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Глобальный объект цвета Deep Sky Blue (#00bfff)
|
||||
var (
|
||||
deepSkyBlueColor = color.New(color.FgHiCyan) // Ярко-голубой, близкий к #00bfff
|
||||
// Альтернатива с точным RGB-кодом (работает в современных терминалах):
|
||||
// deepSkyBlueColor = color.New(color.FgRGB(0, 191, 255))
|
||||
)
|
||||
|
||||
// EnableDeepSkyBlueMode включает режим постоянного цвета.
|
||||
// В библиотеке fatih/color для этого используется функция color.Set().
|
||||
func EnableDeepSkyBlueMode() {
|
||||
deepSkyBlueColor.Set()
|
||||
}
|
||||
|
||||
// DisableDeepSkyBlueMode отключает режим постоянного цвета и сбрасывает настройки.
|
||||
func DisableDeepSkyBlueMode() {
|
||||
color.Unset()
|
||||
}
|
||||
|
||||
// SetDeepSkyBlueEnabled — удобная функция для включения/выключения цвета.
|
||||
func SetDeepSkyBlueEnabled(enabled bool) {
|
||||
if enabled {
|
||||
EnableDeepSkyBlueMode()
|
||||
} else {
|
||||
DisableDeepSkyBlueMode()
|
||||
}
|
||||
}
|
||||
|
||||
// PrintDeepSkyBlueColored выводит текст цветом Deep Sky Blue (без перевода строки)
|
||||
func PrintDeepSkyBlueColored(a ...interface{}) {
|
||||
deepSkyBlueColor.Print(a...)
|
||||
}
|
||||
|
||||
// PrintlnDeepSkyBlueColored выводит строку с цветом Deep Sky Blue и добавляет перевод строки
|
||||
func PrintlnDeepSkyBlueColored(a ...interface{}) {
|
||||
deepSkyBlueColor.Println(a...)
|
||||
}
|
||||
|
||||
// PrintfDeepSkyBlue форматирует и выводит текст цветом Deep Sky Blue.
|
||||
func PrintfDeepSkyBlue(format string, a ...interface{}) {
|
||||
deepSkyBlueColor.Printf(format, a...)
|
||||
}
|
||||
|
||||
// PrintErrorRed выводит сообщение об ошибке красным цветом.
|
||||
func PrintErrorRed(msg string) {
|
||||
errorColor := color.New(color.FgRed)
|
||||
errorColor.Println("Error: " + msg)
|
||||
}
|
||||
|
||||
// ColorizeTextAny преобразует любой тип в цветную строку.
|
||||
func ColorizeTextAny(v interface{}) string {
|
||||
return deepSkyBlueColor.Sprint(v)
|
||||
}
|
||||
|
||||
// ColorizeTextInt преобразует int в цветную строку.
|
||||
func ColorizeTextInt(n int) string {
|
||||
return deepSkyBlueColor.Sprint(n)
|
||||
}
|
||||
|
||||
// ColorizeTextByColor возвращает строку, окрашенную в указанный цвет.
|
||||
// Поддерживаемые цвета: red, green, yellow, blue, magenta, cyan, white, black
|
||||
func ColorizeTextByColor(text string, colorName string) string {
|
||||
switch colorName {
|
||||
case "red":
|
||||
return color.New(color.FgRed).Sprint(text)
|
||||
case "green":
|
||||
return color.New(color.FgGreen).Sprint(text)
|
||||
case "yellow":
|
||||
return color.New(color.FgYellow).Sprint(text)
|
||||
case "blue":
|
||||
return color.New(color.FgBlue).Sprint(text)
|
||||
case "magenta":
|
||||
return color.New(color.FgMagenta).Sprint(text)
|
||||
case "cyan":
|
||||
return color.New(color.FgCyan).Sprint(text)
|
||||
case "white":
|
||||
return color.New(color.FgWhite).Sprint(text)
|
||||
case "black":
|
||||
return color.New(color.FgBlack).Sprint(text)
|
||||
default:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentTimestamp возвращает текущий timestamp в миллисекундах
|
||||
func GetCurrentTimestamp() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
28
plugins/example.lua
Normal file
28
plugins/example.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
-- example.lua
|
||||
version = "1.0.0"
|
||||
author = "futriis"
|
||||
description = "Example plugin"
|
||||
|
||||
function on_load()
|
||||
plugin_log("info", "Example plugin loaded")
|
||||
return true
|
||||
end
|
||||
|
||||
function on_start()
|
||||
plugin_log("info", "Example plugin started")
|
||||
|
||||
-- Получаем коллекцию
|
||||
local coll = get_collection("test_db", "test_coll")
|
||||
if coll then
|
||||
-- Вставляем документ
|
||||
coll:insert({name = "test", value = 42})
|
||||
plugin_log("info", "Document inserted")
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function process_data(data)
|
||||
plugin_log("debug", "Processing: " .. data)
|
||||
return {processed = true, original = data}
|
||||
end
|
||||
11
scripts/build_illumos.sh
Normal file
11
scripts/build_illumos.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Специальная сборка для Illumos (OpenIndiana Hipster / OmniOS)
|
||||
|
||||
export GOOS=illumos
|
||||
export GOARCH=amd64
|
||||
export CGO_ENABLED=1
|
||||
|
||||
echo "🔧 Building for Illumos..."
|
||||
go build -tags=illumos -o futriis-illumos ./cmd/futriis
|
||||
echo "✅ Done: futriis-illumos"
|
||||
Reference in New Issue
Block a user