Compare commits

..
72 Commits
Author SHA1 Message Date
gvsafronov c9a5c8307c Upload files to "internal/cluster" 2026-10-04 17:48:50 +00:00
gvsafronov 0fcc0998ba Delete internal/cluster/raft_coordinator.go 2026-10-04 17:48:27 +00:00
gvsafronov 6c599b94bf Upload files to "internal/storage" 2026-10-04 17:47:46 +00:00
gvsafronov ae12264e27 Delete internal/storage/transactions.go 2026-10-04 17:47:19 +00:00
gvsafronov a7a3cd8cdb Delete cmd/futriis/main-old.go 2026-10-04 17:47:02 +00:00
gvsafronov 73061c2883 Upload files to "cmd/futriis" 2026-10-04 17:46:49 +00:00
gvsafronov 7fb359a462 Update cmd/futriis/main-old.go 2026-10-04 17:46:09 +00:00
gvsafronov ec05741a70 Upload files to "internal/cluster" 2026-10-03 22:53:22 +00:00
gvsafronov b418243503 Delete internal/cluster/node.go 2026-10-03 22:53:07 +00:00
gvsafronov 4adf1b62db Upload files to "internal/repl" 2026-10-03 22:51:14 +00:00
gvsafronov 96930e8e42 Delete internal/repl/repl.go 2026-10-03 22:50:59 +00:00
gvsafronov f0e28510e9 Delete internal/plugin/3plugin.go 2026-10-03 22:50:36 +00:00
gvsafronov e96c5a7a24 Upload files to "internal/plugin" 2026-10-03 22:50:26 +00:00
gvsafronov a9d6aa42e5 Update internal/plugin/3plugin.go 2026-10-03 22:50:05 +00:00
gvsafronov 78bff3e4d8 Upload files to "internal/storage" 2026-10-03 22:49:20 +00:00
gvsafronov d8673b747f Delete internal/storage/trigger.go 2026-10-03 22:49:08 +00:00
gvsafronov 96dc6311ea Upload files to "internal/storage" 2026-10-03 22:48:32 +00:00
gvsafronov 93385022d2 Delete internal/storage/document.go 2026-10-03 22:48:17 +00:00
gvsafronov e41f557719 Upload files to "internal/repl" 2026-10-03 22:42:49 +00:00
gvsafronov 452c8853f4 Delete internal/repl/repl.go 2026-10-03 22:42:34 +00:00
gvsafronov 2a9114083b Upload files to "internal/storage" 2026-10-03 22:42:04 +00:00
gvsafronov 13fe261b40 Upload files to "internal/storage" 2026-10-03 22:41:53 +00:00
gvsafronov 73773c27da Delete internal/storage/audit.go 2026-10-03 22:41:35 +00:00
gvsafronov b934417417 Delete internal/storage/trigger.go 2026-10-03 22:41:28 +00:00
gvsafronov 5422e39ab2 Delete internal/plugin/2plugin.go 2026-10-03 22:40:50 +00:00
gvsafronov 98bae0cbd6 Upload files to "internal/plugin" 2026-10-03 22:40:40 +00:00
gvsafronov 52d0216676 Update internal/plugin/2plugin.go 2026-10-03 22:40:17 +00:00
gvsafronov 3a1de92233 Upload files to "internal/commands" 2026-10-03 22:09:29 +00:00
gvsafronov 56a91d9fc8 Delete internal/commands/commands.go 2026-10-03 22:09:14 +00:00
gvsafronov 1559e12a51 Update README.md 2026-10-03 21:26:09 +00:00
gvsafronov 2e84b86d23 Update README.md 2026-10-03 21:24:45 +00:00
gvsafronov 6b9ee6b3d2 Update README.md 2026-10-03 21:24:07 +00:00
gvsafronov a373f05b18 Upload files to "internal/repl" 2026-10-03 21:16:32 +00:00
gvsafronov a73aa8bc9d Delete internal/repl/repl.go 2026-10-03 21:16:16 +00:00
gvsafronov a024994943 Upload files to "internal/storage" 2026-10-03 21:15:29 +00:00
gvsafronov aaada4008c Upload files to "internal/storage" 2026-10-03 21:15:14 +00:00
gvsafronov c5a5286314 Upload files to "internal/storage" 2026-10-03 21:14:48 +00:00
gvsafronov b26a42ab26 Delete internal/storage/engine.go 2026-10-03 21:14:20 +00:00
gvsafronov d0613d6aa1 Delete internal/storage/document.go 2026-10-03 21:14:12 +00:00
gvsafronov 11e79007cc Delete internal/storage/audit.go 2026-10-03 21:14:03 +00:00
gvsafronov 6b3317ce94 Delete internal/plugin/old_plugin.go 2026-10-03 21:13:30 +00:00
gvsafronov e5b34c75d8 Upload files to "internal/plugin" 2026-10-03 21:13:19 +00:00
gvsafronov d4ea0533d2 Update internal/plugin/old_plugin.go 2026-10-03 21:12:52 +00:00
gvsafronov 9b5bb176b3 Delete cmd/futriis/old_main.go 2026-10-03 20:40:15 +00:00
gvsafronov a533d973c0 Upload files to "cmd/futriis" 2026-10-03 20:40:05 +00:00
gvsafronov 7fd2addabe Update cmd/futriis/old_main.go 2026-10-03 20:39:37 +00:00
gvsafronov 5ffccfe885 Delete internal/api/old-http.go 2026-10-03 20:38:39 +00:00
gvsafronov ffc66dbed7 Upload files to "internal/api" 2026-10-03 20:38:28 +00:00
gvsafronov afff868301 Update internal/api/old-http.go 2026-10-03 20:37:40 +00:00
gvsafronov 87bed4495b Upload files to "internal/repl" 2026-10-03 20:36:36 +00:00
gvsafronov 58ba3b363e Delete internal/repl/repl.go 2026-10-03 20:36:15 +00:00
gvsafronov d59b878cd4 Delete internal/plugin/old_plugin.go 2026-10-03 20:35:26 +00:00
gvsafronov 54d1c0ba09 Upload files to "internal/plugin" 2026-10-03 20:35:11 +00:00
gvsafronov f480b910fc Update internal/plugin/old_plugin.go 2026-10-03 20:34:46 +00:00
gvsafronov a772e2d2a0 Delete internal/migration/old_schema_migrator.go 2026-10-03 20:33:50 +00:00
gvsafronov 90ca83f9a3 Upload files to "internal/migration" 2026-10-03 20:33:34 +00:00
gvsafronov 16267d2ea8 Update internal/migration/old_schema_migrator.go 2026-10-03 20:32:50 +00:00
gvsafronov 5e230b1a9c Upload files to "internal/storage" 2026-10-03 20:32:20 +00:00
gvsafronov ab48d50988 Upload files to "internal/storage" 2026-10-03 20:31:37 +00:00
gvsafronov bc445ad7be Upload files to "internal/storage" 2026-10-03 20:30:46 +00:00
gvsafronov e895568beb Delete internal/storage/document.go 2026-10-03 20:29:33 +00:00
gvsafronov b8217788e4 Delete internal/storage/collection.go 2026-10-03 20:29:24 +00:00
gvsafronov 9c6400ba3e Delete internal/storage/runtime_limits.go 2026-10-03 20:28:57 +00:00
gvsafronov c736906ad6 Delete internal/storage/transactions.go 2026-10-03 20:28:48 +00:00
gvsafronov 69385655a2 Delete internal/storage/skiplist.go 2026-10-03 20:28:34 +00:00
gvsafronov 4bb819b397 Update README.md 2026-10-03 20:00:24 +00:00
gvsafronov 82d073dd82 Delete internal/storage/collection-old.go 2026-09-29 20:45:25 +00:00
gvsafronov 0153d30d05 Delete internal/storage/document-old.go 2026-09-29 20:44:40 +00:00
gvsafronov 3ebbf8a55c Upload files to "internal/storage" 2026-09-29 20:43:58 +00:00
gvsafronov fbd5f209ca Update internal/storage/collection-old.go 2026-09-29 20:43:11 +00:00
gvsafronov 955fa27ab3 Update internal/storage/document-old.go 2026-09-29 20:42:23 +00:00
gvsafronov 233b280835 Update README.md 2026-09-29 18:50:47 +00:00
17 changed files with 7340 additions and 7176 deletions

