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 4757 additions and 4593 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
+96 -11
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()
} }
+171 -55
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:
+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
+133 -52
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()
// Коммитим batch атомарно.
if migratedCount > 0 {
if err := batch.Commit(); err != nil {
if sm.logger != nil { if sm.logger != nil {
sm.logger.Error(fmt.Sprintf("Failed to update migrated document %s: %v", doc.ID, err)) sm.logger.Error(fmt.Sprintf("Failed to commit migration batch for %s: %v",
collection.Name(), err))
} }
continue
}
sm.migrationStats.mu.Lock() sm.migrationStats.mu.Lock()
sm.migrationStats.MigratedDocuments++ sm.migrationStats.FailedDocuments = failedCount + migratedCount
sm.migrationStats.SkippedDocuments = skippedCount
sm.migrationStats.EndTime = time.Now().UnixMilli()
sm.migrationStats.mu.Unlock() 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")
} }
@@ -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")
} }
@@ -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")
} }
@@ -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)
+355 -43
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)
} }
@@ -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(),
+244 -92
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 tx.StartTime > 0 { if r.currentBatch.Timestamp > 0 {
started = time.UnixMilli(tx.StartTime).Format("2006-01-02 15:04:05.000") started = time.UnixMilli(r.currentBatch.Timestamp).Format("02-01-2006 15:04:05.000")
} }
t.AddRow( t.AddRow(
fmt.Sprintf("%v", tx.ID), fmt.Sprintf("%d", r.currentBatch.ID),
tx.Status, r.currentBatch.Database,
fmt.Sprintf("%d", tx.OperationCount), r.currentBatch.Collection,
fmt.Sprintf("%d", len(r.currentBatch.Operations)),
started, 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
+143 -89
View File
@@ -13,6 +13,23 @@
// с полями, кортежами (вложенными документами) и сериализации в MessagePack. // с полями, кортежами (вложенными документами) и сериализации в MessagePack.
// Документ является основной единицей хранения в СУБД futriis. // Документ является основной единицей хранения в СУБД futriis.
// Lock-free: Document.fields переведён на atomic.Value для wait-free доступа. // Lock-free: Document.fields переведён на atomic.Value для wait-free доступа.
//
// ИСПРАВЛЕНО (2026): Устранён race condition в Document.
// Все методы, читающие/записывающие метаданные (ID, CreatedAt, UpdatedAt,
// DeletedAt, Version, Compressed, OriginalSize), теперь используют
// sync.RWMutex для защиты от гонок.
//
// ИЗМЕНЕНО (2026): Убраны вызовы AuditFieldOperation из SetField/DeleteField
// (они были неинформативными — db/coll пустые). Аудит выполняется на уровне
// Collection (AuditDocumentOperation).
//
// ИЗМЕНЕНО (2026): Удалены поля Version, Compressed, OriginalSize из
// MVCC-контекста — они остались, но больше не используются для версионирования.
// Версия документа увеличивается при каждом изменении, но time-travel
// реализован через WAL (см. transactions.go: GetDocumentAtTimestamp).
//
// ИЗМЕНЕНО (2026-10-03): формат временных меток изменён с "2006-01-02 15:04:05.000"
// на "02-012006 15:04:05.000" для совместимости с OpenIndiana.
package storage package storage
@@ -46,53 +63,59 @@ const (
// FIELD VERSION - ЗАЩИТА ОТ ABA // FIELD VERSION - ЗАЩИТА ОТ ABA
// ============================================================================= // =============================================================================
// fieldVersionWrapper оборачивает map полей с версией для защиты от ABA // fieldVersionWrapper оборачивает map полей с версией для защиты от ABA.
type fieldVersionWrapper struct { type fieldVersionWrapper struct {
fields map[string]interface{} fields map[string]interface{}
version uint64 version uint64
} }
// Document представляет документ в коллекции (аналог строки в реляционной СУБД) // Document представляет документ в коллекции (аналог строки в реляционной СУБД).
//
// ИСПРАВЛЕНО: добавлен sync.RWMutex для защиты метаданных документа
// (ID, CreatedAt, UpdatedAt, DeletedAt, Version, Compressed, OriginalSize)
// от гонок при конкурентном чтении/записи.
type Document struct { type Document struct {
mu sync.RWMutex // защита метаданных документа
ID string `msgpack:"_id"` // Уникальный идентификатор документа ID string `msgpack:"_id"` // Уникальный идентификатор документа
fieldsPtr atomic.Value // *fieldVersionWrapper - lock-free хранилище полей с версией fieldsPtr atomic.Value // *fieldVersionWrapper - lock-free хранилище полей с версией
CreatedAt int64 `msgpack:"created_at"` // Время создания (Unix миллисекунды) CreatedAt int64 `msgpack:"created_at"` // Время создания (Unix миллисекунды)
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления
DeletedAt int64 `msgpack:"deleted_at"` // Время удаления (Unix миллисекунды, 0 = не удалён) DeletedAt int64 `msgpack:"deleted_at"` // Время удаления (Unix миллисекунды, 0 = не удалён)
Version uint64 `msgpack:"version"` // Версия документа (для оптимистичных блокировок) Version uint64 `msgpack:"version"` // Версия документа
Compressed bool `msgpack:"compressed"` // Флаг, сжат ли документ Compressed bool `msgpack:"compressed"` // Флаг, сжат ли документ
OriginalSize int64 `msgpack:"original_size"` // Оригинальный размер до сжатия OriginalSize int64 `msgpack:"original_size"` // Оригинальный размер до сжатия
} }
// Tuple представляет вложенный документ (аналог кортежа в реляционной СУБД) // Tuple представляет вложенный документ (аналог кортежа).
type Tuple struct { type Tuple struct {
Fields map[string]interface{} `msgpack:"fields"` Fields map[string]interface{} `msgpack:"fields"`
CreatedAt int64 `msgpack:"created_at"` // Время создания кортежа CreatedAt int64 `msgpack:"created_at"`
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления кортежа UpdatedAt int64 `msgpack:"updated_at"`
mu sync.RWMutex mu sync.RWMutex
} }
// Field представляет отдельное поле документа (аналог колонки) // Field представляет отдельное поле документа.
type Field struct { type Field struct {
Name string `msgpack:"name"` Name string `msgpack:"name"`
Type FieldType `msgpack:"type"` Type FieldType `msgpack:"type"`
Value interface{} `msgpack:"value"` Value interface{} `msgpack:"value"`
UpdatedAt int64 `msgpack:"updated_at"` // Время последнего обновления поля UpdatedAt int64 `msgpack:"updated_at"`
} }
// FieldType определяет тип поля документа // FieldType определяет тип поля документа.
type FieldType int type FieldType int
const ( const (
TypeString FieldType = iota TypeString FieldType = iota
TypeNumber TypeNumber
TypeBoolean TypeBoolean
TypeTuple // Вложенный документ TypeTuple
TypeArray TypeArray
TypeNull TypeNull
) )
// NewDocument создаёт новый документ с автоматической генерацией ID // NewDocument создаёт новый документ с автоматической генерацией ID.
func NewDocument() *Document { func NewDocument() *Document {
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
d := &Document{ d := &Document{
@@ -111,7 +134,7 @@ func NewDocument() *Document {
return d return d
} }
// NewDocumentWithID создаёт документ с указанным ID // NewDocumentWithID создаёт документ с указанным ID.
func NewDocumentWithID(id string) *Document { func NewDocumentWithID(id string) *Document {
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
d := &Document{ d := &Document{
@@ -130,7 +153,7 @@ func NewDocumentWithID(id string) *Document {
return d return d
} }
// loadFieldsWrapper загружает обёртку полей с версией (lock-free) // loadFieldsWrapper загружает обёртку полей с версией (lock-free).
func (d *Document) loadFieldsWrapper() *fieldVersionWrapper { func (d *Document) loadFieldsWrapper() *fieldVersionWrapper {
val := d.fieldsPtr.Load() val := d.fieldsPtr.Load()
if val == nil { if val == nil {
@@ -142,7 +165,8 @@ func (d *Document) loadFieldsWrapper() *fieldVersionWrapper {
return val.(*fieldVersionWrapper) return val.(*fieldVersionWrapper)
} }
// loadFields загружает карту полей (lock-free) // loadFields загружает карту полей (lock-free).
// ВНИМАНИЕ: возвращает ссылку на внутреннюю map. Не мутировать!
func (d *Document) loadFields() map[string]interface{} { func (d *Document) loadFields() map[string]interface{} {
wrapper := d.loadFieldsWrapper() wrapper := d.loadFieldsWrapper()
if wrapper == nil || wrapper.fields == nil { if wrapper == nil || wrapper.fields == nil {
@@ -151,7 +175,7 @@ func (d *Document) loadFields() map[string]interface{} {
return wrapper.fields return wrapper.fields
} }
// storeFields сохраняет карту полей (lock-free) // storeFields сохраняет карту полей (lock-free).
func (d *Document) storeFields(newMap map[string]interface{}) { func (d *Document) storeFields(newMap map[string]interface{}) {
oldWrapper := d.loadFieldsWrapper() oldWrapper := d.loadFieldsWrapper()
newVersion := uint64(0) newVersion := uint64(0)
@@ -164,14 +188,12 @@ func (d *Document) storeFields(newMap map[string]interface{}) {
}) })
} }
// casFields выполняет CAS операцию для полей с защитой от ABA (lock-free) // casFields выполняет CAS операцию для полей с защитой от ABA.
// ИСПРАВЛЕНО: Использует версионирование для защиты от ABA-проблемы
func (d *Document) casFields(oldWrapper, newWrapper *fieldVersionWrapper) bool { func (d *Document) casFields(oldWrapper, newWrapper *fieldVersionWrapper) bool {
if oldWrapper == nil || newWrapper == nil { if oldWrapper == nil || newWrapper == nil {
return false return false
} }
// Проверяем, что версия увеличилась (защита от ABA)
if newWrapper.version <= oldWrapper.version { if newWrapper.version <= oldWrapper.version {
newWrapper.version = oldWrapper.version + 1 newWrapper.version = oldWrapper.version + 1
} }
@@ -179,21 +201,17 @@ func (d *Document) casFields(oldWrapper, newWrapper *fieldVersionWrapper) bool {
return d.fieldsPtr.CompareAndSwap(oldWrapper, newWrapper) return d.fieldsPtr.CompareAndSwap(oldWrapper, newWrapper)
} }
// casFieldsWithBackoff выполняет CAS с backoff для предотвращения livelock // casFieldsWithBackoff выполняет CAS с backoff для предотвращения livelock.
// ИСПРАВЛЕНО: Добавлен экспоненциальный backoff для предотвращения livelock
// ИСПРАВЛЕНО: Исправлены типы int/int64 для устранения ошибок компилятора
func (d *Document) casFieldsWithBackoff(updateFunc func(map[string]interface{}) map[string]interface{}) bool { func (d *Document) casFieldsWithBackoff(updateFunc func(map[string]interface{}) map[string]interface{}) bool {
attempt := 0 attempt := 0
for { for {
oldWrapper := d.loadFieldsWrapper() oldWrapper := d.loadFieldsWrapper()
// Создаём копию полей для модификации
newFields := make(map[string]interface{}) newFields := make(map[string]interface{})
for k, v := range oldWrapper.fields { for k, v := range oldWrapper.fields {
newFields[k] = v newFields[k] = v
} }
// Применяем обновление
newFields = updateFunc(newFields) newFields = updateFunc(newFields)
newWrapper := &fieldVersionWrapper{ newWrapper := &fieldVersionWrapper{
@@ -205,29 +223,24 @@ func (d *Document) casFieldsWithBackoff(updateFunc func(map[string]interface{})
return true return true
} }
// Backoff при неудаче для предотвращения livelock
attempt++ attempt++
if attempt > maxCASRetriesBeforeBackoff { if attempt > maxCASRetriesBeforeBackoff {
// Экспоненциальный backoff с jitter
backoff := baseBackoffNanos * (1 << min(attempt-maxCASRetriesBeforeBackoff, 10)) backoff := baseBackoffNanos * (1 << min(attempt-maxCASRetriesBeforeBackoff, 10))
if backoff > maxBackoffNanos { if backoff > maxBackoffNanos {
backoff = maxBackoffNanos backoff = maxBackoffNanos
} }
// ИСПРАВЛЕНО: Приводим типы для совместимости с rand.Int63n (int64)
// Добавляем jitter для избежания синхронизации потоков
jitter := rand.Int63n(int64(backoff)/2 + 1) jitter := rand.Int63n(int64(backoff)/2 + 1)
totalBackoff := backoff + int(jitter) totalBackoff := backoff + int(jitter)
time.Sleep(time.Duration(totalBackoff)) time.Sleep(time.Duration(totalBackoff))
} }
// Ограничиваем максимальное количество попыток
if attempt > 1000 { if attempt > 1000 {
return false return false
} }
} }
} }
// min возвращает минимальное из двух чисел // min возвращает минимальное из двух чисел.
func min(a, b int) int { func min(a, b int) int {
if a < b { if a < b {
return a return a
@@ -235,23 +248,23 @@ func min(a, b int) int {
return b return b
} }
// SetField устанавливает значение поля документа (lock-free) // SetField устанавливает значение поля документа (lock-free).
// ИСПРАВЛЕНО: Использует casFieldsWithBackoff для защиты от ABA и livelock //
// ИЗМЕНЕНО: убран вызов AuditFieldOperation (был неинформативным).
func (d *Document) SetField(name string, value interface{}) { func (d *Document) SetField(name string, value interface{}) {
d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} { d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} {
fields[name] = value fields[name] = value
return fields return fields
}) })
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli() d.UpdatedAt = time.Now().UnixMilli()
d.Version++ d.Version++
d.Compressed = false d.Compressed = false
d.mu.Unlock()
// Аудит изменения поля
AuditFieldOperation("UPDATE", "", "", d.ID, name, value)
} }
// GetField возвращает значение поля документа (lock-free) // GetField возвращает значение поля документа (lock-free).
func (d *Document) GetField(name string) (interface{}, error) { func (d *Document) GetField(name string) (interface{}, error) {
fields := d.loadFields() fields := d.loadFields()
if val, ok := fields[name]; ok { if val, ok := fields[name]; ok {
@@ -260,30 +273,30 @@ func (d *Document) GetField(name string) (interface{}, error) {
return nil, fmt.Errorf("field not found: %s", name) return nil, fmt.Errorf("field not found: %s", name)
} }
// DeleteField удаляет поле из документа (lock-free) // DeleteField удаляет поле из документа (lock-free).
// ИСПРАВЛЕНО: Использует casFieldsWithBackoff для защиты от ABA и livelock //
// ИЗМЕНЕНО: убран вызов AuditFieldOperation.
func (d *Document) DeleteField(name string) { func (d *Document) DeleteField(name string) {
d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} { d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} {
delete(fields, name) delete(fields, name)
return fields return fields
}) })
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli() d.UpdatedAt = time.Now().UnixMilli()
d.Version++ d.Version++
d.Compressed = false d.Compressed = false
d.mu.Unlock()
// Аудит удаления поля
AuditFieldOperation("DELETE", "", "", d.ID, name, nil)
} }
// HasField проверяет наличие поля в документе (lock-free) // HasField проверяет наличие поля в документе (lock-free).
func (d *Document) HasField(name string) bool { func (d *Document) HasField(name string) bool {
fields := d.loadFields() fields := d.loadFields()
_, ok := fields[name] _, ok := fields[name]
return ok return ok
} }
// GetFields возвращает копию всех полей документа (lock-free) // GetFields возвращает копию всех полей документа (lock-free).
func (d *Document) GetFields() map[string]interface{} { func (d *Document) GetFields() map[string]interface{} {
fields := d.loadFields() fields := d.loadFields()
copy := make(map[string]interface{}) copy := make(map[string]interface{})
@@ -293,33 +306,32 @@ func (d *Document) GetFields() map[string]interface{} {
return copy return copy
} }
// ToMap возвращает полное представление документа в виде map // ToMap возвращает полное представление документа в виде map.
// Включает метаданные (_id, _created_at, _updated_at, _deleted_at, _version) и все поля
func (d *Document) ToMap() map[string]interface{} { func (d *Document) ToMap() map[string]interface{} {
fields := d.GetFields() fields := d.GetFields()
result := make(map[string]interface{}) result := make(map[string]interface{})
// Добавляем все пользовательские поля
for k, v := range fields { for k, v := range fields {
result[k] = v result[k] = v
} }
// Добавляем метаданные d.mu.RLock()
result["_id"] = d.ID result["_id"] = d.ID
result["_created_at"] = d.CreatedAt result["_created_at"] = d.CreatedAt
result["_updated_at"] = d.UpdatedAt result["_updated_at"] = d.UpdatedAt
result["_deleted_at"] = d.DeletedAt result["_deleted_at"] = d.DeletedAt
result["_version"] = d.Version result["_version"] = d.Version
d.mu.RUnlock()
return result return result
} }
// SetTuple устанавливает вложенный документ (кортеж) в поле // SetTuple устанавливает вложенный документ (кортеж) в поле.
func (d *Document) SetTuple(fieldName string, tuple *Tuple) { func (d *Document) SetTuple(fieldName string, tuple *Tuple) {
d.SetField(fieldName, tuple) d.SetField(fieldName, tuple)
} }
// GetTuple возвращает вложенный документ из поля // GetTuple возвращает вложенный документ из поля.
func (d *Document) GetTuple(fieldName string) (*Tuple, error) { func (d *Document) GetTuple(fieldName string) (*Tuple, error) {
val, err := d.GetField(fieldName) val, err := d.GetField(fieldName)
if err != nil { if err != nil {
@@ -332,10 +344,11 @@ func (d *Document) GetTuple(fieldName string) (*Tuple, error) {
return nil, fmt.Errorf("field %s is not a tuple", fieldName) return nil, fmt.Errorf("field %s is not a tuple", fieldName)
} }
// Serialize сериализует документ в MessagePack с поддержкой сжатия // Serialize сериализует документ в MessagePack с поддержкой сжатия.
func (d *Document) Serialize() ([]byte, error) { func (d *Document) Serialize() ([]byte, error) {
// Создаём копию для сериализации
fields := d.GetFields() fields := d.GetFields()
d.mu.RLock()
docCopy := &Document{ docCopy := &Document{
ID: d.ID, ID: d.ID,
CreatedAt: d.CreatedAt, CreatedAt: d.CreatedAt,
@@ -345,6 +358,8 @@ func (d *Document) Serialize() ([]byte, error) {
Compressed: d.Compressed, Compressed: d.Compressed,
OriginalSize: d.OriginalSize, OriginalSize: d.OriginalSize,
} }
d.mu.RUnlock()
docCopy.storeFields(fields) docCopy.storeFields(fields)
data, err := serializer.Marshal(docCopy) data, err := serializer.Marshal(docCopy)
@@ -355,14 +370,13 @@ func (d *Document) Serialize() ([]byte, error) {
return data, nil return data, nil
} }
// SerializeCompressed сериализует и сжимает документ // SerializeCompressed сериализует и сжимает документ.
func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) { func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([]byte, error) {
data, err := d.Serialize() data, err := d.Serialize()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Проверяем, нужно ли сжимать
if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize { if compressionConfig != nil && compressionConfig.Enabled && len(data) >= compressionConfig.MinSize {
compressed, err := compression.Compress(data, compressionConfig) compressed, err := compression.Compress(data, compressionConfig)
if err != nil { if err != nil {
@@ -374,45 +388,52 @@ func (d *Document) SerializeCompressed(compressionConfig *compression.Config) ([
return data, nil return data, nil
} }
// Deserialize десериализует документ из MessagePack (автоматически определяет сжатие) // Deserialize десериализует документ из MessagePack.
func (d *Document) Deserialize(data []byte) error { func (d *Document) Deserialize(data []byte) error {
// Пытаемся определить, сжаты ли данные
decompressed, err := compression.DecompressAuto(data) decompressed, err := compression.DecompressAuto(data)
if err == nil && len(decompressed) < len(data) { if err == nil && len(decompressed) < len(data) {
var doc Document var doc Document
if err := serializer.Unmarshal(decompressed, &doc); err != nil { if err := serializer.Unmarshal(decompressed, &doc); err != nil {
return err return err
} }
d.mu.Lock()
d.ID = doc.ID d.ID = doc.ID
d.storeFields(doc.loadFields())
d.CreatedAt = doc.CreatedAt d.CreatedAt = doc.CreatedAt
d.UpdatedAt = doc.UpdatedAt d.UpdatedAt = doc.UpdatedAt
d.DeletedAt = doc.DeletedAt d.DeletedAt = doc.DeletedAt
d.Version = doc.Version d.Version = doc.Version
d.Compressed = true d.Compressed = true
d.OriginalSize = int64(len(decompressed)) d.OriginalSize = int64(len(decompressed))
d.mu.Unlock()
d.storeFields(doc.loadFields())
} else { } else {
var doc Document var doc Document
if err := serializer.Unmarshal(data, &doc); err != nil { if err := serializer.Unmarshal(data, &doc); err != nil {
return err return err
} }
d.mu.Lock()
d.ID = doc.ID d.ID = doc.ID
d.storeFields(doc.loadFields())
d.CreatedAt = doc.CreatedAt d.CreatedAt = doc.CreatedAt
d.UpdatedAt = doc.UpdatedAt d.UpdatedAt = doc.UpdatedAt
d.DeletedAt = doc.DeletedAt d.DeletedAt = doc.DeletedAt
d.Version = doc.Version d.Version = doc.Version
d.Compressed = false d.Compressed = false
d.OriginalSize = 0 d.OriginalSize = 0
d.mu.Unlock()
d.storeFields(doc.loadFields())
} }
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli() d.UpdatedAt = time.Now().UnixMilli()
d.mu.Unlock()
return nil return nil
} }
// Clone создаёт глубокую копию документа (lock-free) // Clone создаёт глубокую копию документа (lock-free).
func (d *Document) Clone() *Document { func (d *Document) Clone() *Document {
fields := d.GetFields() fields := d.GetFields()
d.mu.RLock()
clone := &Document{ clone := &Document{
ID: d.ID, ID: d.ID,
CreatedAt: d.CreatedAt, CreatedAt: d.CreatedAt,
@@ -422,8 +443,8 @@ func (d *Document) Clone() *Document {
Compressed: d.Compressed, Compressed: d.Compressed,
OriginalSize: d.OriginalSize, OriginalSize: d.OriginalSize,
} }
d.mu.RUnlock()
// Глубокое копирование полей
copiedFields := make(map[string]interface{}) copiedFields := make(map[string]interface{})
for k, v := range fields { for k, v := range fields {
copiedFields[k] = deepCopyValue(v) copiedFields[k] = deepCopyValue(v)
@@ -433,8 +454,7 @@ func (d *Document) Clone() *Document {
return clone return clone
} }
// Update применяет обновление к документу (атомарно, lock-free) // Update применяет обновление к документу (атомарно, lock-free).
// ИСПРАВЛЕНО: Использует casFieldsWithBackoff для защиты от ABA и livelock
func (d *Document) Update(updates map[string]interface{}) error { func (d *Document) Update(updates map[string]interface{}) error {
success := d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} { success := d.casFieldsWithBackoff(func(fields map[string]interface{}) map[string]interface{} {
for k, v := range updates { for k, v := range updates {
@@ -444,54 +464,80 @@ func (d *Document) Update(updates map[string]interface{}) error {
}) })
if success { if success {
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli() d.UpdatedAt = time.Now().UnixMilli()
d.Version++ d.Version++
d.Compressed = false d.Compressed = false
d.mu.Unlock()
return nil return nil
} }
return fmt.Errorf("failed to update document after multiple attempts (possible high contention)") return fmt.Errorf("failed to update document after multiple attempts (possible high contention)")
} }
// SoftDelete мягко удаляет документ (устанавливает метку времени удаления) // SoftDelete мягко удаляет документ.
func (d *Document) SoftDelete() { func (d *Document) SoftDelete() {
d.mu.Lock()
d.DeletedAt = time.Now().UnixMilli() d.DeletedAt = time.Now().UnixMilli()
d.UpdatedAt = d.DeletedAt d.UpdatedAt = d.DeletedAt
d.Version++ d.Version++
d.mu.Unlock()
} }
// IsDeleted проверяет, удалён ли документ (мягкое удаление) // IsDeleted проверяет, удалён ли документ (мягкое удаление).
func (d *Document) IsDeleted() bool { func (d *Document) IsDeleted() bool {
d.mu.RLock()
defer d.mu.RUnlock()
return d.DeletedAt > 0 return d.DeletedAt > 0
} }
// Restore восстанавливает мягко удалённый документ // Restore восстанавливает мягко удалённый документ.
func (d *Document) Restore() { func (d *Document) Restore() {
d.mu.Lock()
d.DeletedAt = 0 d.DeletedAt = 0
d.UpdatedAt = time.Now().UnixMilli() d.UpdatedAt = time.Now().UnixMilli()
d.Version++ d.Version++
d.mu.Unlock()
} }
// GetDeletedAtStr возвращает человекочитаемую строку времени удаления // GetDeletedAtStr возвращает человекочитаемую строку времени удаления.
//
// ИЗМЕНЕНО (2026-10-03): формат изменён на "02-012006 15:04:05.000".
func (d *Document) GetDeletedAtStr() string { func (d *Document) GetDeletedAtStr() string {
if d.DeletedAt == 0 { d.mu.RLock()
deletedAt := d.DeletedAt
d.mu.RUnlock()
if deletedAt == 0 {
return "" return ""
} }
return time.UnixMilli(d.DeletedAt).Format("2006-01-02 15:04:05.000") return time.UnixMilli(deletedAt).Format("02-012006 15:04:05.000")
} }
// GetCreatedAtStr возвращает человекочитаемую строку времени создания // GetCreatedAtStr возвращает человекочитаемую строку времени создания.
//
// ИЗМЕНЕНО (2026-10-03): формат изменён на "02-012006 15:04:05.000".
func (d *Document) GetCreatedAtStr() string { func (d *Document) GetCreatedAtStr() string {
return time.UnixMilli(d.CreatedAt).Format("2006-01-02 15:04:05.000") d.mu.RLock()
createdAt := d.CreatedAt
d.mu.RUnlock()
return time.UnixMilli(createdAt).Format("02-012006 15:04:05.000")
} }
// GetUpdatedAtStr возвращает человекочитаемую строку времени обновления // GetUpdatedAtStr возвращает человекочитаемую строку времени обновления.
//
// ИЗМЕНЕНО (2026-10-03): формат изменён на "02-012006 15:04:05.000".
func (d *Document) GetUpdatedAtStr() string { func (d *Document) GetUpdatedAtStr() string {
return time.UnixMilli(d.UpdatedAt).Format("2006-01-02 15:04:05.000") d.mu.RLock()
updatedAt := d.UpdatedAt
d.mu.RUnlock()
return time.UnixMilli(updatedAt).Format("02-012006 15:04:05.000")
} }
// Compress сжимает документ в памяти // Compress сжимает документ в памяти.
func (d *Document) Compress(config *compression.Config) error { func (d *Document) Compress(config *compression.Config) error {
d.mu.Lock()
defer d.mu.Unlock()
if d.Compressed { if d.Compressed {
return nil return nil
} }
@@ -508,8 +554,11 @@ func (d *Document) Compress(config *compression.Config) error {
return nil return nil
} }
// Decompress распаковывает документ в памяти // Decompress распаковывает документ в памяти.
func (d *Document) Decompress() error { func (d *Document) Decompress() error {
d.mu.Lock()
defer d.mu.Unlock()
if !d.Compressed { if !d.Compressed {
return nil return nil
} }
@@ -520,19 +569,26 @@ func (d *Document) Decompress() error {
return nil return nil
} }
// GetCompressionRatio возвращает коэффициент сжатия // GetCompressionRatio возвращает коэффициент сжатия.
func (d *Document) GetCompressionRatio() float64 { func (d *Document) GetCompressionRatio() float64 {
if !d.Compressed || d.OriginalSize == 0 { d.mu.RLock()
compressed := d.Compressed
originalSize := d.OriginalSize
d.mu.RUnlock()
if !compressed || originalSize == 0 {
return 1.0 return 1.0
} }
fields := d.loadFields() fields := d.loadFields()
currentSize := len(fields) currentSize := len(fields)
return float64(currentSize) / float64(d.OriginalSize) return float64(currentSize) / float64(originalSize)
} }
// GetMetadata возвращает метаданные документа (временные метки) // GetMetadata возвращает метаданные документа (временные метки).
func (d *Document) GetMetadata() map[string]int64 { func (d *Document) GetMetadata() map[string]int64 {
d.mu.RLock()
defer d.mu.RUnlock()
return map[string]int64{ return map[string]int64{
"created_at": d.CreatedAt, "created_at": d.CreatedAt,
"updated_at": d.UpdatedAt, "updated_at": d.UpdatedAt,
@@ -541,7 +597,7 @@ func (d *Document) GetMetadata() map[string]int64 {
} }
} }
// deepCopyValue выполняет глубокое копирование значения // deepCopyValue выполняет глубокое копирование значения.
func deepCopyValue(val interface{}) interface{} { func deepCopyValue(val interface{}) interface{} {
switch v := val.(type) { switch v := val.(type) {
case *Tuple: case *Tuple:
@@ -563,7 +619,7 @@ func deepCopyValue(val interface{}) interface{} {
} }
} }
// NewTuple создаёт новый вложенный документ (кортеж) // NewTuple создаёт новый вложенный документ (кортеж).
func NewTuple() *Tuple { func NewTuple() *Tuple {
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
return &Tuple{ return &Tuple{
@@ -573,7 +629,7 @@ func NewTuple() *Tuple {
} }
} }
// Set устанавливает поле во вложенном документе // Set устанавливает поле во вложенном документе.
func (t *Tuple) Set(name string, value interface{}) { func (t *Tuple) Set(name string, value interface{}) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
@@ -581,7 +637,7 @@ func (t *Tuple) Set(name string, value interface{}) {
t.UpdatedAt = time.Now().UnixMilli() t.UpdatedAt = time.Now().UnixMilli()
} }
// Get возвращает поле из вложенного документа // Get возвращает поле из вложенного документа.
func (t *Tuple) Get(name string) (interface{}, error) { func (t *Tuple) Get(name string) (interface{}, error) {
t.mu.RLock() t.mu.RLock()
defer t.mu.RUnlock() defer t.mu.RUnlock()
@@ -592,7 +648,7 @@ func (t *Tuple) Get(name string) (interface{}, error) {
return nil, fmt.Errorf("tuple field not found: %s", name) return nil, fmt.Errorf("tuple field not found: %s", name)
} }
// Clone создаёт копию кортежа // Clone создаёт копию кортежа.
func (t *Tuple) Clone() *Tuple { func (t *Tuple) Clone() *Tuple {
t.mu.RLock() t.mu.RLock()
defer t.mu.RUnlock() defer t.mu.RUnlock()
@@ -606,7 +662,7 @@ func (t *Tuple) Clone() *Tuple {
return clone return clone
} }
// ToMap конвертирует кортеж в map // ToMap конвертирует кортеж в map.
func (t *Tuple) ToMap() map[string]interface{} { func (t *Tuple) ToMap() map[string]interface{} {
t.mu.RLock() t.mu.RLock()
defer t.mu.RUnlock() defer t.mu.RUnlock()
@@ -618,7 +674,7 @@ func (t *Tuple) ToMap() map[string]interface{} {
return copy return copy
} }
// GetTupleMetadata возвращает метаданные кортежа // GetTupleMetadata возвращает метаданные кортежа.
func (t *Tuple) GetTupleMetadata() map[string]int64 { func (t *Tuple) GetTupleMetadata() map[string]int64 {
t.mu.RLock() t.mu.RLock()
defer t.mu.RUnlock() defer t.mu.RUnlock()
@@ -629,7 +685,7 @@ func (t *Tuple) GetTupleMetadata() map[string]int64 {
} }
} }
// GetNestedField получает значение по точечному пути (например, "user.address.city") // GetNestedField получает значение по точечному пути (например, "user.address.city").
func (d *Document) GetNestedField(path string) (interface{}, error) { func (d *Document) GetNestedField(path string) (interface{}, error) {
parts := strings.Split(path, ".") parts := strings.Split(path, ".")
if len(parts) == 0 { if len(parts) == 0 {
@@ -665,7 +721,7 @@ func (d *Document) GetNestedField(path string) (interface{}, error) {
return current, nil return current, nil
} }
// SetNestedField устанавливает значение по точечному пути // SetNestedField устанавливает значение по точечному пути.
func (d *Document) SetNestedField(path string, value interface{}) error { func (d *Document) SetNestedField(path string, value interface{}) error {
parts := strings.Split(path, ".") parts := strings.Split(path, ".")
if len(parts) == 0 { if len(parts) == 0 {
@@ -677,10 +733,6 @@ func (d *Document) SetNestedField(path string, value interface{}) error {
return nil return nil
} }
// Для простоты реализации используем подход с чтением-модификацией-записью
// В production коде потребуется более сложная lock-free структура
// Сначала проверяем путь
var current interface{} = d var current interface{} = d
for i := 0; i < len(parts)-1; i++ { for i := 0; i < len(parts)-1; i++ {
part := parts[i] part := parts[i]
@@ -726,7 +778,9 @@ func (d *Document) SetNestedField(path string, value interface{}) error {
return fmt.Errorf("cannot set field on non-document value") return fmt.Errorf("cannot set field on non-document value")
} }
d.mu.Lock()
d.UpdatedAt = time.Now().UnixMilli() d.UpdatedAt = time.Now().UnixMilli()
d.Compressed = false d.Compressed = false
d.mu.Unlock()
return nil return nil
} }
+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(),
} }
+36 -31
View File
@@ -11,7 +11,18 @@
// Файл: 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
@@ -78,7 +89,7 @@ type LimitMetrics struct {
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 содержит конфигурацию ограничений
@@ -180,34 +191,28 @@ func (rlm *RuntimeLimitsManager) triggerEviction() {
} }
} }
// isDocInActiveTransaction проверяет, участвует ли документ в активной транзакции // isDocInActiveBatch проверяет, участвует ли документ в активной
// Защита от удаления данных активных транзакций // batch-операции.
func (rlm *RuntimeLimitsManager) isDocInActiveTransaction(docID, dbName, collName string) bool { //
if globalTxManager == nil { // ИСПРАВЛЕНО (2026-10): ранее метод назывался isDocInActiveTransaction
// и обращался к globalTxManager и Transaction, которые были удалены при
// переходе на batch-операции. Теперь проверяем реестр активных batch'ей
// в globalBatchManager (см. transactions.go).
//
// Защита сохраняется: batch живёт коротко (создаётся и коммитится в
// рамках одной функции), но между AddUpdate и Commit документ ещё не
// изменён, и eviction мог бы его удалить.
func (rlm *RuntimeLimitsManager) isDocInActiveBatch(docID, dbName, collName string) bool {
bm := GetBatchManager()
if bm == nil {
return false return false
} }
return bm.IsDocInActiveBatch(docID, dbName, collName)
inTx := false
globalTxManager.activeTransactions.Range(func(key, value interface{}) bool {
tx := value.(*Transaction)
tx.mu.RLock()
for _, op := range tx.Operations {
if op.DocumentID == docID && op.Database == dbName && op.Collection == collName {
inTx = true
break
}
}
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
@@ -226,8 +231,8 @@ func (rlm *RuntimeLimitsManager) EvictFromCollection(coll *Collection, targetByt
} }
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
} }
@@ -252,8 +257,8 @@ func (rlm *RuntimeLimitsManager) EvictFromCollection(coll *Collection, targetByt
} }
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
} }
@@ -286,7 +291,7 @@ func (rlm *RuntimeLimitsManager) EvictFromCollection(coll *Collection, targetByt
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))
} }
+241 -196
View File
@@ -12,32 +12,27 @@
// Назначение: Lock-free skip list (список с пропусками) для инвертированного // Назначение: Lock-free skip list (список с пропусками) для инвертированного
// индекса коллекции. Заменяет sync.Map в Index.data. // индекса коллекции. Заменяет sync.Map в Index.data.
// //
// ОСОБЕННОСТИ РЕАЛИЗАЦИИ: // ИСПРАВЛЕНО (2026-10):
// - Полностью lock-free для операций чтения и записи. // - Баг #6: рассинхронизация size при воскрешении узла (Insert после Delete).
// - Использует atomic.Value и sync/atomic для совместимости с Go < 1.19 // Теперь проверяется wasDeleted и size увеличивается корректно.
// (актуально для сборки на OpenIndiana/illumos, где часто стоит старый Go). // - Баг #7: дубликат на верхних уровнях при конкурентной вставке одного
// - Не использует unsafe и generics — одинаково работает на Linux и illumos. // ключа с разными newLevel. Решение: двухфазная вставка — сначала
// - Удаление через mark-and-sweep: узел помечается как удалённый (tombstone), // уровень 0 (гарантированно единственный узел с таким ключом), затем
// физическое удаление происходит лениво при обходе (Range). // верхние уровни (best-effort, дубликаты безопасны).
// - Хранение: ключ — interface{} (значение индексированного поля),
// значение — string (ID документа).
// //
// АЛГОРИТМ (упрощённый lock-free skip list, основанный на идеях // КЛЮЧЕВОЙ ИНВАРИАНТ:
// William Pugh "Skip Lists: A Probabilistic Alternative to Balanced Trees" // - На уровне 0 не может быть двух узлов с одинаковым ключом.
// и подходах из работ Herlihy & Shavit "The Art of Multiprocessor Programming"): // - На уровнях > 0 дубликаты допустимы (они лишь замедляют поиск, но не
// ломают корректность, потому что Search всегда спускается до уровня 0
// и там находит единственный узел с нужным ключом).
// //
// - Каждый узел имеет массив forward-указателей (уровни). // ДРУГИЕ ИСПРАВЛЕНИЯ (2026):
// - Вставка: находим позицию на каждом уровне, атомарно "вклеиваем" // - Устранён data race при обновлении value существующего узла:
// узел через Compare-And-Swap (CAS). // value хранится в atomic.Value.
// - Поиск: спускаемся по уровням сверху вниз. // - Исправлен обход Range: спуск на базовый уровень теперь корректный.
// - Удаление: помечаем узел как удалённый (tombstone), обходчик // - Исправлена вставка на уровнях выше текущего sl.level.
// (Range) физически удаляет такие узлы лениво. // - Идемпотентный Delete.
// // - RangeWithKeyPrefix корректно останавливается при выходе за префикс.
// ВАЖНО: это не строго "wait-free" реализация в академическом смысле —
// операции вставки используют CAS-цикл (lock-free, но с возможностью
// повторных попыток). Однако, в отличие от sync.Map, здесь нет
// глобального мьютекса и нет внутренних блокировок на уровне шардов,
// что даёт лучшую масштабируемость при конкурентных чтениях/записях.
package storage package storage
@@ -55,8 +50,6 @@ import (
const ( const (
// Максимальное количество уровней в skip list. // Максимальное количество уровней в skip list.
// 32 уровня достаточно для хранения до 4 миллиардов элементов
// с вероятностью 1/2 на каждом уровне.
maxSkipListLevel = 32 maxSkipListLevel = 32
// Вероятность "подъёма" на следующий уровень. // Вероятность "подъёма" на следующий уровень.
@@ -73,20 +66,39 @@ const (
// skipListNode представляет узел skip list. // skipListNode представляет узел skip list.
type skipListNode struct { type skipListNode struct {
// key — значение индексированного поля (может быть любым comparable value). // key — значение индексированного поля.
key interface{} key interface{}
// value — ID документа (строка). // value — ID документа. Хранится в atomic.Value для защиты от
value string // data race при обновлении существующего ключа.
// atomic.Value не может хранить nil строку напрямую, поэтому
// используется обёртка *string.
value atomic.Value // *string
// deleted — tombstone-флаг. Если true, узел логически удалён // deleted — tombstone-флаг.
// и должен быть пропущен при обходе.
deleted atomic.Bool deleted atomic.Bool
// forward — массив атомарных указателей на следующий узел // forward — массив атомарных указателей на следующий узел
// на каждом уровне. forward[0] — базовый уровень. // на каждом уровне.
// Используем atomic.Value для совместимости с Go < 1.19. forward []atomic.Value // *skipListNode
forward []atomic.Value // хранит *skipListNode }
// loadValue атомарно загружает value.
func (n *skipListNode) loadValue() string {
val := n.value.Load()
if val == nil {
return ""
}
ptr, ok := val.(*string)
if !ok || ptr == nil {
return ""
}
return *ptr
}
// storeValue атомарно сохраняет value.
func (n *skipListNode) storeValue(v string) {
n.value.Store(&v)
} }
// loadForward атомарно загружает указатель на следующий узел на уровне. // loadForward атомарно загружает указатель на следующий узел на уровне.
@@ -98,7 +110,11 @@ func (n *skipListNode) loadForward(level int) *skipListNode {
if val == nil { if val == nil {
return nil return nil
} }
return val.(*skipListNode) ptr, ok := val.(*skipListNode)
if !ok {
return nil
}
return ptr
} }
// storeForward атомарно сохраняет указатель на следующий узел на уровне. // storeForward атомарно сохраняет указатель на следующий узел на уровне.
@@ -106,16 +122,10 @@ func (n *skipListNode) storeForward(level int, next *skipListNode) {
if level < 0 || level >= len(n.forward) { if level < 0 || level >= len(n.forward) {
return return
} }
// atomic.Value не может хранить nil — используем типизированный указатель.
if next == nil {
n.forward[level].Store((*skipListNode)(nil))
} else {
n.forward[level].Store(next) n.forward[level].Store(next)
}
} }
// casForward атомарно заменяет указатель, если он равен old. // casForward атомарно заменяет указатель, если он равен old.
// Возвращает true, если замена произошла.
func (n *skipListNode) casForward(level int, old, new *skipListNode) bool { func (n *skipListNode) casForward(level int, old, new *skipListNode) bool {
if level < 0 || level >= len(n.forward) { if level < 0 || level >= len(n.forward) {
return false return false
@@ -123,13 +133,7 @@ func (n *skipListNode) casForward(level int, old, new *skipListNode) bool {
return n.forward[level].CompareAndSwap(old, new) return n.forward[level].CompareAndSwap(old, new)
} }
// SkipList — lock-free список с пропусками для инвертированного индекса. // SkipList — lock-free список с пропусками.
//
// Поля:
// - head: фиктивный головной узел, key == nil.
// - level: текущий максимальный уровень (атомарный).
// - size: приблизительное количество элементов (атомарный счётчик).
// - rngMu: защита rand.Rand (используется только при вставке).
type SkipList struct { type SkipList struct {
head *skipListNode head *skipListNode
level atomic.Int32 level atomic.Int32
@@ -143,14 +147,16 @@ type SkipList struct {
func NewSkipList() *SkipList { func NewSkipList() *SkipList {
head := &skipListNode{ head := &skipListNode{
key: nil, key: nil,
value: "",
forward: make([]atomic.Value, maxSkipListLevel), forward: make([]atomic.Value, maxSkipListLevel),
} }
// Инициализируем forward-указатели nil-ами (atomic.Value требует // Инициализируем forward-указатели типизированным nil.
// типизированного значения, чтобы Load не падал).
for i := 0; i < maxSkipListLevel; i++ { for i := 0; i < maxSkipListLevel; i++ {
head.forward[i].Store((*skipListNode)(nil)) head.forward[i].Store((*skipListNode)(nil))
} }
// Инициализируем value head-а, чтобы atomic.Value имел
// установленный concrete type.
emptyStr := ""
head.value.Store(&emptyStr)
sl := &SkipList{ sl := &SkipList{
head: head, head: head,
@@ -162,7 +168,6 @@ func NewSkipList() *SkipList {
} }
// randomLevel генерирует случайный уровень для нового узла. // randomLevel генерирует случайный уровень для нового узла.
// Использует геометрическое распределение с p = 0.5.
func (sl *SkipList) randomLevel() int { func (sl *SkipList) randomLevel() int {
sl.rngMu.Lock() sl.rngMu.Lock()
defer sl.rngMu.Unlock() defer sl.rngMu.Unlock()
@@ -175,19 +180,93 @@ func (sl *SkipList) randomLevel() int {
} }
// ============================================================================= // =============================================================================
// ВСТАВКА // ПОИСК ПРЕДШЕСТВЕННИКОВ (вспомогательный метод)
// =============================================================================
// findPredecessors заполняет update[] указателями на узлы, после которых
// нужно вставлять/искать узел с заданным ключом на каждом уровне.
//
// Возвращает:
// - update[] — массив предшественников (длина maxSkipListLevel)
// - existing — узел на уровне 0 с ключом == key, либо nil
//
// Этот метод используется и в Insert, и в Delete.
func (sl *SkipList) findPredecessors(key interface{}) ([]*skipListNode, *skipListNode) {
update := make([]*skipListNode, maxSkipListLevel)
currentLevel := int(sl.level.Load())
// Спускаемся с верхнего уровня до базового.
x := sl.head
for i := currentLevel; i >= 0; i-- {
for {
next := x.loadForward(i)
if next == nil {
break
}
if next.deleted.Load() {
// Пытаемся физически вытолкнуть tombstone.
if x.casForward(i, next, next.loadForward(i)) {
continue
}
continue
}
if compareSkipListKeys(next.key, key) < 0 {
x = next
continue
}
break
}
update[i] = x
}
// Для уровней выше currentLevel предшественник — head.
// (Это может понадобиться, если новый уровень больше текущего.)
for i := currentLevel + 1; i < maxSkipListLevel; i++ {
update[i] = sl.head
}
// Ищем существующий узел на уровне 0.
existing := update[0].loadForward(0)
for existing != nil && compareSkipListKeys(existing.key, key) < 0 {
existing = existing.loadForward(0)
}
if existing != nil && compareSkipListKeys(existing.key, key) != 0 {
existing = nil
}
return update, existing
}
// =============================================================================
// ВСТАВКА (ИСПРАВЛЕНО: баги #6 и #7)
// ============================================================================= // =============================================================================
// Insert добавляет пару (key, value) в skip list. // Insert добавляет пару (key, value) в skip list.
// Если ключ уже существует, значение перезаписывается. // Если ключ уже существует, значение перезаписывается.
// //
// Реализация lock-free: используется CAS-цикл на каждом уровне. // РЕАЛИЗАЦИЯ (двухфазная вставка):
// При неудаче CAS — повторная попытка. //
// Фаза 1: вставка на уровень 0.
// - Если ключ уже есть на уровне 0 — обновляем value (с корректной
// обработкой wasDeleted для size).
// - Иначе — CAS-вставка нового узла на уровень 0. Если CAS провалился
// из-за конкурента — retry.
// - После успешной вставки на уровень 0 ключ ГАРАНТИРОВАННО есть в списке
// (в единственном экземпляре на уровне 0).
//
// Фаза 2: вставка на верхние уровни (best-effort).
// - Для каждого уровня i от 1 до newLevel пытаемся CAS-вставить узел.
// - Если CAS провалился — не страшно: узел уже есть на уровне 0,
// а верхние уровни — только ускорители поиска. Дубликаты на верхних
// уровнях безопасны, потому что Search всегда спускается до уровня 0.
func (sl *SkipList) Insert(key interface{}, value string) { func (sl *SkipList) Insert(key interface{}, value string) {
if key == nil { if key == nil {
return return
} }
// Внешний цикл — retry при провале CAS на уровне 0.
for attempt := 0; attempt < 1000; attempt++ {
newLevel := sl.randomLevel() newLevel := sl.randomLevel()
// Поднимаем максимальный уровень, если нужно. // Поднимаем максимальный уровень, если нужно.
@@ -201,82 +280,97 @@ func (sl *SkipList) Insert(key interface{}, value string) {
} }
} }
// Находим предшественников и существующий узел.
update, existing := sl.findPredecessors(key)
// Случай 1: ключ уже есть на уровне 0.
// Обновляем value и, если узел был tombstone, воскрешаем его.
if existing != nil {
wasDeleted := existing.deleted.Load()
existing.storeValue(value)
existing.deleted.Store(false)
// ИСПРАВЛЕНО (баг #6): если узел был tombstone,
// увеличиваем size, потому что Delete его уже уменьшил.
if wasDeleted {
sl.size.Add(1)
}
return
}
// Случай 2: ключа нет — создаём новый узел и вставляем
// сначала на уровень 0.
newNode := &skipListNode{ newNode := &skipListNode{
key: key, key: key,
value: value,
forward: make([]atomic.Value, newLevel+1), forward: make([]atomic.Value, newLevel+1),
} }
for i := 0; i <= newLevel; i++ { for i := 0; i <= newLevel; i++ {
newNode.forward[i].Store((*skipListNode)(nil)) newNode.forward[i].Store((*skipListNode)(nil))
} }
newNode.storeValue(value)
// update[i] — узел, после которого нужно вставить новый // Пытаемся вставить на уровень 0.
// на уровне i. next0 := update[0].loadForward(0)
update := make([]*skipListNode, maxSkipListLevel) // Защита от ситуации, когда между findPredecessors и сейчас
// кто-то вставил узел с таким же ключом.
retry: for next0 != nil && compareSkipListKeys(next0.key, key) < 0 {
// Спускаемся с верхнего уровня до базового, запоминая предшественников. next0 = next0.loadForward(0)
x := sl.head
for i := int(sl.level.Load()); i >= 0; i-- {
for {
next := x.loadForward(i)
if next == nil {
break
}
if next.deleted.Load() {
// Пропускаем tombstone-узлы при поиске предшественника.
// Пытаемся физически "вытолкнуть" удалённый узел.
if x.casForward(i, next, next.loadForward(i)) {
continue
}
// CAS не удался — перечитываем next.
continue
}
if compareSkipListKeys(next.key, key) < 0 {
x = next
continue
}
break
}
update[i] = x
} }
// Теперь update[0].forward[0] — либо nil, либо узел с key >= newKey. // Если между findPredecessors и сейчас появился узел с нашим
existing := update[0].loadForward(0) // ключом — обновляем его value и выходим.
for existing != nil && compareSkipListKeys(existing.key, key) < 0 { if next0 != nil && compareSkipListKeys(next0.key, key) == 0 {
existing = existing.loadForward(0) wasDeleted := next0.deleted.Load()
next0.storeValue(value)
next0.deleted.Store(false)
if wasDeleted {
sl.size.Add(1)
} }
// Если ключ уже есть — обновляем значение (и снимаем tombstone).
if existing != nil && compareSkipListKeys(existing.key, key) == 0 {
existing.value = value
existing.deleted.Store(false)
return return
} }
// Вставляем newNode на всех уровнях от 0 до newLevel. newNode.storeForward(0, next0)
for i := 0; i <= newLevel; i++ { if !update[0].casForward(0, next0, newNode) {
for { // CAS провалился — кто-то другой вставил узел между
next := update[i].loadForward(i) // findPredecessors и сейчас. Retry.
newNode.forward[i].Store(next) continue
if update[i].casForward(i, next, newNode) {
break
}
// CAS не удался — кто-то другой вставил узел.
// Перечитываем update[i] и повторяем.
// Упрощение: перезапускаем всю вставку.
goto retry
}
} }
// Узел успешно вставлен на уровень 0. Теперь увеличиваем size
// (единственное место, где мы это делаем для новых узлов).
sl.size.Add(1) sl.size.Add(1)
// ============================================================
// ФАЗА 2: вставка на верхние уровни (best-effort).
// ============================================================
//
// Дубликаты на верхних уровнях безопасны: Search всегда спускается
// до уровня 0 и там находит единственный узел с нужным ключом.
// Если CAS на верхнем уровне провалился — просто пропускаем уровень.
for i := 1; i <= newLevel; i++ {
if update[i] == nil {
// Предшественник не определён — пропускаем уровень.
continue
}
// Перечитываем next на этом уровне (мог измениться).
next := update[i].loadForward(i)
newNode.storeForward(i, next)
// CAS best-effort: если провалился — не retry, просто пропускаем.
update[i].casForward(i, next, newNode)
}
return
}
// Если за 1000 попыток не удалось — что-то не так.
// Молча выходим (не паникуем).
} }
// ============================================================================= // =============================================================================
// ПОИСК // ПОИСК
// ============================================================================= // =============================================================================
// Search ищет значение по ключу. Возвращает (value, true) при успехе. // Search ищет значение по ключу.
func (sl *SkipList) Search(key interface{}) (string, bool) { func (sl *SkipList) Search(key interface{}) (string, bool) {
if key == nil { if key == nil {
return "", false return "", false
@@ -289,6 +383,13 @@ func (sl *SkipList) Search(key interface{}) (string, bool) {
if next == nil { if next == nil {
break break
} }
if next.deleted.Load() {
// Пропускаем tombstone.
if x.casForward(i, next, next.loadForward(i)) {
continue
}
continue
}
if compareSkipListKeys(next.key, key) < 0 { if compareSkipListKeys(next.key, key) < 0 {
x = next x = next
continue continue
@@ -297,7 +398,6 @@ func (sl *SkipList) Search(key interface{}) (string, bool) {
} }
} }
// x — предшественник искомого ключа на базовом уровне.
candidate := x.loadForward(0) candidate := x.loadForward(0)
if candidate == nil { if candidate == nil {
return "", false return "", false
@@ -306,16 +406,16 @@ func (sl *SkipList) Search(key interface{}) (string, bool) {
if candidate.deleted.Load() { if candidate.deleted.Load() {
return "", false return "", false
} }
return candidate.value, true return candidate.loadValue(), true
} }
return "", false return "", false
} }
// SearchAll возвращает все значения, ключ которых равен заданному. // SearchAll возвращает все значения, ключ которых равен заданному.
// В неуникальном индексе по одному ключу может быть несколько документов — //
// в текущей реализации индекс хранит только последний docID для данного // ВАЖНО: текущая реализация индекса хранит только один docID на ключ,
// ключа (как и sync.Map). Для уникальных индексов это не критично. // поэтому метод возвращает 0 или 1 значение. Для полноценной поддержки
// Для неуникальных индексов метод возвращает единственное значение. // неуникальных индексов потребуется изменить структуру узла.
func (sl *SkipList) SearchAll(key interface{}) []string { func (sl *SkipList) SearchAll(key interface{}) []string {
val, ok := sl.Search(key) val, ok := sl.Search(key)
if !ok { if !ok {
@@ -329,38 +429,22 @@ func (sl *SkipList) SearchAll(key interface{}) []string {
// ============================================================================= // =============================================================================
// Delete помечает узел с заданным ключом как удалённый (tombstone). // Delete помечает узел с заданным ключом как удалённый (tombstone).
// Физическое удаление произойдёт лениво при следующем Range. // Идемпотентен: повторный вызов не уменьшает size.
func (sl *SkipList) Delete(key interface{}) { func (sl *SkipList) Delete(key interface{}) {
if key == nil { if key == nil {
return return
} }
x := sl.head _, existing := sl.findPredecessors(key)
for i := int(sl.level.Load()); i >= 0; i-- { if existing == nil {
for {
next := x.loadForward(i)
if next == nil {
break
}
if compareSkipListKeys(next.key, key) < 0 {
x = next
continue
}
break
}
}
candidate := x.loadForward(0)
if candidate == nil {
return return
} }
if compareSkipListKeys(candidate.key, key) == 0 {
if !candidate.deleted.Load() { // CAS как атомарная проверка+установка.
if candidate.deleted.CompareAndSwap(false, true) { // Уменьшаем size только если мы первыми поставили tombstone.
if existing.deleted.CompareAndSwap(false, true) {
sl.size.Add(-1) sl.size.Add(-1)
} }
}
}
} }
// ============================================================================= // =============================================================================
@@ -369,27 +453,8 @@ func (sl *SkipList) Delete(key interface{}) {
// Range вызывает fn для каждой пары (key, value) в порядке возрастания ключей. // Range вызывает fn для каждой пары (key, value) в порядке возрастания ключей.
// Если fn возвращает false — обход прекращается. // Если fn возвращает false — обход прекращается.
//
// Во время обхода выполняется ленивая физическая очистка tombstone-узлов:
// если подряд встретилось больше skipListSweepThreshold удалённых узлов,
// они физически выталкиваются из списка.
func (sl *SkipList) Range(fn func(key interface{}, value string) bool) { func (sl *SkipList) Range(fn func(key interface{}, value string) bool) {
x := sl.head prev := sl.head
level0 := 0
// Спускаемся на базовый уровень.
for i := int(sl.level.Load()); i > 0; i-- {
for {
next := x.loadForward(i)
if next == nil {
break
}
x = next
}
}
_ = level0
prev := x
tombstoneCount := 0 tombstoneCount := 0
for { for {
@@ -402,9 +467,7 @@ func (sl *SkipList) Range(fn func(key interface{}, value string) bool) {
tombstoneCount++ tombstoneCount++
// Пытаемся физически удалить узел из базового уровня. // Пытаемся физически удалить узел из базового уровня.
if prev.casForward(0, next, next.loadForward(0)) { if prev.casForward(0, next, next.loadForward(0)) {
// Успешно вытолкнули — не двигаем prev.
if tombstoneCount >= skipListSweepThreshold { if tombstoneCount >= skipListSweepThreshold {
// Периодически чистим верхние уровни (lazy).
sl.sweepUpperLevels() sl.sweepUpperLevels()
tombstoneCount = 0 tombstoneCount = 0
} }
@@ -415,7 +478,7 @@ func (sl *SkipList) Range(fn func(key interface{}, value string) bool) {
continue continue
} }
if !fn(next.key, next.value) { if !fn(next.key, next.loadValue()) {
return return
} }
@@ -425,6 +488,9 @@ func (sl *SkipList) Range(fn func(key interface{}, value string) bool) {
// RangeWithKeyPrefix вызывает fn для каждой пары, ключ которой является // RangeWithKeyPrefix вызывает fn для каждой пары, ключ которой является
// строкой с заданным префиксом. // строкой с заданным префиксом.
//
// Благодаря сортированности skip list, обход можно прервать, как только
// ключ перестал соответствовать префиксу.
func (sl *SkipList) RangeWithKeyPrefix(prefix string, fn func(key interface{}, value string) bool) { func (sl *SkipList) RangeWithKeyPrefix(prefix string, fn func(key interface{}, value string) bool) {
sl.Range(func(key interface{}, value string) bool { sl.Range(func(key interface{}, value string) bool {
keyStr, ok := key.(string) keyStr, ok := key.(string)
@@ -434,12 +500,17 @@ func (sl *SkipList) RangeWithKeyPrefix(prefix string, fn func(key interface{}, v
if strings.HasPrefix(keyStr, prefix) { if strings.HasPrefix(keyStr, prefix) {
return fn(key, value) return fn(key, value)
} }
// Если ключ — строка и он больше префикса, дальнейшие ключи
// (отсортированные) тоже не подойдут.
if keyStr > prefix {
return false
}
return true return true
}) })
} }
// sweepUpperLevels проходит по верхним уровням и физически выталкивает // sweepUpperLevels проходит по верхним уровням и физически выталкивает
// tombstone-узлы. Вызывается лениво из Range. // tombstone-узлы.
func (sl *SkipList) sweepUpperLevels() { func (sl *SkipList) sweepUpperLevels() {
for i := 1; i < maxSkipListLevel; i++ { for i := 1; i < maxSkipListLevel; i++ {
x := sl.head x := sl.head
@@ -463,20 +534,13 @@ func (sl *SkipList) sweepUpperLevels() {
// ============================================================================= // =============================================================================
// Size возвращает приблизительное количество активных элементов. // Size возвращает приблизительное количество активных элементов.
func (sl *SkipList) Size() int64 { func (sl *SkipList) Size() int64 { return sl.size.Load() }
return sl.size.Load()
}
// Len возвращает приблизительное количество активных элементов // Len — синоним Size.
// (синоним Size для совместимости с sync.Map-стилем). func (sl *SkipList) Len() int64 { return sl.size.Load() }
func (sl *SkipList) Len() int64 {
return sl.size.Load()
}
// IsEmpty возвращает true, если список пуст. // IsEmpty возвращает true, если список пуст.
func (sl *SkipList) IsEmpty() bool { func (sl *SkipList) IsEmpty() bool { return sl.size.Load() == 0 }
return sl.size.Load() == 0
}
// Clear помечает все узлы как удалённые. // Clear помечает все узлы как удалённые.
func (sl *SkipList) Clear() { func (sl *SkipList) Clear() {
@@ -492,13 +556,6 @@ func (sl *SkipList) Clear() {
// ============================================================================= // =============================================================================
// compareSkipListKeys сравнивает два ключа. // compareSkipListKeys сравнивает два ключа.
// Поддерживает основные типы, используемые в инвертированном индексе:
// string, int, int64, float64, bool, []byte.
//
// Возвращает -1, 0 или 1.
//
// ВАЖНО: если типы ключей несовместимы, используется строковое сравнение
// через fmt.Sprintf, чтобы обеспечить хоть какой-то порядок (не падать).
func compareSkipListKeys(a, b interface{}) int { func compareSkipListKeys(a, b interface{}) int {
if a == nil && b == nil { if a == nil && b == nil {
return 0 return 0
@@ -549,7 +606,6 @@ func compareSkipListKeys(a, b interface{}) int {
} }
} }
// Fallback: строковое сравнение.
return strings.Compare(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b)) return strings.Compare(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b))
} }
@@ -604,7 +660,7 @@ func compareInt64(a, b int64) int {
} }
// ============================================================================= // =============================================================================
// ITERATOR (опционально, для совместимости) // ITERATOR
// ============================================================================= // =============================================================================
// SkipListIterator — итератор по skip list. // SkipListIterator — итератор по skip list.
@@ -619,7 +675,7 @@ func (sl *SkipList) NewIterator() *SkipListIterator {
return &SkipListIterator{sl: sl} return &SkipListIterator{sl: sl}
} }
// Next переходит к следующему элементу. Возвращает (key, value, ok). // Next переходит к следующему элементу.
func (it *SkipListIterator) Next() (interface{}, string, bool) { func (it *SkipListIterator) Next() (interface{}, string, bool) {
if it.sl == nil { if it.sl == nil {
return nil, "", false return nil, "", false
@@ -627,18 +683,7 @@ func (it *SkipListIterator) Next() (interface{}, string, bool) {
if !it.started { if !it.started {
it.started = true it.started = true
// Спускаемся на базовый уровень. it.current = it.sl.head
x := it.sl.head
for i := int(it.sl.level.Load()); i > 0; i-- {
for {
next := x.loadForward(i)
if next == nil {
break
}
x = next
}
}
it.current = x
} }
for { for {
@@ -648,12 +693,12 @@ func (it *SkipListIterator) Next() (interface{}, string, bool) {
} }
it.current = next it.current = next
if !next.deleted.Load() { if !next.deleted.Load() {
return next.key, next.value, true return next.key, next.loadValue(), true
} }
} }
} }
// Close освобождает ресурсы итератора (no-op, оставлен для совместимости). // Close освобождает ресурсы итератора.
func (it *SkipListIterator) Close() { func (it *SkipListIterator) Close() {
it.sl = nil it.sl = nil
it.current = nil it.current = nil
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