Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9a5c8307c | ||
|
|
0fcc0998ba | ||
|
|
6c599b94bf | ||
|
|
ae12264e27 | ||
|
|
a7a3cd8cdb | ||
|
|
73061c2883 | ||
|
|
7fb359a462 | ||
|
|
ec05741a70 | ||
|
|
b418243503 | ||
|
|
4adf1b62db | ||
|
|
96930e8e42 | ||
|
|
f0e28510e9 | ||
|
|
e96c5a7a24 | ||
|
|
a9d6aa42e5 | ||
|
|
78bff3e4d8 | ||
|
|
d8673b747f | ||
|
|
96dc6311ea | ||
|
|
93385022d2 | ||
|
|
e41f557719 | ||
|
|
452c8853f4 | ||
|
|
2a9114083b | ||
|
|
13fe261b40 | ||
|
|
73773c27da | ||
|
|
b934417417 | ||
|
|
5422e39ab2 | ||
|
|
98bae0cbd6 | ||
|
|
52d0216676 | ||
|
|
3a1de92233 | ||
|
|
56a91d9fc8 | ||
|
|
1559e12a51 | ||
|
|
2e84b86d23 | ||
|
|
6b9ee6b3d2 | ||
|
|
a373f05b18 | ||
|
|
a73aa8bc9d | ||
|
|
a024994943 | ||
|
|
aaada4008c | ||
|
|
c5a5286314 | ||
|
|
b26a42ab26 | ||
|
|
d0613d6aa1 | ||
|
|
11e79007cc | ||
|
|
6b3317ce94 | ||
|
|
e5b34c75d8 | ||
|
|
d4ea0533d2 | ||
|
|
9b5bb176b3 | ||
|
|
a533d973c0 | ||
|
|
7fd2addabe | ||
|
|
5ffccfe885 | ||
|
|
ffc66dbed7 | ||
|
|
afff868301 | ||
|
|
87bed4495b | ||
|
|
58ba3b363e | ||
|
|
d59b878cd4 | ||
|
|
54d1c0ba09 | ||
|
|
f480b910fc | ||
|
|
a772e2d2a0 | ||
|
|
90ca83f9a3 | ||
|
|
16267d2ea8 | ||
|
|
5e230b1a9c | ||
|
|
ab48d50988 | ||
|
|
bc445ad7be | ||
|
|
e895568beb | ||
|
|
b8217788e4 | ||
|
|
9c6400ba3e | ||
|
|
c736906ad6 | ||
|
|
69385655a2 | ||
|
|
4bb819b397 | ||
|
|
82d073dd82 | ||
|
|
0153d30d05 | ||
|
|
3ebbf8a55c | ||
|
|
fbd5f209ca | ||
|
|
955fa27ab3 | ||
|
|
233b280835 |
No files matched your search
@@ -41,7 +41,7 @@
|
|||||||
<li><a href="#тестирование">Тестирование</a></li>
|
<li><a href="#тестирование">Тестирование</a></li>
|
||||||
<li><a href="#crud-операции">CRUD операции</a></li>
|
<li><a href="#crud-операции">CRUD операции</a></li>
|
||||||
<li><a href="#индексы">Индексы</a></li>
|
<li><a href="#индексы">Индексы</a></li>
|
||||||
<li><a href="#транзакции">Транзакции</a></li>
|
<li><a href="#batch">Batch</a></li>
|
||||||
<li><a href="#кластеризация-и-шардинг">Кластеризация и шардинг</a></li>
|
<li><a href="#кластеризация-и-шардинг">Кластеризация и шардинг</a></li>
|
||||||
<li><a href="#backpressure">Backpressure</a></li>
|
<li><a href="#backpressure">Backpressure</a></li>
|
||||||
<li><a href="#геораспределенная-миграция">Геораспределенная миграция</a></li>
|
<li><a href="#геораспределенная-миграция">Геораспределенная миграция</a></li>
|
||||||
@@ -68,14 +68,14 @@
|
|||||||
|
|
||||||
futriix — это лёгкая распределённая NoSQL‑СУБД на Go с MongoDB‑совместимым интерфейсом. Работает в памяти (in‑memory), использует неблокирующие структуры (wait‑free и lock‑free) и алгоритм консенсуса Raft — что даёт предсказуемую производительность под высокой нагрузкой.<br>
|
futriix — это лёгкая распределённая NoSQL‑СУБД на Go с MongoDB‑совместимым интерфейсом. Работает в памяти (in‑memory), использует неблокирующие структуры (wait‑free и lock‑free) и алгоритм консенсуса Raft — что даёт предсказуемую производительность под высокой нагрузкой.<br>
|
||||||
|
|
||||||
Это HTAP‑система: сочетает OLTP (с ACID‑транзакциями) и OLAP (через индексы, триггеры, Lua‑плагины и аналитические функции) — можно обрабатывать и анализировать данные почти в реальном времени. <br>
|
Это HTAP‑система: сочетает OLTP (Batch-команды вместо ACID‑транзакций) и OLAP (через индексы, триггеры, Lua‑плагины и аналитические функции) — можно обрабатывать и анализировать данные почти в реальном времени. <br>
|
||||||
|
|
||||||
В основе управления данными лежат — временные метки: они обеспечивают версионирование записей, корректную работу MVCC и упорядочивание событий в распределённой среде. Это позволяет точно отслеживать изменения, гарантировать согласованность данных между узлами и корректно разрешать конфликты при параллельных обновлениях. <br>
|
В основе управления данными лежат — временные метки: они обеспечивают версионирование записей и упорядочивание событий в распределённой среде. Это позволяет точно отслеживать изменения, гарантировать согласованность данных между узлами и корректно разрешать конфликты при параллельных обновлениях. <br>
|
||||||
|
|
||||||
Ключевые механизмы надёжности и масштабируемости:
|
Ключевые механизмы надёжности и масштабируемости:
|
||||||
|
|
||||||
* WAL — устойчивость к сбоям
|
* WAL — устойчивость к сбоям
|
||||||
* MVCC — изоляция транзакций
|
* SAGA- для внешних batch
|
||||||
* Горизонтальное масштабирование
|
* Горизонтальное масштабирование
|
||||||
|
|
||||||
|
|
||||||
@@ -215,14 +215,14 @@ futriix — это лёгкая распределённая NoSQL‑СУБД н
|
|||||||
|----------|----------|----------|
|
|----------|----------|----------|
|
||||||
| **Хранилище (Storage)** | `sync.Map` | Конкурентная хэш‑таблица для хранения баз данных, обеспечивает wait‑free чтение и запись |
|
| **Хранилище (Storage)** | `sync.Map` | Конкурентная хэш‑таблица для хранения баз данных, обеспечивает wait‑free чтение и запись |
|
||||||
| | Атомарные счётчики (`atomic.Int64`) | Для отслеживания общего количества документов без блокировок |
|
| | Атомарные счётчики (`atomic.Int64`) | Для отслеживания общего количества документов без блокировок |
|
||||||
| **База данных (Database)** | `sync.Map` коллекций | Синоним термнина "база данных" в реляционных СУБД |
|
| **База данных (Database)** | `sync.Map` коллекций | Синоним термина "база данных" в реляционных СУБД |
|
||||||
| | Мьютексы (`sync.RWMutex`) | Для операций изменения структуры (создание/удаление коллекций) |
|
| | Мьютексы (`sync.RWMutex`) | Для операций изменения структуры (создание/удаление коллекций) |
|
||||||
| **Коллекция (Collection)** | `sync.Map` документов | Ключ: ID документа, значение: указатель на `Document` |
|
| **Коллекция (Collection)** | `sync.Map` документов | Ключ: ID документа, значение: указатель на `Document` |
|
||||||
| | `sync.Map` индексов | Отдельное хранилище для инвертированных индексов |
|
| | `sync.Map` индексов | Отдельное хранилище для инвертированных индексов |
|
||||||
| | Атомарные счётчики | Количество документов (`docCount`) и размер в байтах (`sizeBytes`) |
|
| | Атомарные счётчики | Количество документов (`docCount`) и размер в байтах (`sizeBytes`) |
|
||||||
| **Индекс (Index)** | `sync.Map` (значение → ID) | Реализация инвертированного индекса |
|
| **Индекс (Index)** | **`lock-free skip-list`** (значение → ID) | Реализация инвертированного индекса. Заменяет `sync.Map`: устраняет внутренние блокировки, даёт упорядоченный обход и эффективный префиксный поиск |
|
||||||
| | Уникальный индекс | Прямая lookup‑операция $O(1)$ |
|
| | Уникальный индекс | Прямая lookup‑операция $O(\log n)$ в среднем (было $O(1)$ у `sync.Map`, но без упорядоченности и с внутренними блокировками) |
|
||||||
| | Неуникальный индекс | Range‑сканирование с оптимизированным сравнением |
|
| | Неуникальный индекс | Range‑сканирование в порядке возрастания ключей (детерминированный результат) |
|
||||||
| | Составной индекс | Конкатенация значений полей с разделителем `"|"` |
|
| | Составной индекс | Конкатенация значений полей с разделителем `"|"` |
|
||||||
| **Документ (Document)** | `map[string]interface{}` | Динамическая схема полей |
|
| **Документ (Document)** | `map[string]interface{}` | Динамическая схема полей |
|
||||||
| | Версия (`uint64`) | Для оптимистичных блокировок |
|
| | Версия (`uint64`) | Для оптимистичных блокировок |
|
||||||
@@ -618,7 +618,7 @@ $ ./futriix
|
|||||||
|
|
||||||
В субд **"futriix"** используется два журнала для ведение логов: `"futriix.log"`-основной журнал, в котором ведутся логи при работе в субд через терминал.
|
В субд **"futriix"** используется два журнала для ведение логов: `"futriix.log"`-основной журнал, в котором ведутся логи при работе в субд через терминал.
|
||||||
|
|
||||||
**futriix.log** — основной системный журнал, фиксирующий все события жизненного цикла СУБД: запуск/остановку сервера, инициализацию компонентов (транзакции, Raft-координатор, ACL), состояние кластера и критические ошибки выполнения запросов.
|
**futriix.log** — основной системный журнал, фиксирующий все события жизненного цикла СУБД: запуск/остановку сервера, инициализацию компонентов (batch, Raft-координатор, ACL), состояние кластера и критические ошибки выполнения запросов.
|
||||||
|
|
||||||
Журнал использует структурированный JSON-формат (для webui.log) и текстовый формат с временными метками (для futriis.log), что обеспечивает удобный парсинг и интеграцию с системами мониторинга.
|
Журнал использует структурированный JSON-формат (для webui.log) и текстовый формат с временными метками (для futriis.log), что обеспечивает удобный парсинг и интеграцию с системами мониторинга.
|
||||||
|
|
||||||
@@ -942,148 +942,105 @@ futriix:~> db.startSession()
|
|||||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||||
|
|
||||||
|
|
||||||
## Транзакции
|
## Batch
|
||||||
|
|
||||||
|
В СУБД **futriix** для обеспечения надёжности (durability), атомарности и быстрого восстановления базы данных реализована двухуровневая система журналирования:
|
||||||
|
|
||||||
|
1. **WAL (Write-Ahead Log)** — сегментированный журнал предзаписи для **batch-операций** (атомарных групп изменений). По умолчанию хранится в каталоге `futriix/wal_segments/`.
|
||||||
|
2. **AOF (Append-Only File)** — журнал дозаписи для **одиночных мутаций**, выполненных вне batch (прямые вызовы `Insert`, `Update`, `Delete`, `Restore`, а также операции из REPL, HTTP-API и Lua-плагинов). По умолчанию хранится в каталоге `futriix/aof/`.
|
||||||
|
|
||||||
|
Такое разделение даёт:
|
||||||
|
- **Атомарность** для сложных составных операций (batch).
|
||||||
|
- **Гарантированную сохранность** каждой отдельной мутации (AOF).
|
||||||
|
- **Быстрое восстановление** через воспроизведение обоих журналов при старте.
|
||||||
|
- **Разделение нагрузки**: batch-операции пишутся в WAL сегментами по 64 МБ, одиночные мутации — в AOF с ротацией по 64 МБ.
|
||||||
|
|
||||||
|
|
||||||
В СУБД **futriix** для обеспечения надёжности (durability) и быстрого восстановления БД из резервной копии реализована полноценная поддержка WAL (Write-Ahead Logging). Журнал WAL по умолчанию хранится в файле futriix.wal, находящемся в каталоге futriix.
|
##№ Что такое Batch
|
||||||
|
|
||||||
СУБД поддерживает ACID‑транзакции с MVCC (Multi‑Version Concurrency Control) — для локальной атомарности и изоляции на уровне отдельных узлов кластера. Для распределённых операций используется паттерн SAGA с оркестратором: глобальная согласованность достигается через последовательность компенсируемых локальных транзакций, управляемых централизованным оркестратором, который отслеживает состояние шагов, инициирует компенсацию при сбоях и гарантирует завершение сценария в согласованном состоянии.
|
**Batch** — это группа операций, применяемых **атомарно**: либо все операции из группы применяются, либо ни одна. Batch заменяет полноценные ACID-транзакции в локальном узле и работает по принципу «всё или ничего» через WAL.
|
||||||
|
|
||||||
Парадигма eventual consistency обеспечивает согласованность данных в распределённой среде: изменения распространяются асинхронно между узлами, а финальное согласованное состояние достигается со временем с учётом гарантий доставки событий и правил разрешения конфликтов. Такой подход позволяет сохранить высокую доступность и производительность даже при временных сетевых сбоях.
|
**Ключевые свойства batch:**
|
||||||
|
|
||||||
Доступны команды, с восстановлением после сбоев через журнал предзаписи:
|
- **Атомарность** через WAL: сначала весь batch сериализуется и записывается в WAL с `fsync`, только после успешной записи операции применяются к in-memory структурам.
|
||||||
|
- **Откат при ошибке применения**: если операция падает на середине применения, уже применённые операции откатываются (best-effort) — полная атомарность гарантируется на уровне WAL при восстановлении.
|
||||||
|
- **Короткое время жизни**: batch создаётся, наполняется операциями и коммитится в рамках одной логической операции. Не поддерживает savepoints, таймауты, распределённую координацию.
|
||||||
|
- **Реестр активных batch'ей**: `BatchManager.activeBatches` (см. `runtime_limits.go`) защищает документы, участвующие в незакоммиченном batch, от вытеснения (eviction) при нехватке памяти.
|
||||||
|
- **AOF-дублирование**: после коммита batch также пишется в AOF для ускорения восстановления.
|
||||||
|
|
||||||
* `startSession()`- Начать сессию (устойчивый контекст взаимодействия клиента с СУБД, в рамках которого поддерживается состояние) в субд в рамках которой будет открыта транзакция
|
**Операции batch:**
|
||||||
* `startTransaction()`- Начать транзакцию
|
|
||||||
* `commitTransaction()`- Сделать коммит транзакцию
|
|
||||||
* `abortTransaction()` - Прервать транзакцию
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
| Команда | Действие |
|
||||||
> **Матрица гарантий согласованности** <br>
|
|---------|----------|
|
||||||
> Ниже приведена матрица гарантий согласованности, призванная дать ответ на потенциальный возможный вопрос пользователя: "А вдруг у меня
|
| `NewBatch(db, coll)` | Создать новый batch для указанной БД и коллекции |
|
||||||
> данные потеряются"?
|
| `batch.AddInsert(doc)` | Добавить операцию вставки документа |
|
||||||
|
| `batch.AddUpdate(docID, updates)` | Добавить операцию обновления |
|
||||||
# Матрица гарантий согласованности Futriix
|
| `batch.AddDelete(docID)` | Добавить операцию удаления |
|
||||||
|
| `batch.AddRestore(docID)` | Добавить операцию восстановления |
|
||||||
| Операция | Уровень изоляции | Поведение при нормальной работе | Поведение при сетевых сбоях | Восстановление после сбоя |
|
| `batch.Commit()` | Применить batch атомарно через WAL |
|
||||||
|----------|------------------|--------------------------------|----------------------------|---------------------------|
|
|
||||||
| **Чтение (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>• SAGA с оркестратором для распределённых транзакций с таймаутом (10 сек)<br>• Pending‑шаги Saga автоматически компенсируются при таймауте | • Асинхронное восстановление из WAL<br>• Восстановление только закоммиченных транзакций<br>• Pending‑шаги SAGA проверяются на таймаут<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>• Восстановление Saga с оркестратором (pending‑шагов и компенсаций) |
|
|
||||||
| **SAGA с оркестратором** | **Read Committed** | • Оркестрированная Saga для распределённых транзакций<br>• Последовательность шагов с компенсационными действиями для каждого этапа<br>• Оркестратор управляет порядком выполнения и обработкой отказов<br>• Таймаут на выполнение шага (10 сек) | • При сбое оркестратора — шаг остаётся в незавершённом состоянии<br>• Автоматическая компенсация при превышении таймаута<br>• Отдельные шаги могут выполняться автономно на узлах<br>• Pending шаги и компенсации сохраняются в WAL | • Восстановление pending Saga‑шагов при старте<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 + SAGA c оркестратором
|
|
||||||
2. **Durability**: fsync + WAL гарантируют сохранность данных
|
|
||||||
3. **Consistency**: Чекпоинты + контрольные суммы + верификация
|
|
||||||
4. **Isolation**: MVCC + Read Committed + Repeatable Read внутри транзакций
|
|
||||||
5. **Availability**: Raft + репликация + автоматическое переизбрание лидера
|
|
||||||
6. **Partition Tolerance**: SAGA c оркестратором + таймауты + автоматический откат
|
|
||||||
|
|
||||||
|
|
||||||
|
### Что такое AOF
|
||||||
|
|
||||||
> [!TIP]
|
**AOF (Append-Only File)** — журнал дозаписи, который логирует **каждую отдельную мутацию**, выполненную вне batch. Это нужно, потому что прямые вызовы `coll.Insert()`, `coll.Update()`, `coll.Delete()` идут мимо WAL, и без AOF такие изменения могли бы потеряться при сбое.
|
||||||
> WAL- это журнал предзаписи транзакций, который в субд **futriix** выполняет следующие действия:
|
|
||||||
> 1. Хранит все операции транзакций до их фиксации (INSERT, UPDATE, DELETE)
|
|
||||||
> 2. Записывает каждое изменение последовательно с LSN (Log Sequence Number)
|
|
||||||
> 3. Используется для восстановления после сбоя через recoverFromWAL()
|
|
||||||
> 4. Формат: бинарный с CRC-контролем, размером записи и типом (Transaction/Checkpoint)
|
|
||||||
> 5. Расположение: /futriix/futriix.wal (один файл)
|
|
||||||
|
|
||||||
> [!TIP]
|
**Ключевые свойства AOF:**
|
||||||
> **Кроме того существуют так называемые **Периодические снимки состояния** журнала предзаписи, которые выполняют следующие функции:**
|
|
||||||
> 1. Фиксируют состояние всех активных транзакций на момент создания
|
|
||||||
> 2. Создаются автоматически каждые **5 минут (300 секунд)**
|
|
||||||
> 3. Именуются по маске: `futriis.wal.checkpoint.{timestamp}`
|
|
||||||
> 4. Хранят сжатое состояние: последний LSN + активные транзакции
|
|
||||||
> 5. Автоматически очищаются: остаётся только 5 последних чекпоинтов
|
|
||||||
> 6. Расположение: `/futriix/wal.checkpoint.wal` (несколько файлов файл)
|
|
||||||
> **Подводя итог, хочется отметить следующее: журнал предзаписи транзакций и периодические снимки состояния работают следующим образом:**
|
|
||||||
> 1. Штатная работа: WAL пишет каждую транзакцию
|
|
||||||
> 2. Периодически: создаётся checkpoint (снимок активных транзакций)
|
|
||||||
> 3. При сбое: восстанавливаемся с последнего checkpoint + доигрываем операции из WAL
|
|
||||||
|
|
||||||
|
- **Формат**: JSON-lines (по одной JSON-записи на строку) с CRC32 для верификации целостности.
|
||||||
|
- **Ротация**: файлы разбиваются по размеру 64 МБ, имена вида `aof_<N>.log`.
|
||||||
|
- **fsync**: каждая запись синхронизируется с диском через `file.Sync()` для гарантии сохранности.
|
||||||
|
- **Восстановление при старте**: `ReplayAOF(storage)` читает все файлы и применяет операции к хранилищу.
|
||||||
|
- **Идемпотентность**: повторное применение операции не ломает состояние (insert/update/delete идемпотентны по ключу).
|
||||||
|
- **Автоматическое логирование**: методы `Collection.Insert`, `Update`, `Delete`, `PermanentDelete`, `RestoreDeleted` вызывают `LogAOFInsert`, `LogAOFUpdate`, `LogAOFDelete`, `LogAOFRestore` соответственно.
|
||||||
|
|
||||||
```sh
|
**Типы записей AOF:**
|
||||||
# Начало сессии (с временем создания сессии)
|
|
||||||
futriix:~> db.startSession()
|
|
||||||
✓ Session started: session_12345 at 2026-01-15 10:55:15.123
|
|
||||||
|
|
||||||
# Начало транзакции в рамках сессии
|
| Тип | Описание |
|
||||||
futriix:~> session.startTransaction()
|
|-----|----------|
|
||||||
✓ Transaction started: TX_67890 at 2026-01-15 10:55:18.456
|
| `insert` | Вставка документа |
|
||||||
|
| `update` | Обновление документа |
|
||||||
|
| `delete` | Удаление документа (soft или permanent) |
|
||||||
|
| `restore` | Восстановление мягко удалённого документа |
|
||||||
|
|
||||||
# Выполнение операций в транзакции (с временем выполнения)
|
---
|
||||||
futriix:~> insert employees name=New User,position=Trainee,age=22
|
|
||||||
✓ Document inserted with ID: 550e8400-e29b-41d4-a716-446655440005 (created at: 2026-01-15 10:55:21.789)
|
|
||||||
|
|
||||||
futriix:~> update employees 550e8400-e29b-41d4-a716-446655440005 status=active
|
### SAGA для распределённых операций
|
||||||
✓ Document '550e8400-e29b-41d4-a716-446655440005' updated at 2026-01-15 10:55:24.012
|
|
||||||
|
|
||||||
# Подтверждение транзакции (с временем коммита)
|
Для **распределённых** операций (между узлами кластера, между сервисами, для долгоживущих бизнес-процессов) используется паттерн **SAGA с оркестратором**:
|
||||||
futriix:~> session.commitTransaction()
|
|
||||||
✓ Transaction committed successfully at 2026-01-15 10:55:27.345
|
|
||||||
|
|
||||||
# Откат транзакции (при ошибке) (с временем отката)
|
- Глобальная согласованность достигается через последовательность **компенсируемых локальных шагов**.
|
||||||
futriix:~> session.startTransaction()
|
- Каждый шаг SAGA может использовать **batch** для гарантии атомарности внутри себя.
|
||||||
✓ Transaction started: TX_67891 at 2026-01-15 10:56:10.123
|
- Оркестратор отслеживает состояние шагов, инициирует **компенсацию** при сбоях и гарантирует завершение сценария в согласованном состоянии.
|
||||||
|
- Состояние SAGA персистентно хранится в JSON-файлах (директория `saga_states/`), при старте pending-шаги восстанавливаются, а просроченные — компенсируются.
|
||||||
|
- Компенсации **идемпотентны**: повторный вызов не ломает состояние.
|
||||||
|
- При превышении таймаута шага (по умолчанию 10 сек) автоматически запускается компенсация.
|
||||||
|
|
||||||
futriix:~> insert employees name=Test User,position=Test,age=25
|
**Пример SAGA с batch внутри шага:**
|
||||||
✓ Document inserted with ID: 550e8400-e29b-41d4-a716-446655440006 (created at: 2026-01-15 10:56:13.456)
|
|
||||||
|
|
||||||
futriix:~> session.abortTransaction()
|
```go
|
||||||
✓ Transaction aborted, changes rolled back at 2026-01-15 10:56:16.789
|
saga := sagaManager.BeginSaga("order_flow")
|
||||||
|
|
||||||
# Просмотр активных транзакций (с временем начала)
|
saga.AddStep("create_order",
|
||||||
futriix:~> show transactions
|
func() error {
|
||||||
=== Active Transactions ===
|
batch := NewBatch("shop", "orders")
|
||||||
ID: TX_67892, Status: active, Operations: 2, Started: 2026-01-15 10:57:00.123
|
batch.AddInsert(orderDoc)
|
||||||
- INSERT: company.employees [user-uuid-001]
|
batch.AddUpdate("inv_1", map[string]interface{}{"qty": 5})
|
||||||
- UPDATE: company.employees [user-uuid-002]
|
return batch.Commit() // атомарность шага через WAL
|
||||||
ID: TX_67893, Status: active, Operations: 1, Started: 2026-01-15 10:57:05.456
|
},
|
||||||
- DELETE: company.employees [user-uuid-003]
|
func() error {
|
||||||
|
batch := NewBatch("shop", "orders")
|
||||||
|
batch.AddDelete(orderDoc.ID)
|
||||||
|
batch.AddUpdate("inv_1", map[string]interface{}{"qty": 10})
|
||||||
|
return batch.Commit() // атомарность компенсации
|
||||||
|
},
|
||||||
|
nil)
|
||||||
|
|
||||||
# Просмотр сессий (с временем создания)
|
saga.AddStep("reserve_payment",
|
||||||
futriix:~> show sessions
|
func() error { return paymentClient.Reserve(orderID, amount) }, // внешний вызов
|
||||||
=== Active Sessions ===
|
func() error { return paymentClient.Cancel(orderID) },
|
||||||
session_12345 - Created: 2026-01-15 10:55:15.123, Last active: 2026-01-15 10:57:30.456
|
nil)
|
||||||
session_12346 - Created: 2026-01-15 10:56:20.789, Last active: 2026-01-15 10:57:15.234
|
|
||||||
|
|
||||||
|
sagaManager.Execute(saga)
|
||||||
# Статистика транзакций с временными метками
|
|
||||||
futriix:~> stats transactions
|
|
||||||
=== Transaction Statistics ===
|
|
||||||
Total transactions: 150
|
|
||||||
Committed: 145 (96.7%)
|
|
||||||
Aborted: 5 (3.3%)
|
|
||||||
|
|
||||||
Average duration: 234 ms
|
|
||||||
Min duration: 45 ms
|
|
||||||
Max duration: 1523 ms
|
|
||||||
|
|
||||||
Transactions by hour:
|
|
||||||
10:00 - 11:00: 45 transactions
|
|
||||||
11:00 - 12:00: 52 transactions
|
|
||||||
12:00 - 13:00: 38 transactions
|
|
||||||
```
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
<p align="right">(<a href="#readme-top">К началу</a>)</p>
|
||||||
|
|
||||||
@@ -2814,7 +2771,7 @@ futriix:~> doc compression employees 550e8400-e29b-41d4-a716-446655440000
|
|||||||
- [x] Реализовать поддержку триггеров (обратных вызовов)
|
- [x] Реализовать поддержку триггеров (обратных вызовов)
|
||||||
- [x] Реализовать поддержку многопоточности
|
- [x] Реализовать поддержку многопоточности
|
||||||
- [x] Реализовать неблокирующие чтение/запись
|
- [x] Реализовать неблокирующие чтение/запись
|
||||||
- [x] Реализовать неблокирующие транзакции
|
- [x] Реализовать неблокирующие batch-команды (аналог batch в субд Valkey)
|
||||||
- [x] Реализовать constraints (Ограничения)
|
- [x] Реализовать constraints (Ограничения)
|
||||||
- [x] Реализовать мульти-мастер асинхронную репликацию через файл конфигурации
|
- [x] Реализовать мульти-мастер асинхронную репликацию через файл конфигурации
|
||||||
- [x] Реализовать логирование для субд
|
- [x] Реализовать логирование для субд
|
||||||
@@ -2832,7 +2789,6 @@ futriix:~> doc compression employees 550e8400-e29b-41d4-a716-446655440000
|
|||||||
- [x] Библиотека "raft-boltdb" заменить на встроенное файловое хранилище
|
- [x] Библиотека "raft-boltdb" заменить на встроенное файловое хранилище
|
||||||
- [x] Реализовать уникальные и составные индексы
|
- [x] Реализовать уникальные и составные индексы
|
||||||
- [x] Реализовать временные метки для основных объектов субд (таппл, коллекция, документ, поле, индекс, транзакция, ACL, узел кластера)
|
- [x] Реализовать временные метки для основных объектов субд (таппл, коллекция, документ, поле, индекс, транзакция, ACL, узел кластера)
|
||||||
- [x] Оптимизировать транзакции под высокие нагрузки (Реализовать: Пакетную запись WAL - вместо синхронной записи каждой операции, используется асинхронная запись пакетами по 100 записей, Буферизацию записей - 64KB буфер для WAL записей, Периодическую синхронизацию - синхронизация с диском каждые 5 секунд вместо каждой записи, Чекпоинты - периодическое создание контрольных точек состояния, Атомарные операции - использование atomic.Value и sync.Map для wait-free доступа)
|
|
||||||
- [x] Реализовать MVCC версионирование - поддержка множественных версий документов
|
- [x] Реализовать MVCC версионирование - поддержка множественных версий документов
|
||||||
- [x] Реализовать временные метки для объектов субд (Ограничений, Импорта-Экспорта, Триггеров, Lua-плагинов)
|
- [x] Реализовать временные метки для объектов субд (Ограничений, Импорта-Экспорта, Триггеров, Lua-плагинов)
|
||||||
- [x] Реализовать обработку состояний "split-brain"
|
- [x] Реализовать обработку состояний "split-brain"
|
||||||
@@ -2852,12 +2808,11 @@ futriix:~> doc compression employees 550e8400-e29b-41d4-a716-446655440000
|
|||||||
- [x] Реализовать Distributed Transactions - двухфазный коммит для распределённых транзакций
|
- [x] Реализовать Distributed Transactions - двухфазный коммит для распределённых транзакций
|
||||||
- [x] Реализовать Deadlock Detection - циклический детектор дедлоков с таймаутами (DFS по графу ожидания)
|
- [x] Реализовать Deadlock Detection - циклический детектор дедлоков с таймаутами (DFS по графу ожидания)
|
||||||
- [x] Реализовать Transaction Timeout - настраиваемый таймаут для долгих транзакций
|
- [x] Реализовать Transaction Timeout - настраиваемый таймаут для долгих транзакций
|
||||||
- [x] Реализовать Savepoints - точки сохранения внутри транзакции для частичного отката
|
|
||||||
- [x] Вынести настройи настройки MVCC,WAL и настройки плагинов из исходного кода проекта в файл конфигурации "config.toml"
|
- [x] Вынести настройи настройки MVCC,WAL и настройки плагинов из исходного кода проекта в файл конфигурации "config.toml"
|
||||||
- [x] Реализовать выделение жирным шифтом все информационные сообщения в скриптах сборки
|
- [x] Реализовать выделение жирным шифтом все информационные сообщения в скриптах сборки
|
||||||
- [x] Реализовать ограничения на количество создаваемых Lua-состояний в плагинах
|
- [x] Реализовать ограничения на количество создаваемых Lua-состояний в плагинах
|
||||||
- [x] Реализовать WAL recovery сделать асинхронным для ускорения скорости работы субд
|
- [x] Реализовать WAL recovery сделать асинхронным для ускорения скорости работы субд
|
||||||
- [x] Реализовать SAGA c оркестратором (Распределённые транзакции)
|
- [x] Реализовать SAGA c оркестратором (Распределённые batch)
|
||||||
- [x] Голосование узлов перед коммитом в протоколе Raft
|
- [x] Голосование узлов перед коммитом в протоколе Raft
|
||||||
- [x] Реализовать автоматическое масштабирование
|
- [x] Реализовать автоматическое масштабирование
|
||||||
- [x] Реализовать TLS, backpressure
|
- [x] Реализовать TLS, backpressure
|
||||||
|
|||||||
+96
-11
@@ -34,6 +34,34 @@
|
|||||||
* - ИСПРАВЛЕНО: acl.NewACLManager() теперь возвращает (*ACLManager, error).
|
* - ИСПРАВЛЕНО: acl.NewACLManager() теперь возвращает (*ACLManager, error).
|
||||||
* Обрабатываем ошибку и завершаемся через os.Exit(1) с логом,
|
* Обрабатываем ошибку и завершаемся через os.Exit(1) с логом,
|
||||||
* без panic и stack trace.
|
* без panic и stack trace.
|
||||||
|
*
|
||||||
|
* ИСПРАВЛЕНО (2026-10): после замены ACID-транзакций на batch-операции
|
||||||
|
* функции storage.InitTransactionManager, storage.SetTransactionLogger,
|
||||||
|
* storage.StopTransactionManager были удалены. Заменены на:
|
||||||
|
* - storage.InitBatchManager(walPath) — инициализирует batch-менеджер,
|
||||||
|
* WAL и AOF (append-only file для мутаций вне batch);
|
||||||
|
* - storage.SetBatchLogger(logger) — устанавливает логгер;
|
||||||
|
* - storage.StopBatchManager() — останавливает batch-менеджер,
|
||||||
|
* WAL и AOF.
|
||||||
|
* Дополнительно: при shutdown логируется состояние batch-статистики
|
||||||
|
* (количество выполненных batch'ей, операций, ошибок) для диагностики.
|
||||||
|
*
|
||||||
|
* ДОБАВЛЕНО (2026-10, выборы лидера SAGA через Raft):
|
||||||
|
* - SAGA-оркестратор теперь создаётся через
|
||||||
|
* storage.NewSagaOrchestratorWithRaft и получает bridge из
|
||||||
|
* raftCoordinator (raftCoordinator.GetSagaLeaderBridge()).
|
||||||
|
* Это позволяет в кластерном режиме выбирать SAGA-лидера через Raft
|
||||||
|
* и выполнять компенсации только на одном узле.
|
||||||
|
* - В single-node режиме bridge также возвращает корректные значения
|
||||||
|
* (IsLeader() == true), так что поведение не меняется.
|
||||||
|
* - Раньше оркестратор создавался через NewSagaOrchestratorWithConfig
|
||||||
|
* с raftAccessor == nil, что в кластерном режиме приводило к тому,
|
||||||
|
* что каждый узел считал себя SAGA-лидером. Идемпотентность шагов
|
||||||
|
* через saga_executed_keys.json защищала от дублей операций, но не
|
||||||
|
* от одновременной компенсации одной SAGA с двух узлов.
|
||||||
|
* - Порядок shutdown безопасен: sagaOrchestrator.Stop() вызывается
|
||||||
|
* раньше raftCoordinator.Stop(), поэтому bridge ещё жив, когда
|
||||||
|
* оркестратор отписывается от него.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@@ -118,12 +146,21 @@ func main() {
|
|||||||
store := storage.NewStorage(cfg.Storage.PageSizeMB, logger)
|
store := storage.NewStorage(cfg.Storage.PageSizeMB, logger)
|
||||||
storage.SetGlobalStorage(store)
|
storage.SetGlobalStorage(store)
|
||||||
|
|
||||||
|
// ========== BATCH MANAGER (замена TransactionManager) ==========
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): InitTransactionManager заменён на InitBatchManager.
|
||||||
|
// InitBatchManager инициализирует:
|
||||||
|
// - WAL (сегментированный журнал для batch-операций);
|
||||||
|
// - AOF (append-only file для мутаций вне batch).
|
||||||
|
// Директория AOF создаётся автоматически как filepath.Join(filepath.Dir(walPath), "aof").
|
||||||
walPath := filepath.Join(dataDir, "futriis.wal")
|
walPath := filepath.Join(dataDir, "futriis.wal")
|
||||||
if err := storage.InitTransactionManager(walPath); err != nil {
|
if err := storage.InitBatchManager(walPath); err != nil {
|
||||||
logger.Warn("Failed to initialize transaction manager: " + err.Error())
|
logger.Warn("Failed to initialize batch manager: " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
storage.SetTransactionLogger(logger)
|
storage.SetBatchLogger(logger)
|
||||||
logger.Info("Transaction manager initialized")
|
logger.Info("Batch manager initialized (WAL + AOF)")
|
||||||
|
logger.Info(fmt.Sprintf(" WAL path: %s", walPath))
|
||||||
|
logger.Info(fmt.Sprintf(" AOF path: %s", filepath.Join(dataDir, "aof")))
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.InitTriggerManager(logger)
|
storage.InitTriggerManager(logger)
|
||||||
@@ -147,18 +184,43 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== SAGA ORCHESTRATOR ==========
|
// ========== SAGA ORCHESTRATOR ==========
|
||||||
|
//
|
||||||
|
// ДОБАВЛЕНО (2026-10): используем NewSagaOrchestratorWithRaft и
|
||||||
|
// передаём bridge из raftCoordinator. Это позволяет в кластерном
|
||||||
|
// режиме выбирать SAGA-лидера через Raft, чтобы компенсации
|
||||||
|
// выполнялись только на одном узле.
|
||||||
|
//
|
||||||
|
// В single-node режиме bridge возвращает IsLeader() == true, так что
|
||||||
|
// поведение не меняется. В кластерном режиме только лидер Raft
|
||||||
|
// будет IsSagaLeader() == true, и только он будет выполнять SAGA.
|
||||||
|
//
|
||||||
|
// Если raftCoordinator == nil (не должно случаться — выше os.Exit(1)
|
||||||
|
// при ошибке), создаём оркестратор без Raft (single-node fallback).
|
||||||
var sagaOrchestrator *storage.SagaOrchestrator
|
var sagaOrchestrator *storage.SagaOrchestrator
|
||||||
if cfg.Saga.Enabled {
|
if cfg.Saga.Enabled {
|
||||||
sagaConfig := &cfg.Saga
|
sagaConfig := &cfg.Saga
|
||||||
sagaOrchestrator, err = storage.NewSagaOrchestratorWithConfig(
|
nodeID := cfg.Cluster.NodeIP + ":" + fmt.Sprint(cfg.Cluster.NodePort)
|
||||||
|
|
||||||
|
// Передаём bridge из Raft в качестве storage.SagaRaftAccessor.
|
||||||
|
// Тип bridge реализует интерфейс storage.SagaRaftAccessor
|
||||||
|
// (IsSagaLeader, GetSagaLeaderID, GetSagaCurrentTerm,
|
||||||
|
// RegisterSagaLeaderObserver).
|
||||||
|
var raftAccessor storage.SagaRaftAccessor
|
||||||
|
if raftCoordinator != nil {
|
||||||
|
raftAccessor = raftCoordinator.GetSagaLeaderBridge()
|
||||||
|
}
|
||||||
|
|
||||||
|
sagaOrchestrator, err = storage.NewSagaOrchestratorWithRaft(
|
||||||
sagaConfig,
|
sagaConfig,
|
||||||
cfg.Cluster.NodeIP+":"+fmt.Sprint(cfg.Cluster.NodePort),
|
nodeID,
|
||||||
|
raftAccessor,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(fmt.Sprintf("Failed to initialize saga orchestrator: %v", err))
|
logger.Warn(fmt.Sprintf("Failed to initialize saga orchestrator: %v", err))
|
||||||
} else {
|
} else {
|
||||||
logger.Info(fmt.Sprintf("Saga orchestrator initialized with %d coordinators", sagaConfig.GetCoordinatorCount()))
|
logger.Info(fmt.Sprintf("Saga orchestrator initialized with %d coordinators (raft leader election: %v)",
|
||||||
|
sagaConfig.GetCoordinatorCount(), raftAccessor != nil))
|
||||||
storage.SetGlobalSagaOrchestrator(sagaOrchestrator)
|
storage.SetGlobalSagaOrchestrator(sagaOrchestrator)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -369,6 +431,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ВАЖНО: sagaOrchestrator останавливается ДО raftCoordinator.
|
||||||
|
// Это гарантирует, что оркестратор отпишется от sagaLeaderBridge
|
||||||
|
// (и остановит leaderElector) до того, как bridge будет остановлен
|
||||||
|
// вместе с координатором. Двойной остановки bridge не будет.
|
||||||
if sagaOrchestrator != nil {
|
if sagaOrchestrator != nil {
|
||||||
logger.Info("Stopping SAGA orchestrator...")
|
logger.Info("Stopping SAGA orchestrator...")
|
||||||
sagaStopDone := make(chan struct{})
|
sagaStopDone := make(chan struct{})
|
||||||
@@ -431,16 +497,35 @@ func main() {
|
|||||||
logger.Warn("Node shutdown timeout")
|
logger.Warn("Node shutdown timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИСПРАВЛЕНО (2026-10): StopTransactionManager заменён на StopBatchManager.
|
||||||
|
// StopBatchManager синхронизирует WAL, закрывает его и закрывает AOF.
|
||||||
|
logger.Info("Stopping batch manager (WAL + AOF)...")
|
||||||
|
if stats := storage.GetBatchStats(); stats != nil {
|
||||||
|
logger.Info(fmt.Sprintf(" Batch stats: total_batches=%v, total_operations=%v, total_failed=%v, active_batches=%v",
|
||||||
|
stats["total_batches"], stats["total_operations"], stats["total_failed"], stats["active_batches"]))
|
||||||
|
}
|
||||||
|
batchStopDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
if err := storage.StopBatchManager(); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("Failed to stop batch manager: %v", err))
|
||||||
|
} else {
|
||||||
|
logger.Info("Batch manager stopped")
|
||||||
|
}
|
||||||
|
close(batchStopDone)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-batchStopDone:
|
||||||
|
// уже залогировано внутри горутины
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
logger.Warn("Batch manager shutdown timeout")
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Finalizing logger...")
|
logger.Info("Finalizing logger...")
|
||||||
if err := logger.Sync(); err != nil {
|
if err := logger.Sync(); err != nil {
|
||||||
fmt.Printf("Failed to sync logger: %v\n", err)
|
fmt.Printf("Failed to sync logger: %v\n", err)
|
||||||
}
|
}
|
||||||
logger.Close()
|
logger.Close()
|
||||||
|
|
||||||
if err := storage.StopTransactionManager(); err != nil {
|
|
||||||
fmt.Printf("Failed to stop transaction manager: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if aclManager != nil {
|
if aclManager != nil {
|
||||||
aclManager.Stop()
|
aclManager.Stop()
|
||||||
}
|
}
|
||||||
|
|||||||
+171
-55
@@ -20,6 +20,20 @@
|
|||||||
// Конструктор NewHTTPServerWithTLS принимает certFile/keyFile,
|
// Конструктор NewHTTPServerWithTLS принимает certFile/keyFile,
|
||||||
// Start() вызывает ListenAndServeTLS при включённом TLS.
|
// Start() вызывает ListenAndServeTLS при включённом TLS.
|
||||||
// Старый NewHTTPServer оставлен как обёртка для обратной совместимости.
|
// Старый NewHTTPServer оставлен как обёртка для обратной совместимости.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): после замены ACID-транзакций на batch-операции
|
||||||
|
// функции storage.InitTransactionManager, storage.BeginTransactionOnCollection,
|
||||||
|
// storage.CommitCurrentTransaction, storage.AbortCurrentTransaction,
|
||||||
|
// storage.GetCurrentTransactionID, storage.GetActiveTransactions,
|
||||||
|
// storage.HasActiveTransaction были удалены. HTTP-API для транзакций
|
||||||
|
// переписан поверх batch-операций:
|
||||||
|
// - POST /api/transaction/begin — создаёт *storage.Batch и сохраняет
|
||||||
|
// в карте HTTPServer.activeBatches (ключ — ID сессии);
|
||||||
|
// - POST /api/transaction/commit — коммитит batch и удаляет из карты;
|
||||||
|
// - POST /api/transaction/abort — снимает регистрацию batch без коммита;
|
||||||
|
// - GET /api/transaction/ — возвращает список активных batch'ей
|
||||||
|
// текущей сессии.
|
||||||
|
// Старые URL-пути сохранены для совместимости с существующими клиентами.
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
@@ -259,6 +273,13 @@ type HTTPServer struct {
|
|||||||
tlsEnabled bool
|
tlsEnabled bool
|
||||||
certFile string
|
certFile string
|
||||||
keyFile string
|
keyFile string
|
||||||
|
|
||||||
|
// ДОБАВЛЕНО (2026-10): реестр активных batch'ей по ID сессии.
|
||||||
|
// Ключ — sessionID (string), значение — *storage.Batch.
|
||||||
|
// Используется HTTP-API транзакций: /api/transaction/begin создаёт
|
||||||
|
// batch и регистрирует здесь, /api/transaction/commit извлекает и
|
||||||
|
// коммитит, /api/transaction/abort просто снимает регистрацию.
|
||||||
|
activeBatches sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIResponse представляет стандартный ответ API
|
// APIResponse представляет стандартный ответ API
|
||||||
@@ -505,6 +526,8 @@ func (s *HTTPServer) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.logOperation("LOGOUT", username.(string), "success", "", nil)
|
s.logOperation("LOGOUT", username.(string), "success", "", nil)
|
||||||
s.sessions.Delete(sessionID)
|
s.sessions.Delete(sessionID)
|
||||||
}
|
}
|
||||||
|
// Снимаем все активные batch'и сессии.
|
||||||
|
s.discardSessionBatches(sessionID)
|
||||||
s.aclManager.Logout(sessionID)
|
s.aclManager.Logout(sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -883,8 +906,7 @@ func (s *HTTPServer) handleIndexRequest(w http.ResponseWriter, r *http.Request)
|
|||||||
if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil {
|
if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil {
|
||||||
s.logOperation("CREATE_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, req.Name), "error", err.Error(), nil)
|
s.logOperation("CREATE_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, req.Name), "error", err.Error(), nil)
|
||||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
s.sendError(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return }
|
||||||
}
|
|
||||||
s.logOperation("CREATE_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, req.Name), "success", "", map[string]interface{}{
|
s.logOperation("CREATE_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, req.Name), "success", "", map[string]interface{}{
|
||||||
"fields": req.Fields,
|
"fields": req.Fields,
|
||||||
"unique": req.Unique,
|
"unique": req.Unique,
|
||||||
@@ -1605,22 +1627,87 @@ func (s *HTTPServer) handleTriggerRequest(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTransactionRequest обрабатывает запросы к транзакциям
|
// =============================================================================
|
||||||
|
// BATCH API (замена транзакций)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// getSessionBatch возвращает активный batch для сессии (или nil).
|
||||||
|
func (s *HTTPServer) getSessionBatch(sessionID string) (*storage.Batch, bool) {
|
||||||
|
if sessionID == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
val, ok := s.activeBatches.Load(sessionID)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
batch, ok := val.(*storage.Batch)
|
||||||
|
return batch, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSessionBatch сохраняет batch для сессии.
|
||||||
|
func (s *HTTPServer) setSessionBatch(sessionID string, batch *storage.Batch) {
|
||||||
|
if sessionID == "" || batch == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.activeBatches.Store(sessionID, batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearSessionBatch снимает batch с сессии.
|
||||||
|
func (s *HTTPServer) clearSessionBatch(sessionID string) {
|
||||||
|
if sessionID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.activeBatches.Delete(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// discardSessionBatches снимает все активные batch'и сессии.
|
||||||
|
// Вызывается при logout.
|
||||||
|
func (s *HTTPServer) discardSessionBatches(sessionID string) {
|
||||||
|
if sessionID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if batch, ok := s.getSessionBatch(sessionID); ok && batch != nil {
|
||||||
|
// Снимаем регистрацию в глобальном менеджере (без коммита).
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
|
s.clearSessionBatch(sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTransactionRequest обрабатывает запросы к batch-операциям.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): переписано с транзакций на batch-операции.
|
||||||
|
// URL-пути сохранены для совместимости с существующими клиентами:
|
||||||
|
// - POST /api/transaction/begin — создать batch
|
||||||
|
// - POST /api/transaction/commit — закоммитить batch
|
||||||
|
// - POST /api/transaction/abort — отменить batch (без коммита)
|
||||||
|
// - GET /api/transaction/ — список активных batch'ей сессии
|
||||||
func (s *HTTPServer) handleTransactionRequest(w http.ResponseWriter, r *http.Request) {
|
func (s *HTTPServer) handleTransactionRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
path := strings.TrimPrefix(r.URL.Path, "/api/transaction/")
|
path := strings.TrimPrefix(r.URL.Path, "/api/transaction/")
|
||||||
parts := strings.Split(path, "/")
|
parts := strings.Split(path, "/")
|
||||||
|
|
||||||
sessionID := s.getSessionID(r)
|
sessionID := s.getSessionID(r)
|
||||||
if !s.aclManager.CheckPermission(sessionID, "*", "*", "write") {
|
if !s.aclManager.CheckPermission(sessionID, "*", "*", "write") {
|
||||||
s.logOperation("TRANSACTION_OPERATION", "", "error", "Write access required", nil)
|
s.logOperation("BATCH_OPERATION", "", "error", "Write access required", nil)
|
||||||
s.sendError(w, "Write access required", http.StatusForbidden)
|
s.sendError(w, "Write access required", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
transactions := storage.GetActiveTransactions()
|
// Возвращаем список активных batch'ей сессии.
|
||||||
s.sendSuccess(w, transactions)
|
active := make([]map[string]interface{}, 0)
|
||||||
|
if batch, ok := s.getSessionBatch(sessionID); ok && batch != nil {
|
||||||
|
active = append(active, map[string]interface{}{
|
||||||
|
"id": batch.ID,
|
||||||
|
"database": batch.Database,
|
||||||
|
"collection": batch.Collection,
|
||||||
|
"operation_count": len(batch.Operations),
|
||||||
|
"start_time": batch.Timestamp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
s.sendSuccess(w, active)
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
if len(parts) == 0 || parts[0] == "" {
|
if len(parts) == 0 || parts[0] == "" {
|
||||||
@@ -1631,67 +1718,73 @@ func (s *HTTPServer) handleTransactionRequest(w http.ResponseWriter, r *http.Req
|
|||||||
action := parts[0]
|
action := parts[0]
|
||||||
switch action {
|
switch action {
|
||||||
case "begin":
|
case "begin":
|
||||||
if err := storage.InitTransactionManager("futriis.wal"); err != nil {
|
// Если у сессии уже есть активный batch — возвращаем ошибку.
|
||||||
s.logOperation("BEGIN_TRANSACTION", "", "error", err.Error(), nil)
|
if existing, ok := s.getSessionBatch(sessionID); ok && existing != nil {
|
||||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
s.sendError(w, "Session already has active batch", http.StatusConflict)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
databases := s.store.ListDatabases()
|
// Читаем параметры: database, collection.
|
||||||
if len(databases) == 0 {
|
var req struct {
|
||||||
s.sendError(w, "No database available", http.StatusBadRequest)
|
Database string `json:"database"`
|
||||||
|
Collection string `json:"collection"`
|
||||||
|
}
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
batch := storage.NewBatch(req.Database, req.Collection)
|
||||||
|
if batch == nil {
|
||||||
|
s.logOperation("BEGIN_BATCH", "", "error", "failed to create batch", nil)
|
||||||
|
s.sendError(w, "Failed to create batch", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := s.store.GetDatabase(databases[0])
|
s.setSessionBatch(sessionID, batch)
|
||||||
if err != nil {
|
s.logOperation("BEGIN_BATCH", fmt.Sprintf("%d", batch.ID), "success", "", nil)
|
||||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
collections := db.ListCollections()
|
|
||||||
if len(collections) == 0 {
|
|
||||||
s.sendError(w, "No collection available", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
coll, err := db.GetCollection(collections[0])
|
|
||||||
if err != nil {
|
|
||||||
s.sendError(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := storage.BeginTransactionOnCollection(coll); err != nil {
|
|
||||||
s.logOperation("BEGIN_TRANSACTION", "", "error", err.Error(), nil)
|
|
||||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.logOperation("BEGIN_TRANSACTION", "", "success", "", nil)
|
|
||||||
s.sendSuccess(w, map[string]interface{}{
|
s.sendSuccess(w, map[string]interface{}{
|
||||||
"status": "transaction_started",
|
"status": "batch_started",
|
||||||
"id": storage.GetCurrentTransactionID(),
|
"id": batch.ID,
|
||||||
|
"database": batch.Database,
|
||||||
|
"collection": batch.Collection,
|
||||||
})
|
})
|
||||||
|
|
||||||
case "commit":
|
case "commit":
|
||||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
batch, ok := s.getSessionBatch(sessionID)
|
||||||
s.logOperation("COMMIT_TRANSACTION", "", "error", err.Error(), nil)
|
if !ok || batch == nil {
|
||||||
|
s.sendError(w, "No active batch", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := batch.Commit(); err != nil {
|
||||||
|
s.logOperation("COMMIT_BATCH", fmt.Sprintf("%d", batch.ID), "error", err.Error(), nil)
|
||||||
|
s.clearSessionBatch(sessionID)
|
||||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logOperation("COMMIT_TRANSACTION", "", "success", "", nil)
|
|
||||||
|
s.logOperation("COMMIT_BATCH", fmt.Sprintf("%d", batch.ID), "success", "", nil)
|
||||||
|
s.clearSessionBatch(sessionID)
|
||||||
s.sendSuccess(w, map[string]interface{}{
|
s.sendSuccess(w, map[string]interface{}{
|
||||||
"status": "transaction_committed",
|
"status": "batch_committed",
|
||||||
|
"id": batch.ID,
|
||||||
})
|
})
|
||||||
|
|
||||||
case "abort":
|
case "abort":
|
||||||
if err := storage.AbortCurrentTransaction(); err != nil {
|
batch, ok := s.getSessionBatch(sessionID)
|
||||||
s.logOperation("ABORT_TRANSACTION", "", "error", err.Error(), nil)
|
if !ok || batch == nil {
|
||||||
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
s.sendError(w, "No active batch", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logOperation("ABORT_TRANSACTION", "", "success", "", nil)
|
|
||||||
|
// Снимаем регистрацию в глобальном менеджере.
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
|
s.clearSessionBatch(sessionID)
|
||||||
|
|
||||||
|
s.logOperation("ABORT_BATCH", fmt.Sprintf("%d", batch.ID), "success", "", nil)
|
||||||
s.sendSuccess(w, map[string]interface{}{
|
s.sendSuccess(w, map[string]interface{}{
|
||||||
"status": "transaction_aborted",
|
"status": "batch_aborted",
|
||||||
|
"id": batch.ID,
|
||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1700,25 +1793,48 @@ func (s *HTTPServer) handleTransactionRequest(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
s.sendError(w, "Transaction ID and action required", http.StatusBadRequest)
|
s.sendError(w, "Batch ID and action required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
txID := parts[0]
|
batchIDStr := parts[0]
|
||||||
action := parts[1]
|
action := parts[1]
|
||||||
|
|
||||||
|
batch, ok := s.getSessionBatch(sessionID)
|
||||||
|
if !ok || batch == nil {
|
||||||
|
s.sendError(w, "No active batch", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что ID совпадает (необязательно, но полезно).
|
||||||
|
if fmt.Sprintf("%d", batch.ID) != batchIDStr {
|
||||||
|
s.sendError(w, "Batch ID mismatch", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "commit":
|
case "commit":
|
||||||
s.logOperation("COMMIT_TRANSACTION", txID, "success", "", nil)
|
if err := batch.Commit(); err != nil {
|
||||||
|
s.logOperation("COMMIT_BATCH", batchIDStr, "error", err.Error(), nil)
|
||||||
|
s.clearSessionBatch(sessionID)
|
||||||
|
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logOperation("COMMIT_BATCH", batchIDStr, "success", "", nil)
|
||||||
|
s.clearSessionBatch(sessionID)
|
||||||
s.sendSuccess(w, map[string]interface{}{
|
s.sendSuccess(w, map[string]interface{}{
|
||||||
"status": "transaction_committed",
|
"status": "batch_committed",
|
||||||
"id": txID,
|
"id": batchIDStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
case "abort":
|
case "abort":
|
||||||
s.logOperation("ABORT_TRANSACTION", txID, "success", "", nil)
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
|
s.clearSessionBatch(sessionID)
|
||||||
|
s.logOperation("ABORT_BATCH", batchIDStr, "success", "", nil)
|
||||||
s.sendSuccess(w, map[string]interface{}{
|
s.sendSuccess(w, map[string]interface{}{
|
||||||
"status": "transaction_aborted",
|
"status": "batch_aborted",
|
||||||
"id": txID,
|
"id": batchIDStr,
|
||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -33,14 +33,31 @@
|
|||||||
// SetReplicationFactor, HandleStatusSync), чтобы подобные ошибки
|
// SetReplicationFactor, HandleStatusSync), чтобы подобные ошибки
|
||||||
// в будущем давали внятную ошибку, а не панику.
|
// в будущем давали внятную ошибку, а не панику.
|
||||||
//
|
//
|
||||||
// ДОПОЛНИТЕЛЬНО (исправление ошибок компиляции в repl.go):
|
// ДОБАВЛЕНО (2026-10, выборы лидера SAGA):
|
||||||
// Добавлены методы GetOrchestrator(), GetMetrics() и GetActiveSagas()
|
// - Тип SagaLeaderBridge реализует storage.SagaRaftAccessor.
|
||||||
// для *SagaManager (cluster.SagaManager), которые используются в
|
// Он подписывается на изменения лидерства Raft (через monitorLeadership)
|
||||||
// internal/repl/repl.go для команд 'saga status' и 'saga list'.
|
// и уведомляет подписчиков.
|
||||||
// GetOrchestrator() возвращает сам *SagaManager (в кластерной версии
|
// - RaftCoordinator.sagaLeaderBridge создаётся в NewRaftCoordinator.
|
||||||
// он выполняет роль оркестратора), что позволяет repl.go единообразно
|
// - В monitorLeadership при смене лидерства вызывается
|
||||||
// вызывать .GetMetrics() как у storage.SagaOrchestrator, так и у
|
// coord.sagaLeaderBridge.NotifyLeadership(...), что обновляет
|
||||||
// cluster.SagaManager.
|
// состояние bridge (для storage.SagaLeaderElector, если он где-то
|
||||||
|
// используется).
|
||||||
|
// - В Stop() останавливаем bridge.
|
||||||
|
// - GetSagaLeaderBridge() — публичный геттер.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (компиляция):
|
||||||
|
// Ранее NewSagaManagerWithRaftForCoordinator пыталась вернуть
|
||||||
|
// *storage.SagaManager как *cluster.SagaManager (локальный тип).
|
||||||
|
// Это конфликт двух разных типов с одинаковым именем.
|
||||||
|
// Теперь:
|
||||||
|
// - coord.sagaManager по-прежнему *cluster.SagaManager
|
||||||
|
// (создаётся через NewSagaManager(logger));
|
||||||
|
// - bridge остаётся отдельным компонентом (storage.SagaRaftAccessor),
|
||||||
|
// который при необходимости можно получить через GetSagaLeaderBridge();
|
||||||
|
// - вызов storage.NewSagaManagerWithRaft из NewRaftCoordinator убран.
|
||||||
|
// Все публичные методы *cluster.SagaManager (GetOrchestrator,
|
||||||
|
// GetMetrics, GetActiveSagas, Stop) сохранены — они используются
|
||||||
|
// в repl.go и main.go.
|
||||||
|
|
||||||
package cluster
|
package cluster
|
||||||
|
|
||||||
@@ -156,6 +173,162 @@ func (lc *LamportClock) Value() uint64 {
|
|||||||
return lc.counter.Load()
|
return lc.counter.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SAGA LEADER BRIDGE — РЕАЛИЗАЦИЯ storage.SagaRaftAccessor
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// SagaLeaderBridge — мост между Raft-координатором и оркестратором SAGA.
|
||||||
|
//
|
||||||
|
// Реализует интерфейс storage.SagaRaftAccessor:
|
||||||
|
// - IsSagaLeader() — сообщает, является ли текущий узел лидером Raft.
|
||||||
|
// - GetSagaLeaderID() — возвращает ID текущего лидера.
|
||||||
|
// - GetSagaCurrentTerm() — возвращает текущий терм Raft.
|
||||||
|
// - RegisterSagaLeaderObserver(id, cb) — регистрирует наблюдателя,
|
||||||
|
// который будет уведомлён при смене лидерства.
|
||||||
|
//
|
||||||
|
// Мост используется storage.SagaLeaderElector, который опрашивает
|
||||||
|
// его и обновляет своё состояние (isLeader, leaderID).
|
||||||
|
//
|
||||||
|
// Все методы потокобезопасны.
|
||||||
|
type SagaLeaderBridge struct {
|
||||||
|
coordinator *RaftCoordinator
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
isLeader bool
|
||||||
|
leaderID string
|
||||||
|
term uint64
|
||||||
|
|
||||||
|
observersMu sync.RWMutex
|
||||||
|
observers map[string]func(isLeader bool, leaderID string)
|
||||||
|
|
||||||
|
stopChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSagaLeaderBridge создаёт новый мост.
|
||||||
|
func NewSagaLeaderBridge(coord *RaftCoordinator) *SagaLeaderBridge {
|
||||||
|
b := &SagaLeaderBridge{
|
||||||
|
coordinator: coord,
|
||||||
|
observers: make(map[string]func(isLeader bool, leaderID string)),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start запускает фоновый опрос состояния Raft.
|
||||||
|
func (b *SagaLeaderBridge) Start() {
|
||||||
|
b.wg.Add(1)
|
||||||
|
go b.pollLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollLoop периодически опрашивает Raft и обновляет состояние.
|
||||||
|
func (b *SagaLeaderBridge) pollLoop() {
|
||||||
|
defer b.wg.Done()
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
b.refreshFromRaft()
|
||||||
|
case <-b.stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshFromRaft читает состояние Raft и уведомляет наблюдателей,
|
||||||
|
// если что-то изменилось.
|
||||||
|
func (b *SagaLeaderBridge) refreshFromRaft() {
|
||||||
|
if b.coordinator == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeader := b.coordinator.IsLeader()
|
||||||
|
|
||||||
|
leaderID := ""
|
||||||
|
if leader := b.coordinator.GetLeader(); leader != nil {
|
||||||
|
leaderID = leader.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
term := b.coordinator.GetCurrentTerm()
|
||||||
|
|
||||||
|
b.NotifyLeadership(isLeader, leaderID, term)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyLeadership вызывается извне (например, monitorLeadership)
|
||||||
|
// для немедленного обновления состояния.
|
||||||
|
func (b *SagaLeaderBridge) NotifyLeadership(isLeader bool, leaderID string, term uint64) {
|
||||||
|
b.mu.Lock()
|
||||||
|
oldIsLeader := b.isLeader
|
||||||
|
oldLeaderID := b.leaderID
|
||||||
|
b.isLeader = isLeader
|
||||||
|
b.leaderID = leaderID
|
||||||
|
b.term = term
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
if oldIsLeader != isLeader || oldLeaderID != leaderID {
|
||||||
|
b.observersMu.RLock()
|
||||||
|
obs := make([]func(isLeader bool, leaderID string), 0, len(b.observers))
|
||||||
|
for _, cb := range b.observers {
|
||||||
|
obs = append(obs, cb)
|
||||||
|
}
|
||||||
|
b.observersMu.RUnlock()
|
||||||
|
|
||||||
|
for _, cb := range obs {
|
||||||
|
func(cb func(isLeader bool, leaderID string)) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Не позволяем панике наблюдателя уронить процесс.
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cb(isLeader, leaderID)
|
||||||
|
}(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSagaLeader — реализация storage.SagaRaftAccessor.
|
||||||
|
func (b *SagaLeaderBridge) IsSagaLeader() bool {
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
return b.isLeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSagaLeaderID — реализация storage.SagaRaftAccessor.
|
||||||
|
func (b *SagaLeaderBridge) GetSagaLeaderID() string {
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
return b.leaderID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSagaCurrentTerm — реализация storage.SagaRaftAccessor.
|
||||||
|
func (b *SagaLeaderBridge) GetSagaCurrentTerm() uint64 {
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
return b.term
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterSagaLeaderObserver — реализация storage.SagaRaftAccessor.
|
||||||
|
func (b *SagaLeaderBridge) RegisterSagaLeaderObserver(id string, cb func(isLeader bool, leaderID string)) {
|
||||||
|
b.observersMu.Lock()
|
||||||
|
defer b.observersMu.Unlock()
|
||||||
|
b.observers[id] = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnregisterSagaLeaderObserver снимает регистрацию наблюдателя.
|
||||||
|
func (b *SagaLeaderBridge) UnregisterSagaLeaderObserver(id string) {
|
||||||
|
b.observersMu.Lock()
|
||||||
|
defer b.observersMu.Unlock()
|
||||||
|
delete(b.observers, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop останавливает мост.
|
||||||
|
func (b *SagaLeaderBridge) Stop() {
|
||||||
|
close(b.stopChan)
|
||||||
|
b.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// ДИАПАЗОННЫЕ ШАРДЫ (RANGE SHARDS)
|
// ДИАПАЗОННЫЕ ШАРДЫ (RANGE SHARDS)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -1309,7 +1482,6 @@ func (sm *SagaManager) GetSagaStatus(id string) (string, error) {
|
|||||||
return saga.Status, nil
|
return saga.Status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// === ДОБАВЛЕНО ДЛЯ ИСПРАВЛЕНИЯ ОШИБОК КОМПИЛЯЦИИ ===
|
|
||||||
// GetOrchestrator возвращает менеджер SAGA, который в кластерной версии
|
// GetOrchestrator возвращает менеджер SAGA, который в кластерной версии
|
||||||
// сам выполняет роль оркестратора. Возвращаемый тип — *SagaManager,
|
// сам выполняет роль оркестратора. Возвращаемый тип — *SagaManager,
|
||||||
// у которого есть метод GetMetrics(), используемый в repl.go.
|
// у которого есть метод GetMetrics(), используемый в repl.go.
|
||||||
@@ -1378,7 +1550,17 @@ func (sm *SagaManager) GetActiveSagas() []*SagaTransaction {
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
// === КОНЕЦ ДОБАВЛЕННОГО БЛОКА ===
|
|
||||||
|
// Stop останавливает Saga-менеджер.
|
||||||
|
// Реализован, чтобы main.go мог вызвать sagaManager.Stop() единообразно
|
||||||
|
// как для storage.SagaOrchestrator, так и для cluster.SagaManager.
|
||||||
|
func (sm *SagaManager) Stop() {
|
||||||
|
close(sm.stopChan)
|
||||||
|
sm.wg.Wait()
|
||||||
|
if sm.logger != nil {
|
||||||
|
sm.logger.Info("Cluster saga manager stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TCC (TRY-CONFIRM-CANCEL)
|
// TCC (TRY-CONFIRM-CANCEL)
|
||||||
@@ -3046,6 +3228,12 @@ type RaftCoordinator struct {
|
|||||||
|
|
||||||
// Логические часы для eventual consistency
|
// Логические часы для eventual consistency
|
||||||
lamportClock *LamportClock
|
lamportClock *LamportClock
|
||||||
|
|
||||||
|
// SAGA LEADER BRIDGE — реализация storage.SagaRaftAccessor.
|
||||||
|
// Позволяет оркестратору SAGA (storage) корректно определять лидера,
|
||||||
|
// если он создаётся через storage.NewSagaManagerWithRaft.
|
||||||
|
// Для локального cluster.SagaManager не используется.
|
||||||
|
sagaLeaderBridge *SagaLeaderBridge
|
||||||
}
|
}
|
||||||
|
|
||||||
// initRaftFSM создаёт и инициализирует FSM и его состояние.
|
// initRaftFSM создаёт и инициализирует FSM и его состояние.
|
||||||
@@ -3087,6 +3275,17 @@ func (rc *RaftCoordinator) initRaftStores(dataDir string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRaftCoordinator создаёт новый координатор Raft.
|
// NewRaftCoordinator создаёт новый координатор Raft.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (компиляция):
|
||||||
|
// Ранее здесь вызывалась storage.NewSagaManagerWithRaft и результат
|
||||||
|
// присваивался полю *cluster.SagaManager, что давало ошибку
|
||||||
|
// "cannot use *storage.SagaManager as *cluster.SagaManager".
|
||||||
|
// Теперь создаётся локальный cluster.SagaManager через NewSagaManager.
|
||||||
|
// Он уже имеет собственные GetOrchestrator, GetMetrics, GetActiveSagas
|
||||||
|
// и Stop — то, что ожидают repl.go и main.go.
|
||||||
|
// Bridge остаётся отдельным компонентом (sagaLeaderBridge) и
|
||||||
|
// доступен через GetSagaLeaderBridge() для тех, кто хочет
|
||||||
|
// использовать storage.SagaLeaderElector.
|
||||||
func NewRaftCoordinator(cfg *config.Config, store *storage.Storage, logger *log.Logger) (*RaftCoordinator, error) {
|
func NewRaftCoordinator(cfg *config.Config, store *storage.Storage, logger *log.Logger) (*RaftCoordinator, error) {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
return nil, fmt.Errorf("logger is required")
|
return nil, fmt.Errorf("logger is required")
|
||||||
@@ -3129,7 +3328,18 @@ func NewRaftCoordinator(cfg *config.Config, store *storage.Storage, logger *log.
|
|||||||
coord.panicRecoveryMgr = NewPanicRecoveryManager(logger)
|
coord.panicRecoveryMgr = NewPanicRecoveryManager(logger)
|
||||||
coord.fallbackManager = NewLeaderFallbackManager(coord, logger, nil)
|
coord.fallbackManager = NewLeaderFallbackManager(coord, logger, nil)
|
||||||
|
|
||||||
|
// ДОБАВЛЕНО: создаём bridge для выборов лидера SAGA.
|
||||||
|
// Bridge реализует storage.SagaRaftAccessor.
|
||||||
|
// Он не заменяет cluster.SagaManager, а живёт отдельно.
|
||||||
|
coord.sagaLeaderBridge = NewSagaLeaderBridge(coord)
|
||||||
|
coord.sagaLeaderBridge.Start()
|
||||||
|
|
||||||
|
// Создаём локальный кластерный SAGA-менеджер.
|
||||||
|
// В кластерной версии он сам выполняет роль оркестратора
|
||||||
|
// (см. GetOrchestrator), и его GetMetrics/GetActiveSagas/Stop
|
||||||
|
// используются в repl.go и main.go.
|
||||||
coord.sagaManager = NewSagaManager(logger)
|
coord.sagaManager = NewSagaManager(logger)
|
||||||
|
|
||||||
coord.tccManager = NewTCCManager(logger)
|
coord.tccManager = NewTCCManager(logger)
|
||||||
coord.replicaReadManager = NewReplicaReadManager(coord, logger)
|
coord.replicaReadManager = NewReplicaReadManager(coord, logger)
|
||||||
coord.multiRaftManager = NewMultiRaftManager(store, logger)
|
coord.multiRaftManager = NewMultiRaftManager(store, logger)
|
||||||
@@ -3163,6 +3373,9 @@ func NewRaftCoordinator(cfg *config.Config, store *storage.Storage, logger *log.
|
|||||||
coord.fsm.state.Nodes[coord.localNodeInfo.ID] = coord.localNodeInfo
|
coord.fsm.state.Nodes[coord.localNodeInfo.ID] = coord.localNodeInfo
|
||||||
coord.fsm.state.UpdatedAt = time.Now().UnixMilli()
|
coord.fsm.state.UpdatedAt = time.Now().UnixMilli()
|
||||||
coord.fsm.state.mu.Unlock()
|
coord.fsm.state.mu.Unlock()
|
||||||
|
|
||||||
|
// В single-node режиме мы сразу лидер — сообщаем bridge.
|
||||||
|
coord.sagaLeaderBridge.NotifyLeadership(true, coord.localNodeInfo.ID, 1)
|
||||||
} else {
|
} else {
|
||||||
logger.Info("Running in cluster mode")
|
logger.Info("Running in cluster mode")
|
||||||
if err := coord.setupClusterMode(); err != nil {
|
if err := coord.setupClusterMode(); err != nil {
|
||||||
@@ -3270,6 +3483,13 @@ func (rc *RaftCoordinator) GetMigrationStatus() *migration.MigrationStatus {
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSagaLeaderBridge возвращает bridge выборов лидера SAGA.
|
||||||
|
// Полезно для диагностики и для передачи в другие компоненты
|
||||||
|
// (например, если кто-то хочет использовать storage.SagaLeaderElector).
|
||||||
|
func (rc *RaftCoordinator) GetSagaLeaderBridge() *SagaLeaderBridge {
|
||||||
|
return rc.sagaLeaderBridge
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// МЕТОДЫ ДЛЯ КРОСС-ДАТАЦЕНТРОВОЙ МИГРАЦИИ
|
// МЕТОДЫ ДЛЯ КРОСС-ДАТАЦЕНТРОВОЙ МИГРАЦИИ
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -3780,6 +4000,10 @@ func (rc *RaftCoordinator) GetJointConsensusStatus() map[string]interface{} {
|
|||||||
|
|
||||||
// monitorLeadership отслеживает изменения лидера.
|
// monitorLeadership отслеживает изменения лидера.
|
||||||
// Использует реальное состояние Raft и защиту от ложных срабатываний.
|
// Использует реальное состояние Raft и защиту от ложных срабатываний.
|
||||||
|
//
|
||||||
|
// ДОБАВЛЕНО: при смене лидерства вызываем sagaLeaderBridge.NotifyLeadership,
|
||||||
|
// чтобы bridge знал, кто лидер. Это используется, если кто-то создаст
|
||||||
|
// storage.SagaLeaderElector на базе этого bridge.
|
||||||
func (rc *RaftCoordinator) monitorLeadership() {
|
func (rc *RaftCoordinator) monitorLeadership() {
|
||||||
ticker := time.NewTicker(rc.config.Cluster.GetHeartbeatTimeout() / 2)
|
ticker := time.NewTicker(rc.config.Cluster.GetHeartbeatTimeout() / 2)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -3796,6 +4020,16 @@ func (rc *RaftCoordinator) monitorLeadership() {
|
|||||||
}
|
}
|
||||||
// Проверяем реальное состояние Raft
|
// Проверяем реальное состояние Raft
|
||||||
isLeader := rc.raft.State() == raft.Leader
|
isLeader := rc.raft.State() == raft.Leader
|
||||||
|
|
||||||
|
// ДОБАВЛЕНО: уведомляем bridge о текущем лидерстве.
|
||||||
|
leaderID := ""
|
||||||
|
if leader := rc.GetLeader(); leader != nil {
|
||||||
|
leaderID = leader.ID
|
||||||
|
}
|
||||||
|
if rc.sagaLeaderBridge != nil {
|
||||||
|
rc.sagaLeaderBridge.NotifyLeadership(isLeader, leaderID, rc.currentTerm.Load())
|
||||||
|
}
|
||||||
|
|
||||||
if isLeader != wasLeader {
|
if isLeader != wasLeader {
|
||||||
wasLeader = isLeader
|
wasLeader = isLeader
|
||||||
select {
|
select {
|
||||||
@@ -3848,6 +4082,9 @@ func (rc *RaftCoordinator) rebalanceMonitor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop останавливает координатор.
|
// Stop останавливает координатор.
|
||||||
|
//
|
||||||
|
// ДОБАВЛЕНО: останавливаем sagaLeaderBridge и sagaManager.
|
||||||
|
// ИСПРАВЛЕНО: sagaManager теперь *cluster.SagaManager, у которого есть Stop().
|
||||||
func (rc *RaftCoordinator) Stop() {
|
func (rc *RaftCoordinator) Stop() {
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
@@ -3905,6 +4142,14 @@ func (rc *RaftCoordinator) Stop() {
|
|||||||
rc.crossDCMigrator.Stop()
|
rc.crossDCMigrator.Stop()
|
||||||
rc.logger.Debug("Cross-datacenter migrator stopped")
|
rc.logger.Debug("Cross-datacenter migrator stopped")
|
||||||
}
|
}
|
||||||
|
if rc.sagaManager != nil {
|
||||||
|
rc.sagaManager.Stop()
|
||||||
|
rc.logger.Debug("Saga manager stopped")
|
||||||
|
}
|
||||||
|
if rc.sagaLeaderBridge != nil {
|
||||||
|
rc.sagaLeaderBridge.Stop()
|
||||||
|
rc.logger.Debug("Saga leader bridge stopped")
|
||||||
|
}
|
||||||
|
|
||||||
close(rc.stopChan)
|
close(rc.stopChan)
|
||||||
if rc.raft != nil {
|
if rc.raft != nil {
|
||||||
|
|||||||
@@ -105,11 +105,54 @@ ACL MANAGEMENT:
|
|||||||
acl listUsers - List all users
|
acl listUsers - List all users
|
||||||
acl listRoles - List all roles
|
acl listRoles - List all roles
|
||||||
|
|
||||||
TRANSACTIONS (MongoDB-like syntax):
|
BATCH OPERATIONS (MongoDB-like syntax):
|
||||||
session = db.startSession() - Start a new session
|
batch = db.startBatch() - Start a new batch (atomically applied group of operations)
|
||||||
session.startTransaction() - Begin a transaction
|
batch.addInsert({...}) - Add insert operation to batch
|
||||||
session.commitTransaction() - Commit current transaction
|
batch.addUpdate({_id: "..."}, {...}) - Add update operation to batch
|
||||||
session.abortTransaction() - Abort/Rollback current transaction
|
batch.addDelete({_id: "..."}) - Add delete operation to batch
|
||||||
|
batch.addRestore({_id: "..."}) - Add restore operation to batch
|
||||||
|
batch.commit() - Commit batch (applies all operations atomically via WAL)
|
||||||
|
batch.abort() - Abort batch (operations are discarded)
|
||||||
|
|
||||||
|
BATCH OPERATIONS (REPL syntax):
|
||||||
|
batch begin [collection] - Begin a new batch
|
||||||
|
batch commit - Commit the current batch
|
||||||
|
batch abort - Abort the current batch
|
||||||
|
show batches - Show the active batch of the current session
|
||||||
|
|
||||||
|
BATCH ALIASES (legacy compatibility):
|
||||||
|
begin transaction - Alias for 'batch begin'
|
||||||
|
commit - Alias for 'batch commit'
|
||||||
|
rollback - Alias for 'batch abort'
|
||||||
|
show transactions - Alias for 'show batches'
|
||||||
|
|
||||||
|
BATCH NOTES:
|
||||||
|
• Batch — это группа операций, применяемых атомарно: либо все, либо ни одна.
|
||||||
|
• Атомарность обеспечивается через WAL (Write-Ahead Log): сначала весь batch
|
||||||
|
записывается в WAL с fsync, только после этого операции применяются к
|
||||||
|
in-memory структурам.
|
||||||
|
• Batch короткоживущий: создаётся, наполняется операциями и коммитится в
|
||||||
|
рамках одной логической операции. Не поддерживает savepoints, таймауты,
|
||||||
|
распределённую координацию — для распределённых операций используйте SAGA.
|
||||||
|
• Для распределённых операций между узлами/сервисами используйте SAGA
|
||||||
|
(saga status, saga list).
|
||||||
|
• Time-travel: для получения версии документа на момент времени используйте
|
||||||
|
WAL-based replay (GetDocumentAtTimestamp).
|
||||||
|
|
||||||
|
BATCH EXAMPLE:
|
||||||
|
// Begin batch
|
||||||
|
batch = db.startBatch("shop", "orders")
|
||||||
|
|
||||||
|
// Add operations
|
||||||
|
batch.addInsert({_id: "order_1", status: "new", total: 100})
|
||||||
|
batch.addUpdate({_id: "inv_1"}, {qty: 5})
|
||||||
|
batch.addDelete({_id: "cart_1"})
|
||||||
|
|
||||||
|
// Commit atomically
|
||||||
|
batch.commit()
|
||||||
|
|
||||||
|
// Or abort if something went wrong
|
||||||
|
// batch.abort()
|
||||||
|
|
||||||
EXPORT/IMPORT (MessagePack format):
|
EXPORT/IMPORT (MessagePack format):
|
||||||
export "database_name" "filename.msgpack" - Export entire database
|
export "database_name" "filename.msgpack" - Export entire database
|
||||||
@@ -192,7 +235,7 @@ CLUSTER MANAGEMENT:
|
|||||||
|
|
||||||
HTTP API:
|
HTTP API:
|
||||||
The database also exposes HTTP RESTful API on port 8080 (configurable)
|
The database also exposes HTTP RESTful API on port 8080 (configurable)
|
||||||
See documentation for endpoints: /api/db/, /api/index/, /api/acl/, /api/constraint/, /api/trigger/, /api/migration/
|
See documentation for endpoints: /api/db/, /api/index/, /api/acl/, /api/constraint/, /api/trigger/, /api/transaction/, /api/migration/
|
||||||
|
|
||||||
UTILITIES:
|
UTILITIES:
|
||||||
help - Show this help message
|
help - Show this help message
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
// Файл: internal/migration/schema_migrator.go
|
// Файл: internal/migration/schema_migrator.go
|
||||||
// Назначение: Миграция схемы данных при обновлении версии СУБД
|
// Назначение: Миграция схемы данных при обновлении версии СУБД
|
||||||
// Новая функциональность: Прозрачное обновление схемы данных (автоматическая миграция документов)
|
// Новая функциональность: Прозрачное обновление схемы данных (автоматическая миграция документов)
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): после замены ACID-транзакций на batch-операции
|
||||||
|
// тип storage.Transaction был удалён. Все ссылки на *storage.Transaction
|
||||||
|
// заменены на *storage.Batch. Миграции схемы работают в основном с
|
||||||
|
// in-memory структурой SchemaDefinition, поэтому batch используется
|
||||||
|
// только как маркер атомарности: если пользовательская миграция пишет
|
||||||
|
// в СУБД, она может добавлять операции в batch, который коммитится
|
||||||
|
// в конце MigrateSchema атомарно через batch.Commit().
|
||||||
|
|
||||||
package migration
|
package migration
|
||||||
|
|
||||||
@@ -59,13 +67,20 @@ type IndexDefinition struct {
|
|||||||
Unique bool `json:"unique"`
|
Unique bool `json:"unique"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SchemaMigration представляет миграцию схемы
|
// SchemaMigration представляет миграцию схемы.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10): функции Up/Down теперь принимают *storage.Batch
|
||||||
|
// вместо *storage.Transaction. Batch — это группа операций, применяемых
|
||||||
|
// атомарно через WAL. Если миграции нужно изменить данные в СУБД, она
|
||||||
|
// добавляет операции в batch. Если миграция меняет только in-memory
|
||||||
|
// SchemaDefinition — batch не используется, но остаётся доступным для
|
||||||
|
// совместимости и возможного расширения.
|
||||||
type SchemaMigration struct {
|
type SchemaMigration struct {
|
||||||
ID string
|
ID string
|
||||||
Version string
|
Version string
|
||||||
Description string
|
Description string
|
||||||
Up func(tx *storage.Transaction, schema *SchemaDefinition) error
|
Up func(batch *storage.Batch, schema *SchemaDefinition) error
|
||||||
Down func(tx *storage.Transaction, schema *SchemaDefinition) error
|
Down func(batch *storage.Batch, schema *SchemaDefinition) error
|
||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
AppliedAt int64
|
AppliedAt int64
|
||||||
}
|
}
|
||||||
@@ -190,7 +205,7 @@ func (sm *SchemaMigrator) loadSchema() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveSchema сохраняет определение схемы атомарно (через временный файл + rename).
|
// saveSchemaLocked сохраняет определение схемы атомарно (через временный файл + rename).
|
||||||
//
|
//
|
||||||
// ВАЖНО: метод НЕ берёт sm.mu, чтобы его можно было вызывать из функций,
|
// ВАЖНО: метод НЕ берёт sm.mu, чтобы его можно было вызывать из функций,
|
||||||
// уже удерживающих sm.mu.Lock(). Вызывающий сам отвечает за консистентность
|
// уже удерживающих sm.mu.Lock(). Вызывающий сам отвечает за консистентность
|
||||||
@@ -358,6 +373,12 @@ func (sm *SchemaMigrator) saveAppliedMigrationsLocked() error {
|
|||||||
|
|
||||||
// MigrateSchema выполняет миграцию схемы до указанной версии.
|
// MigrateSchema выполняет миграцию схемы до указанной версии.
|
||||||
//
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10): вместо storage.BeginTransaction/CommitCurrentTransaction
|
||||||
|
// используется storage.NewBatch + batch.Commit. Batch создаётся для
|
||||||
|
// (db="_schema", coll="_migrations") — это виртуальная коллекция, куда
|
||||||
|
// миграции могут добавлять операции. Если миграция не пишет в СУБД,
|
||||||
|
// batch просто останется пустым и его Commit — no-op.
|
||||||
|
//
|
||||||
// ВАЖНО: этот метод НЕ удерживает sm.mu на всё время выполнения,
|
// ВАЖНО: этот метод НЕ удерживает sm.mu на всё время выполнения,
|
||||||
// чтобы избежать deadlock с saveSchema/saveAppliedMigrations и
|
// чтобы избежать deadlock с saveSchema/saveAppliedMigrations и
|
||||||
// чтобы не блокировать чтение схемы на время долгих миграций.
|
// чтобы не блокировать чтение схемы на время долгих миграций.
|
||||||
@@ -425,16 +446,26 @@ func (sm *SchemaMigrator) MigrateSchema(targetVersion string) (err error) {
|
|||||||
return fmt.Errorf("failed to copy schema")
|
return fmt.Errorf("failed to copy schema")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Начинаем транзакцию
|
// Создаём batch для миграции. Batch — это группа операций,
|
||||||
tx := storage.BeginTransaction()
|
// применяемых атомарно. Если ни одна миграция не пишет в СУБД,
|
||||||
if tx == nil {
|
// batch останется пустым, а его Commit — no-op.
|
||||||
return fmt.Errorf("failed to begin transaction")
|
//
|
||||||
|
// Используем специальную виртуальную коллекцию "_schema_migrations",
|
||||||
|
// чтобы отделить batch миграций от пользовательских batch'ей.
|
||||||
|
batch := storage.NewBatch("_system", "_schema_migrations")
|
||||||
|
if batch == nil {
|
||||||
|
return fmt.Errorf("failed to create batch")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка паники с именованным возвратом err
|
// Обработка паники с именованным возвратом err
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
storage.AbortCurrentTransaction()
|
// Batch не коммитим — просто забываем о нём.
|
||||||
|
// Регистрация в activeBatches будет снята в Commit,
|
||||||
|
// но мы его не вызываем. Нужно явно снять регистрацию.
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
if sm.logger != nil {
|
if sm.logger != nil {
|
||||||
sm.logger.Error(fmt.Sprintf("Panic during schema migration: %v", r))
|
sm.logger.Error(fmt.Sprintf("Panic during schema migration: %v", r))
|
||||||
}
|
}
|
||||||
@@ -453,12 +484,17 @@ func (sm *SchemaMigrator) MigrateSchema(targetVersion string) (err error) {
|
|||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
if m.Up == nil {
|
if m.Up == nil {
|
||||||
storage.AbortCurrentTransaction()
|
// Снимаем регистрацию batch перед выходом с ошибкой.
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
return fmt.Errorf("migration %s has nil Up function", m.ID)
|
return fmt.Errorf("migration %s has nil Up function", m.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Up(tx, schemaCopy); err != nil {
|
if err := m.Up(batch, schemaCopy); err != nil {
|
||||||
storage.AbortCurrentTransaction()
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
return fmt.Errorf("schema migration %s failed: %v", m.ID, err)
|
return fmt.Errorf("schema migration %s failed: %v", m.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,9 +514,11 @@ func (sm *SchemaMigrator) MigrateSchema(targetVersion string) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Коммитим транзакцию ДО записи на диск
|
// Коммитим batch ДО записи на диск.
|
||||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
// Batch.Commit снимает регистрацию в activeBatches автоматически.
|
||||||
return fmt.Errorf("failed to commit schema migration: %v", err)
|
// Если batch пуст — Commit вернёт nil без записи в WAL.
|
||||||
|
if err := batch.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit schema migration batch: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Только теперь применяем изменения в память
|
// Только теперь применяем изменения в память
|
||||||
@@ -607,7 +645,11 @@ func copyCollectionSchema(src *CollectionSchema) *CollectionSchema {
|
|||||||
return ©
|
return ©
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrateCollectionDocuments прозрачно мигрирует все документы коллекции
|
// MigrateCollectionDocuments прозрачно мигрирует все документы коллекции.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10): вместо поэлементного collection.Update используем
|
||||||
|
// batch для атомарности всей миграции коллекции. Это даёт атомарность
|
||||||
|
// на уровне коллекции: либо все документы мигрированы, либо ни один.
|
||||||
func (sm *SchemaMigrator) MigrateCollectionDocuments(collection *storage.Collection) error {
|
func (sm *SchemaMigrator) MigrateCollectionDocuments(collection *storage.Collection) error {
|
||||||
if collection == nil {
|
if collection == nil {
|
||||||
return fmt.Errorf("nil collection")
|
return fmt.Errorf("nil collection")
|
||||||
@@ -630,12 +672,30 @@ func (sm *SchemaMigrator) MigrateCollectionDocuments(collection *storage.Collect
|
|||||||
|
|
||||||
docs := collection.GetAllDocuments()
|
docs := collection.GetAllDocuments()
|
||||||
|
|
||||||
|
// Создаём один batch для всей коллекции — атомарность на уровне коллекции.
|
||||||
|
batch := storage.NewBatch(collection.DBName(), collection.Name())
|
||||||
|
if batch == nil {
|
||||||
|
return fmt.Errorf("failed to create batch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Снимаем регистрацию batch при выходе в любом случае.
|
||||||
|
committed := false
|
||||||
|
defer func() {
|
||||||
|
if !committed {
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
migratedCount := int64(0)
|
||||||
|
failedCount := int64(0)
|
||||||
|
skippedCount := int64(0)
|
||||||
|
|
||||||
for _, doc := range docs {
|
for _, doc := range docs {
|
||||||
migratedDoc, changed, err := sm.MigrateDocument(doc, collection.Name())
|
migratedDoc, changed, err := sm.MigrateDocument(doc, collection.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sm.migrationStats.mu.Lock()
|
failedCount++
|
||||||
sm.migrationStats.FailedDocuments++
|
|
||||||
sm.migrationStats.mu.Unlock()
|
|
||||||
if sm.logger != nil {
|
if sm.logger != nil {
|
||||||
sm.logger.Error(fmt.Sprintf("Failed to migrate document %s: %v", doc.ID, err))
|
sm.logger.Error(fmt.Sprintf("Failed to migrate document %s: %v", doc.ID, err))
|
||||||
}
|
}
|
||||||
@@ -643,39 +703,49 @@ func (sm *SchemaMigrator) MigrateCollectionDocuments(collection *storage.Collect
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !changed {
|
if !changed {
|
||||||
sm.migrationStats.mu.Lock()
|
skippedCount++
|
||||||
sm.migrationStats.SkippedDocuments++
|
|
||||||
sm.migrationStats.mu.Unlock()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := migratedDoc.GetFields()
|
updates := migratedDoc.GetFields()
|
||||||
if err := collection.Update(doc.ID, updates); err != nil {
|
batch.AddUpdate(doc.ID, updates)
|
||||||
sm.migrationStats.mu.Lock()
|
migratedCount++
|
||||||
sm.migrationStats.FailedDocuments++
|
}
|
||||||
sm.migrationStats.mu.Unlock()
|
|
||||||
if sm.logger != nil {
|
|
||||||
sm.logger.Error(fmt.Sprintf("Failed to update migrated document %s: %v", doc.ID, err))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sm.migrationStats.mu.Lock()
|
// Коммитим batch атомарно.
|
||||||
sm.migrationStats.MigratedDocuments++
|
if migratedCount > 0 {
|
||||||
sm.migrationStats.mu.Unlock()
|
if err := batch.Commit(); err != nil {
|
||||||
|
if sm.logger != nil {
|
||||||
|
sm.logger.Error(fmt.Sprintf("Failed to commit migration batch for %s: %v",
|
||||||
|
collection.Name(), err))
|
||||||
|
}
|
||||||
|
sm.migrationStats.mu.Lock()
|
||||||
|
sm.migrationStats.FailedDocuments = failedCount + migratedCount
|
||||||
|
sm.migrationStats.SkippedDocuments = skippedCount
|
||||||
|
sm.migrationStats.EndTime = time.Now().UnixMilli()
|
||||||
|
sm.migrationStats.mu.Unlock()
|
||||||
|
return fmt.Errorf("failed to commit migration batch: %v", err)
|
||||||
|
}
|
||||||
|
committed = true
|
||||||
|
} else {
|
||||||
|
// Batch пуст — просто снимаем регистрацию.
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
|
committed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.migrationStats.mu.Lock()
|
sm.migrationStats.mu.Lock()
|
||||||
|
sm.migrationStats.MigratedDocuments = migratedCount
|
||||||
|
sm.migrationStats.FailedDocuments = failedCount
|
||||||
|
sm.migrationStats.SkippedDocuments = skippedCount
|
||||||
sm.migrationStats.EndTime = time.Now().UnixMilli()
|
sm.migrationStats.EndTime = time.Now().UnixMilli()
|
||||||
total := sm.migrationStats.TotalDocuments
|
total := sm.migrationStats.TotalDocuments
|
||||||
migrated := sm.migrationStats.MigratedDocuments
|
|
||||||
failed := sm.migrationStats.FailedDocuments
|
|
||||||
skipped := sm.migrationStats.SkippedDocuments
|
|
||||||
sm.migrationStats.mu.Unlock()
|
sm.migrationStats.mu.Unlock()
|
||||||
|
|
||||||
if sm.logger != nil {
|
if sm.logger != nil {
|
||||||
sm.logger.Info(fmt.Sprintf("Migration of collection %s completed in %v: %d migrated, %d failed, %d skipped (total %d)",
|
sm.logger.Info(fmt.Sprintf("Migration of collection %s completed in %v: %d migrated, %d failed, %d skipped (total %d)",
|
||||||
collection.Name(), time.Since(startTime), migrated, failed, skipped, total))
|
collection.Name(), time.Since(startTime), migratedCount, failedCount, skippedCount, total))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -813,9 +883,15 @@ func compareVersions(v1, v2 string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== Конкретные реализации миграций схемы ==========
|
// ========== Конкретные реализации миграций схемы ==========
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10): сигнатуры принимают *storage.Batch вместо
|
||||||
|
// *storage.Transaction. Сами миграции работают только с in-memory
|
||||||
|
// структурой SchemaDefinition, поэтому параметр batch не используется
|
||||||
|
// в большинстве реализаций — он оставлен для совместимости с API и
|
||||||
|
// для случаев, когда миграция хочет добавить операции в СУБД.
|
||||||
|
|
||||||
// migrateSchemaAddTimestamps добавляет поля created_at и updated_at
|
// migrateSchemaAddTimestamps добавляет поля created_at и updated_at
|
||||||
func (sm *SchemaMigrator) migrateSchemaAddTimestamps(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaAddTimestamps(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -825,16 +901,16 @@ func (sm *SchemaMigrator) migrateSchemaAddTimestamps(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
if _, exists := collSchema.Fields["created_at"]; !exists {
|
if _, exists := collSchema.Fields["created_at"]; !exists {
|
||||||
collSchema.Fields["created_at"] = &FieldDefinition{
|
collSchema.Fields["created_at"] = &FieldDefinition{
|
||||||
Type: "int64",
|
Type: "int64",
|
||||||
Required: false,
|
Required: false,
|
||||||
Default: int64(0),
|
Default: int64(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, exists := collSchema.Fields["updated_at"]; !exists {
|
if _, exists := collSchema.Fields["updated_at"]; !exists {
|
||||||
collSchema.Fields["updated_at"] = &FieldDefinition{
|
collSchema.Fields["updated_at"] = &FieldDefinition{
|
||||||
Type: "int64",
|
Type: "int64",
|
||||||
Required: false,
|
Required: false,
|
||||||
Default: int64(0),
|
Default: int64(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collSchema.UpdatedAt = time.Now().UnixMilli()
|
collSchema.UpdatedAt = time.Now().UnixMilli()
|
||||||
@@ -843,7 +919,7 @@ func (sm *SchemaMigrator) migrateSchemaAddTimestamps(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaRemoveTimestamps удаляет поля created_at и updated_at
|
// migrateSchemaRemoveTimestamps удаляет поля created_at и updated_at
|
||||||
func (sm *SchemaMigrator) migrateSchemaRemoveTimestamps(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaRemoveTimestamps(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -856,7 +932,7 @@ func (sm *SchemaMigrator) migrateSchemaRemoveTimestamps(tx *storage.Transaction,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaAddSoftDelete добавляет поддержку мягкого удаления
|
// migrateSchemaAddSoftDelete добавляет поддержку мягкого удаления
|
||||||
func (sm *SchemaMigrator) migrateSchemaAddSoftDelete(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaAddSoftDelete(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -866,16 +942,16 @@ func (sm *SchemaMigrator) migrateSchemaAddSoftDelete(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
if _, exists := collSchema.Fields["deleted_at"]; !exists {
|
if _, exists := collSchema.Fields["deleted_at"]; !exists {
|
||||||
collSchema.Fields["deleted_at"] = &FieldDefinition{
|
collSchema.Fields["deleted_at"] = &FieldDefinition{
|
||||||
Type: "int64",
|
Type: "int64",
|
||||||
Required: false,
|
Required: false,
|
||||||
Default: int64(0),
|
Default: int64(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, exists := collSchema.Fields["deleted"]; !exists {
|
if _, exists := collSchema.Fields["deleted"]; !exists {
|
||||||
collSchema.Fields["deleted"] = &FieldDefinition{
|
collSchema.Fields["deleted"] = &FieldDefinition{
|
||||||
Type: "bool",
|
Type: "bool",
|
||||||
Required: false,
|
Required: false,
|
||||||
Default: false,
|
Default: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collSchema.UpdatedAt = time.Now().UnixMilli()
|
collSchema.UpdatedAt = time.Now().UnixMilli()
|
||||||
@@ -884,7 +960,7 @@ func (sm *SchemaMigrator) migrateSchemaAddSoftDelete(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaRemoveSoftDelete удаляет поддержку мягкого удаления
|
// migrateSchemaRemoveSoftDelete удаляет поддержку мягкого удаления
|
||||||
func (sm *SchemaMigrator) migrateSchemaRemoveSoftDelete(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaRemoveSoftDelete(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -897,7 +973,7 @@ func (sm *SchemaMigrator) migrateSchemaRemoveSoftDelete(tx *storage.Transaction,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaAddVersioning добавляет версионирование
|
// migrateSchemaAddVersioning добавляет версионирование
|
||||||
func (sm *SchemaMigrator) migrateSchemaAddVersioning(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaAddVersioning(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -907,9 +983,9 @@ func (sm *SchemaMigrator) migrateSchemaAddVersioning(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
if _, exists := collSchema.Fields["_version"]; !exists {
|
if _, exists := collSchema.Fields["_version"]; !exists {
|
||||||
collSchema.Fields["_version"] = &FieldDefinition{
|
collSchema.Fields["_version"] = &FieldDefinition{
|
||||||
Type: "int64",
|
Type: "int64",
|
||||||
Required: false,
|
Required: false,
|
||||||
Default: int64(1),
|
Default: int64(1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collSchema.UpdatedAt = time.Now().UnixMilli()
|
collSchema.UpdatedAt = time.Now().UnixMilli()
|
||||||
@@ -918,7 +994,7 @@ func (sm *SchemaMigrator) migrateSchemaAddVersioning(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaRemoveVersioning удаляет версионирование
|
// migrateSchemaRemoveVersioning удаляет версионирование
|
||||||
func (sm *SchemaMigrator) migrateSchemaRemoveVersioning(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaRemoveVersioning(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -930,7 +1006,7 @@ func (sm *SchemaMigrator) migrateSchemaRemoveVersioning(tx *storage.Transaction,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaAddIndexes добавляет индексы
|
// migrateSchemaAddIndexes добавляет индексы
|
||||||
func (sm *SchemaMigrator) migrateSchemaAddIndexes(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaAddIndexes(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -962,7 +1038,7 @@ func (sm *SchemaMigrator) migrateSchemaAddIndexes(tx *storage.Transaction, schem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaRemoveIndexes удаляет индексы
|
// migrateSchemaRemoveIndexes удаляет индексы
|
||||||
func (sm *SchemaMigrator) migrateSchemaRemoveIndexes(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaRemoveIndexes(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -980,7 +1056,7 @@ func (sm *SchemaMigrator) migrateSchemaRemoveIndexes(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaAddValidation добавляет валидацию полей
|
// migrateSchemaAddValidation добавляет валидацию полей
|
||||||
func (sm *SchemaMigrator) migrateSchemaAddValidation(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaAddValidation(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -994,7 +1070,7 @@ func (sm *SchemaMigrator) migrateSchemaAddValidation(tx *storage.Transaction, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// migrateSchemaRemoveValidation удаляет валидацию
|
// migrateSchemaRemoveValidation удаляет валидацию
|
||||||
func (sm *SchemaMigrator) migrateSchemaRemoveValidation(tx *storage.Transaction, schema *SchemaDefinition) error {
|
func (sm *SchemaMigrator) migrateSchemaRemoveValidation(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
return fmt.Errorf("nil schema")
|
return fmt.Errorf("nil schema")
|
||||||
}
|
}
|
||||||
@@ -1382,13 +1458,18 @@ func (sm *SchemaMigrator) GetStatus() *MigrationStatus {
|
|||||||
|
|
||||||
// ========== Совместимость со старым кодом ==========
|
// ========== Совместимость со старым кодом ==========
|
||||||
|
|
||||||
// Migration (старая структура для совместимости)
|
// Migration (старая структура для совместимости).
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10): сигнатуры Up/Down принимают *storage.Batch вместо
|
||||||
|
// *storage.Transaction. Batch — это группа операций, применяемых
|
||||||
|
// атомарно через WAL. Семантика та же: миграция может добавить
|
||||||
|
// операции в batch, а batch.Commit() применит их атомарно.
|
||||||
type Migration struct {
|
type Migration struct {
|
||||||
ID string
|
ID string
|
||||||
Version string
|
Version string
|
||||||
Description string
|
Description string
|
||||||
Up func(tx *storage.Transaction) error
|
Up func(batch *storage.Batch) error
|
||||||
Down func(tx *storage.Transaction) error
|
Down func(batch *storage.Batch) error
|
||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
AppliedAt int64
|
AppliedAt int64
|
||||||
}
|
}
|
||||||
@@ -1417,14 +1498,14 @@ func (sm *SchemaMigrator) RegisterMigration(m *Migration) {
|
|||||||
}
|
}
|
||||||
if m.Up != nil {
|
if m.Up != nil {
|
||||||
up := m.Up
|
up := m.Up
|
||||||
schemaMig.Up = func(tx *storage.Transaction, schema *SchemaDefinition) error {
|
schemaMig.Up = func(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
return up(tx)
|
return up(batch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if m.Down != nil {
|
if m.Down != nil {
|
||||||
down := m.Down
|
down := m.Down
|
||||||
schemaMig.Down = func(tx *storage.Transaction, schema *SchemaDefinition) error {
|
schemaMig.Down = func(batch *storage.Batch, schema *SchemaDefinition) error {
|
||||||
return down(tx)
|
return down(batch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sm.RegisterSchemaMigration(schemaMig)
|
sm.RegisterSchemaMigration(schemaMig)
|
||||||
|
|||||||
+356
-44
@@ -36,6 +36,25 @@
|
|||||||
// Это даёт реальное ограничение по времени выполнения, но не по точному
|
// Это даёт реальное ограничение по времени выполнения, но не по точному
|
||||||
// числу инструкций. Поле MaxInstructions сохранено для совместимости с
|
// числу инструкций. Поле MaxInstructions сохранено для совместимости с
|
||||||
// конфигурацией и возможного будущего использования.
|
// конфигурацией и возможного будущего использования.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): после замены ACID-транзакций на batch-операции
|
||||||
|
// функции storage.BeginTransaction, storage.CommitCurrentTransaction,
|
||||||
|
// storage.AbortCurrentTransaction, storage.HasActiveTransaction,
|
||||||
|
// storage.GetCurrentTransactionID, storage.GetActiveTransactions и тип
|
||||||
|
// storage.Transaction были удалены. Lua-API для транзакций переписан
|
||||||
|
// поверх batch-операций. Сохранены старые имена Lua-функций
|
||||||
|
// (begin_transaction, commit_transaction и т.д.) как обёртки над
|
||||||
|
// batch-API, чтобы существующие Lua-плагины не сломались.
|
||||||
|
//
|
||||||
|
// Семантика batch отличается от транзакции: batch — это короткоживущий
|
||||||
|
// объект для группы операций, применяемых атомарно. Он не поддерживает
|
||||||
|
// savepoints, не имеет таймаутов, не отслеживается глобально как
|
||||||
|
// долгоживущее состояние. Поэтому Lua-функция `begin_transaction`
|
||||||
|
// создаёт batch и сохраняет его в реестре активных batch'ей плагина,
|
||||||
|
// а `commit_transaction` применяет его.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат временных меток изменён с "2006-01-02 15:04:05.000"
|
||||||
|
// на "02-01-2006 15:04:05.000" для совместимости с OpenIndiana.
|
||||||
|
|
||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
@@ -1239,6 +1258,13 @@ type Plugin struct {
|
|||||||
closed bool
|
closed bool
|
||||||
isEngine bool
|
isEngine bool
|
||||||
engineName string
|
engineName string
|
||||||
|
|
||||||
|
// Реестр активных batch'ей, созданных через Lua-API.
|
||||||
|
// Ключ — ID batch'а (uint64), значение — *storage.Batch.
|
||||||
|
// Используется функциями begin_transaction/commit_transaction
|
||||||
|
// в Lua-API: `begin_transaction` создаёт batch и регистрирует его
|
||||||
|
// здесь, `commit_transaction` извлекает и коммитит.
|
||||||
|
activeBatches sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsClosed возвращает статус закрытия плагина.
|
// IsClosed возвращает статус закрытия плагина.
|
||||||
@@ -1268,6 +1294,35 @@ func (p *Plugin) IsEnginePlugin() bool { return p.isEngine }
|
|||||||
// GetEngineName возвращает имя движка, если плагин является движком.
|
// GetEngineName возвращает имя движка, если плагин является движком.
|
||||||
func (p *Plugin) GetEngineName() string { return p.engineName }
|
func (p *Plugin) GetEngineName() string { return p.engineName }
|
||||||
|
|
||||||
|
// RegisterBatch регистрирует batch в плагине.
|
||||||
|
func (p *Plugin) RegisterBatch(id uint64, batch *storage.Batch) {
|
||||||
|
if p == nil || batch == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.activeBatches.Store(id, batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnregisterBatch снимает регистрацию batch.
|
||||||
|
func (p *Plugin) UnregisterBatch(id uint64) {
|
||||||
|
if p == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.activeBatches.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBatch возвращает batch по ID.
|
||||||
|
func (p *Plugin) GetBatch(id uint64) (*storage.Batch, bool) {
|
||||||
|
if p == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
val, ok := p.activeBatches.Load(id)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
batch, ok := val.(*storage.Batch)
|
||||||
|
return batch, ok
|
||||||
|
}
|
||||||
|
|
||||||
// ========== PluginManager ==========
|
// ========== PluginManager ==========
|
||||||
|
|
||||||
// PluginManager управляет всеми загруженными плагинами.
|
// PluginManager управляет всеми загруженными плагинами.
|
||||||
@@ -1284,6 +1339,9 @@ type PluginManager struct {
|
|||||||
marketplace *PluginMarketplace
|
marketplace *PluginMarketplace
|
||||||
statePool *LuaStatePool
|
statePool *LuaStatePool
|
||||||
engineRegistry *EngineRegistry
|
engineRegistry *EngineRegistry
|
||||||
|
|
||||||
|
// nextBatchID — счётчик для ID batch'ей, созданных через Lua-API.
|
||||||
|
nextBatchID atomicUint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginEvent представляет событие от плагина.
|
// PluginEvent представляет событие от плагина.
|
||||||
@@ -1979,6 +2037,8 @@ func (pm *PluginManager) LoadPlugin(name, filePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadPluginWithSandbox загружает плагин с ограничениями, используя пул состояний.
|
// LoadPluginWithSandbox загружает плагин с ограничениями, используя пул состояний.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат loaded_at_str теперь "02-01-2006 15:04:05.000".
|
||||||
func (pm *PluginManager) LoadPluginWithSandbox(name, filePath string, limits SandboxLimits) error {
|
func (pm *PluginManager) LoadPluginWithSandbox(name, filePath string, limits SandboxLimits) error {
|
||||||
if !pm.enabled {
|
if !pm.enabled {
|
||||||
return fmt.Errorf("plugin system is disabled")
|
return fmt.Errorf("plugin system is disabled")
|
||||||
@@ -2005,8 +2065,14 @@ func (pm *PluginManager) LoadPluginWithSandbox(name, filePath string, limits San
|
|||||||
return fmt.Errorf("failed to acquire Lua state: %v", err)
|
return fmt.Errorf("failed to acquire Lua state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Регистрируем API до выполнения скрипта.
|
||||||
|
// Для транзакционных функций нам нужна ссылка на Plugin,
|
||||||
|
// но она создаётся только после DoString. Поэтому регистрируем
|
||||||
|
// функции, которые будут искать плагин по имени через pm.plugins.
|
||||||
|
// Это работает, потому что к моменту вызова Lua-функции плагин
|
||||||
|
// уже будет зарегистрирован в pm.plugins.
|
||||||
pm.registerDatabaseFunctions(L.LState)
|
pm.registerDatabaseFunctions(L.LState)
|
||||||
pm.registerTransactionFunctions(L.LState)
|
pm.registerTransactionFunctions(L.LState, name)
|
||||||
pm.registerTriggerFunctions(L.LState)
|
pm.registerTriggerFunctions(L.LState)
|
||||||
pm.registerTimestampFunctions(L.LState)
|
pm.registerTimestampFunctions(L.LState)
|
||||||
pm.registerEngineFunctions(L.LState, name)
|
pm.registerEngineFunctions(L.LState, name)
|
||||||
@@ -2055,7 +2121,8 @@ func (pm *PluginManager) LoadPluginWithSandbox(name, filePath string, limits San
|
|||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nowMs := now.UnixMilli()
|
nowMs := now.UnixMilli()
|
||||||
nowStr := now.Format("2006-01-02 15:04:05.000")
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
nowStr := now.Format("02-01-2006 15:04:05.000")
|
||||||
|
|
||||||
basePlugin := &Plugin{
|
basePlugin := &Plugin{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -2197,6 +2264,8 @@ func (pm *PluginManager) checkDependencies(deps []PluginDependency) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// registerTimestampFunctions регистрирует функции для работы с временными метками в Lua.
|
// registerTimestampFunctions регистрирует функции для работы с временными метками в Lua.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (pm *PluginManager) registerTimestampFunctions(L *lua.LState) {
|
func (pm *PluginManager) registerTimestampFunctions(L *lua.LState) {
|
||||||
L.SetGlobal("get_current_timestamp_ms", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("get_current_timestamp_ms", L.NewFunction(func(L *lua.LState) int {
|
||||||
L.Push(lua.LNumber(time.Now().UnixMilli()))
|
L.Push(lua.LNumber(time.Now().UnixMilli()))
|
||||||
@@ -2204,7 +2273,7 @@ func (pm *PluginManager) registerTimestampFunctions(L *lua.LState) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
L.SetGlobal("get_current_timestamp_str", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("get_current_timestamp_str", L.NewFunction(func(L *lua.LState) int {
|
||||||
L.Push(lua.LString(time.Now().Format("2006-01-02 15:04:05.000")))
|
L.Push(lua.LString(time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -2213,7 +2282,7 @@ func (pm *PluginManager) registerTimestampFunctions(L *lua.LState) {
|
|||||||
table := L.NewTable()
|
table := L.NewTable()
|
||||||
table.RawSetString("unix_ms", lua.LNumber(now.UnixMilli()))
|
table.RawSetString("unix_ms", lua.LNumber(now.UnixMilli()))
|
||||||
table.RawSetString("unix_sec", lua.LNumber(now.Unix()))
|
table.RawSetString("unix_sec", lua.LNumber(now.Unix()))
|
||||||
table.RawSetString("string", lua.LString(now.Format("2006-01-02 15:04:05.000")))
|
table.RawSetString("string", lua.LString(now.Format("02-01-2006 15:04:05.000")))
|
||||||
table.RawSetString("year", lua.LNumber(now.Year()))
|
table.RawSetString("year", lua.LNumber(now.Year()))
|
||||||
table.RawSetString("month", lua.LNumber(int(now.Month())))
|
table.RawSetString("month", lua.LNumber(int(now.Month())))
|
||||||
table.RawSetString("day", lua.LNumber(now.Day()))
|
table.RawSetString("day", lua.LNumber(now.Day()))
|
||||||
@@ -2311,7 +2380,8 @@ func (pm *PluginManager) registerDatabaseFunctions(L *lua.LState) {
|
|||||||
eventData := L.CheckAny(2)
|
eventData := L.CheckAny(2)
|
||||||
|
|
||||||
nowMs := time.Now().UnixMilli()
|
nowMs := time.Now().UnixMilli()
|
||||||
nowStr := time.Now().Format("2006-01-02 15:04:05.000")
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
nowStr := time.Now().Format("02-01-2006 15:04:05.000")
|
||||||
event := PluginEvent{
|
event := PluginEvent{
|
||||||
EventType: eventType,
|
EventType: eventType,
|
||||||
Data: pm.luaValueToGo(eventData),
|
Data: pm.luaValueToGo(eventData),
|
||||||
@@ -2500,76 +2570,246 @@ func (pm *PluginManager) registerEngineFunctions(L *lua.LState, pluginName strin
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerTransactionFunctions регистрирует функции для работы с транзакциями.
|
// registerTransactionFunctions регистрирует функции для работы с batch-операциями
|
||||||
func (pm *PluginManager) registerTransactionFunctions(L *lua.LState) {
|
// в Lua. Сохранены старые имена (begin_transaction и т.д.) для совместимости
|
||||||
|
// с существующими Lua-плагинами, но семантически это batch-операции.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10): storage.BeginTransaction и связанные функции были
|
||||||
|
// удалены при переходе на batch. Теперь:
|
||||||
|
// - begin_transaction создаёт *storage.Batch через storage.NewBatch
|
||||||
|
// и регистрирует его в Plugin.activeBatches по ID.
|
||||||
|
// - commit_transaction извлекает batch из Plugin.activeBatches и
|
||||||
|
// вызывает batch.Commit().
|
||||||
|
// - abort_transaction просто снимает регистрацию batch (не коммитит).
|
||||||
|
// - has_active_transaction проверяет, есть ли у плагина активные batch'и.
|
||||||
|
// - get_current_transaction_id возвращает ID последнего созданного batch'а.
|
||||||
|
// - get_active_transactions возвращает список активных batch'ей плагина.
|
||||||
|
//
|
||||||
|
// Параметр pluginName используется для поиска Plugin в pm.plugins.
|
||||||
|
func (pm *PluginManager) registerTransactionFunctions(L *lua.LState, pluginName string) {
|
||||||
|
// Вспомогательная функция для получения текущего плагина.
|
||||||
|
getCurrentPlugin := func() *Plugin {
|
||||||
|
val, ok := pm.plugins.Load(pluginName)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, p := getPluginLState(val)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
L.SetGlobal("begin_transaction", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("begin_transaction", L.NewFunction(func(L *lua.LState) int {
|
||||||
tx := storage.BeginTransaction()
|
p := getCurrentPlugin()
|
||||||
if tx == nil {
|
if p == nil {
|
||||||
L.Push(lua.LNil)
|
L.Push(lua.LNil)
|
||||||
L.Push(lua.LString("Failed to begin transaction"))
|
L.Push(lua.LString("plugin not registered"))
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем, что storage доступен.
|
||||||
|
if pm.storage == nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString("storage not available"))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем параметры: database, collection.
|
||||||
|
database := L.OptString(1, "")
|
||||||
|
collection := L.OptString(2, "")
|
||||||
|
|
||||||
|
batch := storage.NewBatch(database, collection)
|
||||||
|
if batch == nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString("failed to create batch"))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
batchID := pm.nextBatchID.Add(1)
|
||||||
|
p.RegisterBatch(batchID, batch)
|
||||||
|
|
||||||
ud := L.NewUserData()
|
ud := L.NewUserData()
|
||||||
ud.Value = tx
|
ud.Value = batch
|
||||||
L.SetMetatable(ud, L.GetTypeMetatable("transaction"))
|
L.SetMetatable(ud, L.GetTypeMetatable("transaction"))
|
||||||
|
L.SetField(ud, "id", lua.LNumber(batchID))
|
||||||
L.Push(ud)
|
L.Push(ud)
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
L.SetGlobal("commit_transaction", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("commit_transaction", L.NewFunction(func(L *lua.LState) int {
|
||||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
p := getCurrentPlugin()
|
||||||
|
if p == nil {
|
||||||
|
L.Push(lua.LString("plugin not registered"))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пробуем получить batch из userdata, если передан.
|
||||||
|
var batchID uint64
|
||||||
|
if L.GetTop() >= 1 {
|
||||||
|
ud := L.CheckUserData(1)
|
||||||
|
if idVal := L.GetField(ud, "id"); idVal != lua.LNil {
|
||||||
|
if num, ok := idVal.(lua.LNumber); ok {
|
||||||
|
batchID = uint64(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если batchID == 0 — коммитим последний активный batch.
|
||||||
|
// Для этого используем любой batch из activeBatches.
|
||||||
|
if batchID == 0 {
|
||||||
|
var found *storage.Batch
|
||||||
|
var foundID uint64
|
||||||
|
p.activeBatches.Range(func(key, value interface{}) bool {
|
||||||
|
if b, ok := value.(*storage.Batch); ok {
|
||||||
|
found = b
|
||||||
|
if id, ok := key.(uint64); ok {
|
||||||
|
foundID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if found == nil {
|
||||||
|
L.Push(lua.LString("no active batch"))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
batchID = foundID
|
||||||
|
}
|
||||||
|
|
||||||
|
batch, ok := p.GetBatch(batchID)
|
||||||
|
if !ok {
|
||||||
|
L.Push(lua.LString("batch not found"))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := batch.Commit(); err != nil {
|
||||||
|
p.UnregisterBatch(batchID)
|
||||||
L.Push(lua.LString(err.Error()))
|
L.Push(lua.LString(err.Error()))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit уже снял регистрацию через defer, но на всякий случай.
|
||||||
|
p.UnregisterBatch(batchID)
|
||||||
L.Push(lua.LNil)
|
L.Push(lua.LNil)
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
L.SetGlobal("abort_transaction", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("abort_transaction", L.NewFunction(func(L *lua.LState) int {
|
||||||
if err := storage.AbortCurrentTransaction(); err != nil {
|
p := getCurrentPlugin()
|
||||||
L.Push(lua.LString(err.Error()))
|
if p == nil {
|
||||||
|
L.Push(lua.LString("plugin not registered"))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var batchID uint64
|
||||||
|
if L.GetTop() >= 1 {
|
||||||
|
ud := L.CheckUserData(1)
|
||||||
|
if idVal := L.GetField(ud, "id"); idVal != lua.LNil {
|
||||||
|
if num, ok := idVal.(lua.LNumber); ok {
|
||||||
|
batchID = uint64(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if batchID == 0 {
|
||||||
|
// Снимаем регистрацию со всех batch'ей плагина.
|
||||||
|
p.activeBatches.Range(func(key, value interface{}) bool {
|
||||||
|
if id, ok := key.(uint64); ok {
|
||||||
|
p.UnregisterBatch(id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
p.UnregisterBatch(batchID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// batch просто забывается — его операции не применяются.
|
||||||
|
// Регистрация в globalBatchManager.activeBatches снимается
|
||||||
|
// через UnregisterBatch (мы вызываем его внутри).
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
// Если batch всё ещё зарегистрирован в глобальном менеджере,
|
||||||
|
// снимаем и там.
|
||||||
|
if b, ok := p.GetBatch(batchID); ok && b != nil {
|
||||||
|
bm.UnregisterBatch(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
L.Push(lua.LNil)
|
L.Push(lua.LNil)
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
L.SetGlobal("has_active_transaction", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("has_active_transaction", L.NewFunction(func(L *lua.LState) int {
|
||||||
L.Push(lua.LBool(storage.HasActiveTransaction()))
|
p := getCurrentPlugin()
|
||||||
|
if p == nil {
|
||||||
|
L.Push(lua.LBool(false))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hasActive := false
|
||||||
|
p.activeBatches.Range(func(key, value interface{}) bool {
|
||||||
|
hasActive = true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
L.Push(lua.LBool(hasActive))
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
L.SetGlobal("get_current_transaction_id", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("get_current_transaction_id", L.NewFunction(func(L *lua.LState) int {
|
||||||
txID := storage.GetCurrentTransactionID()
|
p := getCurrentPlugin()
|
||||||
if txID == "" {
|
if p == nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastID uint64
|
||||||
|
p.activeBatches.Range(func(key, value interface{}) bool {
|
||||||
|
if id, ok := key.(uint64); ok {
|
||||||
|
if id > lastID {
|
||||||
|
lastID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if lastID == 0 {
|
||||||
L.Push(lua.LNil)
|
L.Push(lua.LNil)
|
||||||
} else {
|
} else {
|
||||||
L.Push(lua.LString(txID))
|
L.Push(lua.LString(fmt.Sprintf("%d", lastID)))
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
L.SetGlobal("get_active_transactions", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("get_active_transactions", L.NewFunction(func(L *lua.LState) int {
|
||||||
txs := storage.GetActiveTransactions()
|
p := getCurrentPlugin()
|
||||||
table := L.NewTable()
|
table := L.NewTable()
|
||||||
for i, tx := range txs {
|
if p == nil {
|
||||||
txTable := L.NewTable()
|
L.Push(table)
|
||||||
txTable.RawSetString("id", lua.LString(tx.ID))
|
return 1
|
||||||
txTable.RawSetString("status", lua.LString(tx.Status))
|
|
||||||
txTable.RawSetString("start_time", lua.LNumber(tx.StartTime))
|
|
||||||
txTable.RawSetString("operation_count", lua.LNumber(tx.OperationCount))
|
|
||||||
table.RawSetInt(i+1, txTable)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
idx := 0
|
||||||
|
p.activeBatches.Range(func(key, value interface{}) bool {
|
||||||
|
batch, ok := value.(*storage.Batch)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
txTable := L.NewTable()
|
||||||
|
txTable.RawSetString("id", lua.LString(fmt.Sprintf("%d", batch.ID)))
|
||||||
|
txTable.RawSetString("status", lua.LString("active"))
|
||||||
|
txTable.RawSetString("start_time", lua.LNumber(batch.Timestamp))
|
||||||
|
txTable.RawSetString("operation_count", lua.LNumber(len(batch.Operations)))
|
||||||
|
table.RawSetInt(idx, txTable)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
L.Push(table)
|
L.Push(table)
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Настройка метатаблицы для транзакций
|
// Настройка метатаблицы для batch'ей (для совместимости с "transaction").
|
||||||
mt := L.NewTypeMetatable("transaction")
|
mt := L.NewTypeMetatable("transaction")
|
||||||
L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
|
L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
|
||||||
ud := L.CheckUserData(1)
|
ud := L.CheckUserData(1)
|
||||||
tx, ok := ud.Value.(*storage.Transaction)
|
batch, ok := ud.Value.(*storage.Batch)
|
||||||
if !ok {
|
if !ok {
|
||||||
L.Push(lua.LNil)
|
L.Push(lua.LNil)
|
||||||
return 1
|
return 1
|
||||||
@@ -2578,13 +2818,58 @@ func (pm *PluginManager) registerTransactionFunctions(L *lua.LState) {
|
|||||||
|
|
||||||
switch method {
|
switch method {
|
||||||
case "get_id":
|
case "get_id":
|
||||||
L.Push(lua.LString(fmt.Sprintf("%d", tx.ID)))
|
L.Push(lua.LString(fmt.Sprintf("%d", batch.ID)))
|
||||||
case "get_operation_count":
|
case "get_operation_count":
|
||||||
L.Push(lua.LNumber(0))
|
L.Push(lua.LNumber(len(batch.Operations)))
|
||||||
case "get_start_time":
|
case "get_start_time":
|
||||||
L.Push(lua.LNumber(tx.StartTime))
|
L.Push(lua.LNumber(batch.Timestamp))
|
||||||
case "get_status":
|
case "get_status":
|
||||||
L.Push(lua.LString("active"))
|
L.Push(lua.LString("active"))
|
||||||
|
case "add_insert":
|
||||||
|
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||||
|
docData := L.CheckTable(1)
|
||||||
|
doc := storage.NewDocument()
|
||||||
|
docData.ForEach(func(key, value lua.LValue) {
|
||||||
|
if key.Type() == lua.LTString {
|
||||||
|
doc.SetField(key.String(), pm.luaValueToGo(value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
batch.AddInsert(doc)
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
case "add_update":
|
||||||
|
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||||
|
docID := L.CheckString(1)
|
||||||
|
updates := L.CheckTable(2)
|
||||||
|
updatesMap := pm.luaTableToMap(updates)
|
||||||
|
batch.AddUpdate(docID, updatesMap)
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
case "add_delete":
|
||||||
|
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||||
|
docID := L.CheckString(1)
|
||||||
|
batch.AddDelete(docID)
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
case "add_restore":
|
||||||
|
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||||
|
docID := L.CheckString(1)
|
||||||
|
batch.AddRestore(docID)
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
case "commit":
|
||||||
|
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||||||
|
if err := batch.Commit(); err != nil {
|
||||||
|
L.Push(lua.LString(err.Error()))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
default:
|
default:
|
||||||
L.Push(lua.LNil)
|
L.Push(lua.LNil)
|
||||||
}
|
}
|
||||||
@@ -2669,7 +2954,7 @@ func (pm *PluginManager) registerTriggerFunctions(L *lua.LState) {
|
|||||||
L.Push(lua.LString(err.Error()))
|
L.Push(lua.LString(err.Error()))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
L.Push(lua.LNil)
|
L.Push(lua.LNil)
|
||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -2995,6 +3280,8 @@ func (pm *PluginManager) callPluginFunction(plugin interface{}, funcName string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addPluginEventLog добавляет событие в лог плагина.
|
// addPluginEventLog добавляет событие в лог плагина.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат nowStr теперь "02-01-2006 15:04:05.000".
|
||||||
func (pm *PluginManager) addPluginEventLog(plugin interface{}, eventType string, data interface{}, errMsg string) {
|
func (pm *PluginManager) addPluginEventLog(plugin interface{}, eventType string, data interface{}, errMsg string) {
|
||||||
var p *Plugin
|
var p *Plugin
|
||||||
|
|
||||||
@@ -3011,7 +3298,8 @@ func (pm *PluginManager) addPluginEventLog(plugin interface{}, eventType string,
|
|||||||
defer p.eventLogMu.Unlock()
|
defer p.eventLogMu.Unlock()
|
||||||
|
|
||||||
nowMs := time.Now().UnixMilli()
|
nowMs := time.Now().UnixMilli()
|
||||||
nowStr := time.Now().Format("2006-01-02 15:04:05.000")
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
nowStr := time.Now().Format("02-01-2006 15:04:05.000")
|
||||||
eventLog := PluginEventLog{
|
eventLog := PluginEventLog{
|
||||||
PluginName: p.Name,
|
PluginName: p.Name,
|
||||||
EventType: eventType,
|
EventType: eventType,
|
||||||
@@ -3157,6 +3445,8 @@ func (pm *PluginManager) ExecutePlugin(pluginName, funcName string, args ...inte
|
|||||||
// Используем поле plugin.instrumented вместо type assertion
|
// Используем поле plugin.instrumented вместо type assertion
|
||||||
// interface{}(L).(*InstrumentedLState), который всегда возвращал false,
|
// interface{}(L).(*InstrumentedLState), который всегда возвращал false,
|
||||||
// что приводило к утечке Lua-состояний и некорректному уменьшению active.
|
// что приводило к утечке Lua-состояний и некорректному уменьшению active.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат unloaded_at_str — "02-01-2006 15:04:05.000".
|
||||||
func (pm *PluginManager) UnloadPlugin(name string) error {
|
func (pm *PluginManager) UnloadPlugin(name string) error {
|
||||||
if !pm.enabled {
|
if !pm.enabled {
|
||||||
return fmt.Errorf("plugin system is disabled")
|
return fmt.Errorf("plugin system is disabled")
|
||||||
@@ -3167,6 +3457,29 @@ func (pm *PluginManager) UnloadPlugin(name string) error {
|
|||||||
return fmt.Errorf("plugin not found: %s", name)
|
return fmt.Errorf("plugin not found: %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сначала отменяем все активные batch'и плагина,
|
||||||
|
// чтобы не оставить их зарегистрированными в глобальном менеджере.
|
||||||
|
var p *Plugin
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *Plugin:
|
||||||
|
p = v
|
||||||
|
case *PluginWithDeps:
|
||||||
|
p = v.Plugin
|
||||||
|
}
|
||||||
|
if p != nil {
|
||||||
|
p.activeBatches.Range(func(key, value interface{}) bool {
|
||||||
|
if b, ok := value.(*storage.Batch); ok && b != nil {
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if id, ok := key.(uint64); ok {
|
||||||
|
p.UnregisterBatch(id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err := pm.callPluginFunction(val, "on_unload"); err != nil {
|
if err := pm.callPluginFunction(val, "on_unload"); err != nil {
|
||||||
if pm.logger != nil {
|
if pm.logger != nil {
|
||||||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_unload error: %v", name, err))
|
pm.logger.Warn(fmt.Sprintf("Plugin %s on_unload error: %v", name, err))
|
||||||
@@ -3179,14 +3492,6 @@ func (pm *PluginManager) UnloadPlugin(name string) error {
|
|||||||
// Извлекаем InstrumentedLState из плагина
|
// Извлекаем InstrumentedLState из плагина
|
||||||
inst := getPluginInstrumented(val)
|
inst := getPluginInstrumented(val)
|
||||||
|
|
||||||
var p *Plugin
|
|
||||||
switch v := val.(type) {
|
|
||||||
case *Plugin:
|
|
||||||
p = v
|
|
||||||
case *PluginWithDeps:
|
|
||||||
p = v.Plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
if p != nil {
|
if p != nil {
|
||||||
p.Status.Store(int32(StatusStopped))
|
p.Status.Store(int32(StatusStopped))
|
||||||
p.closePlugin()
|
p.closePlugin()
|
||||||
@@ -3200,7 +3505,8 @@ func (pm *PluginManager) UnloadPlugin(name string) error {
|
|||||||
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
||||||
storage.LogAudit("PLUGIN_UNLOAD", "PLUGIN", name, map[string]interface{}{
|
storage.LogAudit("PLUGIN_UNLOAD", "PLUGIN", name, map[string]interface{}{
|
||||||
"unloaded_at": time.Now().UnixMilli(),
|
"unloaded_at": time.Now().UnixMilli(),
|
||||||
"unloaded_at_str": time.Now().Format("2006-01-02 15:04:05.000"),
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
"unloaded_at_str": time.Now().Format("02-01-2006 15:04:05.000"),
|
||||||
"audit_time": auditTimeMs,
|
"audit_time": auditTimeMs,
|
||||||
"audit_time_str": auditTimeStr,
|
"audit_time_str": auditTimeStr,
|
||||||
"active_lua_states": pm.statePool.GetActiveCount(),
|
"active_lua_states": pm.statePool.GetActiveCount(),
|
||||||
@@ -3215,6 +3521,8 @@ func (pm *PluginManager) UnloadPlugin(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartPlugin запускает плагин.
|
// StartPlugin запускает плагин.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат started_at_str — "02-01-2006 15:04:05.000".
|
||||||
func (pm *PluginManager) StartPlugin(name string) error {
|
func (pm *PluginManager) StartPlugin(name string) error {
|
||||||
if !pm.enabled {
|
if !pm.enabled {
|
||||||
return fmt.Errorf("plugin system is disabled")
|
return fmt.Errorf("plugin system is disabled")
|
||||||
@@ -3252,7 +3560,8 @@ func (pm *PluginManager) StartPlugin(name string) error {
|
|||||||
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
||||||
storage.LogAudit("PLUGIN_START", "PLUGIN", name, map[string]interface{}{
|
storage.LogAudit("PLUGIN_START", "PLUGIN", name, map[string]interface{}{
|
||||||
"started_at": time.Now().UnixMilli(),
|
"started_at": time.Now().UnixMilli(),
|
||||||
"started_at_str": time.Now().Format("2006-01-02 15:04:05.000"),
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
"started_at_str": time.Now().Format("02-01-2006 15:04:05.000"),
|
||||||
"audit_time": auditTimeMs,
|
"audit_time": auditTimeMs,
|
||||||
"audit_time_str": auditTimeStr,
|
"audit_time_str": auditTimeStr,
|
||||||
"active_lua_states": pm.statePool.GetActiveCount(),
|
"active_lua_states": pm.statePool.GetActiveCount(),
|
||||||
@@ -3265,6 +3574,8 @@ func (pm *PluginManager) StartPlugin(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StopPlugin останавливает плагин.
|
// StopPlugin останавливает плагин.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат stopped_at_str — "02-01-2006 15:04:05.000".
|
||||||
func (pm *PluginManager) StopPlugin(name string) error {
|
func (pm *PluginManager) StopPlugin(name string) error {
|
||||||
if !pm.enabled {
|
if !pm.enabled {
|
||||||
return fmt.Errorf("plugin system is disabled")
|
return fmt.Errorf("plugin system is disabled")
|
||||||
@@ -3298,7 +3609,8 @@ func (pm *PluginManager) StopPlugin(name string) error {
|
|||||||
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
||||||
storage.LogAudit("PLUGIN_STOP", "PLUGIN", name, map[string]interface{}{
|
storage.LogAudit("PLUGIN_STOP", "PLUGIN", name, map[string]interface{}{
|
||||||
"stopped_at": time.Now().UnixMilli(),
|
"stopped_at": time.Now().UnixMilli(),
|
||||||
"stopped_at_str": time.Now().Format("2006-01-02 15:04:05.000"),
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
"stopped_at_str": time.Now().Format("02-01-2006 15:04:05.000"),
|
||||||
"audit_time": auditTimeMs,
|
"audit_time": auditTimeMs,
|
||||||
"audit_time_str": auditTimeStr,
|
"audit_time_str": auditTimeStr,
|
||||||
"active_lua_states": pm.statePool.GetActiveCount(),
|
"active_lua_states": pm.statePool.GetActiveCount(),
|
||||||
|
|||||||
+248
-96
@@ -19,6 +19,26 @@
|
|||||||
// - пейджер для длинных результатов;
|
// - пейджер для длинных результатов;
|
||||||
// - табличный вывод по умолчанию.
|
// - табличный вывод по умолчанию.
|
||||||
// Совместимо с Linux и OpenIndiana.
|
// Совместимо с Linux и OpenIndiana.
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): после замены ACID-транзакций на batch-операции
|
||||||
|
// функции storage.BeginTransaction, storage.CommitCurrentTransaction,
|
||||||
|
// storage.AbortCurrentTransaction, storage.HasActiveTransaction,
|
||||||
|
// storage.GetActiveTransactions были удалены. Команды REPL для транзакций
|
||||||
|
// переписаны поверх batch-API:
|
||||||
|
// - "begin transaction" → создаёт *storage.Batch (алиас "batch begin");
|
||||||
|
// - "commit" → batch.Commit() (алиас "batch commit");
|
||||||
|
// - "rollback" → снимает регистрацию batch (алиас "batch abort");
|
||||||
|
// - "show transactions" → показывает активный batch сессии.
|
||||||
|
// Старые имена команд сохранены как алиасы для совместимости с привычками
|
||||||
|
// пользователей и существующими скриптами.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат временных меток изменён с "2006-01-02 15:04:05.000"
|
||||||
|
// на "02-01-2006 15:04:05.000" для совместимости с OpenIndiana.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): формат временных меток УНИФИЦИРОВАН — везде используется
|
||||||
|
// "02-01-2006 15:04:05.000" с миллисекундами. Ранее в некоторых табличных выводах
|
||||||
|
// (индексы, плагины, триггеры, узлы кластера, статус миграции) формат был без
|
||||||
|
// миллисекунд — теперь это исправлено для консистентности.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
package repl
|
package repl
|
||||||
@@ -67,6 +87,12 @@ type Repl struct {
|
|||||||
authenticated bool
|
authenticated bool
|
||||||
sessionID string
|
sessionID string
|
||||||
|
|
||||||
|
// ДОБАВЛЕНО (2026-10): текущий активный batch (замена транзакции).
|
||||||
|
// Создаётся командой "begin transaction"/"batch begin", коммитится
|
||||||
|
// командой "commit"/"batch commit", снимается командой "rollback"/"batch abort".
|
||||||
|
currentBatch *storage.Batch
|
||||||
|
currentBatchID uint64
|
||||||
|
|
||||||
commands map[string]*Command
|
commands map[string]*Command
|
||||||
history []string
|
history []string
|
||||||
historyPos int
|
historyPos int
|
||||||
@@ -218,12 +244,24 @@ func (r *Repl) registerCommands() {
|
|||||||
r.commands["trigger log"] = &Command{Name: "trigger log", Description: "Show trigger execution log", Handler: r.handleTriggerLog}
|
r.commands["trigger log"] = &Command{Name: "trigger log", Description: "Show trigger execution log", Handler: r.handleTriggerLog}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Транзакции
|
// Batch-операции (замена транзакций).
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): старые имена команд (begin transaction, commit,
|
||||||
|
// rollback, show transactions) сохранены как алиасы, чтобы существующие
|
||||||
|
// скрипты и привычки пользователей не сломались. Новые канонические
|
||||||
|
// имена — batch begin, batch commit, batch abort, show batches.
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
r.commands["begin transaction"] = &Command{Name: "begin transaction", Description: "Start a new transaction", Handler: r.handleBeginTransaction}
|
// Канонические batch-команды.
|
||||||
r.commands["commit"] = &Command{Name: "commit", Description: "Commit current transaction", Handler: r.handleCommitTransaction}
|
r.commands["batch begin"] = &Command{Name: "batch begin", Description: "Begin a new batch (atomically applied group of operations)", Handler: r.handleBeginTransaction}
|
||||||
r.commands["rollback"] = &Command{Name: "rollback", Description: "Rollback current transaction", Handler: r.handleRollbackTransaction}
|
r.commands["batch commit"] = &Command{Name: "batch commit", Description: "Commit the current batch", Handler: r.handleCommitTransaction}
|
||||||
r.commands["show transactions"] = &Command{Name: "show transactions", Description: "Show active transactions", Handler: r.handleShowTransactions}
|
r.commands["batch abort"] = &Command{Name: "batch abort", Description: "Abort the current batch (operations are discarded)", Handler: r.handleRollbackTransaction}
|
||||||
|
r.commands["show batches"] = &Command{Name: "show batches", Description: "Show active batch of the current session", Handler: r.handleShowTransactions}
|
||||||
|
|
||||||
|
// Алиасы для обратной совместимости.
|
||||||
|
r.commands["begin transaction"] = &Command{Name: "begin transaction", Description: "Alias for 'batch begin'", Handler: r.handleBeginTransaction}
|
||||||
|
r.commands["commit"] = &Command{Name: "commit", Description: "Alias for 'batch commit'", Handler: r.handleCommitTransaction}
|
||||||
|
r.commands["rollback"] = &Command{Name: "rollback", Description: "Alias for 'batch abort'", Handler: r.handleRollbackTransaction}
|
||||||
|
r.commands["show transactions"] = &Command{Name: "show transactions", Description: "Alias for 'show batches'", Handler: r.handleShowTransactions}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Плагины
|
// Плагины
|
||||||
@@ -379,11 +417,6 @@ func (r *Repl) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// collectMultiline продолжает ввод, если строка не закончена.
|
// collectMultiline продолжает ввод, если строка не закончена.
|
||||||
// Признаки продолжения:
|
|
||||||
// - обратный слэш в конце;
|
|
||||||
// - незакрытые {}, [], ().
|
|
||||||
//
|
|
||||||
// Пустая строка завершает многострочный ввод.
|
|
||||||
func (r *Repl) collectMultiline(first string) (string, error) {
|
func (r *Repl) collectMultiline(first string) (string, error) {
|
||||||
acc := first
|
acc := first
|
||||||
depthCurly, depthSquare, depthParen := countBrackets(acc)
|
depthCurly, depthSquare, depthParen := countBrackets(acc)
|
||||||
@@ -476,6 +509,11 @@ func (r *Repl) buildPrompt() string {
|
|||||||
prompt += color.New(color.FgHiGreen).Sprint(" (" + r.currentUser + ")")
|
prompt += color.New(color.FgHiGreen).Sprint(" (" + r.currentUser + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ДОБАВЛЕНО: индикатор активного batch'а.
|
||||||
|
if r.currentBatch != nil {
|
||||||
|
prompt += color.New(color.FgHiMagenta).Sprint(" [batch]")
|
||||||
|
}
|
||||||
|
|
||||||
prompt += color.New(color.FgHiCyan).Sprint(":~> ")
|
prompt += color.New(color.FgHiCyan).Sprint(":~> ")
|
||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
@@ -535,9 +573,6 @@ func (r *Repl) addToHistory(cmd string) {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// PgUp/PgDown — программная навигация
|
// PgUp/PgDown — программная навигация
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Эти методы сохранены для случаев, когда readline недоступен (например,
|
|
||||||
// в неинтерактивном режиме или при юнит-тестах). В интерактивном режиме
|
|
||||||
// PgUp/PgDown обрабатываются самим readline.
|
|
||||||
|
|
||||||
// HandlePgUp возвращает предыдущую команду из истории.
|
// HandlePgUp возвращает предыдущую команду из истории.
|
||||||
func (r *Repl) HandlePgUp() string {
|
func (r *Repl) HandlePgUp() string {
|
||||||
@@ -584,6 +619,15 @@ func (r *Repl) output(text string) {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
func (r *Repl) Close() error {
|
func (r *Repl) Close() error {
|
||||||
|
// ДОБАВЛЕНО: снимаем активный batch при выходе, чтобы не оставить
|
||||||
|
// его зарегистрированным в глобальном менеджере.
|
||||||
|
if r.currentBatch != nil {
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(r.currentBatch)
|
||||||
|
}
|
||||||
|
r.currentBatch = nil
|
||||||
|
r.currentBatchID = 0
|
||||||
|
}
|
||||||
if r.historyFile != nil {
|
if r.historyFile != nil {
|
||||||
_ = r.historyFile.Save()
|
_ = r.historyFile.Save()
|
||||||
}
|
}
|
||||||
@@ -601,6 +645,7 @@ func (r *Repl) Close() error {
|
|||||||
// Базы данных
|
// Базы данных
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleCreateSlice(args []string) error {
|
func (r *Repl) handleCreateSlice(args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return fmt.Errorf("usage: create slice <name>")
|
return fmt.Errorf("usage: create slice <name>")
|
||||||
@@ -609,10 +654,11 @@ func (r *Repl) handleCreateSlice(args []string) error {
|
|||||||
if err := r.store.CreateDatabase(name); err != nil {
|
if err := r.store.CreateDatabase(name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Slice '%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("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleDropDatabase(args []string) error {
|
func (r *Repl) handleDropDatabase(args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return fmt.Errorf("usage: drop database <name>")
|
return fmt.Errorf("usage: drop database <name>")
|
||||||
@@ -624,7 +670,7 @@ func (r *Repl) handleDropDatabase(args []string) error {
|
|||||||
if r.currentDB == name {
|
if r.currentDB == name {
|
||||||
r.currentDB = ""
|
r.currentDB = ""
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Database '%s' dropped at %s", name, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Database '%s' dropped at %s", name, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,6 +710,7 @@ func (r *Repl) handleShowDatabases(args []string) error {
|
|||||||
// Коллекции
|
// Коллекции
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleCreateCollection(args []string) error {
|
func (r *Repl) handleCreateCollection(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -679,7 +726,7 @@ func (r *Repl) handleCreateCollection(args []string) error {
|
|||||||
if err := db.CreateCollection(name); err != nil {
|
if err := db.CreateCollection(name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Collection '%s' created in database '%s' at %s", name, r.currentDB, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Collection '%s' created in database '%s' at %s", name, r.currentDB, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,6 +775,7 @@ func (r *Repl) handleShowCollections(args []string) error {
|
|||||||
// Документы
|
// Документы
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleInsert(args []string) error {
|
func (r *Repl) handleInsert(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -775,10 +823,11 @@ func (r *Repl) handleInsert(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Document inserted with ID: %s (created at: %s)",
|
utils.PrintSuccess(fmt.Sprintf("Document inserted with ID: %s (created at: %s)",
|
||||||
doc.ID, time.UnixMilli(doc.CreatedAt).Format("2006-01-02 15:04:05.000")))
|
doc.ID, time.UnixMilli(doc.CreatedAt).Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleFind(args []string) error {
|
func (r *Repl) handleFind(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -813,10 +862,10 @@ func (r *Repl) handleFind(args []string) error {
|
|||||||
t := MapToTable(rows)
|
t := MapToTable(rows)
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
|
|
||||||
utils.PrintInfo(fmt.Sprintf("created_at: %s", time.UnixMilli(doc.CreatedAt).Format("2006-01-02 15:04:05.000")))
|
utils.PrintInfo(fmt.Sprintf("created_at: %s", time.UnixMilli(doc.CreatedAt).Format("02-01-2006 15:04:05.000")))
|
||||||
utils.PrintInfo(fmt.Sprintf("updated_at: %s", time.UnixMilli(doc.UpdatedAt).Format("2006-01-02 15:04:05.000")))
|
utils.PrintInfo(fmt.Sprintf("updated_at: %s", time.UnixMilli(doc.UpdatedAt).Format("02-01-2006 15:04:05.000")))
|
||||||
if doc.DeletedAt > 0 {
|
if doc.DeletedAt > 0 {
|
||||||
utils.PrintWarning(fmt.Sprintf("deleted_at: %s", time.UnixMilli(doc.DeletedAt).Format("2006-01-02 15:04:05.000")))
|
utils.PrintWarning(fmt.Sprintf("deleted_at: %s", time.UnixMilli(doc.DeletedAt).Format("02-01-2006 15:04:05.000")))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -939,6 +988,7 @@ func (r *Repl) handleFindByTime(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleUpdate(args []string) error {
|
func (r *Repl) handleUpdate(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -969,10 +1019,11 @@ func (r *Repl) handleUpdate(args []string) error {
|
|||||||
if err := coll.Update(docID, updates); err != nil {
|
if err := coll.Update(docID, updates); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Document '%s' updated at %s", docID, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Document '%s' updated at %s", docID, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleDelete(args []string) error {
|
func (r *Repl) handleDelete(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -995,7 +1046,7 @@ func (r *Repl) handleDelete(args []string) error {
|
|||||||
if err := coll.Delete(docID); err != nil {
|
if err := coll.Delete(docID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Document '%s' deleted at %s", docID, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Document '%s' deleted at %s", docID, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,6 +1076,7 @@ func (r *Repl) handlePermanentDelete(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleRestore(args []string) error {
|
func (r *Repl) handleRestore(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -1047,10 +1099,11 @@ func (r *Repl) handleRestore(args []string) error {
|
|||||||
if err := coll.RestoreDeleted(docID); err != nil {
|
if err := coll.RestoreDeleted(docID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Document '%s' restored at %s", docID, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Document '%s' restored at %s", docID, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleShowDeleted(args []string) error {
|
func (r *Repl) handleShowDeleted(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -1087,7 +1140,7 @@ func (r *Repl) handleShowDeleted(args []string) error {
|
|||||||
for _, doc := range deleted {
|
for _, doc := range deleted {
|
||||||
t.AddRow(
|
t.AddRow(
|
||||||
doc.ID,
|
doc.ID,
|
||||||
time.UnixMilli(doc.DeletedAt).Format("2006-01-02 15:04:05.000"),
|
time.UnixMilli(doc.DeletedAt).Format("02-01-2006 15:04:05.000"),
|
||||||
utils.ColorizeTextAny(doc.GetFields()),
|
utils.ColorizeTextAny(doc.GetFields()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1130,6 +1183,7 @@ func (r *Repl) handleCount(args []string) error {
|
|||||||
// Временные метки
|
// Временные метки
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleShowTimestamps(args []string) error {
|
func (r *Repl) handleShowTimestamps(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -1155,10 +1209,10 @@ func (r *Repl) handleShowTimestamps(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t := NewTable("FIELD", "VALUE")
|
t := NewTable("FIELD", "VALUE")
|
||||||
t.AddRow("Created", time.UnixMilli(doc.CreatedAt).Format("2006-01-02 15:04:05.000"))
|
t.AddRow("Created", time.UnixMilli(doc.CreatedAt).Format("02-01-2006 15:04:05.000"))
|
||||||
t.AddRow("Updated", time.UnixMilli(doc.UpdatedAt).Format("2006-01-02 15:04:05.000"))
|
t.AddRow("Updated", time.UnixMilli(doc.UpdatedAt).Format("02-01-2006 15:04:05.000"))
|
||||||
if doc.DeletedAt > 0 {
|
if doc.DeletedAt > 0 {
|
||||||
t.AddRow("Deleted", time.UnixMilli(doc.DeletedAt).Format("2006-01-02 15:04:05.000"))
|
t.AddRow("Deleted", time.UnixMilli(doc.DeletedAt).Format("02-01-2006 15:04:05.000"))
|
||||||
} else {
|
} else {
|
||||||
t.AddRow("Deleted", "not deleted")
|
t.AddRow("Deleted", "not deleted")
|
||||||
}
|
}
|
||||||
@@ -1167,6 +1221,7 @@ func (r *Repl) handleShowTimestamps(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleStatsTimestamps(args []string) error {
|
func (r *Repl) handleStatsTimestamps(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -1218,9 +1273,15 @@ func (r *Repl) handleStatsTimestamps(args []string) error {
|
|||||||
|
|
||||||
t := NewTable("METRIC", "CREATED", "UPDATED")
|
t := NewTable("METRIC", "CREATED", "UPDATED")
|
||||||
t.AddRow("Documents", fmt.Sprintf("%d", len(docs)), "")
|
t.AddRow("Documents", fmt.Sprintf("%d", len(docs)), "")
|
||||||
t.AddRow("Earliest", time.UnixMilli(minCreated).Format("2006-01-02 15:04:05.000"), time.UnixMilli(minUpdated).Format("2006-01-02 15:04:05.000"))
|
t.AddRow("Earliest",
|
||||||
t.AddRow("Latest", time.UnixMilli(maxCreated).Format("2006-01-02 15:04:05.000"), time.UnixMilli(maxUpdated).Format("2006-01-02 15:04:05.000"))
|
time.UnixMilli(minCreated).Format("02-01-2006 15:04:05.000"),
|
||||||
t.AddRow("Average", time.UnixMilli(avgCreated).Format("2006-01-02 15:04:05.000"), time.UnixMilli(avgUpdated).Format("2006-01-02 15:04:05.000"))
|
time.UnixMilli(minUpdated).Format("02-01-2006 15:04:05.000"))
|
||||||
|
t.AddRow("Latest",
|
||||||
|
time.UnixMilli(maxCreated).Format("02-01-2006 15:04:05.000"),
|
||||||
|
time.UnixMilli(maxUpdated).Format("02-01-2006 15:04:05.000"))
|
||||||
|
t.AddRow("Average",
|
||||||
|
time.UnixMilli(avgCreated).Format("02-01-2006 15:04:05.000"),
|
||||||
|
time.UnixMilli(avgUpdated).Format("02-01-2006 15:04:05.000"))
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1278,6 +1339,7 @@ func (r *Repl) handleAuditFilter(args []string) error {
|
|||||||
// Индексы
|
// Индексы
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleCreateIndex(args []string) error {
|
func (r *Repl) handleCreateIndex(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -1302,7 +1364,7 @@ func (r *Repl) handleCreateIndex(args []string) error {
|
|||||||
if err := coll.CreateIndex(indexName, fields, unique); err != nil {
|
if err := coll.CreateIndex(indexName, fields, unique); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Index '%s' created on collection '%s' at %s", indexName, collName, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Index '%s' created on collection '%s' at %s", indexName, collName, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,6 +1394,8 @@ func (r *Repl) handleDropIndex(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): УНИФИЦИРОВАН — добавлены миллисекунды.
|
||||||
func (r *Repl) handleShowIndexes(args []string) error {
|
func (r *Repl) handleShowIndexes(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -1363,7 +1427,7 @@ func (r *Repl) handleShowIndexes(args []string) error {
|
|||||||
if idx["unique"].(bool) {
|
if idx["unique"].(bool) {
|
||||||
uniqueStr = "true"
|
uniqueStr = "true"
|
||||||
}
|
}
|
||||||
createdAt := time.UnixMilli(idx["created_at"].(int64)).Format("2006-01-02 15:04:05")
|
createdAt := time.UnixMilli(idx["created_at"].(int64)).Format("02-01-2006 15:04:05.000")
|
||||||
t.AddRow(idx["name"].(string), fmt.Sprintf("%v", idx["fields"]), uniqueStr, createdAt)
|
t.AddRow(idx["name"].(string), fmt.Sprintf("%v", idx["fields"]), uniqueStr, createdAt)
|
||||||
}
|
}
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
@@ -1507,64 +1571,118 @@ func (r *Repl) handleAddEnum(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Транзакции
|
// Batch-операции (замена транзакций).
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): обработчики переписаны поверх batch-API.
|
||||||
|
// Семантика:
|
||||||
|
// - handleBeginTransaction → создаёт *storage.Batch (в памяти REPL);
|
||||||
|
// - handleCommitTransaction → batch.Commit() (атомарно через WAL);
|
||||||
|
// - handleRollbackTransaction→ снимает регистрацию batch без коммита;
|
||||||
|
// - handleShowTransactions → показывает активный batch сессии.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат временных меток "02-01-2006 15:04:05.000".
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// handleBeginTransaction создаёт новый batch.
|
||||||
|
// Команда: "begin transaction" или "batch begin".
|
||||||
func (r *Repl) handleBeginTransaction(args []string) error {
|
func (r *Repl) handleBeginTransaction(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
}
|
}
|
||||||
tx := storage.BeginTransaction()
|
if r.currentBatch != nil {
|
||||||
if tx == nil {
|
return fmt.Errorf("batch already active (ID %d); commit or abort first", r.currentBatchID)
|
||||||
return fmt.Errorf("failed to begin transaction")
|
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Transaction %d started at %s", tx.ID, time.Now().Format("2006-01-02 15:04:05.000")))
|
|
||||||
|
// Создаём batch для текущей БД. Коллекция может быть указана как аргумент.
|
||||||
|
collection := ""
|
||||||
|
if len(args) >= 1 {
|
||||||
|
collection = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := storage.NewBatch(r.currentDB, collection)
|
||||||
|
if batch == nil {
|
||||||
|
return fmt.Errorf("failed to create batch")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.currentBatch = batch
|
||||||
|
r.currentBatchID = uint64(batch.ID)
|
||||||
|
|
||||||
|
utils.PrintSuccess(fmt.Sprintf("Batch %d started at %s (database=%s, collection=%s)",
|
||||||
|
batch.ID, time.Now().Format("02-01-2006 15:04:05.000"),
|
||||||
|
batch.Database, batch.Collection))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleCommitTransaction коммитит текущий batch.
|
||||||
|
// Команда: "commit" или "batch commit".
|
||||||
func (r *Repl) handleCommitTransaction(args []string) error {
|
func (r *Repl) handleCommitTransaction(args []string) error {
|
||||||
if !storage.HasActiveTransaction() {
|
if r.currentBatch == nil {
|
||||||
return fmt.Errorf("no active transaction to commit")
|
return fmt.Errorf("no active batch to commit")
|
||||||
}
|
}
|
||||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
|
||||||
|
batch := r.currentBatch
|
||||||
|
if err := batch.Commit(); err != nil {
|
||||||
|
// Снимаем регистрацию даже при ошибке.
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
|
}
|
||||||
|
r.currentBatch = nil
|
||||||
|
r.currentBatchID = 0
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Transaction committed successfully at %s", time.Now().Format("2006-01-02 15:04:05.000")))
|
|
||||||
|
// Commit сам снимает регистрацию через defer.
|
||||||
|
r.currentBatch = nil
|
||||||
|
r.currentBatchID = 0
|
||||||
|
|
||||||
|
utils.PrintSuccess(fmt.Sprintf("Batch %d committed successfully at %s",
|
||||||
|
batch.ID, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRollbackTransaction отменяет текущий batch (без коммита).
|
||||||
|
// Команда: "rollback" или "batch abort".
|
||||||
func (r *Repl) handleRollbackTransaction(args []string) error {
|
func (r *Repl) handleRollbackTransaction(args []string) error {
|
||||||
if !storage.HasActiveTransaction() {
|
if r.currentBatch == nil {
|
||||||
return fmt.Errorf("no active transaction to rollback")
|
return fmt.Errorf("no active batch to rollback")
|
||||||
}
|
}
|
||||||
if err := storage.AbortCurrentTransaction(); err != nil {
|
|
||||||
return err
|
batch := r.currentBatch
|
||||||
|
// Снимаем регистрацию в глобальном менеджере.
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(batch)
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Transaction rolled back at %s", time.Now().Format("2006-01-02 15:04:05.000")))
|
batchID := r.currentBatchID
|
||||||
|
r.currentBatch = nil
|
||||||
|
r.currentBatchID = 0
|
||||||
|
|
||||||
|
utils.PrintSuccess(fmt.Sprintf("Batch %d aborted at %s (operations discarded)",
|
||||||
|
batchID, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleShowTransactions выводит список активных транзакций.
|
// handleShowTransactions выводит список активных batch'ей сессии.
|
||||||
|
// Команда: "show transactions" или "show batches".
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleShowTransactions(args []string) error {
|
func (r *Repl) handleShowTransactions(args []string) error {
|
||||||
transactions := storage.GetActiveTransactions()
|
if r.currentBatch == nil {
|
||||||
if len(transactions) == 0 {
|
utils.PrintInfo("No active batches")
|
||||||
utils.PrintInfo("No active transactions")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
t := NewTable("ID", "STATUS", "OPERATIONS", "STARTED")
|
t := NewTable("ID", "DATABASE", "COLLECTION", "OPERATIONS", "STARTED")
|
||||||
for _, tx := range transactions {
|
started := ""
|
||||||
started := ""
|
if r.currentBatch.Timestamp > 0 {
|
||||||
if tx.StartTime > 0 {
|
started = time.UnixMilli(r.currentBatch.Timestamp).Format("02-01-2006 15:04:05.000")
|
||||||
started = time.UnixMilli(tx.StartTime).Format("2006-01-02 15:04:05.000")
|
|
||||||
}
|
|
||||||
t.AddRow(
|
|
||||||
fmt.Sprintf("%v", tx.ID),
|
|
||||||
tx.Status,
|
|
||||||
fmt.Sprintf("%d", tx.OperationCount),
|
|
||||||
started,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
t.AddRow(
|
||||||
|
fmt.Sprintf("%d", r.currentBatch.ID),
|
||||||
|
r.currentBatch.Database,
|
||||||
|
r.currentBatch.Collection,
|
||||||
|
fmt.Sprintf("%d", len(r.currentBatch.Operations)),
|
||||||
|
started,
|
||||||
|
)
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1573,6 +1691,8 @@ func (r *Repl) handleShowTransactions(args []string) error {
|
|||||||
// Плагины
|
// Плагины
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): УНИФИЦИРОВАН — добавлены миллисекунды.
|
||||||
func (r *Repl) handlePluginList(args []string) error {
|
func (r *Repl) handlePluginList(args []string) error {
|
||||||
if r.pluginManager == nil || !r.pluginManager.IsEnabled() {
|
if r.pluginManager == nil || !r.pluginManager.IsEnabled() {
|
||||||
return fmt.Errorf("plugin system is disabled")
|
return fmt.Errorf("plugin system is disabled")
|
||||||
@@ -1595,12 +1715,13 @@ func (r *Repl) handlePluginList(args []string) error {
|
|||||||
case 3:
|
case 3:
|
||||||
status = "error"
|
status = "error"
|
||||||
}
|
}
|
||||||
t.AddRow(p.Name, p.Version(), status, p.Author(), p.LoadedAt().Format("2006-01-02 15:04:05"))
|
t.AddRow(p.Name, p.Version(), status, p.Author(), p.LoadedAt().Format("02-01-2006 15:04:05.000"))
|
||||||
}
|
}
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handlePluginLoad(args []string) error {
|
func (r *Repl) handlePluginLoad(args []string) error {
|
||||||
if r.pluginManager == nil || !r.pluginManager.IsEnabled() {
|
if r.pluginManager == nil || !r.pluginManager.IsEnabled() {
|
||||||
return fmt.Errorf("plugin system is disabled")
|
return fmt.Errorf("plugin system is disabled")
|
||||||
@@ -1613,7 +1734,7 @@ func (r *Repl) handlePluginLoad(args []string) error {
|
|||||||
if err := r.pluginManager.LoadPlugin(name, filepath); err != nil {
|
if err := r.pluginManager.LoadPlugin(name, filepath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Plugin '%s' loaded from %s at %s", name, filepath, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Plugin '%s' loaded from %s at %s", name, filepath, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1632,6 +1753,7 @@ func (r *Repl) handlePluginUnload(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handlePluginStart(args []string) error {
|
func (r *Repl) handlePluginStart(args []string) error {
|
||||||
if r.pluginManager == nil || !r.pluginManager.IsEnabled() {
|
if r.pluginManager == nil || !r.pluginManager.IsEnabled() {
|
||||||
return fmt.Errorf("plugin system is disabled")
|
return fmt.Errorf("plugin system is disabled")
|
||||||
@@ -1643,7 +1765,7 @@ func (r *Repl) handlePluginStart(args []string) error {
|
|||||||
if err := r.pluginManager.StartPlugin(name); err != nil {
|
if err := r.pluginManager.StartPlugin(name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Plugin '%s' started at %s", name, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Plugin '%s' started at %s", name, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1690,6 +1812,7 @@ func (r *Repl) handlePluginExec(args []string) error {
|
|||||||
// Экспорт/импорт
|
// Экспорт/импорт
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleExport(args []string) error {
|
func (r *Repl) handleExport(args []string) error {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return fmt.Errorf("usage: export <database> <filename>")
|
return fmt.Errorf("usage: export <database> <filename>")
|
||||||
@@ -1701,7 +1824,7 @@ func (r *Repl) handleExport(args []string) error {
|
|||||||
return fmt.Errorf("database '%s' does not exist", dbName)
|
return fmt.Errorf("database '%s' does not exist", dbName)
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.PrintInfo(fmt.Sprintf("Exporting database '%s' to %s at %s...", dbName, fileName, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintInfo(fmt.Sprintf("Exporting database '%s' to %s at %s...", dbName, fileName, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
|
|
||||||
db, err := r.store.GetDatabase(dbName)
|
db, err := r.store.GetDatabase(dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1718,6 +1841,7 @@ func (r *Repl) handleExport(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleImport(args []string) error {
|
func (r *Repl) handleImport(args []string) error {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return fmt.Errorf("usage: import <database> <filename>")
|
return fmt.Errorf("usage: import <database> <filename>")
|
||||||
@@ -1725,7 +1849,7 @@ func (r *Repl) handleImport(args []string) error {
|
|||||||
dbName := args[0]
|
dbName := args[0]
|
||||||
fileName := args[1]
|
fileName := args[1]
|
||||||
|
|
||||||
utils.PrintInfo(fmt.Sprintf("Importing data from %s to database '%s' at %s...", fileName, dbName, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintInfo(fmt.Sprintf("Importing data from %s to database '%s' at %s...", fileName, dbName, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
|
|
||||||
data, err := os.ReadFile(fileName)
|
data, err := os.ReadFile(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1753,6 +1877,7 @@ func (r *Repl) handleImport(args []string) error {
|
|||||||
// ACL
|
// ACL
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleACLLogin(args []string) error {
|
func (r *Repl) handleACLLogin(args []string) error {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return fmt.Errorf("usage: acl login <username> <password>")
|
return fmt.Errorf("usage: acl login <username> <password>")
|
||||||
@@ -1778,10 +1903,11 @@ func (r *Repl) handleACLLogin(args []string) error {
|
|||||||
r.currentRole = roles[0]
|
r.currentRole = roles[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.PrintSuccess(fmt.Sprintf("Logged in as '%s' with role '%s' at %s", username, r.currentRole, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Logged in as '%s' with role '%s' at %s", username, r.currentRole, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleACLLogout(args []string) error {
|
func (r *Repl) handleACLLogout(args []string) error {
|
||||||
if r.sessionID != "" && r.aclManager != nil {
|
if r.sessionID != "" && r.aclManager != nil {
|
||||||
r.aclManager.Logout(r.sessionID)
|
r.aclManager.Logout(r.sessionID)
|
||||||
@@ -1790,10 +1916,11 @@ func (r *Repl) handleACLLogout(args []string) error {
|
|||||||
r.currentUser = ""
|
r.currentUser = ""
|
||||||
r.currentRole = "anonymous"
|
r.currentRole = "anonymous"
|
||||||
r.sessionID = ""
|
r.sessionID = ""
|
||||||
utils.PrintSuccess(fmt.Sprintf("Logged out at %s", time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Logged out at %s", time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleACLGrant(args []string) error {
|
func (r *Repl) handleACLGrant(args []string) error {
|
||||||
if !r.authenticated || r.currentRole != "admin" {
|
if !r.authenticated || r.currentRole != "admin" {
|
||||||
return fmt.Errorf("permission denied: admin access required")
|
return fmt.Errorf("permission denied: admin access required")
|
||||||
@@ -1828,7 +1955,7 @@ func (r *Repl) handleACLGrant(args []string) error {
|
|||||||
|
|
||||||
coll.SetACL(role, canRead, canWrite, canDelete, isAdmin)
|
coll.SetACL(role, canRead, canWrite, canDelete, isAdmin)
|
||||||
utils.PrintSuccess(fmt.Sprintf("Permissions '%s' granted to role '%s' on collection '%s' at %s",
|
utils.PrintSuccess(fmt.Sprintf("Permissions '%s' granted to role '%s' on collection '%s' at %s",
|
||||||
perms, role, collName, time.Now().Format("2006-01-02 15:04:05.000")))
|
perms, role, collName, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1936,6 +2063,7 @@ func (r *Repl) handleCompressionStats(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleCompressCollection(args []string) error {
|
func (r *Repl) handleCompressCollection(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -1957,7 +2085,7 @@ func (r *Repl) handleCompressCollection(args []string) error {
|
|||||||
docs := coll.GetAllDocuments()
|
docs := coll.GetAllDocuments()
|
||||||
compressed := 0
|
compressed := 0
|
||||||
|
|
||||||
utils.PrintInfo(fmt.Sprintf("Compressing collection '%s' at %s...", collName, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintInfo(fmt.Sprintf("Compressing collection '%s' at %s...", collName, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
|
|
||||||
for _, doc := range docs {
|
for _, doc := range docs {
|
||||||
if !doc.Compressed {
|
if !doc.Compressed {
|
||||||
@@ -2029,6 +2157,7 @@ func (r *Repl) handleCompressionConfig(args []string) error {
|
|||||||
// Триггеры
|
// Триггеры
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleCreateTrigger(args []string) error {
|
func (r *Repl) handleCreateTrigger(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -2078,7 +2207,7 @@ func (r *Repl) handleCreateTrigger(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' created on collection '%s' for event %s at %s",
|
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' created on collection '%s' for event %s at %s",
|
||||||
triggerName, collName, event, time.Now().Format("2006-01-02 15:04:05.000")))
|
triggerName, collName, event, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2107,6 +2236,8 @@ func (r *Repl) handleDropTrigger(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): УНИФИЦИРОВАН — добавлены миллисекунды.
|
||||||
func (r *Repl) handleShowTriggers(args []string) error {
|
func (r *Repl) handleShowTriggers(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -2138,12 +2269,13 @@ func (r *Repl) handleShowTriggers(args []string) error {
|
|||||||
status = "disabled"
|
status = "disabled"
|
||||||
}
|
}
|
||||||
t.AddRow(tr.Name, tr.Event, tr.Action, status,
|
t.AddRow(tr.Name, tr.Event, tr.Action, status,
|
||||||
time.UnixMilli(tr.CreatedAt).Format("2006-01-02 15:04:05"), tr.Description)
|
time.UnixMilli(tr.CreatedAt).Format("02-01-2006 15:04:05.000"), tr.Description)
|
||||||
}
|
}
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleEnableTrigger(args []string) error {
|
func (r *Repl) handleEnableTrigger(args []string) error {
|
||||||
if r.currentDB == "" {
|
if r.currentDB == "" {
|
||||||
return fmt.Errorf("no database selected")
|
return fmt.Errorf("no database selected")
|
||||||
@@ -2165,7 +2297,7 @@ func (r *Repl) handleEnableTrigger(args []string) error {
|
|||||||
if err := coll.EnableTrigger(triggerName); err != nil {
|
if err := coll.EnableTrigger(triggerName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' enabled at %s", triggerName, time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' enabled at %s", triggerName, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2194,6 +2326,8 @@ func (r *Repl) handleDisableTrigger(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): УНИФИЦИРОВАН — добавлены миллисекунды.
|
||||||
func (r *Repl) handleTriggerLog(args []string) error {
|
func (r *Repl) handleTriggerLog(args []string) error {
|
||||||
logs := storage.GetTriggerExecutionLog()
|
logs := storage.GetTriggerExecutionLog()
|
||||||
if len(logs) == 0 {
|
if len(logs) == 0 {
|
||||||
@@ -2204,7 +2338,7 @@ func (r *Repl) handleTriggerLog(args []string) error {
|
|||||||
t := NewTable("TIMESTAMP", "TRIGGER", "EVENT", "COLLECTION", "DOCUMENT")
|
t := NewTable("TIMESTAMP", "TRIGGER", "EVENT", "COLLECTION", "DOCUMENT")
|
||||||
for _, entry := range logs {
|
for _, entry := range logs {
|
||||||
t.AddRow(
|
t.AddRow(
|
||||||
entry.Timestamp.Format("2006-01-02 15:04:05"),
|
entry.Timestamp.Format("02-01-2006 15:04:05.000"),
|
||||||
entry.TriggerName,
|
entry.TriggerName,
|
||||||
entry.Event,
|
entry.Event,
|
||||||
entry.Collection,
|
entry.Collection,
|
||||||
@@ -2258,6 +2392,8 @@ func (r *Repl) handleStatus(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): УНИФИЦИРОВАН — добавлены миллисекунды.
|
||||||
func (r *Repl) handleNodes(args []string) error {
|
func (r *Repl) handleNodes(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2289,7 +2425,7 @@ func (r *Repl) handleNodes(args []string) error {
|
|||||||
if node.Port > 0 {
|
if node.Port > 0 {
|
||||||
nodePort = node.Port
|
nodePort = node.Port
|
||||||
}
|
}
|
||||||
lastSeen := time.UnixMilli(node.LastSeen).Format("15:04:05")
|
lastSeen := time.UnixMilli(node.LastSeen).Format("02-01-2006 15:04:05.000")
|
||||||
t.AddRow(prefix, node.ID, fmt.Sprintf("%s:%d", nodeIP, nodePort), node.Status, lastSeen)
|
t.AddRow(prefix, node.ID, fmt.Sprintf("%s:%d", nodeIP, nodePort), node.Status, lastSeen)
|
||||||
}
|
}
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
@@ -2300,7 +2436,6 @@ func (r *Repl) handleNodes(args []string) error {
|
|||||||
// Кластер (расширенные команды)
|
// Кластер (расширенные команды)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// handleClusterPipeline показывает статистику pipeline replicator.
|
|
||||||
func (r *Repl) handleClusterPipeline(args []string) error {
|
func (r *Repl) handleClusterPipeline(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2314,7 +2449,7 @@ func (r *Repl) handleClusterPipeline(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleClusterReshard запускает ручной решардинг.
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleClusterReshard(args []string) error {
|
func (r *Repl) handleClusterReshard(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2322,7 +2457,7 @@ func (r *Repl) handleClusterReshard(args []string) error {
|
|||||||
if err := r.coordinator.TriggerResharding("manual"); err != nil {
|
if err := r.coordinator.TriggerResharding("manual"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
utils.PrintSuccess(fmt.Sprintf("Resharding triggered at %s", time.Now().Format("2006-01-02 15:04:05.000")))
|
utils.PrintSuccess(fmt.Sprintf("Resharding triggered at %s", time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2391,6 +2526,8 @@ func (r *Repl) handleMigration(args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): УНИФИЦИРОВАН — добавлены миллисекунды.
|
||||||
func (r *Repl) handleMigrationStart(migrator *cluster.CrossDCMigrator, args []string) error {
|
func (r *Repl) handleMigrationStart(migrator *cluster.CrossDCMigrator, args []string) error {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return fmt.Errorf("usage: migration start <source_dc> <target_dc> [database] [collection]")
|
return fmt.Errorf("usage: migration start <source_dc> <target_dc> [database] [collection]")
|
||||||
@@ -2410,7 +2547,7 @@ func (r *Repl) handleMigrationStart(migrator *cluster.CrossDCMigrator, args []st
|
|||||||
}
|
}
|
||||||
|
|
||||||
utils.PrintInfo(fmt.Sprintf("Starting migration from %s to %s at %s...",
|
utils.PrintInfo(fmt.Sprintf("Starting migration from %s to %s at %s...",
|
||||||
sourceDC, targetDC, time.Now().Format("2006-01-02 15:04:05")))
|
sourceDC, targetDC, time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
|
|
||||||
task, err := migrator.StartMigration(sourceDC, targetDC, databases, collections)
|
task, err := migrator.StartMigration(sourceDC, targetDC, databases, collections)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2427,6 +2564,8 @@ func (r *Repl) handleMigrationStart(migrator *cluster.CrossDCMigrator, args []st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
|
// ИЗМЕНЕНО (2026-10-04): УНИФИЦИРОВАН — добавлены миллисекунды.
|
||||||
func (r *Repl) handleMigrationStatus(migrator *cluster.CrossDCMigrator, args []string) error {
|
func (r *Repl) handleMigrationStatus(migrator *cluster.CrossDCMigrator, args []string) error {
|
||||||
var taskID string
|
var taskID string
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@@ -2455,10 +2594,10 @@ func (r *Repl) handleMigrationStatus(migrator *cluster.CrossDCMigrator, args []s
|
|||||||
t.AddRow("Skipped", fmt.Sprintf("%d", task.SkippedDocs))
|
t.AddRow("Skipped", fmt.Sprintf("%d", task.SkippedDocs))
|
||||||
|
|
||||||
if task.StartTime > 0 {
|
if task.StartTime > 0 {
|
||||||
t.AddRow("Started", time.UnixMilli(task.StartTime).Format("2006-01-02 15:04:05"))
|
t.AddRow("Started", time.UnixMilli(task.StartTime).Format("02-01-2006 15:04:05.000"))
|
||||||
}
|
}
|
||||||
if task.EndTime > 0 {
|
if task.EndTime > 0 {
|
||||||
t.AddRow("Completed", time.UnixMilli(task.EndTime).Format("2006-01-02 15:04:05"))
|
t.AddRow("Completed", time.UnixMilli(task.EndTime).Format("02-01-2006 15:04:05.000"))
|
||||||
}
|
}
|
||||||
if task.Error != "" {
|
if task.Error != "" {
|
||||||
t.AddRow("Error", task.Error)
|
t.AddRow("Error", task.Error)
|
||||||
@@ -2498,6 +2637,7 @@ func (r *Repl) handleMigrationList(migrator *cluster.CrossDCMigrator) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleMigrationPause(migrator *cluster.CrossDCMigrator, taskID string) error {
|
func (r *Repl) handleMigrationPause(migrator *cluster.CrossDCMigrator, taskID string) error {
|
||||||
if err := migrator.PauseMigration(taskID); err != nil {
|
if err := migrator.PauseMigration(taskID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2506,6 +2646,7 @@ func (r *Repl) handleMigrationPause(migrator *cluster.CrossDCMigrator, taskID st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleMigrationResume(migrator *cluster.CrossDCMigrator, taskID string) error {
|
func (r *Repl) handleMigrationResume(migrator *cluster.CrossDCMigrator, taskID string) error {
|
||||||
if err := migrator.ResumeMigration(taskID); err != nil {
|
if err := migrator.ResumeMigration(taskID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2514,6 +2655,7 @@ func (r *Repl) handleMigrationResume(migrator *cluster.CrossDCMigrator, taskID s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleMigrationCancel(migrator *cluster.CrossDCMigrator, taskID string) error {
|
func (r *Repl) handleMigrationCancel(migrator *cluster.CrossDCMigrator, taskID string) error {
|
||||||
if err := migrator.CancelMigration(taskID); err != nil {
|
if err := migrator.CancelMigration(taskID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2581,7 +2723,6 @@ func (r *Repl) handleMigrationQueue(migrator *cluster.CrossDCMigrator) error {
|
|||||||
// Миграция схемы
|
// Миграция схемы
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// handleMigrateStatus показывает статус миграции схемы.
|
|
||||||
func (r *Repl) handleMigrateStatus(args []string) error {
|
func (r *Repl) handleMigrateStatus(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2601,7 +2742,6 @@ func (r *Repl) handleMigrateStatus(args []string) error {
|
|||||||
// Fallback (SPoF protection)
|
// Fallback (SPoF protection)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// handleFallbackStatus показывает статус SPoF protection.
|
|
||||||
func (r *Repl) handleFallbackStatus(args []string) error {
|
func (r *Repl) handleFallbackStatus(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2619,7 +2759,6 @@ func (r *Repl) handleFallbackStatus(args []string) error {
|
|||||||
// Panic recovery
|
// Panic recovery
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// handlePanicStats показывает статистику panic recovery.
|
|
||||||
func (r *Repl) handlePanicStats(args []string) error {
|
func (r *Repl) handlePanicStats(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2637,7 +2776,6 @@ func (r *Repl) handlePanicStats(args []string) error {
|
|||||||
// Persistence
|
// Persistence
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// handlePersistStatus показывает статус персистентности.
|
|
||||||
func (r *Repl) handlePersistStatus(args []string) error {
|
func (r *Repl) handlePersistStatus(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2659,7 +2797,6 @@ func (r *Repl) handlePersistStatus(args []string) error {
|
|||||||
// SAGA
|
// SAGA
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// handleSagaStatus показывает статус SAGA orchestrator.
|
|
||||||
func (r *Repl) handleSagaStatus(args []string) error {
|
func (r *Repl) handleSagaStatus(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2682,7 +2819,7 @@ func (r *Repl) handleSagaStatus(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSagaList выводит список активных SAGA транзакций.
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleSagaList(args []string) error {
|
func (r *Repl) handleSagaList(args []string) error {
|
||||||
if r.coordinator == nil {
|
if r.coordinator == nil {
|
||||||
return fmt.Errorf("cluster coordinator not available")
|
return fmt.Errorf("cluster coordinator not available")
|
||||||
@@ -2703,7 +2840,7 @@ func (r *Repl) handleSagaList(args []string) error {
|
|||||||
saga.Status,
|
saga.Status,
|
||||||
fmt.Sprintf("%d", len(saga.Steps)),
|
fmt.Sprintf("%d", len(saga.Steps)),
|
||||||
fmt.Sprintf("%d", saga.CurrentStep),
|
fmt.Sprintf("%d", saga.CurrentStep),
|
||||||
time.UnixMilli(saga.CreatedAt).Format("2006-01-02 15:04:05.000"),
|
time.UnixMilli(saga.CreatedAt).Format("02-01-2006 15:04:05.000"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
r.output(t.String())
|
r.output(t.String())
|
||||||
@@ -2763,11 +2900,18 @@ func (r *Repl) handleHelp(args []string) error {
|
|||||||
{"add max <collection> <field> <value>", "Add a maximum value constraint for numeric fields"},
|
{"add max <collection> <field> <value>", "Add a maximum value constraint for numeric fields"},
|
||||||
{"add enum <collection> <field> <values...>", "Add allowed values constraint (enum)"},
|
{"add enum <collection> <field> <values...>", "Add allowed values constraint (enum)"},
|
||||||
},
|
},
|
||||||
"Transactions": {
|
// ИСПРАВЛЕНО (2026-10): раздел "Transactions" заменён на "Batch Operations".
|
||||||
{"begin transaction", "Start a new transaction"},
|
// Старые команды (begin transaction, commit, rollback, show transactions)
|
||||||
{"commit", "Commit the current transaction"},
|
// оставлены как алиасы для обратной совместимости.
|
||||||
{"rollback", "Rollback the current transaction"},
|
"Batch Operations": {
|
||||||
{"show transactions", "List all active transactions"},
|
{"batch begin [collection]", "Begin a new batch (atomically applied group of operations)"},
|
||||||
|
{"batch commit", "Commit the current batch (operations applied atomically via WAL)"},
|
||||||
|
{"batch abort", "Abort the current batch (operations are discarded)"},
|
||||||
|
{"show batches", "Show the active batch of the current session"},
|
||||||
|
{"begin transaction", "Alias for 'batch begin'"},
|
||||||
|
{"commit", "Alias for 'batch commit'"},
|
||||||
|
{"rollback", "Alias for 'batch abort'"},
|
||||||
|
{"show transactions", "Alias for 'show batches'"},
|
||||||
},
|
},
|
||||||
"Triggers (MongoDB-like)": {
|
"Triggers (MongoDB-like)": {
|
||||||
{"create trigger <collection> <name> <event> <action> [options]", "Create a trigger on collection events"},
|
{"create trigger <collection> <name> <event> <action> [options]", "Create a trigger on collection events"},
|
||||||
@@ -2866,7 +3010,6 @@ func (r *Repl) handleHelp(args []string) error {
|
|||||||
sb.WriteString(" Empty line finishes multi-line input\n")
|
sb.WriteString(" Empty line finishes multi-line input\n")
|
||||||
sb.WriteString(" \n")
|
sb.WriteString(" \n")
|
||||||
|
|
||||||
// Унифицируем цвет с информационными сообщениями (ярко-голубой).
|
|
||||||
utils.PrintInfo(sb.String())
|
utils.PrintInfo(sb.String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -2876,8 +3019,17 @@ func (r *Repl) handleClear(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат "02-01-2006 15:04:05.000".
|
||||||
func (r *Repl) handleQuit(args []string) error {
|
func (r *Repl) handleQuit(args []string) error {
|
||||||
utils.PrintInfo(fmt.Sprintf("Session ended at %s", time.Now().Format("2006-01-02 15:04:05.000")))
|
// Снимаем активный batch при выходе, если есть.
|
||||||
|
if r.currentBatch != nil {
|
||||||
|
if bm := storage.GetBatchManager(); bm != nil {
|
||||||
|
bm.UnregisterBatch(r.currentBatch)
|
||||||
|
}
|
||||||
|
r.currentBatch = nil
|
||||||
|
r.currentBatchID = 0
|
||||||
|
}
|
||||||
|
utils.PrintInfo(fmt.Sprintf("Session ended at %s", time.Now().Format("02-01-2006 15:04:05.000")))
|
||||||
if r.rl != nil {
|
if r.rl != nil {
|
||||||
_ = r.rl.Close()
|
_ = r.rl.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,481 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Safronov Grigorii
|
||||||
|
*
|
||||||
|
* Licensed under the CDDL, Version 1.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
*
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* https://opensource.org/licenses/CDDL-1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Файл: internal/storage/aof.go
|
||||||
|
// Назначение: Append-Only File (AOF) для мутаций, выполненных вне batch.
|
||||||
|
//
|
||||||
|
// ЗАЧЕМ НУЖЕН AOF:
|
||||||
|
// WAL покрывает только batch-операции (групповые мутации).
|
||||||
|
// Прямые вызовы coll.Insert / coll.Update / coll.Delete идут мимо WAL.
|
||||||
|
// AOF логирует каждую отдельную мутацию и позволяет:
|
||||||
|
// 1. Восстановить состояние после краха без потери данных.
|
||||||
|
// 2. Вести полный audit trail.
|
||||||
|
// 3. Использовать для репликации.
|
||||||
|
//
|
||||||
|
// ФОРМАТ:
|
||||||
|
// Каждая запись — JSON-строка, заканчивающаяся '\n'.
|
||||||
|
// Поля: type, database, collection, document_id, data, timestamp, crc.
|
||||||
|
// Файлы разбиваются по размеру (по умолчанию 64 МБ).
|
||||||
|
// При старте AOFManager сканирует директорию и загружает все записи.
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// КОНСТАНТЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Максимальный размер одного AOF-файла.
|
||||||
|
AOFMaxFileSize = 64 * 1024 * 1024
|
||||||
|
|
||||||
|
// Префикс и суффикс AOF-файлов.
|
||||||
|
AOFFilePrefix = "aof_"
|
||||||
|
AOFFileSuffix = ".log"
|
||||||
|
|
||||||
|
// Размер буфера для bufio.Writer.
|
||||||
|
AOFBufferSize = 64 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ТИПЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// AOFOperation — одна операция в AOF.
|
||||||
|
type AOFOperation struct {
|
||||||
|
Type string `json:"type"` // insert, update, delete, restore
|
||||||
|
Database string `json:"database"`
|
||||||
|
Collection string `json:"collection"`
|
||||||
|
DocumentID string `json:"document_id"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
CRC uint32 `json:"crc,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AOFManager управляет append-only file.
|
||||||
|
type AOFManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
dir string
|
||||||
|
currentFile *os.File
|
||||||
|
writer *bufio.Writer
|
||||||
|
currentSize int64
|
||||||
|
fileIndex uint32
|
||||||
|
fsyncEnabled bool
|
||||||
|
logger LoggerInterface
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAOFManager создаёт AOF-менеджер.
|
||||||
|
func NewAOFManager(dir string, fsyncEnabled bool, logger LoggerInterface) (*AOFManager, error) {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create AOF directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aof := &AOFManager{
|
||||||
|
dir: dir,
|
||||||
|
fsyncEnabled: fsyncEnabled,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим максимальный индекс файла.
|
||||||
|
files, err := aof.listFiles()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(files) > 0 {
|
||||||
|
lastFile := files[len(files)-1]
|
||||||
|
var idx uint32
|
||||||
|
if _, err := fmt.Sscanf(filepath.Base(lastFile), AOFFilePrefix+"%d"+AOFFileSuffix, &idx); err == nil {
|
||||||
|
aof.fileIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открываем текущий файл на дозапись.
|
||||||
|
if err := aof.openCurrentFile(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return aof, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger устанавливает логгер.
|
||||||
|
func (aof *AOFManager) SetLogger(logger LoggerInterface) {
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
aof.logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// listFiles возвращает отсортированный список AOF-файлов.
|
||||||
|
func (aof *AOFManager) listFiles() ([]string, error) {
|
||||||
|
pattern := filepath.Join(aof.dir, AOFFilePrefix+"*"+AOFFileSuffix)
|
||||||
|
files, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Strings(files)
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// openCurrentFile открывает текущий файл для дозаписи.
|
||||||
|
func (aof *AOFManager) openCurrentFile() error {
|
||||||
|
path := filepath.Join(aof.dir, fmt.Sprintf("%s%d%s", AOFFilePrefix, aof.fileIndex, AOFFileSuffix))
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open AOF file: %v", err)
|
||||||
|
}
|
||||||
|
stat, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
aof.currentFile = f
|
||||||
|
aof.writer = bufio.NewWriterSize(f, AOFBufferSize)
|
||||||
|
aof.currentSize = stat.Size()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotateIfNeeded проверяет размер и ротирует файл.
|
||||||
|
func (aof *AOFManager) rotateIfNeeded() error {
|
||||||
|
if aof.currentSize < AOFMaxFileSize {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := aof.writer.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if aof.fsyncEnabled {
|
||||||
|
RealFsync(aof.currentFile)
|
||||||
|
}
|
||||||
|
if err := aof.currentFile.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
aof.fileIndex++
|
||||||
|
return aof.openCurrentFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append записывает одну операцию в AOF.
|
||||||
|
func (aof *AOFManager) Append(op *AOFOperation) error {
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
if aof.closed {
|
||||||
|
return fmt.Errorf("AOF is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
op.Timestamp = time.Now().UnixMilli()
|
||||||
|
|
||||||
|
// Вычисляем CRC.
|
||||||
|
data, err := json.Marshal(op)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
op.CRC = crc32(data)
|
||||||
|
data, err = json.Marshal(op)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = append(data, '\n')
|
||||||
|
|
||||||
|
if _, err := aof.writer.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
aof.currentSize += int64(len(data))
|
||||||
|
|
||||||
|
if err := aof.writer.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if aof.fsyncEnabled {
|
||||||
|
RealFsync(aof.currentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aof.rotateIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendBatch записывает все операции batch в AOF.
|
||||||
|
func (aof *AOFManager) AppendBatch(batch *Batch) error {
|
||||||
|
for _, op := range batch.Operations {
|
||||||
|
aofOp := &AOFOperation{
|
||||||
|
Type: op.Type,
|
||||||
|
Database: op.Database,
|
||||||
|
Collection: op.Collection,
|
||||||
|
DocumentID: op.DocumentID,
|
||||||
|
Data: op.Data,
|
||||||
|
}
|
||||||
|
if err := aof.Append(aofOp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAll читает все операции из всех AOF-файлов.
|
||||||
|
func (aof *AOFManager) ReadAll() ([]*AOFOperation, error) {
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
files, err := aof.listFiles()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := make([]*AOFOperation, 0)
|
||||||
|
for _, file := range files {
|
||||||
|
fileOps, err := aof.readFile(file)
|
||||||
|
if err != nil {
|
||||||
|
if aof.logger != nil {
|
||||||
|
aof.logger.Warn(fmt.Sprintf("Failed to read AOF file %s: %v", file, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ops = append(ops, fileOps...)
|
||||||
|
}
|
||||||
|
return ops, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFile читает операции из одного файла.
|
||||||
|
func (aof *AOFManager) readFile(path string) ([]*AOFOperation, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
ops := make([]*AOFOperation, 0)
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var op AOFOperation
|
||||||
|
if err := json.Unmarshal(line, &op); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Проверяем CRC.
|
||||||
|
expectedCRC := op.CRC
|
||||||
|
op.CRC = 0
|
||||||
|
data, _ := json.Marshal(&op)
|
||||||
|
calculatedCRC := crc32(data)
|
||||||
|
op.CRC = expectedCRC
|
||||||
|
if calculatedCRC != expectedCRC {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ops = append(ops, &op)
|
||||||
|
}
|
||||||
|
return ops, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replay воспроизводит все операции AOF, применяя их к хранилищу.
|
||||||
|
//
|
||||||
|
// Используется при старте для восстановления состояния после краха.
|
||||||
|
func (aof *AOFManager) Replay(storage *Storage) error {
|
||||||
|
ops, err := aof.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(ops) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if aof.logger != nil {
|
||||||
|
aof.logger.Info(fmt.Sprintf("Replaying %d AOF operations", len(ops)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
db, err := storage.GetDatabase(op.Database)
|
||||||
|
if err != nil {
|
||||||
|
// База данных могла быть создана позже — пропускаем.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coll, err := db.GetCollection(op.Collection)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op.Type {
|
||||||
|
case "insert":
|
||||||
|
doc := NewDocumentWithID(op.DocumentID)
|
||||||
|
for k, v := range op.Data {
|
||||||
|
doc.SetField(k, v)
|
||||||
|
}
|
||||||
|
doc.CreatedAt = op.Timestamp
|
||||||
|
doc.UpdatedAt = op.Timestamp
|
||||||
|
coll.Insert(doc)
|
||||||
|
case "update":
|
||||||
|
coll.Update(op.DocumentID, op.Data)
|
||||||
|
case "delete":
|
||||||
|
coll.Delete(op.DocumentID)
|
||||||
|
case "restore":
|
||||||
|
coll.RestoreDeleted(op.DocumentID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close закрывает AOF-менеджер.
|
||||||
|
func (aof *AOFManager) Close() error {
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
if aof.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
aof.closed = true
|
||||||
|
|
||||||
|
if aof.writer != nil {
|
||||||
|
if err := aof.writer.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if aof.currentFile != nil {
|
||||||
|
if aof.fsyncEnabled {
|
||||||
|
RealFsync(aof.currentFile)
|
||||||
|
}
|
||||||
|
return aof.currentFile.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats возвращает статистику AOF.
|
||||||
|
func (aof *AOFManager) GetStats() map[string]interface{} {
|
||||||
|
aof.mu.Lock()
|
||||||
|
defer aof.mu.Unlock()
|
||||||
|
|
||||||
|
files, _ := aof.listFiles()
|
||||||
|
var totalSize int64
|
||||||
|
for _, f := range files {
|
||||||
|
if stat, err := os.Stat(f); err == nil {
|
||||||
|
totalSize += stat.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"file_count": len(files),
|
||||||
|
"total_size": totalSize,
|
||||||
|
"current_file": fmt.Sprintf("%s%d%s", AOFFilePrefix, aof.fileIndex, AOFFileSuffix),
|
||||||
|
"current_size": aof.currentSize,
|
||||||
|
"fsync_enabled": aof.fsyncEnabled,
|
||||||
|
"dir": aof.dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ИНТЕГРАЦИЯ С COLLECTION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// LogAOFInsert логирует вставку документа.
|
||||||
|
func LogAOFInsert(dbName, collName string, doc *Document) {
|
||||||
|
if globalBatchManager == nil || globalBatchManager.aof == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
op := &AOFOperation{
|
||||||
|
Type: "insert",
|
||||||
|
Database: dbName,
|
||||||
|
Collection: collName,
|
||||||
|
DocumentID: doc.ID,
|
||||||
|
Data: doc.GetFields(),
|
||||||
|
}
|
||||||
|
if err := globalBatchManager.aof.Append(op); err != nil {
|
||||||
|
if globalBatchManager.logger != nil {
|
||||||
|
globalBatchManager.logger.Warn(fmt.Sprintf("AOF insert failed: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogAOFUpdate логирует обновление документа.
|
||||||
|
func LogAOFUpdate(dbName, collName, docID string, updates map[string]interface{}) {
|
||||||
|
if globalBatchManager == nil || globalBatchManager.aof == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
op := &AOFOperation{
|
||||||
|
Type: "update",
|
||||||
|
Database: dbName,
|
||||||
|
Collection: collName,
|
||||||
|
DocumentID: docID,
|
||||||
|
Data: updates,
|
||||||
|
}
|
||||||
|
if err := globalBatchManager.aof.Append(op); err != nil {
|
||||||
|
if globalBatchManager.logger != nil {
|
||||||
|
globalBatchManager.logger.Warn(fmt.Sprintf("AOF update failed: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogAOFDelete логирует удаление документа.
|
||||||
|
func LogAOFDelete(dbName, collName, docID string) {
|
||||||
|
if globalBatchManager == nil || globalBatchManager.aof == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
op := &AOFOperation{
|
||||||
|
Type: "delete",
|
||||||
|
Database: dbName,
|
||||||
|
Collection: collName,
|
||||||
|
DocumentID: docID,
|
||||||
|
}
|
||||||
|
if err := globalBatchManager.aof.Append(op); err != nil {
|
||||||
|
if globalBatchManager.logger != nil {
|
||||||
|
globalBatchManager.logger.Warn(fmt.Sprintf("AOF delete failed: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogAOFRestore логирует восстановление документа.
|
||||||
|
func LogAOFRestore(dbName, collName, docID string) {
|
||||||
|
if globalBatchManager == nil || globalBatchManager.aof == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
op := &AOFOperation{
|
||||||
|
Type: "restore",
|
||||||
|
Database: dbName,
|
||||||
|
Collection: collName,
|
||||||
|
DocumentID: docID,
|
||||||
|
}
|
||||||
|
if err := globalBatchManager.aof.Append(op); err != nil {
|
||||||
|
if globalBatchManager.logger != nil {
|
||||||
|
globalBatchManager.logger.Warn(fmt.Sprintf("AOF restore failed: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAOFStats возвращает статистику AOF.
|
||||||
|
func GetAOFStats() map[string]interface{} {
|
||||||
|
if globalBatchManager == nil || globalBatchManager.aof == nil {
|
||||||
|
return map[string]interface{}{"error": "AOF not initialized"}
|
||||||
|
}
|
||||||
|
return globalBatchManager.aof.GetStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplayAOF воспроизводит AOF при старте.
|
||||||
|
func ReplayAOF(storage *Storage) error {
|
||||||
|
if globalBatchManager == nil || globalBatchManager.aof == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return globalBatchManager.aof.Replay(storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ВСПОМОГАТЕЛЬНЫЕ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// trimAOFExtension убирает суффикс из имени файла.
|
||||||
|
func trimAOFExtension(name string) string {
|
||||||
|
return strings.TrimSuffix(name, AOFFileSuffix)
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
// Назначение: Аудит всех операций создания, изменения, удаления данных
|
// Назначение: Аудит всех операций создания, изменения, удаления данных
|
||||||
// с записью временной метки с точностью до миллисекунды
|
// с записью временной метки с точностью до миллисекунды
|
||||||
// Аудит теперь персистентный - пишется в файл с fsync
|
// Аудит теперь персистентный - пишется в файл с fsync
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат временных меток изменён с "2006-01-02 15:04:05.000"
|
||||||
|
// на "02-01-2006 15:04:05.000" для совместимости с OpenIndiana.
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
@@ -150,11 +153,17 @@ func CloseAuditLogger() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentTimestamp возвращает текущую временную метку с миллисекундами
|
// GetCurrentTimestamp возвращает текущую временную метку с миллисекундами.
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат строки изменён с "2006-01-02 15:04:05.000"
|
||||||
|
// на "02-01-2006 15:04:05.000". Это сделано для того, чтобы временные
|
||||||
|
// метки отображались как "03-10-2026" вместо "2026-10-03", как требуется
|
||||||
|
// для совместимости с OpenIndiana (и, в целом, для единообразия во всех
|
||||||
|
// компонентах СУБД). Используется глобально: audit, document, engine.
|
||||||
func GetCurrentTimestamp() (int64, string) {
|
func GetCurrentTimestamp() (int64, string) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
timestampMs := now.UnixMilli()
|
timestampMs := now.UnixMilli()
|
||||||
timestampStr := now.Format("2006-01-02 15:04:05.000")
|
timestampStr := now.Format("02-01-2006 15:04:05.000")
|
||||||
return timestampMs, timestampStr
|
return timestampMs, timestampStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1459
-1402
File diff suppressed because it is too large.
Load diff
+577
-523
File diff suppressed because it is too large.
Load diff
@@ -14,6 +14,9 @@
|
|||||||
// Полностью wait-free с использованием sync.Map и атомарных операций.
|
// Полностью wait-free с использованием sync.Map и атомарных операций.
|
||||||
// InmemStore теперь использует fsync для сохранности Raft-лога
|
// InmemStore теперь использует fsync для сохранности Raft-лога
|
||||||
// Restore() теперь полностью очищает существующие данные
|
// Restore() теперь полностью очищает существующие данные
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат временных меток изменён с "2006-01-02 15:04:05.000"
|
||||||
|
// на "02-012006 15:04:05.000" для совместимости с OpenIndiana.
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
@@ -529,16 +532,16 @@ func (db *Database) SerializeDatabaseWithMetadata(exportedBy string) ([]byte, *E
|
|||||||
DocumentsCount: totalDocuments,
|
DocumentsCount: totalDocuments,
|
||||||
ExportedBy: exportedBy,
|
ExportedBy: exportedBy,
|
||||||
Additional: map[string]interface{}{
|
Additional: map[string]interface{}{
|
||||||
"storage_created_at": time.UnixMilli(db.createdAt).Format("2006-01-02 15:04:05.000"),
|
"storage_created_at": time.UnixMilli(db.createdAt).Format("02-012006 15:04:05.000"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
dbData["_metadata"] = map[string]interface{}{
|
dbData["_metadata"] = map[string]interface{}{
|
||||||
"name": db.name,
|
"name": db.name,
|
||||||
"created_at": db.GetCreatedAt(),
|
"created_at": db.GetCreatedAt(),
|
||||||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("02-012006 15:04:05.000"),
|
||||||
"updated_at": db.GetUpdatedAt(),
|
"updated_at": db.GetUpdatedAt(),
|
||||||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("02-012006 15:04:05.000"),
|
||||||
"export_metadata": metadata,
|
"export_metadata": metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,7 +909,7 @@ func (s *Storage) Backup(backupPath string) error {
|
|||||||
backup := make(map[string]interface{})
|
backup := make(map[string]interface{})
|
||||||
backup["created_at"] = s.createdAt
|
backup["created_at"] = s.createdAt
|
||||||
backup["backup_time"] = time.Now().UnixMilli()
|
backup["backup_time"] = time.Now().UnixMilli()
|
||||||
backup["backup_time_str"] = time.Now().Format("2006-01-02 15:04:05.000")
|
backup["backup_time_str"] = time.Now().Format("02-012006 15:04:05.000")
|
||||||
|
|
||||||
databases := make(map[string][]byte)
|
databases := make(map[string][]byte)
|
||||||
databasesMetadata := make(map[string]interface{})
|
databasesMetadata := make(map[string]interface{})
|
||||||
@@ -1046,7 +1049,7 @@ func (s *Storage) GetStats() map[string]interface{} {
|
|||||||
"total_documents": s.GetTotalDocuments(),
|
"total_documents": s.GetTotalDocuments(),
|
||||||
"page_size_bytes": s.pageSize,
|
"page_size_bytes": s.pageSize,
|
||||||
"created_at": s.createdAt,
|
"created_at": s.createdAt,
|
||||||
"created_at_str": time.UnixMilli(s.createdAt).Format("2006-01-02 15:04:05.000"),
|
"created_at_str": time.UnixMilli(s.createdAt).Format("02-012006 15:04:05.000"),
|
||||||
}
|
}
|
||||||
|
|
||||||
databases := make([]map[string]interface{}, 0)
|
databases := make([]map[string]interface{}, 0)
|
||||||
@@ -1056,9 +1059,9 @@ func (s *Storage) GetStats() map[string]interface{} {
|
|||||||
"name": db.name,
|
"name": db.name,
|
||||||
"collections": len(db.ListCollections()),
|
"collections": len(db.ListCollections()),
|
||||||
"created_at": db.GetCreatedAt(),
|
"created_at": db.GetCreatedAt(),
|
||||||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("02-012006 15:04:05.000"),
|
||||||
"updated_at": db.GetUpdatedAt(),
|
"updated_at": db.GetUpdatedAt(),
|
||||||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("02-012006 15:04:05.000"),
|
||||||
"deleted_at": db.GetDeletedAt(),
|
"deleted_at": db.GetDeletedAt(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+326
-321
@@ -11,436 +11,441 @@
|
|||||||
// Файл: internal/storage/runtime_limits.go
|
// Файл: internal/storage/runtime_limits.go
|
||||||
// Назначение: Ограничения на размер коллекции/документа в рантайме
|
// Назначение: Ограничения на размер коллекции/документа в рантайме
|
||||||
// Добавлен механизм eviction при нехватке памяти (OOM protection)
|
// Добавлен механизм eviction при нехватке памяти (OOM protection)
|
||||||
// Eviction теперь проверяет активные транзакции перед удалением
|
// Eviction проверяет активные batch-операции перед удалением
|
||||||
|
//
|
||||||
|
// ИСПРАВЛЕНО (2026-10): после замены ACID-транзакций на batch-операции
|
||||||
|
// функция isDocInActiveTransaction ссылалась на удалённые globalTxManager
|
||||||
|
// и Transaction. Заменена на isDocInActiveBatch, который проверяет
|
||||||
|
// реестр активных batch'ей в globalBatchManager.
|
||||||
|
//
|
||||||
|
// Логика та же: не вытеснять документы, которые в данный момент
|
||||||
|
// участвуют в незакоммиченной batch-операции. Batch живёт коротко
|
||||||
|
// (создаётся, коммитится и удаляется в рамках одной функции), но
|
||||||
|
// между AddUpdate и Commit документ ещё не изменён, и eviction
|
||||||
|
// мог бы его удалить — защита сохраняется.
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoggerInterface определяет интерфейс для логирования
|
// LoggerInterface определяет интерфейс для логирования
|
||||||
type LoggerInterface interface {
|
type LoggerInterface interface {
|
||||||
Debug(msg string)
|
Debug(msg string)
|
||||||
Info(msg string)
|
Info(msg string)
|
||||||
Error(msg string)
|
Error(msg string)
|
||||||
Warn(msg string)
|
Warn(msg string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvictionPolicy определяет политику вытеснения
|
// EvictionPolicy определяет политику вытеснения
|
||||||
type EvictionPolicy int
|
type EvictionPolicy int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EvictionNone EvictionPolicy = iota
|
EvictionNone EvictionPolicy = iota
|
||||||
EvictionLRU
|
EvictionLRU
|
||||||
EvictionTTL
|
EvictionTTL
|
||||||
EvictionOldest
|
EvictionOldest
|
||||||
)
|
)
|
||||||
|
|
||||||
// RuntimeLimitsManager управляет runtime-ограничениями
|
// RuntimeLimitsManager управляет runtime-ограничениями
|
||||||
type RuntimeLimitsManager struct {
|
type RuntimeLimitsManager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
globalMaxDocSize int64
|
globalMaxDocSize int64
|
||||||
globalMaxCollSize int64
|
globalMaxCollSize int64
|
||||||
globalMaxDocsPerColl int64
|
globalMaxDocsPerColl int64
|
||||||
globalMaxMemory int64
|
globalMaxMemory int64
|
||||||
collectionOverrides map[string]*CollectionLimits
|
collectionOverrides map[string]*CollectionLimits
|
||||||
metrics *LimitMetrics
|
metrics *LimitMetrics
|
||||||
logger LoggerInterface
|
logger LoggerInterface
|
||||||
enabled bool
|
enabled bool
|
||||||
evictionPolicy EvictionPolicy
|
evictionPolicy EvictionPolicy
|
||||||
memoryThreshold float64
|
memoryThreshold float64
|
||||||
evictionChan chan string
|
evictionChan chan string
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// CollectionLimits содержит лимиты для конкретной коллекции
|
// CollectionLimits содержит лимиты для конкретной коллекции
|
||||||
type CollectionLimits struct {
|
type CollectionLimits struct {
|
||||||
MaxDocSize int64
|
MaxDocSize int64
|
||||||
MaxCollectionSize int64
|
MaxCollectionSize int64
|
||||||
MaxDocuments int64
|
MaxDocuments int64
|
||||||
EvictionPolicy EvictionPolicy
|
EvictionPolicy EvictionPolicy
|
||||||
LastUpdated int64
|
LastUpdated int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// LimitMetrics хранит метрики ограничений
|
// LimitMetrics хранит метрики ограничений
|
||||||
type LimitMetrics struct {
|
type LimitMetrics struct {
|
||||||
RejectedBySize atomic.Uint64
|
RejectedBySize atomic.Uint64
|
||||||
RejectedByDocCount atomic.Uint64
|
RejectedByDocCount atomic.Uint64
|
||||||
RejectedByCollSize atomic.Uint64
|
RejectedByCollSize atomic.Uint64
|
||||||
RejectedByMemory atomic.Uint64
|
RejectedByMemory atomic.Uint64
|
||||||
EvictedDocuments atomic.Uint64
|
EvictedDocuments atomic.Uint64
|
||||||
EvictedBytes atomic.Uint64
|
EvictedBytes atomic.Uint64
|
||||||
LastCheckTime atomic.Int64
|
LastCheckTime atomic.Int64
|
||||||
LastEvictionTime atomic.Int64
|
LastEvictionTime atomic.Int64
|
||||||
SkippedEviction atomic.Uint64 // Пропущено из-за активных транзакций
|
SkippedEviction atomic.Uint64 // Пропущено из-за активных batch-операций
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuntimeLimitsConfig содержит конфигурацию ограничений
|
// RuntimeLimitsConfig содержит конфигурацию ограничений
|
||||||
type RuntimeLimitsConfig struct {
|
type RuntimeLimitsConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
GlobalMaxDocSizeMB int `json:"global_max_doc_size_mb"`
|
GlobalMaxDocSizeMB int `json:"global_max_doc_size_mb"`
|
||||||
GlobalMaxCollSizeMB int64 `json:"global_max_coll_size_mb"`
|
GlobalMaxCollSizeMB int64 `json:"global_max_coll_size_mb"`
|
||||||
GlobalMaxDocsPerColl int64 `json:"global_max_docs_per_coll"`
|
GlobalMaxDocsPerColl int64 `json:"global_max_docs_per_coll"`
|
||||||
GlobalMaxMemoryMB int64 `json:"global_max_memory_mb"`
|
GlobalMaxMemoryMB int64 `json:"global_max_memory_mb"`
|
||||||
EvictionPolicy EvictionPolicy `json:"eviction_policy"`
|
EvictionPolicy EvictionPolicy `json:"eviction_policy"`
|
||||||
MemoryThreshold float64 `json:"memory_threshold"`
|
MemoryThreshold float64 `json:"memory_threshold"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultRuntimeLimitsConfig возвращает конфигурацию по умолчанию
|
// DefaultRuntimeLimitsConfig возвращает конфигурацию по умолчанию
|
||||||
func DefaultRuntimeLimitsConfig() *RuntimeLimitsConfig {
|
func DefaultRuntimeLimitsConfig() *RuntimeLimitsConfig {
|
||||||
return &RuntimeLimitsConfig{
|
return &RuntimeLimitsConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
GlobalMaxDocSizeMB: 16,
|
GlobalMaxDocSizeMB: 16,
|
||||||
GlobalMaxCollSizeMB: 10240,
|
GlobalMaxCollSizeMB: 10240,
|
||||||
GlobalMaxDocsPerColl: 10000000,
|
GlobalMaxDocsPerColl: 10000000,
|
||||||
GlobalMaxMemoryMB: 0,
|
GlobalMaxMemoryMB: 0,
|
||||||
EvictionPolicy: EvictionLRU,
|
EvictionPolicy: EvictionLRU,
|
||||||
MemoryThreshold: 0.85,
|
MemoryThreshold: 0.85,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRuntimeLimitsManager создаёт новый менеджер ограничений
|
// NewRuntimeLimitsManager создаёт новый менеджер ограничений
|
||||||
func NewRuntimeLimitsManager(cfg *RuntimeLimitsConfig, logger LoggerInterface) *RuntimeLimitsManager {
|
func NewRuntimeLimitsManager(cfg *RuntimeLimitsConfig, logger LoggerInterface) *RuntimeLimitsManager {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = DefaultRuntimeLimitsConfig()
|
cfg = DefaultRuntimeLimitsConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
maxMemory := cfg.GlobalMaxMemoryMB * 1024 * 1024
|
maxMemory := cfg.GlobalMaxMemoryMB * 1024 * 1024
|
||||||
if maxMemory <= 0 {
|
if maxMemory <= 0 {
|
||||||
var memStats runtime.MemStats
|
var memStats runtime.MemStats
|
||||||
runtime.ReadMemStats(&memStats)
|
runtime.ReadMemStats(&memStats)
|
||||||
maxMemory = int64(float64(memStats.Sys) * 0.8)
|
maxMemory = int64(float64(memStats.Sys) * 0.8)
|
||||||
}
|
}
|
||||||
|
|
||||||
rlm := &RuntimeLimitsManager{
|
rlm := &RuntimeLimitsManager{
|
||||||
globalMaxDocSize: int64(cfg.GlobalMaxDocSizeMB) * 1024 * 1024,
|
globalMaxDocSize: int64(cfg.GlobalMaxDocSizeMB) * 1024 * 1024,
|
||||||
globalMaxCollSize: cfg.GlobalMaxCollSizeMB * 1024 * 1024,
|
globalMaxCollSize: cfg.GlobalMaxCollSizeMB * 1024 * 1024,
|
||||||
globalMaxDocsPerColl: cfg.GlobalMaxDocsPerColl,
|
globalMaxDocsPerColl: cfg.GlobalMaxDocsPerColl,
|
||||||
globalMaxMemory: maxMemory,
|
globalMaxMemory: maxMemory,
|
||||||
collectionOverrides: make(map[string]*CollectionLimits),
|
collectionOverrides: make(map[string]*CollectionLimits),
|
||||||
metrics: &LimitMetrics{},
|
metrics: &LimitMetrics{},
|
||||||
logger: logger,
|
logger: logger,
|
||||||
enabled: cfg.Enabled,
|
enabled: cfg.Enabled,
|
||||||
evictionPolicy: cfg.EvictionPolicy,
|
evictionPolicy: cfg.EvictionPolicy,
|
||||||
memoryThreshold: cfg.MemoryThreshold,
|
memoryThreshold: cfg.MemoryThreshold,
|
||||||
evictionChan: make(chan string, 100),
|
evictionChan: make(chan string, 100),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
rlm.wg.Add(1)
|
rlm.wg.Add(1)
|
||||||
go rlm.memoryMonitorLoop()
|
go rlm.memoryMonitorLoop()
|
||||||
|
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
logger.Debug(fmt.Sprintf("Runtime limits manager initialized: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d, maxMemory=%dMB, eviction=%d",
|
logger.Debug(fmt.Sprintf("Runtime limits manager initialized: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d, maxMemory=%dMB, eviction=%d",
|
||||||
cfg.GlobalMaxDocSizeMB, cfg.GlobalMaxCollSizeMB, cfg.GlobalMaxDocsPerColl, maxMemory/(1024*1024), cfg.EvictionPolicy))
|
cfg.GlobalMaxDocSizeMB, cfg.GlobalMaxCollSizeMB, cfg.GlobalMaxDocsPerColl, maxMemory/(1024*1024), cfg.EvictionPolicy))
|
||||||
}
|
}
|
||||||
|
|
||||||
return rlm
|
return rlm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) memoryMonitorLoop() {
|
func (rlm *RuntimeLimitsManager) memoryMonitorLoop() {
|
||||||
defer rlm.wg.Done()
|
defer rlm.wg.Done()
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if !rlm.enabled {
|
if !rlm.enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var memStats runtime.MemStats
|
var memStats runtime.MemStats
|
||||||
runtime.ReadMemStats(&memStats)
|
runtime.ReadMemStats(&memStats)
|
||||||
currentUsage := int64(memStats.Alloc)
|
currentUsage := int64(memStats.Alloc)
|
||||||
usageRatio := float64(currentUsage) / float64(rlm.globalMaxMemory)
|
usageRatio := float64(currentUsage) / float64(rlm.globalMaxMemory)
|
||||||
|
|
||||||
if usageRatio > rlm.memoryThreshold {
|
if usageRatio > rlm.memoryThreshold {
|
||||||
if rlm.logger != nil {
|
if rlm.logger != nil {
|
||||||
rlm.logger.Warn(fmt.Sprintf("Memory usage %.2f%% exceeds threshold %.2f%%, triggering eviction",
|
rlm.logger.Warn(fmt.Sprintf("Memory usage %.2f%% exceeds threshold %.2f%%, triggering eviction",
|
||||||
usageRatio*100, rlm.memoryThreshold*100))
|
usageRatio*100, rlm.memoryThreshold*100))
|
||||||
}
|
}
|
||||||
rlm.triggerEviction()
|
rlm.triggerEviction()
|
||||||
}
|
}
|
||||||
case <-rlm.stopChan:
|
case <-rlm.stopChan:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) triggerEviction() {
|
func (rlm *RuntimeLimitsManager) triggerEviction() {
|
||||||
select {
|
select {
|
||||||
case rlm.evictionChan <- "global":
|
case rlm.evictionChan <- "global":
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isDocInActiveTransaction проверяет, участвует ли документ в активной транзакции
|
// isDocInActiveBatch проверяет, участвует ли документ в активной
|
||||||
// Защита от удаления данных активных транзакций
|
// batch-операции.
|
||||||
func (rlm *RuntimeLimitsManager) isDocInActiveTransaction(docID, dbName, collName string) bool {
|
//
|
||||||
if globalTxManager == nil {
|
// ИСПРАВЛЕНО (2026-10): ранее метод назывался isDocInActiveTransaction
|
||||||
return false
|
// и обращался к globalTxManager и Transaction, которые были удалены при
|
||||||
}
|
// переходе на batch-операции. Теперь проверяем реестр активных batch'ей
|
||||||
|
// в globalBatchManager (см. transactions.go).
|
||||||
inTx := false
|
//
|
||||||
globalTxManager.activeTransactions.Range(func(key, value interface{}) bool {
|
// Защита сохраняется: batch живёт коротко (создаётся и коммитится в
|
||||||
tx := value.(*Transaction)
|
// рамках одной функции), но между AddUpdate и Commit документ ещё не
|
||||||
tx.mu.RLock()
|
// изменён, и eviction мог бы его удалить.
|
||||||
for _, op := range tx.Operations {
|
func (rlm *RuntimeLimitsManager) isDocInActiveBatch(docID, dbName, collName string) bool {
|
||||||
if op.DocumentID == docID && op.Database == dbName && op.Collection == collName {
|
bm := GetBatchManager()
|
||||||
inTx = true
|
if bm == nil {
|
||||||
break
|
return false
|
||||||
}
|
}
|
||||||
}
|
return bm.IsDocInActiveBatch(docID, dbName, collName)
|
||||||
tx.mu.RUnlock()
|
|
||||||
if inTx {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return inTx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvictFromCollection выполняет вытеснение документов из коллекции
|
// EvictFromCollection выполняет вытеснение документов из коллекции.
|
||||||
// Проверка активных транзакций перед удалением
|
//
|
||||||
|
// Проверка активных batch-операций перед удалением.
|
||||||
func (rlm *RuntimeLimitsManager) EvictFromCollection(coll *Collection, targetBytes int64) (int64, error) {
|
func (rlm *RuntimeLimitsManager) EvictFromCollection(coll *Collection, targetBytes int64) (int64, error) {
|
||||||
if !rlm.enabled {
|
if !rlm.enabled {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
evictedBytes := int64(0)
|
evictedBytes := int64(0)
|
||||||
evictedCount := int64(0)
|
evictedCount := int64(0)
|
||||||
skippedCount := int64(0)
|
skippedCount := int64(0)
|
||||||
|
|
||||||
docs := coll.GetAllDocumentsIncludingDeleted()
|
docs := coll.GetAllDocumentsIncludingDeleted()
|
||||||
|
|
||||||
// Сначала вытесняем удалённые документы
|
// Сначала вытесняем удалённые документы
|
||||||
for _, doc := range docs {
|
for _, doc := range docs {
|
||||||
if evictedBytes >= targetBytes {
|
if evictedBytes >= targetBytes {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.IsDeleted() {
|
if doc.IsDeleted() {
|
||||||
// Проверяем активные транзакции
|
// Проверяем активные batch-операции
|
||||||
if rlm.isDocInActiveTransaction(doc.ID, coll.DBName(), coll.Name()) {
|
if rlm.isDocInActiveBatch(doc.ID, coll.DBName(), coll.Name()) {
|
||||||
skippedCount++
|
skippedCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
size := doc.OriginalSize
|
size := doc.OriginalSize
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
size = 1024
|
size = 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := coll.PermanentDelete(doc.ID); err == nil {
|
if err := coll.PermanentDelete(doc.ID); err == nil {
|
||||||
evictedBytes += size
|
evictedBytes += size
|
||||||
evictedCount++
|
evictedCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Затем вытесняем самые старые
|
// Затем вытесняем самые старые
|
||||||
if evictedBytes < targetBytes && rlm.evictionPolicy == EvictionLRU {
|
if evictedBytes < targetBytes && rlm.evictionPolicy == EvictionLRU {
|
||||||
for _, doc := range docs {
|
for _, doc := range docs {
|
||||||
if evictedBytes >= targetBytes {
|
if evictedBytes >= targetBytes {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !doc.IsDeleted() {
|
if !doc.IsDeleted() {
|
||||||
// Проверяем активные транзакции
|
// Проверяем активные batch-операции
|
||||||
if rlm.isDocInActiveTransaction(doc.ID, coll.DBName(), coll.Name()) {
|
if rlm.isDocInActiveBatch(doc.ID, coll.DBName(), coll.Name()) {
|
||||||
skippedCount++
|
skippedCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
size := doc.OriginalSize
|
size := doc.OriginalSize
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
size = 1024
|
size = 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
if coll.metadata.Settings.SoftDelete {
|
if coll.metadata.Settings.SoftDelete {
|
||||||
if err := coll.Delete(doc.ID); err == nil {
|
if err := coll.Delete(doc.ID); err == nil {
|
||||||
evictedBytes += size
|
evictedBytes += size
|
||||||
evictedCount++
|
evictedCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := coll.PermanentDelete(doc.ID); err == nil {
|
if err := coll.PermanentDelete(doc.ID); err == nil {
|
||||||
evictedBytes += size
|
evictedBytes += size
|
||||||
evictedCount++
|
evictedCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rlm.metrics.EvictedDocuments.Add(uint64(evictedCount))
|
rlm.metrics.EvictedDocuments.Add(uint64(evictedCount))
|
||||||
rlm.metrics.EvictedBytes.Add(uint64(evictedBytes))
|
rlm.metrics.EvictedBytes.Add(uint64(evictedBytes))
|
||||||
if skippedCount > 0 {
|
if skippedCount > 0 {
|
||||||
rlm.metrics.SkippedEviction.Add(uint64(skippedCount))
|
rlm.metrics.SkippedEviction.Add(uint64(skippedCount))
|
||||||
}
|
}
|
||||||
rlm.metrics.LastEvictionTime.Store(time.Now().UnixMilli())
|
rlm.metrics.LastEvictionTime.Store(time.Now().UnixMilli())
|
||||||
|
|
||||||
if rlm.logger != nil {
|
if rlm.logger != nil {
|
||||||
rlm.logger.Info(fmt.Sprintf("Evicted %d documents (%d bytes) from collection %s.%s (skipped %d in active transactions)",
|
rlm.logger.Info(fmt.Sprintf("Evicted %d documents (%d bytes) from collection %s.%s (skipped %d in active batches)",
|
||||||
evictedCount, evictedBytes, coll.DBName(), coll.Name(), skippedCount))
|
evictedCount, evictedBytes, coll.DBName(), coll.Name(), skippedCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
return evictedBytes, nil
|
return evictedBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) ValidateDocumentSize(dbName, collName string, docSize int64) error {
|
func (rlm *RuntimeLimitsManager) ValidateDocumentSize(dbName, collName string, docSize int64) error {
|
||||||
if !rlm.enabled {
|
if !rlm.enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := rlm.getCollectionLimit(dbName, collName)
|
limit := rlm.getCollectionLimit(dbName, collName)
|
||||||
maxSize := rlm.globalMaxDocSize
|
maxSize := rlm.globalMaxDocSize
|
||||||
if limit != nil && limit.MaxDocSize > 0 {
|
if limit != nil && limit.MaxDocSize > 0 {
|
||||||
maxSize = limit.MaxDocSize
|
maxSize = limit.MaxDocSize
|
||||||
}
|
}
|
||||||
|
|
||||||
if docSize > maxSize {
|
if docSize > maxSize {
|
||||||
rlm.metrics.RejectedBySize.Add(1)
|
rlm.metrics.RejectedBySize.Add(1)
|
||||||
return fmt.Errorf("document size %d bytes exceeds limit %d bytes", docSize, maxSize)
|
return fmt.Errorf("document size %d bytes exceeds limit %d bytes", docSize, maxSize)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) ValidateCollectionSize(coll *Collection, newDocSize int64) error {
|
func (rlm *RuntimeLimitsManager) ValidateCollectionSize(coll *Collection, newDocSize int64) error {
|
||||||
if !rlm.enabled {
|
if !rlm.enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
|
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
|
||||||
maxSize := rlm.globalMaxCollSize
|
maxSize := rlm.globalMaxCollSize
|
||||||
if limit != nil && limit.MaxCollectionSize > 0 {
|
if limit != nil && limit.MaxCollectionSize > 0 {
|
||||||
maxSize = limit.MaxCollectionSize
|
maxSize = limit.MaxCollectionSize
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSize := coll.Size()
|
currentSize := coll.Size()
|
||||||
if currentSize+newDocSize > maxSize {
|
if currentSize+newDocSize > maxSize {
|
||||||
neededBytes := currentSize + newDocSize - maxSize
|
neededBytes := currentSize + newDocSize - maxSize
|
||||||
if evicted, err := rlm.EvictFromCollection(coll, neededBytes); err == nil && evicted >= neededBytes {
|
if evicted, err := rlm.EvictFromCollection(coll, neededBytes); err == nil && evicted >= neededBytes {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rlm.metrics.RejectedByCollSize.Add(1)
|
rlm.metrics.RejectedByCollSize.Add(1)
|
||||||
return fmt.Errorf("collection size would exceed limit %d bytes (current: %d, new: %d)",
|
return fmt.Errorf("collection size would exceed limit %d bytes (current: %d, new: %d)",
|
||||||
maxSize, currentSize, newDocSize)
|
maxSize, currentSize, newDocSize)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) ValidateDocumentCount(coll *Collection) error {
|
func (rlm *RuntimeLimitsManager) ValidateDocumentCount(coll *Collection) error {
|
||||||
if !rlm.enabled {
|
if !rlm.enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
|
limit := rlm.getCollectionLimit(coll.DBName(), coll.Name())
|
||||||
maxDocs := rlm.globalMaxDocsPerColl
|
maxDocs := rlm.globalMaxDocsPerColl
|
||||||
if limit != nil && limit.MaxDocuments > 0 {
|
if limit != nil && limit.MaxDocuments > 0 {
|
||||||
maxDocs = limit.MaxDocuments
|
maxDocs = limit.MaxDocuments
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCount := coll.Count()
|
currentCount := coll.Count()
|
||||||
if currentCount >= maxDocs {
|
if currentCount >= maxDocs {
|
||||||
targetBytes := int64((currentCount - maxDocs + 1) * 1024)
|
targetBytes := int64((currentCount - maxDocs + 1) * 1024)
|
||||||
if evicted, err := rlm.EvictFromCollection(coll, targetBytes); err == nil && evicted > 0 {
|
if evicted, err := rlm.EvictFromCollection(coll, targetBytes); err == nil && evicted > 0 {
|
||||||
if coll.Count() < maxDocs {
|
if coll.Count() < maxDocs {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rlm.metrics.RejectedByDocCount.Add(1)
|
rlm.metrics.RejectedByDocCount.Add(1)
|
||||||
return fmt.Errorf("collection has reached maximum document count %d", maxDocs)
|
return fmt.Errorf("collection has reached maximum document count %d", maxDocs)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) CheckMemoryUsage() error {
|
func (rlm *RuntimeLimitsManager) CheckMemoryUsage() error {
|
||||||
if !rlm.enabled {
|
if !rlm.enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var memStats runtime.MemStats
|
var memStats runtime.MemStats
|
||||||
runtime.ReadMemStats(&memStats)
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
currentUsage := int64(memStats.Alloc)
|
currentUsage := int64(memStats.Alloc)
|
||||||
if currentUsage > rlm.globalMaxMemory {
|
if currentUsage > rlm.globalMaxMemory {
|
||||||
rlm.metrics.RejectedByMemory.Add(1)
|
rlm.metrics.RejectedByMemory.Add(1)
|
||||||
return fmt.Errorf("memory usage %d bytes exceeds limit %d bytes", currentUsage, rlm.globalMaxMemory)
|
return fmt.Errorf("memory usage %d bytes exceeds limit %d bytes", currentUsage, rlm.globalMaxMemory)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) getCollectionLimit(dbName, collName string) *CollectionLimits {
|
func (rlm *RuntimeLimitsManager) getCollectionLimit(dbName, collName string) *CollectionLimits {
|
||||||
rlm.mu.RLock()
|
rlm.mu.RLock()
|
||||||
defer rlm.mu.RUnlock()
|
defer rlm.mu.RUnlock()
|
||||||
|
|
||||||
key := fmt.Sprintf("%s.%s", dbName, collName)
|
key := fmt.Sprintf("%s.%s", dbName, collName)
|
||||||
if limits, ok := rlm.collectionOverrides[key]; ok {
|
if limits, ok := rlm.collectionOverrides[key]; ok {
|
||||||
return limits
|
return limits
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) SetCollectionLimits(dbName, collName string, maxDocSizeMB int, maxCollSizeMB int64, maxDocuments int64) {
|
func (rlm *RuntimeLimitsManager) SetCollectionLimits(dbName, collName string, maxDocSizeMB int, maxCollSizeMB int64, maxDocuments int64) {
|
||||||
rlm.mu.Lock()
|
rlm.mu.Lock()
|
||||||
defer rlm.mu.Unlock()
|
defer rlm.mu.Unlock()
|
||||||
|
|
||||||
key := fmt.Sprintf("%s.%s", dbName, collName)
|
key := fmt.Sprintf("%s.%s", dbName, collName)
|
||||||
rlm.collectionOverrides[key] = &CollectionLimits{
|
rlm.collectionOverrides[key] = &CollectionLimits{
|
||||||
MaxDocSize: int64(maxDocSizeMB) * 1024 * 1024,
|
MaxDocSize: int64(maxDocSizeMB) * 1024 * 1024,
|
||||||
MaxCollectionSize: maxCollSizeMB * 1024 * 1024,
|
MaxCollectionSize: maxCollSizeMB * 1024 * 1024,
|
||||||
MaxDocuments: maxDocuments,
|
MaxDocuments: maxDocuments,
|
||||||
EvictionPolicy: rlm.evictionPolicy,
|
EvictionPolicy: rlm.evictionPolicy,
|
||||||
LastUpdated: time.Now().UnixMilli(),
|
LastUpdated: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if rlm.logger != nil {
|
if rlm.logger != nil {
|
||||||
rlm.logger.Info(fmt.Sprintf("Set limits for %s: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d",
|
rlm.logger.Info(fmt.Sprintf("Set limits for %s: maxDoc=%dMB, maxColl=%dMB, maxDocs=%d",
|
||||||
key, maxDocSizeMB, maxCollSizeMB, maxDocuments))
|
key, maxDocSizeMB, maxCollSizeMB, maxDocuments))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) RemoveCollectionLimits(dbName, collName string) {
|
func (rlm *RuntimeLimitsManager) RemoveCollectionLimits(dbName, collName string) {
|
||||||
rlm.mu.Lock()
|
rlm.mu.Lock()
|
||||||
defer rlm.mu.Unlock()
|
defer rlm.mu.Unlock()
|
||||||
|
|
||||||
key := fmt.Sprintf("%s.%s", dbName, collName)
|
key := fmt.Sprintf("%s.%s", dbName, collName)
|
||||||
delete(rlm.collectionOverrides, key)
|
delete(rlm.collectionOverrides, key)
|
||||||
|
|
||||||
if rlm.logger != nil {
|
if rlm.logger != nil {
|
||||||
rlm.logger.Info(fmt.Sprintf("Removed limits override for %s", key))
|
rlm.logger.Info(fmt.Sprintf("Removed limits override for %s", key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) GetMetrics() map[string]interface{} {
|
func (rlm *RuntimeLimitsManager) GetMetrics() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"rejected_by_size": rlm.metrics.RejectedBySize.Load(),
|
"rejected_by_size": rlm.metrics.RejectedBySize.Load(),
|
||||||
"rejected_by_doc_count": rlm.metrics.RejectedByDocCount.Load(),
|
"rejected_by_doc_count": rlm.metrics.RejectedByDocCount.Load(),
|
||||||
"rejected_by_coll_size": rlm.metrics.RejectedByCollSize.Load(),
|
"rejected_by_coll_size": rlm.metrics.RejectedByCollSize.Load(),
|
||||||
"rejected_by_memory": rlm.metrics.RejectedByMemory.Load(),
|
"rejected_by_memory": rlm.metrics.RejectedByMemory.Load(),
|
||||||
"evicted_documents": rlm.metrics.EvictedDocuments.Load(),
|
"evicted_documents": rlm.metrics.EvictedDocuments.Load(),
|
||||||
"evicted_bytes": rlm.metrics.EvictedBytes.Load(),
|
"evicted_bytes": rlm.metrics.EvictedBytes.Load(),
|
||||||
"skipped_eviction": rlm.metrics.SkippedEviction.Load(),
|
"skipped_eviction": rlm.metrics.SkippedEviction.Load(),
|
||||||
"last_eviction_time": rlm.metrics.LastEvictionTime.Load(),
|
"last_eviction_time": rlm.metrics.LastEvictionTime.Load(),
|
||||||
"global_max_doc_size_mb": rlm.globalMaxDocSize / (1024 * 1024),
|
"global_max_doc_size_mb": rlm.globalMaxDocSize / (1024 * 1024),
|
||||||
"global_max_coll_size_mb": rlm.globalMaxCollSize / (1024 * 1024),
|
"global_max_coll_size_mb": rlm.globalMaxCollSize / (1024 * 1024),
|
||||||
"global_max_docs_per_coll": rlm.globalMaxDocsPerColl,
|
"global_max_docs_per_coll": rlm.globalMaxDocsPerColl,
|
||||||
"global_max_memory_mb": rlm.globalMaxMemory / (1024 * 1024),
|
"global_max_memory_mb": rlm.globalMaxMemory / (1024 * 1024),
|
||||||
"eviction_policy": rlm.evictionPolicy,
|
"eviction_policy": rlm.evictionPolicy,
|
||||||
"memory_threshold": rlm.memoryThreshold,
|
"memory_threshold": rlm.memoryThreshold,
|
||||||
"enabled": rlm.enabled,
|
"enabled": rlm.enabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlm *RuntimeLimitsManager) Stop() {
|
func (rlm *RuntimeLimitsManager) Stop() {
|
||||||
close(rlm.stopChan)
|
close(rlm.stopChan)
|
||||||
rlm.wg.Wait()
|
rlm.wg.Wait()
|
||||||
}
|
}
|
||||||
+524
-479
File diff suppressed because it is too large.
Load diff
+2456
-3937
File diff suppressed because it is too large.
Load diff
@@ -678,6 +678,8 @@ func (tm *TriggerManager) resolveValue(value interface{}, execCtx *TriggerExecut
|
|||||||
}
|
}
|
||||||
|
|
||||||
// logExecution логирует выполнение триггера
|
// logExecution логирует выполнение триггера
|
||||||
|
//
|
||||||
|
// ИЗМЕНЕНО (2026-10-03): формат временной метки изменён на "02-01-2006 15:04:05.000".
|
||||||
func (tm *TriggerManager) logExecution(execCtx *TriggerExecution, trigger *Trigger, result string, startTime time.Time) {
|
func (tm *TriggerManager) logExecution(execCtx *TriggerExecution, trigger *Trigger, result string, startTime time.Time) {
|
||||||
tm.mu.Lock()
|
tm.mu.Lock()
|
||||||
defer tm.mu.Unlock()
|
defer tm.mu.Unlock()
|
||||||
@@ -685,7 +687,7 @@ func (tm *TriggerManager) logExecution(execCtx *TriggerExecution, trigger *Trigg
|
|||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nowMs := now.UnixMilli()
|
nowMs := now.UnixMilli()
|
||||||
nowStr := now.Format("2006-01-02 15:04:05.000")
|
nowStr := now.Format("02-01-2006 15:04:05.000")
|
||||||
|
|
||||||
execCtx.TriggerName = trigger.Name
|
execCtx.TriggerName = trigger.Name
|
||||||
execCtx.Timestamp = now
|
execCtx.Timestamp = now
|
||||||
|
|||||||
Reference in new issue
Block a user