first commit

This commit is contained in:
2026-04-08 21:43:35 +03:00
commit be7a1a3ea2
33 changed files with 9609 additions and 0 deletions

BIN
Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

246
README.md Normal file
View 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>
&middot;
<!-- <a href="">Предложение новой функциональности</a> -->
</p>
</div>
## Краткая документация проекта FutriiS
<!-- TABLE OF CONTENTS -->
<br>
<!-- <details> -->
<summary><b>Содержание</b></summary></br>
<ol>
<li>
<a href="#о-проекте">О проекте</a>
<li><a href="#лицензия">Лицензия</a></li>
<li><a href="#глоссарий">Глоссарий</a></li>
<li><a href="#типы-данных-субд">Типы данных субд</a></li>
<li><a href="#системные-требования">Системные требования</a></li>
<li><a href="#подготовка-и-компиляция">Подготовка и компиляция</a></li>
<li><a href="#тестирование">Тестирование</a></li>
<li><a href="#примеры-команд-субд">Примеры команд субд</a></li>
<li><a href="#индексы">Индексы</a></li>
<li><a href="#транзакции">Транзакции</a></li>
<li><a href="#кластеризация-и-шардинг">Кластеризация и шардинг</a></li>
<li><a href="#сжатие-данных">Сжатие данных</a></li>
<li><a href="#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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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 запроса
}

View 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)
}

View 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
View 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
}

View 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)
}

View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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,
})
}

View 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
}
}

View 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
View 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
}

View 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

File diff suppressed because it is too large Load Diff

99
pkg/utils/color.go Normal file
View 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
View 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
View 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"