No files matched your search

+87 -132
View File
@@ -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
+97 -12
View File
@@ -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()
} }
@@ -666,4 +751,4 @@ func printJSON(data interface{}) {
return return
} }
fmt.Println(string(jsonData)) fmt.Println(string(jsonData))
} }
+172 -56
View File
@@ -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:
@@ -1809,4 +1925,4 @@ func (s *HTTPServer) sendError(w http.ResponseWriter, errMsg string, statusCode
Success: false, Success: false,
Error: errMsg, Error: errMsg,
}) })
} }
+255 -10
View File
@@ -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 {
+49 -6
View File
@@ -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
+147 -66
View File
@@ -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 &copy return &copy
} }
// 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)
@@ -1433,4 +1514,4 @@ func (sm *SchemaMigrator) RegisterMigration(m *Migration) {
// Migrate выполняет миграцию (старый интерфейс) // Migrate выполняет миграцию (старый интерфейс)
func (sm *SchemaMigrator) Migrate(targetVersion string) error { func (sm *SchemaMigrator) Migrate(targetVersion string) error {
return sm.MigrateSchema(targetVersion) return sm.MigrateSchema(targetVersion)
} }
+357 -45
View File
@@ -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(),
@@ -3469,4 +3781,4 @@ func (pm *PluginManager) Close() error {
pm.statePool.Close() pm.statePool.Close()
return nil return nil
} }
+248 -96
View File
@@ -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()
} }
+481
View File
@@ -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)
}
+11 -2
View File
@@ -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
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+10 -7
View File
@@ -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(),
} }
+361 -356
View File
@@ -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()
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -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