Compare commits
46 Commits
v.2.i²_18.
...
devel
| Author | SHA1 | Date | |
|---|---|---|---|
| f092723a1a | |||
| 8b6f2e2c0c | |||
| 55bb0e7cc0 | |||
| f494fca375 | |||
| 6cde3c4cc9 | |||
| 5447aa45d1 | |||
| 2da91c8bc1 | |||
| cbaf5a4918 | |||
| 29e66d01fb | |||
| 564d0ac688 | |||
| 46c73814f6 | |||
| 0f2117d8a1 | |||
| e7ab44041c | |||
| 45dd4ccdb8 | |||
| 75506b9fb7 | |||
| 2de05922c5 | |||
| e6d064b6cf | |||
| d1a57ee199 | |||
| 08dbde5731 | |||
| a75f29a231 | |||
| c858b1eec3 | |||
| 2cc2249625 | |||
| 0a6cfe8f0e | |||
| 4593c30076 | |||
| 9150dbab1f | |||
| 8d26429021 | |||
| ab9e8375cb | |||
| 234a8c23c5 | |||
| a037cc4bb8 | |||
| c235c959e6 | |||
| 5201fc4055 | |||
| 77d12a6f8c | |||
| 592becc486 | |||
| 96b20b7d58 | |||
| 3ca246432d | |||
| 29a1f8dc27 | |||
| d49729bf1a | |||
| 12dbd16f7b | |||
| 8fa28ada45 | |||
| 881bdbc5dc | |||
| f63ae0a523 | |||
| 50d3b72d0b | |||
| aa306c4159 | |||
| 9214729d2f | |||
| 5c7100a67c | |||
| a002d6038b |
59
ISSUE_TEMPLATE/bug_report.yaml
Normal file
59
ISSUE_TEMPLATE/bug_report.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: "🐛 Сообщить об ошибке"
|
||||||
|
description: "Создайте отчёт, чтобы помочь нам исправить проблему"
|
||||||
|
title: "[BUG]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Спасибо, что помогаете сделать Futriix лучше!
|
||||||
|
Пожалуйста, опишите проблему максимально подробно.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: "Описание ошибки"
|
||||||
|
description: "Чётко и ясно опишите, в чём заключается проблема."
|
||||||
|
placeholder: "Например: При попытке сохранить документ с полем типа 'timestamp' возникает паника..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: "Шаги для воспроизведения"
|
||||||
|
description: "Опишите пошагово, что нужно сделать, чтобы увидеть ошибку."
|
||||||
|
placeholder: |
|
||||||
|
1. Запустить СУБД с конфигом...
|
||||||
|
2. Отправить запрос...
|
||||||
|
3. Увидеть ошибку...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: "Версия Futriix"
|
||||||
|
description: "Какую версию вы используете? (Например, 2.0 i²)"
|
||||||
|
placeholder: "v2.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: "Операционная система"
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- Windows
|
||||||
|
- macOS
|
||||||
|
- Другая
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: "Логи или вывод терминала"
|
||||||
|
description: "Вставьте сюда любые сообщения об ошибках или логи (можно в виде текста)."
|
||||||
|
render: shell
|
||||||
36
ISSUE_TEMPLATE/feature_request.yaml
Normal file
36
ISSUE_TEMPLATE/feature_request.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: "🚀 Предложить новую функцию"
|
||||||
|
description: "Поделитесь идеей, которая улучшит Futriix"
|
||||||
|
title: "[FEATURE]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Мы открыты к вашим идеям!
|
||||||
|
Расскажите, какую возможность вы хотели бы видеть.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: idea
|
||||||
|
attributes:
|
||||||
|
label: "Ваша идея"
|
||||||
|
description: "Опишите, что бы вы хотели добавить или изменить."
|
||||||
|
placeholder: "Было бы здорово реализовать поддержку индексов для поля 'name'..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: benefit
|
||||||
|
attributes:
|
||||||
|
label: "Какую проблему это решает?"
|
||||||
|
description: "Объясните, зачем это нужно вам и другим пользователям."
|
||||||
|
placeholder: "Это ускорит поиск по документам в 3 раза..."
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: lua_example
|
||||||
|
attributes:
|
||||||
|
label: "Пример использования (если связано с Lua-плагином)"
|
||||||
|
description: "Если ваша идея касается системы плагинов, приведите пример кода на Lua."
|
||||||
|
render: lua
|
||||||
|
placeholder: |
|
||||||
|
-- Например, как бы вы хотели использовать новый движок?
|
||||||
|
local my_engine = futriix.new_engine("custom")
|
||||||
27
ISSUE_TEMPLATE/question.yaml
Normal file
27
ISSUE_TEMPLATE/question.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: "❓ Задать вопрос"
|
||||||
|
description: "Нужна помощь или совет по использованию Futriix"
|
||||||
|
title: "[QUESTION]: "
|
||||||
|
labels: ["question"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Мы здесь, чтобы помочь!
|
||||||
|
Не стесняйтесь задавать любые вопросы по установке, настройке или использованию СУБД.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: question
|
||||||
|
attributes:
|
||||||
|
label: "Ваш вопрос"
|
||||||
|
description: "Чётко сформулируйте, что именно вас интересует."
|
||||||
|
placeholder: "Как правильно настроить кластер из двух узлов?"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: "Дополнительный контекст"
|
||||||
|
description: "Расскажите, что вы уже попробовали, или приложите фрагмент кода/конфига."
|
||||||
|
placeholder: "Мой конфиг выглядит так: ..."
|
||||||
|
render: yaml
|
||||||
106
README.md
106
README.md
@@ -63,12 +63,13 @@
|
|||||||
## О проекте
|
## О проекте
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> **ALPHA VERSION**<br><br>**Проект стабилен в тестовых сценариях, но категорически не рекомендуется для промышленного использования до выхода версии 3.0. Мы открыты к обратной связи и тестированию!**
|
> **ALPHA VERSION**<br><br>**Проект достаточно стабилен в тестовых сценариях, но категорически не рекомендуется для промышленного использования до выхода версии 3.0. Мы открыты для предложений, и рады обратной связи!**
|
||||||
|
|
||||||
futriix - это легковесная, распределённая NOSQL, использующая алгоритмы неблокирующей синхронизации - `wait-free` и `lock-free` in-memory СУБД, реализованная на языке Go с поддержкой плагинов на языке lua использующая алгоритм консенсуса Raft. <br>
|
futriix - это легковесная, распределённая NOSQL, использующая алгоритмы неблокирующей синхронизации - `wait-free` и `lock-free` in-memory СУБД, реализованная на языке Go с поддержкой плагинов на языке lua использующая алгоритм консенсуса Raft. <br>
|
||||||
По своей сути, Futriis является лёгкой HTAP-СУБД с уклоном в OLTP благодаря wait-free структурам и ACID-транзакциям, а также мощными OLAP-возможностями через индексы, триггеры, Lua-плагины и аналитические функции. Это позволяет обрабатывать и анализировать данные в режиме, близком к реальному времени, в первую очередь для эксплуатации в замкнутых программных средах под управлением ОС на базе ядра Illumos и Solaris (OpenIndiana Hipster, Oracle Solaris). Архитектурное решение использовать отдельные индексы от данных, атомарные операции и версионирование позволяет эффективно обрабатывать смешанную нагрузку без необходимости применять отдельные системы для OLTP и OLAP.<br>
|
По своей сути, Futriis является лёгкой HTAP-СУБД с уклоном в OLTP благодаря wait-free структурам и ACID-транзакциям, а также мощными OLAP-возможностями через индексы, триггеры, Lua-плагины и аналитические функции. Это позволяет обрабатывать и анализировать данные в режиме, близком к реальному времени, в первую очередь для эксплуатации в замкнутых программных средах под управлением ОС на базе ядра Illumos и Solaris (OpenIndiana Hipster, Oracle Solaris). <br>
|
||||||
|
Архитектурное решение использовать отдельные индексы от данных, атомарные операции и версионирование позволяет эффективно обрабатывать смешанную нагрузку без необходимости применять отдельные системы для OLTP и OLAP.<br>
|
||||||
В futriix реализован WUI-интерфейс для удобства её администрирования и эксплуатации.
|
В futriix реализован WUI-интерфейс для удобства её администрирования и эксплуатации.
|
||||||
Но она также совместима с популярными Linux-дистрибутивами (Debian,Ubuntu,Fedora), т.е. операционными системами построенными на базе ядра Linux.
|
Кроме того она совместима с популярными Linux-дистрибутивами (Debian,Ubuntu,Fedora).
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||||
|
|
||||||
@@ -85,14 +86,16 @@ futriix - это легковесная, распределённая NOSQL, и
|
|||||||
|
|
||||||
* **База Данных(БД)** - это структурированное, организованное хранилище данных, которое позволяет удобно собирать, хранить, управлять и извлекать информацию.
|
* **База Данных(БД)** - это структурированное, организованное хранилище данных, которое позволяет удобно собирать, хранить, управлять и извлекать информацию.
|
||||||
* **Система Управления Базами Данных(СУБД)** - это программное обеспечение, которое позволяет создавать, управлять и взаимодействовать с базами данных
|
* **Система Управления Базами Данных(СУБД)** - это программное обеспечение, которое позволяет создавать, управлять и взаимодействовать с базами данных
|
||||||
* **Таппл (Tapple)** - аналог базы данных в РСУБД
|
* **Слайс (Slice-в пер.с англ.слой)**- в терминах субд Futriix имеет два значения:
|
||||||
* **Слайс (Slice)** - аналог таблицы
|
* **1.** Cиноним "база данных"
|
||||||
* **Кортеж (Tuple)** - аналог записи в таблице
|
* **2.** Термин пришедший из терминологии суд MongoDB, а именно: Логический и физически изолированный фрагмент коллекции документов, полученный в результате горизонтального партиционирования (шардирования) и размещенный на определенном узле кластера с целью масштабирования производительности и объема данных.
|
||||||
|
* **Коллекция (Collection)** - аналог таблицы
|
||||||
|
* **Поле (Field)** - аттрибут документов
|
||||||
|
* **Кортеж (Tuple)** - вложенный документ в субд Futriix
|
||||||
* **Мультимодельная СУБД** - это СУБД, которая объединяет в себе поддержку нескольких моделей данных (реляционной, документной, графовой, ключ-значение и др.) в рамках единого интегрированного ядра.
|
* **Мультимодельная СУБД** - это СУБД, которая объединяет в себе поддержку нескольких моделей данных (реляционной, документной, графовой, ключ-значение и др.) в рамках единого интегрированного ядра.
|
||||||
* **Резидентная СУБД** - это СУБД, которая работает непрерывно в оперативной памяти (RAM).
|
* **Резидентная СУБД** - это СУБД, которая работает непрерывно в оперативной памяти (RAM).
|
||||||
* **Инстанс** - это запущенный экземляр базы данных.
|
* **Инстанс** - это запущенный экземляр базы данных.
|
||||||
* **Узел (хост,нода,шард)** - это отдельный сервер (физический или виртуальный), который является частью кластера или распределенной системы и выполняет часть общей работы.
|
* **Узел (хост,нода,шард)** - это отдельный сервер (физический или виртуальный), который является частью кластера или распределенной системы и выполняет часть общей работы.
|
||||||
* **Слайс (от англ. "slice"-слой)** - это логический и физически изолированный фрагмент коллекции документов, полученный в результате горизонтального партиционирования (шардирования) и размещенный на определенном узле кластера с целью масштабирования производительности и объема данных.
|
|
||||||
* **Репликасет** - это группа серверов СУБД, объединенных в отказоустойчивую конфигурацию, где один узел выполняет роль первичного (принимающего операции записи), а один или несколько других - роль вторичных (синхронизирующих свои данные с первичным и обслуживающих чтение), с автоматическим переизбранием первичного узла в случае его сбоя.
|
* **Репликасет** - это группа серверов СУБД, объединенных в отказоустойчивую конфигурацию, где один узел выполняет роль первичного (принимающего операции записи), а один или несколько других - роль вторичных (синхронизирующих свои данные с первичным и обслуживающих чтение), с автоматическим переизбранием первичного узла в случае его сбоя.
|
||||||
* **Временная метка (time stamp)** - это автоматически фиксируемое значение времени создания, изменения или удаления объекта в СУБД, представленное в формате Unix-миллисекунд (количество миллисекунд, прошедших с 1 января 1970 года) с возможностью отображения в человекочитаемом виде YYYY-MM-DD HH:MM:SS.mmm. Необходима для понимание последовательности операций при сбоях, выявления аномалий в поведении системы,архивация старых записей, отслеживание несанкционированных изменений, автоматический выход из сессий с ограниченным временем действия, Статистика активности за период)
|
* **Временная метка (time stamp)** - это автоматически фиксируемое значение времени создания, изменения или удаления объекта в СУБД, представленное в формате Unix-миллисекунд (количество миллисекунд, прошедших с 1 января 1970 года) с возможностью отображения в человекочитаемом виде YYYY-MM-DD HH:MM:SS.mmm. Необходима для понимание последовательности операций при сбоях, выявления аномалий в поведении системы,архивация старых записей, отслеживание несанкционированных изменений, автоматический выход из сессий с ограниченным временем действия, Статистика активности за период)
|
||||||
* **OLTP (Online Transactional Processing-Онлайн обработка транзакций)**- это технология обработки транзакций в режиме реального времени. Её основная задача заключается в обеспечении быстрого и надёжного выполнения операций, которые происходят ежесекундно в бизнесе. Они обеспечивают быстрое выполнение операций вставки, обновления и удаления данных, поддерживая целостность и надежность транзакций.
|
* **OLTP (Online Transactional Processing-Онлайн обработка транзакций)**- это технология обработки транзакций в режиме реального времени. Её основная задача заключается в обеспечении быстрого и надёжного выполнения операций, которые происходят ежесекундно в бизнесе. Они обеспечивают быстрое выполнение операций вставки, обновления и удаления данных, поддерживая целостность и надежность транзакций.
|
||||||
@@ -364,20 +367,27 @@ done
|
|||||||
> СУБД futriiX полностью совместима по синтаксису запросов с NOSQL-субд MongoDB.
|
> СУБД futriiX полностью совместима по синтаксису запросов с NOSQL-субд MongoDB.
|
||||||
> Вы можете использовать знакомые NoSQL-конструкции и драйверы, легко мигрируя с существующей инфраструктуры.
|
> Вы можете использовать знакомые NoSQL-конструкции и драйверы, легко мигрируя с существующей инфраструктуры.
|
||||||
|
|
||||||
В субд futriix поддерживаются MongoDB-подобный синтаксис для команд, являющийся основными в любой субд из класса **CRUD (Create,Read, Update, Delete- Создание, Чтение, Обновление, Удаление**) работы с документами: db.collection.insert(), find(), update(), delete(). Операции выполняются wait-free с использованием атомарных sync.Map, поддерживают автоматическую генерацию _id и валидацию схемы. Кроме того, каждый объект субд будь то таппл или поле, содержат в себе автоматически создаваемую временную метку, обладающей следующими свойствами:
|
Futriix поддерживает привычный MongoDB‑синтаксис для работы с документами.
|
||||||
|
Вы можете использовать стандартные команды: insert(), find(), update(), delete() — точно так же, как в MongoDB.
|
||||||
|
Все CRUD‑операции (создание, чтение, обновление, удаление) выполняются без блокировок (wait‑free) и гарантируют корректную работу при параллельном доступе за счёт атомарных структур данных.
|
||||||
|
При этом система автоматически:
|
||||||
|
|
||||||
|
* Создаёт уникальный идентификатор _id для каждого документа;
|
||||||
|
* Проверяет соответствие схеме (валидация);
|
||||||
|
* Добавляет временную метку к каждому объекту (таблице или полю).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>**Выполнение операций**<br>
|
||||||
|
> Операции выполняются в рамках гарантий изоляции, обеспечиваемых MVCC и протоколом консенсуса Raft.
|
||||||
|
> Для распределённых транзакций применяется протокол 2PC.
|
||||||
|
|
||||||
* Автоматические — не требуют ручного указания
|
|
||||||
* Атомарные — фиксируются на уровне каждой операции
|
|
||||||
* Высокоточные — до миллисекунды
|
|
||||||
* Неизменяемые — created_at никогда не меняется
|
|
||||||
* Аудируемые — все изменения протоколируются в audit log
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Создание новой базы данных
|
# Создание новой базы данных
|
||||||
futriix:~> create database company
|
futriix:~> create slice company
|
||||||
✓ Database 'company' created at 2026-01-15 10:30:45.123
|
✓ Database 'company' created at 2026-01-15 10:30:45.123
|
||||||
|
|
||||||
futriix:~> create database shop
|
futriix:~> create slice shop
|
||||||
✓ Database 'shop' created at 2026-01-15 10:30:46.456
|
✓ Database 'shop' created at 2026-01-15 10:30:46.456
|
||||||
|
|
||||||
# Переключение на базу данных
|
# Переключение на базу данных
|
||||||
@@ -624,6 +634,48 @@ futriix:~> db.startSession()
|
|||||||
В субд **futriix** для обеспечения надёжности (durability) и быстрого восстановления бд из резевной копии, реализована полноценная поддержка WAL (Write-Ahead Logging). Журнал WAL по умолчанию хранится в файле **futriix.wal**, находящимся в каталоге **futriix**, а также поддержка ACID-транзакций с MVCC (Multi-Version Concurrency Control).
|
В субд **futriix** для обеспечения надёжности (durability) и быстрого восстановления бд из резевной копии, реализована полноценная поддержка WAL (Write-Ahead Logging). Журнал WAL по умолчанию хранится в файле **futriix.wal**, находящимся в каталоге **futriix**, а также поддержка ACID-транзакций с MVCC (Multi-Version Concurrency Control).
|
||||||
Доступны команды startSession(), startTransaction(), commitTransaction(), abortTransaction() с восстановлением после сбоев через журнал предзаписи.
|
Доступны команды startSession(), startTransaction(), commitTransaction(), abortTransaction() с восстановлением после сбоев через журнал предзаписи.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Матрица гарантий согласованности** <br>
|
||||||
|
> Ниже приведена матрица гарантий согласованности, призванная дать ответ на потенциальный возможный вопрос пользователя: "А вдруг у меня
|
||||||
|
> данные потеряются"?
|
||||||
|
|
||||||
|
# Матрица гарантий согласованности Futriix
|
||||||
|
|
||||||
|
| Операция | Уровень изоляции | Поведение при нормальной работе | Поведение при сетевых сбоях | Восстановление после сбоя |
|
||||||
|
|----------|------------------|--------------------------------|----------------------------|---------------------------|
|
||||||
|
| **Чтение (Read)** | **Read Committed** + MVCC | • Чтение всегда возвращает только закоммиченные данные<br>• MVCC предоставляет снапшот на момент начала чтения<br>• Используется Visibility Map для быстрого доступа к версиям<br>• Кеширование через ReadTimestampCache | • При потере соединения - операция повторяется автоматически (идиемпотентность)<br>• Если узел недоступен - запрос перенаправляется к реплике (Raft)<br>• Чтение из кэша возможно при недоступности основного узла | • После восстановления узла - чтение продолжается с момента прерывания<br>• WAL не требуется для чтения (только для записи)<br>• Снапшоты MVCC восстанавливаются из documentVersions |
|
||||||
|
| **Запись (Write)** | **Read Committed** | • Запись сначала сохраняется в WAL (атомарно)<br>• Затем применяется к in-memory структурам<br>• Индексы обновляются атомарно с документом<br>• Атомарное обновление метаданных коллекции | • Если WAL запись не подтверждена - операция не применяется<br>• При сбое до коммита - данные не теряются (WAL)<br>• При потере лидера (Raft) - запись перенаправляется к новому лидеру<br>• Используется fsync для гарантии записи на диск | • После восстановления - запись повторяется из WAL<br>• Автоматическое применение WAL при загрузке<br>• Консистентность через LSN (Log Sequence Number)<br>• Segmented WAL позволяет восстанавливать частичные записи |
|
||||||
|
| **Транзакция (BEGIN...COMMIT)** | **Read Committed / Repeatable Read** (MVCC) | • Транзакция атомарна: все или ничего<br>• MVCC предоставляет снапшот на момент начала транзакции<br>• Операции внутри транзакции изолированы<br>• Savepoints для частичного отката<br>• Deadlock detection с автоматическим abort-ом | • При сбое в середине транзакции - автоматический откат<br>• WAL гарантирует восстановление закоммиченных транзакций<br>• 2PC для распределённых транзакций с таймаутом (10 сек)<br>• Prepared транзакции автоматически откатываются при таймауте | • Асинхронное восстановление из WAL<br>• Восстановление только закоммиченных транзакций<br>• Pending 2PC транзакции проверяются на таймаут<br>• Recovery Manager обрабатывает записи параллельно |
|
||||||
|
| **Индексация** | **Read Committed** | • Индексы обновляются атомарно с документом<br>• Уникальные индексы проверяются перед вставкой<br>• Индексы хранятся в sync.Map (wait-free)<br>• Поддержка составных индексов | • Индексы восстанавливаются из данных при загрузке<br>• При сбое - индексы перестраиваются автоматически<br>• Консистентность через LSN<br>• Уникальные индексы проверяются при восстановлении | • Индексы перестраиваются из документов при старте<br>• WAL содержит операции обновления индексов<br>• Автоматическое восстановление целостности индексов<br>• Проверка дубликатов при восстановлении уникальных индексов |
|
||||||
|
| **Удаление (Delete)** | **Read Committed** | • Soft delete сохраняет данные (метка deleted_at)<br>• Удаление из индексов происходит атомарно<br>• Физическое удаление - только после подтверждения<br>• Атомарное обновление счётчиков | • При сбое - операция повторяется из WAL<br>• Soft delete защищает от потери данных<br>• Окончательное удаление - после подтверждения всеми репликами<br>• Permanent delete требует подтверждения | • Soft delete записи восстанавливаются из WAL<br>• Permanent delete повторяется из WAL<br>• Счётчики документов восстанавливаются атомарно |
|
||||||
|
| **Репликация (Raft)** | **Linearizable** (для записей) / **Read Committed** (для чтений) | • Записи проходят через Raft-журнал<br>• Линейная консистентность для записей<br>• Чтения могут быть из реплик (Read Committed)<br>• Автоматическое переизбрание лидера | • При сбое лидера - автоматический выбор нового (Raft)<br>• Запросы перенаправляются к новому лидеру<br>• Данные согласованы через журнал Raft<br>• Read-запросы могут обслуживаться репликами | • Raft журнал восстанавливается из WAL<br>• Новый лидер применяет все закоммиченные записи<br>• Реплики синхронизируются с лидером<br>• Автоматическое восстановление кластера |
|
||||||
|
| **Шардирование (Range Shards)** | **Read Committed** | • Диапазонные шарды с динамическим сплитом/мерджем<br>• Данные распределены по диапазонам ключей<br>• Предсказуемая нагрузка на узлы<br>• Возможность предварительного добавления узлов под горячие диапазоны | • При сбое узла - шард перераспределяется<br>• Данные реплицируются для отказоустойчивости<br>• Автоматический сплит при переполнении шарда<br>• Мердж при уменьшении нагрузки | • Шарды восстанавливаются из чекпоинтов<br>• WAL применяется к каждому шарду<br>• Балансировка после восстановления<br>• Автоматическое перераспределение |
|
||||||
|
| **Checkpoint / Snapshot** | **Read Committed** | • Периодическое сохранение снапшотов (каждые 5 мин)<br>• Атомарная запись с временным файлом<br>• Контрольная сумма для верификации<br>• Сжатие снапшотов (gzip)<br>• Хранение последних N чекпоинтов | • При сбое во время создания - временный файл удаляется<br>• Предыдущий чекпоинт остаётся целым<br>• Атомарное переименование гарантирует целостность<br>• WAL LSN сохраняется в чекпоинте | • Загрузка из последнего валидного чекпоинта<br>• Применение WAL после чекпоинта<br>• Проверка контрольной суммы при загрузке<br>• Автоматическое восстановление из чекпоинта |
|
||||||
|
| **WAL (Write-Ahead Log)** | **Persistent** | • Все записи сначала идут в WAL<br>• Segmented WAL с ротацией (64 MB)<br>• Пакетная запись с fsync<br>• CRC32 для верификации целостности<br>• Асинхронная запись с буферизацией | • При сбое - WAL сохраняет все записи<br>• fsync гарантирует запись на диск<br>• Segments позволяют восстанавливать частичные данные<br>• Индекс LSN для быстрого поиска | • Полное восстановление из WAL<br>• Асинхронный Recovery Manager<br>• Параллельное применение записей (4 воркера)<br>• Восстановление 2PC транзакций |
|
||||||
|
| **2PC (Distributed Transactions)** | **Read Committed** | • Двухфазный коммит для распределённых транзакций<br>• Состояния: Active → Prepared → Committed/Aborted<br>• Координатор отслеживает статус узлов<br>• Таймаут Prepared транзакций (10 сек) | • При сбое координатора - транзакция в Prepared состоянии<br>• Автоматический откат по таймауту<br>• Узлы могут завершить транзакцию автономно<br>• Pending транзакции сохраняются в WAL | • Восстановление pending 2PC транзакций<br>• Проверка таймаута при старте<br>• Автоматический откат просроченных транзакций<br>• Голосование узлов для завершения |
|
||||||
|
| **Deadlock Detection** | **N/A** | • Обнаружение циклических ожиданий<br>• Автоматический abort одной из транзакций<br>• Периодическая проверка (каждую секунду)<br>• Поддержка распределённых deadlock-ов | • При сетевых задержках - возможны ложные срабатывания<br>• Abort транзакции с последующим повтором<br>• Распределённый deadlock detection через координатор | • Deadlock граф восстанавливается из активных транзакций<br>• Автоматическое разрешение при старте<br>• Логирование deadlock-ов для анализа |
|
||||||
|
| **MVCC (Multi-Version Concurrency Control)** | **Repeatable Read** (внутри транзакции) | • Каждая запись хранит несколько версий<br>• Чтение получает версию на момент начала транзакции<br>• Автоматическая очистка старых версий (каждые 5 мин)<br>• Хранение до 100 версий на документ<br>• Retention: 7 дней | • Версии сохраняются в памяти и WAL<br>• При сбое - версии восстанавливаются из WAL<br>• Visibility Map ускоряет проверку видимости<br>• ReadTimestampCache для быстрого доступа | • Версии восстанавливаются из WAL записей<br>• Автоматическая очистка устаревших версий<br>• Восстановление Visibility Map<br>• Кеш перестраивается при загрузке |
|
||||||
|
|
||||||
|
### Условные обозначения:
|
||||||
|
|
||||||
|
| Символ | Значение |
|
||||||
|
|--------|----------|
|
||||||
|
| ✅ **Гарантировано** | Операция гарантирует консистентность при любых условиях |
|
||||||
|
| ⚠️ **Частично** | Требует дополнительных проверок или имеет ограничения |
|
||||||
|
| 🔄 **Автоматически** | Восстановление происходит без вмешательства пользователя |
|
||||||
|
| 📊 **Настраивается** | Поведение можно настроить через конфигурацию |
|
||||||
|
|
||||||
|
## Ключевые гарантии:
|
||||||
|
|
||||||
|
1. **ACID**: Все операции соответствуют ACID благодаря WAL + MVCC + 2PC
|
||||||
|
2. **Durability**: fsync + WAL гарантируют сохранность данных
|
||||||
|
3. **Consistency**: Чекпоинты + контрольные суммы + верификация
|
||||||
|
4. **Isolation**: MVCC + Read Committed + Repeatable Read внутри транзакций
|
||||||
|
5. **Availability**: Raft + репликация + автоматическое переизбрание лидера
|
||||||
|
6. **Partition Tolerance**: 2PC + таймауты + автоматический откат
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> WAL- это журнал предзаписи транзакций, который в субд **futriix** выполняет следующие действия:
|
> WAL- это журнал предзаписи транзакций, который в субд **futriix** выполняет следующие действия:
|
||||||
> 1. Хранит все операции транзакций до их фиксации (INSERT, UPDATE, DELETE)
|
> 1. Хранит все операции транзакций до их фиксации (INSERT, UPDATE, DELETE)
|
||||||
@@ -1735,6 +1787,18 @@ futriix:~> doc compression employees 550e8400-e29b-41d4-a716-446655440000
|
|||||||
- [x] Реализовать Прозрачное обновление схемы данных
|
- [x] Реализовать Прозрачное обновление схемы данных
|
||||||
- [x] Реализовать в системе бекапов только "планировщик бэкапов" и "инкрементальность (на основе WAL)"
|
- [x] Реализовать в системе бекапов только "планировщик бэкапов" и "инкрементальность (на основе WAL)"
|
||||||
- [x] Реализовать механизм плагинов, с помощью которого можно на языке lua реализовать движки для субд, не изменяя её исходный код
|
- [x] Реализовать механизм плагинов, с помощью которого можно на языке lua реализовать движки для субд, не изменяя её исходный код
|
||||||
|
- [x] Реализовать атомарную запись через временный файл (Обеспечение целостности чекпоинтов (снапшотов) базы данных)
|
||||||
|
- [x] Реализовать контрольную сумму SHA-256 для чекпоинтов
|
||||||
|
- [x] Реализовать блокировку при восстановлении
|
||||||
|
- [x] Реализовать проверку целостности при загрузке
|
||||||
|
- [x] Реализовать вывод информации о восстановлении
|
||||||
|
- [x] Реализовать блокировку при восстановлении
|
||||||
|
- [x] Реализовать очистку WAL сегментов после бэкапа
|
||||||
|
- [x] Реализовать Проверку версии бэкапа
|
||||||
|
- [x] Реализовать статусы бэкапов (pending-running-completed-failed)
|
||||||
|
- [x] Реализовать атомарнуя запись бэкапов (Обеспечение целостности файлов бэкапов)
|
||||||
|
- [x] Реализована оптимизация ядра субд (удалён файл "/internal/storage/storage.go", методы "Storage" и "NewStorage" вынесены в "/internal/storage/engine.go")
|
||||||
|
- [x] Реализация записи "пакетами записи" для WAL-журнала (flushBatch)
|
||||||
- [ ] Реализовать Multi-Raft (Несколько независимых Raft групп для разных шардов)
|
- [ ] Реализовать Multi-Raft (Несколько независимых Raft групп для разных шардов)
|
||||||
- [ ] Реализовать (Linearizable чтения) чтения через лидера с кворумом для строгой консистентности в кластере
|
- [ ] Реализовать (Linearizable чтения) чтения через лидера с кворумом для строгой консистентности в кластере
|
||||||
- [ ] Реализовать Learner узлы узлы для репликации без права голоса (для бэкапов)
|
- [ ] Реализовать Learner узлы узлы для репликации без права голоса (для бэкапов)
|
||||||
@@ -1757,6 +1821,16 @@ futriix:~> doc compression employees 550e8400-e29b-41d4-a716-446655440000
|
|||||||
|
|
||||||
## Контакты
|
## Контакты
|
||||||
|
|
||||||
Григорий Сафронов - [E-mail](gvsafronov@yandex.ru)
|
По вопросам эксплуатации, производительности и консультации по настройки конфигурации, просьба обращаться по контактам указанными ниже в данном разделе.<br>
|
||||||
|
**Обращаем ваше внимание, что личная почта — это технический контакт, предназначенный только для:**
|
||||||
|
|
||||||
|
* **Сообщений о критических уязвимостях (с подробным PoC)**
|
||||||
|
* **Предложений о сотрудничестве**
|
||||||
|
* **Преложений по улучшений futriix**
|
||||||
|
|
||||||
|
Обращаем ваше внимание, что письма без четко сформулированной темы, вопроса или предложения рассматриваться не будут.
|
||||||
|
|
||||||
|
Григорий Сафронов - [E-mail](gvsafronov@yandex.ru) <br>
|
||||||
|
Сообщество - [Сообщество Futriix](https://source.futriix.ru/gvsafronov/futriix/issues/new/choose)
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
// Файл: internal/backup/backup_scheduler.go
|
// Файл: internal/backup/backup_scheduler.go
|
||||||
// Назначение: Планировщик бэкапов и инкрементальные бэкапы на основе WAL
|
// Назначение: Планировщик бэкапов и инкрементальные бэкапы на основе WAL
|
||||||
|
// Использует 2PC для инкрементных бекапов и статистику по 2PC транзакциям
|
||||||
|
|
||||||
package backup
|
package backup
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"futriis/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackupType тип бэкапа
|
// BackupType тип бэкапа
|
||||||
@@ -66,6 +69,8 @@ type BackupInfo struct {
|
|||||||
ParentID string `json:"parent_id,omitempty"`
|
ParentID string `json:"parent_id,omitempty"`
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
// ДОБАВЛЕНО: информация о 2PC состоянии
|
||||||
|
TwoPCCount int `json:"two_pc_count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WALEntry запись WAL
|
// WALEntry запись WAL
|
||||||
@@ -431,27 +436,32 @@ type BackupScheduler struct {
|
|||||||
running atomic.Bool
|
running atomic.Bool
|
||||||
backupCounter atomic.Uint64
|
backupCounter atomic.Uint64
|
||||||
config *BackupSchedulerConfig
|
config *BackupSchedulerConfig
|
||||||
|
// ДОБАВЛЕНО: ссылка на менеджер транзакций для 2PC
|
||||||
|
txManager *storage.TransactionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupSchedulerConfig конфигурация планировщика
|
// BackupSchedulerConfig конфигурация планировщика
|
||||||
type BackupSchedulerConfig struct {
|
type BackupSchedulerConfig struct {
|
||||||
BackupDir string `json:"backup_dir"`
|
BackupDir string `json:"backup_dir"`
|
||||||
MaxConcurrent int `json:"max_concurrent"`
|
MaxConcurrent int `json:"max_concurrent"`
|
||||||
CompressEnabled bool `json:"compress_enabled"`
|
CompressEnabled bool `json:"compress_enabled"`
|
||||||
CompressionLevel int `json:"compression_level"`
|
CompressionLevel int `json:"compression_level"`
|
||||||
MaxBackupSizeMB int64 `json:"max_backup_size_mb"`
|
MaxBackupSizeMB int64 `json:"max_backup_size_mb"`
|
||||||
EnableIncremental bool `json:"enable_incremental"`
|
EnableIncremental bool `json:"enable_incremental"`
|
||||||
|
// ДОБАВЛЕНО: включить сохранение 2PC состояния
|
||||||
|
IncludeTwoPCState bool `json:"include_two_pc_state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultBackupSchedulerConfig возвращает конфигурацию по умолчанию
|
// DefaultBackupSchedulerConfig возвращает конфигурацию по умолчанию
|
||||||
func DefaultBackupSchedulerConfig() *BackupSchedulerConfig {
|
func DefaultBackupSchedulerConfig() *BackupSchedulerConfig {
|
||||||
return &BackupSchedulerConfig{
|
return &BackupSchedulerConfig{
|
||||||
BackupDir: "backups",
|
BackupDir: "backups",
|
||||||
MaxConcurrent: 1,
|
MaxConcurrent: 1,
|
||||||
CompressEnabled: true,
|
CompressEnabled: true,
|
||||||
CompressionLevel: 6,
|
CompressionLevel: 6,
|
||||||
MaxBackupSizeMB: 10240, // 10GB
|
MaxBackupSizeMB: 10240, // 10GB
|
||||||
EnableIncremental: true,
|
EnableIncremental: true,
|
||||||
|
IncludeTwoPCState: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,6 +489,8 @@ func NewBackupScheduler(cfg *BackupSchedulerConfig, walReader WALReader, logger
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
// ДОБАВЛЕНО: получаем менеджер транзакций
|
||||||
|
txManager: storage.GetTransactionManager(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загружаем существующие расписания
|
// Загружаем существующие расписания
|
||||||
@@ -490,6 +502,11 @@ func NewBackupScheduler(cfg *BackupSchedulerConfig, walReader WALReader, logger
|
|||||||
return bs, nil
|
return bs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTransactionManager возвращает менеджер транзакций (ДОБАВЛЕНО)
|
||||||
|
func GetTransactionManager() *storage.TransactionManager {
|
||||||
|
return storage.GetTransactionManager()
|
||||||
|
}
|
||||||
|
|
||||||
// loadSchedules загружает расписания из хранилища
|
// loadSchedules загружает расписания из хранилища
|
||||||
func (bs *BackupScheduler) loadSchedules() {
|
func (bs *BackupScheduler) loadSchedules() {
|
||||||
data, err := bs.storage.LoadBackup("schedules.json")
|
data, err := bs.storage.LoadBackup("schedules.json")
|
||||||
@@ -650,12 +667,30 @@ func (bs *BackupScheduler) performFullBackup(scheduleID string) error {
|
|||||||
|
|
||||||
bs.logger.Info(fmt.Sprintf("Starting full backup %s", backupID))
|
bs.logger.Info(fmt.Sprintf("Starting full backup %s", backupID))
|
||||||
|
|
||||||
|
// ========== ДОБАВЛЕНО: Блокировка для 2PC ==========
|
||||||
|
if bs.txManager != nil && bs.config.IncludeTwoPCState {
|
||||||
|
bs.logger.Info("Locking transaction manager for backup...")
|
||||||
|
bs.txManager.LockForBackup()
|
||||||
|
defer bs.txManager.UnlockForBackup()
|
||||||
|
}
|
||||||
|
|
||||||
// Получаем текущую позицию WAL
|
// Получаем текущую позицию WAL
|
||||||
walIndex, err := bs.walReader.GetCurrentIndex()
|
walIndex, err := bs.walReader.GetCurrentIndex()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get WAL index: %v", err)
|
return fmt.Errorf("failed to get WAL index: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== ДОБАВЛЕНО: Получаем незавершённые 2PC транзакции ==========
|
||||||
|
var pending2PC []*storage.TransactionRecord
|
||||||
|
var twoPCCount int
|
||||||
|
if bs.txManager != nil && bs.config.IncludeTwoPCState {
|
||||||
|
pending2PC = bs.txManager.GetPending2PCTransactions()
|
||||||
|
twoPCCount = len(pending2PC)
|
||||||
|
if twoPCCount > 0 {
|
||||||
|
bs.logger.Info(fmt.Sprintf("Found %d pending 2PC transactions, including in backup", twoPCCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Сохраняем последний индекс для инкрементальных бэкапов
|
// Сохраняем последний индекс для инкрементальных бэкапов
|
||||||
if err := bs.walReader.SetLastBackupIndex(walIndex); err != nil {
|
if err := bs.walReader.SetLastBackupIndex(walIndex); err != nil {
|
||||||
bs.logger.Warn(fmt.Sprintf("Failed to set last backup index: %v", err))
|
bs.logger.Warn(fmt.Sprintf("Failed to set last backup index: %v", err))
|
||||||
@@ -668,14 +703,27 @@ func (bs *BackupScheduler) performFullBackup(scheduleID string) error {
|
|||||||
walEntries = []WALEntry{}
|
walEntries = []WALEntry{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== ДОБАВЛЕНО: Сохраняем состояние 2PC ==========
|
||||||
|
twoPCState := make(map[string]interface{})
|
||||||
|
if twoPCCount > 0 && bs.config.IncludeTwoPCState {
|
||||||
|
// Сохраняем только метаданные, не сами транзакции (они уже в WAL)
|
||||||
|
twoPCState["pending_count"] = twoPCCount
|
||||||
|
twoPCState["timestamp"] = time.Now().UnixMilli()
|
||||||
|
twoPCState["transaction_ids"] = make([]string, 0, twoPCCount)
|
||||||
|
for _, tx := range pending2PC {
|
||||||
|
twoPCState["transaction_ids"] = append(twoPCState["transaction_ids"].([]string), fmt.Sprintf("%d", tx.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Создаём структуру бэкапа
|
// Создаём структуру бэкапа
|
||||||
backupData := struct {
|
backupData := struct {
|
||||||
BackupID string `json:"backup_id"`
|
BackupID string `json:"backup_id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
StartTime int64 `json:"start_time"`
|
StartTime int64 `json:"start_time"`
|
||||||
WALIndex uint64 `json:"wal_index"`
|
WALIndex uint64 `json:"wal_index"`
|
||||||
Entries []WALEntry `json:"entries,omitempty"`
|
Entries []WALEntry `json:"entries,omitempty"`
|
||||||
Metadata interface{} `json:"metadata,omitempty"`
|
Metadata interface{} `json:"metadata,omitempty"`
|
||||||
|
TwoPCState interface{} `json:"two_pc_state,omitempty"`
|
||||||
}{
|
}{
|
||||||
BackupID: backupID,
|
BackupID: backupID,
|
||||||
Type: "full",
|
Type: "full",
|
||||||
@@ -683,10 +731,13 @@ func (bs *BackupScheduler) performFullBackup(scheduleID string) error {
|
|||||||
WALIndex: walIndex,
|
WALIndex: walIndex,
|
||||||
Entries: walEntries,
|
Entries: walEntries,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]interface{}{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"created_by": "backup_scheduler",
|
"created_by": "backup_scheduler",
|
||||||
"wal_entries_count": len(walEntries),
|
"wal_entries_count": len(walEntries),
|
||||||
|
"two_pc_pending_count": twoPCCount,
|
||||||
|
"include_two_pc": bs.config.IncludeTwoPCState,
|
||||||
},
|
},
|
||||||
|
TwoPCState: twoPCState,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(backupData)
|
data, err := json.Marshal(backupData)
|
||||||
@@ -726,6 +777,7 @@ func (bs *BackupScheduler) performFullBackup(scheduleID string) error {
|
|||||||
WALStart: 0,
|
WALStart: 0,
|
||||||
WALEnd: walIndex,
|
WALEnd: walIndex,
|
||||||
Checksum: checksum,
|
Checksum: checksum,
|
||||||
|
TwoPCCount: twoPCCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
bs.mu.Lock()
|
bs.mu.Lock()
|
||||||
@@ -740,8 +792,8 @@ func (bs *BackupScheduler) performFullBackup(scheduleID string) error {
|
|||||||
bs.cleanupOldBackups(scheduleID)
|
bs.cleanupOldBackups(scheduleID)
|
||||||
|
|
||||||
duration := (endTime - startTime) / 1000
|
duration := (endTime - startTime) / 1000
|
||||||
bs.logger.Info(fmt.Sprintf("Full backup %s completed in %d seconds, size=%d bytes, entries=%d",
|
bs.logger.Info(fmt.Sprintf("Full backup %s completed in %d seconds, size=%d bytes, entries=%d, pending_2pc=%d",
|
||||||
backupID, duration, backupInfo.SizeBytes, len(walEntries)))
|
backupID, duration, backupInfo.SizeBytes, len(walEntries), twoPCCount))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -756,6 +808,13 @@ func (bs *BackupScheduler) performIncrementalBackup(scheduleID string) error {
|
|||||||
startTime := time.Now().UnixMilli()
|
startTime := time.Now().UnixMilli()
|
||||||
backupID := fmt.Sprintf("inc_%d_%d", startTime, bs.backupCounter.Add(1))
|
backupID := fmt.Sprintf("inc_%d_%d", startTime, bs.backupCounter.Add(1))
|
||||||
|
|
||||||
|
// ========== ДОБАВЛЕНО: Блокировка для 2PC ==========
|
||||||
|
if bs.txManager != nil && bs.config.IncludeTwoPCState {
|
||||||
|
bs.logger.Info("Locking transaction manager for incremental backup...")
|
||||||
|
bs.txManager.LockForBackup()
|
||||||
|
defer bs.txManager.UnlockForBackup()
|
||||||
|
}
|
||||||
|
|
||||||
// Находим последний полный бэкап
|
// Находим последний полный бэкап
|
||||||
lastFullBackup := bs.findLastFullBackup()
|
lastFullBackup := bs.findLastFullBackup()
|
||||||
if lastFullBackup == nil {
|
if lastFullBackup == nil {
|
||||||
@@ -790,6 +849,17 @@ func (bs *BackupScheduler) performIncrementalBackup(scheduleID string) error {
|
|||||||
return fmt.Errorf("failed to get current WAL index: %v", err)
|
return fmt.Errorf("failed to get current WAL index: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== ДОБАВЛЕНО: Получаем незавершённые 2PC транзакции ==========
|
||||||
|
var pending2PC []*storage.TransactionRecord
|
||||||
|
var twoPCCount int
|
||||||
|
if bs.txManager != nil && bs.config.IncludeTwoPCState {
|
||||||
|
pending2PC = bs.txManager.GetPending2PCTransactions()
|
||||||
|
twoPCCount = len(pending2PC)
|
||||||
|
if twoPCCount > 0 {
|
||||||
|
bs.logger.Info(fmt.Sprintf("Found %d pending 2PC transactions in incremental backup", twoPCCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Создаём структуру инкрементального бэкапа
|
// Создаём структуру инкрементального бэкапа
|
||||||
backupData := struct {
|
backupData := struct {
|
||||||
BackupID string `json:"backup_id"`
|
BackupID string `json:"backup_id"`
|
||||||
@@ -800,6 +870,7 @@ func (bs *BackupScheduler) performIncrementalBackup(scheduleID string) error {
|
|||||||
ParentID string `json:"parent_id"`
|
ParentID string `json:"parent_id"`
|
||||||
Entries []WALEntry `json:"entries"`
|
Entries []WALEntry `json:"entries"`
|
||||||
Metadata interface{} `json:"metadata,omitempty"`
|
Metadata interface{} `json:"metadata,omitempty"`
|
||||||
|
TwoPCState interface{} `json:"two_pc_state,omitempty"`
|
||||||
}{
|
}{
|
||||||
BackupID: backupID,
|
BackupID: backupID,
|
||||||
Type: "incremental",
|
Type: "incremental",
|
||||||
@@ -809,12 +880,29 @@ func (bs *BackupScheduler) performIncrementalBackup(scheduleID string) error {
|
|||||||
ParentID: lastFullBackup.ID,
|
ParentID: lastFullBackup.ID,
|
||||||
Entries: walEntries,
|
Entries: walEntries,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]interface{}{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"created_by": "backup_scheduler",
|
"created_by": "backup_scheduler",
|
||||||
"entries_count": len(walEntries),
|
"entries_count": len(walEntries),
|
||||||
"parent_backup": lastFullBackup.ID,
|
"parent_backup": lastFullBackup.ID,
|
||||||
"parent_time": lastFullBackup.StartTime,
|
"parent_time": lastFullBackup.StartTime,
|
||||||
|
"two_pc_pending_count": twoPCCount,
|
||||||
|
"include_two_pc": bs.config.IncludeTwoPCState,
|
||||||
},
|
},
|
||||||
|
TwoPCState: map[string]interface{}{
|
||||||
|
"pending_count": twoPCCount,
|
||||||
|
"timestamp": time.Now().UnixMilli(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если есть 2PC транзакции, добавляем их ID
|
||||||
|
if twoPCCount > 0 && bs.config.IncludeTwoPCState {
|
||||||
|
txIDs := make([]string, 0, twoPCCount)
|
||||||
|
for _, tx := range pending2PC {
|
||||||
|
txIDs = append(txIDs, fmt.Sprintf("%d", tx.ID))
|
||||||
|
}
|
||||||
|
if twoPCState, ok := backupData.TwoPCState.(map[string]interface{}); ok {
|
||||||
|
twoPCState["transaction_ids"] = txIDs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(backupData)
|
data, err := json.Marshal(backupData)
|
||||||
@@ -824,7 +912,6 @@ func (bs *BackupScheduler) performIncrementalBackup(scheduleID string) error {
|
|||||||
|
|
||||||
// Проверяем размер бэкапа
|
// Проверяем размер бэкапа
|
||||||
if bs.config.MaxBackupSizeMB > 0 && int64(len(data)) > bs.config.MaxBackupSizeMB*1024*1024 {
|
if bs.config.MaxBackupSizeMB > 0 && int64(len(data)) > bs.config.MaxBackupSizeMB*1024*1024 {
|
||||||
// Если инкрементальный бэкап слишком большой, делаем полный
|
|
||||||
bs.logger.Info("Incremental backup too large, performing full backup instead")
|
bs.logger.Info("Incremental backup too large, performing full backup instead")
|
||||||
return bs.performFullBackup(scheduleID)
|
return bs.performFullBackup(scheduleID)
|
||||||
}
|
}
|
||||||
@@ -860,6 +947,7 @@ func (bs *BackupScheduler) performIncrementalBackup(scheduleID string) error {
|
|||||||
WALEnd: currentIndex,
|
WALEnd: currentIndex,
|
||||||
ParentID: lastFullBackup.ID,
|
ParentID: lastFullBackup.ID,
|
||||||
Checksum: checksum,
|
Checksum: checksum,
|
||||||
|
TwoPCCount: twoPCCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
bs.mu.Lock()
|
bs.mu.Lock()
|
||||||
@@ -873,8 +961,8 @@ func (bs *BackupScheduler) performIncrementalBackup(scheduleID string) error {
|
|||||||
bs.updateScheduleLastRun(scheduleID, startTime)
|
bs.updateScheduleLastRun(scheduleID, startTime)
|
||||||
|
|
||||||
duration := (endTime - startTime) / 1000
|
duration := (endTime - startTime) / 1000
|
||||||
bs.logger.Info(fmt.Sprintf("Incremental backup %s completed in %d seconds, %d entries, size=%d bytes",
|
bs.logger.Info(fmt.Sprintf("Incremental backup %s completed in %d seconds, %d entries, size=%d bytes, pending_2pc=%d",
|
||||||
backupID, duration, len(walEntries), backupInfo.SizeBytes))
|
backupID, duration, len(walEntries), backupInfo.SizeBytes, twoPCCount))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1017,10 +1105,11 @@ func (bs *BackupScheduler) Restore(backupID string) error {
|
|||||||
bs.logger.Info("Restoring from full backup...")
|
bs.logger.Info("Restoring from full backup...")
|
||||||
|
|
||||||
var fullData struct {
|
var fullData struct {
|
||||||
BackupID string `json:"backup_id"`
|
BackupID string `json:"backup_id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Entries []WALEntry `json:"entries"`
|
Entries []WALEntry `json:"entries"`
|
||||||
Metadata interface{} `json:"metadata"`
|
Metadata interface{} `json:"metadata"`
|
||||||
|
TwoPCState interface{} `json:"two_pc_state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &fullData); err != nil {
|
if err := json.Unmarshal(data, &fullData); err != nil {
|
||||||
@@ -1028,6 +1117,23 @@ func (bs *BackupScheduler) Restore(backupID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bs.logger.Info(fmt.Sprintf("Full backup contains %d WAL entries", len(fullData.Entries)))
|
bs.logger.Info(fmt.Sprintf("Full backup contains %d WAL entries", len(fullData.Entries)))
|
||||||
|
|
||||||
|
// ========== ДОБАВЛЕНО: Восстановление 2PC состояния ==========
|
||||||
|
if fullData.TwoPCState != nil && bs.config.IncludeTwoPCState {
|
||||||
|
bs.logger.Info("Restoring 2PC state from backup...")
|
||||||
|
// Здесь логика восстановления 2PC состояния
|
||||||
|
if twoPCState, ok := fullData.TwoPCState.(map[string]interface{}); ok {
|
||||||
|
if pendingCount, ok := twoPCState["pending_count"].(float64); ok && pendingCount > 0 {
|
||||||
|
bs.logger.Info(fmt.Sprintf("Found %d pending 2PC transactions in backup", int(pendingCount)))
|
||||||
|
// Восстанавливаем pending транзакции в менеджере
|
||||||
|
if bs.txManager != nil {
|
||||||
|
// Здесь можно восстановить состояние 2PC
|
||||||
|
// В реальной реализации нужно восстановить транзакции из WAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Здесь реальное восстановление данных
|
// Здесь реальное восстановление данных
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -1050,11 +1156,12 @@ func (bs *BackupScheduler) Restore(backupID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var incData struct {
|
var incData struct {
|
||||||
BackupID string `json:"backup_id"`
|
BackupID string `json:"backup_id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
WALStart uint64 `json:"wal_start"`
|
WALStart uint64 `json:"wal_start"`
|
||||||
WALEnd uint64 `json:"wal_end"`
|
WALEnd uint64 `json:"wal_end"`
|
||||||
Entries []WALEntry `json:"entries"`
|
Entries []WALEntry `json:"entries"`
|
||||||
|
TwoPCState interface{} `json:"two_pc_state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &incData); err != nil {
|
if err := json.Unmarshal(data, &incData); err != nil {
|
||||||
@@ -1063,6 +1170,17 @@ func (bs *BackupScheduler) Restore(backupID string) error {
|
|||||||
|
|
||||||
bs.logger.Info(fmt.Sprintf("Applying %d incremental changes (WAL %d -> %d)",
|
bs.logger.Info(fmt.Sprintf("Applying %d incremental changes (WAL %d -> %d)",
|
||||||
len(incData.Entries), incData.WALStart, incData.WALEnd))
|
len(incData.Entries), incData.WALStart, incData.WALEnd))
|
||||||
|
|
||||||
|
// ========== ДОБАВЛЕНО: Восстановление 2PC состояния из инкремента ==========
|
||||||
|
if incData.TwoPCState != nil && bs.config.IncludeTwoPCState {
|
||||||
|
bs.logger.Info("Restoring 2PC state from incremental backup...")
|
||||||
|
if twoPCState, ok := incData.TwoPCState.(map[string]interface{}); ok {
|
||||||
|
if pendingCount, ok := twoPCState["pending_count"].(float64); ok && pendingCount > 0 {
|
||||||
|
bs.logger.Info(fmt.Sprintf("Found %d pending 2PC transactions in incremental backup", int(pendingCount)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Здесь применяем инкрементальные изменения
|
// Здесь применяем инкрементальные изменения
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,6 +1271,7 @@ func (bs *BackupScheduler) GetStats() map[string]interface{} {
|
|||||||
fullCount := 0
|
fullCount := 0
|
||||||
incCount := 0
|
incCount := 0
|
||||||
totalSize := int64(0)
|
totalSize := int64(0)
|
||||||
|
total2PC := 0
|
||||||
|
|
||||||
for _, b := range bs.backups {
|
for _, b := range bs.backups {
|
||||||
if b.Type == FullBackup {
|
if b.Type == FullBackup {
|
||||||
@@ -1161,17 +1280,20 @@ func (bs *BackupScheduler) GetStats() map[string]interface{} {
|
|||||||
incCount++
|
incCount++
|
||||||
}
|
}
|
||||||
totalSize += b.SizeBytes
|
totalSize += b.SizeBytes
|
||||||
|
total2PC += b.TwoPCCount
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"total_backups": len(bs.backups),
|
"total_backups": len(bs.backups),
|
||||||
"full_backups": fullCount,
|
"full_backups": fullCount,
|
||||||
"incremental_backups": incCount,
|
"incremental_backups": incCount,
|
||||||
"total_size_bytes": totalSize,
|
"total_size_bytes": totalSize,
|
||||||
"schedules": len(bs.schedules),
|
"schedules": len(bs.schedules),
|
||||||
"is_running": bs.running.Load(),
|
"is_running": bs.running.Load(),
|
||||||
"backup_dir": bs.config.BackupDir,
|
"backup_dir": bs.config.BackupDir,
|
||||||
"compress_enabled": bs.config.CompressEnabled,
|
"compress_enabled": bs.config.CompressEnabled,
|
||||||
"incremental_enabled": bs.config.EnableIncremental,
|
"incremental_enabled": bs.config.EnableIncremental,
|
||||||
|
"include_two_pc_state": bs.config.IncludeTwoPCState,
|
||||||
|
"total_2pc_transactions": total2PC,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ func executeInsertWithTransaction(coll *storage.Collection, operationPart, dbNam
|
|||||||
}
|
}
|
||||||
|
|
||||||
if storage.HasActiveTransaction() {
|
if storage.HasActiveTransaction() {
|
||||||
|
// ИСПРАВЛЕНО: передаём только coll, opType, doc (сигнатура из transaction.go)
|
||||||
if err := storage.AddToTransaction(coll, "insert", doc); err != nil {
|
if err := storage.AddToTransaction(coll, "insert", doc); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,10 +94,11 @@ func NewRepl(store *storage.Storage, coordinator *cluster.RaftCoordinator, logge
|
|||||||
// registerCommands регистрирует все команды REPL
|
// registerCommands регистрирует все команды REPL
|
||||||
func (r *Repl) registerCommands() {
|
func (r *Repl) registerCommands() {
|
||||||
// Команды управления базами данных
|
// Команды управления базами данных
|
||||||
r.commands["create database"] = &Command{
|
// ИСПРАВЛЕНО: заменено "slice database" на "create slice"
|
||||||
Name: "create database",
|
r.commands["create slice"] = &Command{
|
||||||
Description: "Create a new database",
|
Name: "create slice",
|
||||||
Handler: r.handleCreateDatabase,
|
Description: "Create a new database (slice)",
|
||||||
|
Handler: r.handleCreateSlice,
|
||||||
}
|
}
|
||||||
|
|
||||||
r.commands["drop database"] = &Command{
|
r.commands["drop database"] = &Command{
|
||||||
@@ -574,9 +575,10 @@ func (r *Repl) addToHistory(cmd string) {
|
|||||||
|
|
||||||
// ========== Обработчики команд ==========
|
// ========== Обработчики команд ==========
|
||||||
|
|
||||||
func (r *Repl) handleCreateDatabase(args []string) error {
|
// ИСПРАВЛЕНО: переименован обработчик с handleSliceDatabase на handleCreateSlice
|
||||||
|
func (r *Repl) handleCreateSlice(args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return fmt.Errorf("usage: create database <name>")
|
return fmt.Errorf("usage: create slice <name>")
|
||||||
}
|
}
|
||||||
|
|
||||||
name := args[0]
|
name := args[0]
|
||||||
@@ -584,7 +586,7 @@ func (r *Repl) handleCreateDatabase(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.PrintSuccess(fmt.Sprintf("Database '%s' created at %s", name, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Slice '%s' created at %s", name, time.Now().Format("2006-01-02 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2400,7 +2402,8 @@ func (r *Repl) handleHelp(args []string) error {
|
|||||||
description string
|
description string
|
||||||
}{
|
}{
|
||||||
"Database Management": {
|
"Database Management": {
|
||||||
{"create database <name>", "Create a new database"},
|
// ИСПРАВЛЕНО: обновлена справка с "create slice" вместо "slice database"
|
||||||
|
{"create slice <name>", "Create a new database (slice)"},
|
||||||
{"drop database <name>", "Delete an existing database"},
|
{"drop database <name>", "Delete an existing database"},
|
||||||
{"use <database>", "Switch to a specific database"},
|
{"use <database>", "Switch to a specific database"},
|
||||||
{"show databases", "List all available databases"},
|
{"show databases", "List all available databases"},
|
||||||
@@ -2507,7 +2510,8 @@ func (r *Repl) handleHelp(args []string) error {
|
|||||||
|
|
||||||
utils.Println("")
|
utils.Println("")
|
||||||
utils.PrintInfo("Examples:")
|
utils.PrintInfo("Examples:")
|
||||||
utils.Println(" create database test")
|
// ИСПРАВЛЕНО: обновлены примеры с "create slice" вместо "slice database"
|
||||||
|
utils.Println(" create slice test")
|
||||||
utils.Println(" use test")
|
utils.Println(" use test")
|
||||||
utils.Println(" create collection users")
|
utils.Println(" create collection users")
|
||||||
utils.Println(" insert users name=John,age=30")
|
utils.Println(" insert users name=John,age=30")
|
||||||
|
|||||||
@@ -455,12 +455,15 @@ func (c *Collection) Insert(doc *Document) error {
|
|||||||
// Обновление индексов (wait-free)
|
// Обновление индексов (wait-free)
|
||||||
c.updateIndexes(doc, true)
|
c.updateIndexes(doc, true)
|
||||||
|
|
||||||
// Обновление метаданных
|
// Обновление метаданных - ИСПРАВЛЕНО: добавлена блокировка для согласованности
|
||||||
c.docCount.Add(1)
|
c.docCount.Add(1)
|
||||||
c.sizeBytes.Add(int64(len(data)))
|
c.sizeBytes.Add(int64(len(data)))
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
c.metadata.DocumentCount = c.docCount.Load()
|
c.metadata.DocumentCount = c.docCount.Load()
|
||||||
c.metadata.SizeBytes = c.sizeBytes.Load()
|
c.metadata.SizeBytes = c.sizeBytes.Load()
|
||||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Аудит вставки документа
|
// Аудит вставки документа
|
||||||
AuditDocumentOperation("INSERT", c.dbName, c.name, doc.ID, doc.GetFields())
|
AuditDocumentOperation("INSERT", c.dbName, c.name, doc.ID, doc.GetFields())
|
||||||
@@ -628,7 +631,9 @@ func (c *Collection) Update(id string, updates map[string]interface{}) error {
|
|||||||
// Сохраняем обновлённый документ
|
// Сохраняем обновлённый документ
|
||||||
c.docs.Store(id, newDoc)
|
c.docs.Store(id, newDoc)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Аудит обновления документа
|
// Аудит обновления документа
|
||||||
AuditDocumentOperation("UPDATE", c.dbName, c.name, id, updates)
|
AuditDocumentOperation("UPDATE", c.dbName, c.name, id, updates)
|
||||||
@@ -671,8 +676,10 @@ func (c *Collection) Delete(id string) error {
|
|||||||
|
|
||||||
// Обновляем метаданные
|
// Обновляем метаданные
|
||||||
c.docCount.Add(-1)
|
c.docCount.Add(-1)
|
||||||
|
c.mu.Lock()
|
||||||
c.metadata.DocumentCount = c.docCount.Load()
|
c.metadata.DocumentCount = c.docCount.Load()
|
||||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -698,8 +705,10 @@ func (c *Collection) PermanentDelete(id string) error {
|
|||||||
|
|
||||||
// Обновляем метаданные
|
// Обновляем метаданные
|
||||||
c.docCount.Add(-1)
|
c.docCount.Add(-1)
|
||||||
|
c.mu.Lock()
|
||||||
c.metadata.DocumentCount = c.docCount.Load()
|
c.metadata.DocumentCount = c.docCount.Load()
|
||||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Аудит физического удаления
|
// Аудит физического удаления
|
||||||
AuditDocumentOperation("PERMANENT_DELETE", c.dbName, c.name, id, nil)
|
AuditDocumentOperation("PERMANENT_DELETE", c.dbName, c.name, id, nil)
|
||||||
@@ -726,7 +735,9 @@ func (c *Collection) RestoreDeleted(id string) error {
|
|||||||
c.addToIndexes(doc)
|
c.addToIndexes(doc)
|
||||||
|
|
||||||
c.docs.Store(id, doc)
|
c.docs.Store(id, doc)
|
||||||
|
c.mu.Lock()
|
||||||
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
c.metadata.UpdatedAt = time.Now().UnixMilli()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Аудит восстановления
|
// Аудит восстановления
|
||||||
AuditDocumentOperation("RESTORE", c.dbName, c.name, id, nil)
|
AuditDocumentOperation("RESTORE", c.dbName, c.name, id, nil)
|
||||||
|
|||||||
@@ -714,6 +714,11 @@ func (pm *PersistenceManager) LoadDatabase(dbName string) error {
|
|||||||
|
|
||||||
// applyWALEntry применяет одну запись WAL к базе данных
|
// applyWALEntry применяет одну запись WAL к базе данных
|
||||||
func (pm *PersistenceManager) applyWALEntry(db *Database, entry WALEntry) error {
|
func (pm *PersistenceManager) applyWALEntry(db *Database, entry WALEntry) error {
|
||||||
|
// Проверяем тип записи (1 = Transaction) - ИСПРАВЛЕНО
|
||||||
|
if entry.Type != "1" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var txRecord struct {
|
var txRecord struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
State int32 `json:"state"`
|
State int32 `json:"state"`
|
||||||
@@ -732,13 +737,12 @@ func (pm *PersistenceManager) applyWALEntry(db *Database, entry WALEntry) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Применяем только закоммиченные транзакции
|
// Применяем только закоммиченные транзакции (state = 1)
|
||||||
if txRecord.State != 1 { // TransactionCommitted
|
if txRecord.State != 1 { // TransactionCommitted
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, op := range txRecord.Operations {
|
for _, op := range txRecord.Operations {
|
||||||
// Пропускаем операции для других баз данных
|
|
||||||
if op.Database != db.name {
|
if op.Database != db.name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user