Update internal/storage/transactions.go
This commit is contained in:
1 parent
cc4ec0c029
commit
f191e5b328
1 file changed
+68
-177
@@ -33,11 +33,11 @@
|
|||||||
// Transaction — теперь эти ссылки заменены на GetBatchManager() и *Batch.
|
// Transaction — теперь эти ссылки заменены на GetBatchManager() и *Batch.
|
||||||
//
|
//
|
||||||
// ДОБАВЛЕНО (2026-10, идемпотентность SAGA + выборы лидера):
|
// ДОБАВЛЕНО (2026-10, идемпотентность SAGA + выборы лидера):
|
||||||
// - SagaPersistentStorage теперь ведёт персистентную таблицу
|
// - SagaPersistentStorage ведёт персистентную таблицу выполненных
|
||||||
// выполненных ExecutionID (saga_executed_keys.json). Перед выполнением
|
// ExecutionID (saga_executed_keys.json). Перед выполнением шага
|
||||||
// шага и перед его компенсацией оркестратор проверяет эту таблицу,
|
// и перед его компенсацией оркестратор проверяет эту таблицу,
|
||||||
// чтобы повторный запуск SAGA после падения не приводил к дублированию
|
// чтобы повторный запуск SAGA после падения не приводил к
|
||||||
// операций на удалённых узлах.
|
// дублированию операций на удалённых узлах.
|
||||||
// - SagaOrchestrator получил SagaLeaderElector, который берёт лидера
|
// - SagaOrchestrator получил SagaLeaderElector, который берёт лидера
|
||||||
// из Raft (через SagaRaftAccessor). Если Raft недоступен —
|
// из Raft (через SagaRaftAccessor). Если Raft недоступен —
|
||||||
// используется резервный режим «единственный узел — лидер».
|
// используется резервный режим «единственный узел — лидер».
|
||||||
@@ -48,8 +48,12 @@
|
|||||||
// узел знал, был ли он лидером, и корректно себя вёл до первых
|
// узел знал, был ли он лидером, и корректно себя вёл до первых
|
||||||
// выборов.
|
// выборов.
|
||||||
// - Добавлен интерфейс SagaRaftAccessor, чтобы storage не зависел
|
// - Добавлен интерфейс SagaRaftAccessor, чтобы storage не зависел
|
||||||
// напрямую от hashicorp/raft (это позволяет избежать циклических
|
// напрямую от hashicorp/raft.
|
||||||
// импортов и упрощает тестирование).
|
//
|
||||||
|
// ДОБАВЛЕНО (2026-10, ревизия):
|
||||||
|
// - GetAllSagas у SagaManager — читает все SAGA (включая завершённые)
|
||||||
|
// с диска через SagaPersistentStorage.ListAll(). Используется
|
||||||
|
// REPL-командой "saga list --all".
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
@@ -127,14 +131,12 @@ const (
|
|||||||
SagaCleanupInterval = 1 * time.Minute
|
SagaCleanupInterval = 1 * time.Minute
|
||||||
|
|
||||||
// SagaExecutedKeysFile — персистентная таблица выполненных ExecutionID.
|
// SagaExecutedKeysFile — персистентная таблица выполненных ExecutionID.
|
||||||
// Нужна для идемпотентности шагов SAGA при повторе после падения.
|
|
||||||
SagaExecutedKeysFile = "saga_executed_keys.json"
|
SagaExecutedKeysFile = "saga_executed_keys.json"
|
||||||
|
|
||||||
// SagaLeaderStateFile — персистентное состояние лидера SAGA.
|
// SagaLeaderStateFile — персистентное состояние лидера SAGA.
|
||||||
SagaLeaderStateFile = "saga_leader_state.json"
|
SagaLeaderStateFile = "saga_leader_state.json"
|
||||||
|
|
||||||
// SagaLeaderElectionInterval — как часто опрашиваем Raft на предмет
|
// SagaLeaderElectionInterval — как часто опрашиваем Raft на предмет лидерства.
|
||||||
// лидерства. Меньше — быстрее реакция, но больше нагрузка.
|
|
||||||
SagaLeaderElectionInterval = 2 * time.Second
|
SagaLeaderElectionInterval = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -197,7 +199,7 @@ func crc32(data []byte) uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// BATCH MANAGER - замена TransactionManager
|
// BATCH MANAGER — замена TransactionManager
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// BatchManager управляет batch-операциями и WAL.
|
// BatchManager управляет batch-операциями и WAL.
|
||||||
@@ -207,12 +209,9 @@ type BatchManager struct {
|
|||||||
logger LoggerInterface
|
logger LoggerInterface
|
||||||
walPath string
|
walPath string
|
||||||
stats *BatchStats
|
stats *BatchStats
|
||||||
aof *AOFManager // append-only file для мутаций вне batch
|
aof *AOFManager
|
||||||
|
|
||||||
// activeBatches — реестр активных (незакоммиченных) batch'ей.
|
// activeBatches — реестр активных (незакоммиченных) batch'ей.
|
||||||
// Ключ — Batch.ID, значение — *Batch.
|
|
||||||
// Используется eviction-механизмом (runtime_limits.go), чтобы не
|
|
||||||
// вытеснять документы, участвующие в незакоммиченной batch-операции.
|
|
||||||
activeBatches sync.Map
|
activeBatches sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +262,6 @@ func InitBatchManagerWithConfig(walPath string, config map[string]interface{}) e
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализируем AOF для мутаций вне batch.
|
|
||||||
globalBatchManager.aof, err = NewAOFManager(
|
globalBatchManager.aof, err = NewAOFManager(
|
||||||
filepath.Join(filepath.Dir(walPath), "aof"),
|
filepath.Join(filepath.Dir(walPath), "aof"),
|
||||||
fsyncEnabled, nil)
|
fsyncEnabled, nil)
|
||||||
@@ -301,7 +299,6 @@ func GetGlobalStorage() *Storage { return globalStorage }
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// RegisterBatch регистрирует batch как активный.
|
// RegisterBatch регистрирует batch как активный.
|
||||||
// Вызывается в NewBatch.
|
|
||||||
func (bm *BatchManager) RegisterBatch(b *Batch) {
|
func (bm *BatchManager) RegisterBatch(b *Batch) {
|
||||||
if bm == nil || b == nil {
|
if bm == nil || b == nil {
|
||||||
return
|
return
|
||||||
@@ -310,7 +307,6 @@ func (bm *BatchManager) RegisterBatch(b *Batch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnregisterBatch снимает регистрацию batch.
|
// UnregisterBatch снимает регистрацию batch.
|
||||||
// Вызывается в Commit (успешном или неуспешном).
|
|
||||||
func (bm *BatchManager) UnregisterBatch(b *Batch) {
|
func (bm *BatchManager) UnregisterBatch(b *Batch) {
|
||||||
if bm == nil || b == nil {
|
if bm == nil || b == nil {
|
||||||
return
|
return
|
||||||
@@ -318,11 +314,7 @@ func (bm *BatchManager) UnregisterBatch(b *Batch) {
|
|||||||
bm.activeBatches.Delete(b.ID)
|
bm.activeBatches.Delete(b.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDocInActiveBatch проверяет, участвует ли документ в активной
|
// IsDocInActiveBatch проверяет, участвует ли документ в активной batch-операции.
|
||||||
// (незакоммиченной) batch-операции.
|
|
||||||
//
|
|
||||||
// Используется в runtime_limits.go (eviction), чтобы не вытеснять
|
|
||||||
// документы, которые в данный момент участвуют в batch.
|
|
||||||
func (bm *BatchManager) IsDocInActiveBatch(docID, dbName, collName string) bool {
|
func (bm *BatchManager) IsDocInActiveBatch(docID, dbName, collName string) bool {
|
||||||
if bm == nil {
|
if bm == nil {
|
||||||
return false
|
return false
|
||||||
@@ -347,7 +339,6 @@ func (bm *BatchManager) IsDocInActiveBatch(docID, dbName, collName string) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveBatchCount возвращает количество активных batch'ей.
|
// GetActiveBatchCount возвращает количество активных batch'ей.
|
||||||
// Полезно для метрик и отладки.
|
|
||||||
func (bm *BatchManager) GetActiveBatchCount() int {
|
func (bm *BatchManager) GetActiveBatchCount() int {
|
||||||
if bm == nil {
|
if bm == nil {
|
||||||
return 0
|
return 0
|
||||||
@@ -376,8 +367,6 @@ func NewBatch(database, collection string) *Batch {
|
|||||||
Database: database,
|
Database: database,
|
||||||
Collection: collection,
|
Collection: collection,
|
||||||
}
|
}
|
||||||
// Регистрируем batch как активный, чтобы eviction не вытеснил
|
|
||||||
// документы, участвующие в нём.
|
|
||||||
globalBatchManager.RegisterBatch(b)
|
globalBatchManager.RegisterBatch(b)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@@ -426,29 +415,17 @@ func (b *Batch) AddRestore(docID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Commit применяет batch атомарно.
|
// Commit применяет batch атомарно.
|
||||||
//
|
|
||||||
// Гарантия атомарности:
|
|
||||||
// 1. Сериализуем весь batch и пишем в WAL (fsync).
|
|
||||||
// 2. Только после успешной записи в WAL применяем операции.
|
|
||||||
// 3. Если применение падает на середине — мы можем восстановить
|
|
||||||
// состояние, проиграв WAL (операции идемпотентны или
|
|
||||||
// откатываются вручную).
|
|
||||||
//
|
|
||||||
// В любом случае (успех или ошибка) batch снимается с регистрации
|
|
||||||
// в activeBatches.
|
|
||||||
func (b *Batch) Commit() error {
|
func (b *Batch) Commit() error {
|
||||||
if globalBatchManager == nil {
|
if globalBatchManager == nil {
|
||||||
return fmt.Errorf("batch manager not initialized")
|
return fmt.Errorf("batch manager not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Снимаем регистрацию в любом случае — batch короткоживущий.
|
|
||||||
defer globalBatchManager.UnregisterBatch(b)
|
defer globalBatchManager.UnregisterBatch(b)
|
||||||
|
|
||||||
if len(b.Operations) == 0 {
|
if len(b.Operations) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Пишем batch в WAL.
|
|
||||||
data, err := json.Marshal(b)
|
data, err := json.Marshal(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal batch: %v", err)
|
return fmt.Errorf("failed to marshal batch: %v", err)
|
||||||
@@ -460,13 +437,11 @@ func (b *Batch) Commit() error {
|
|||||||
return fmt.Errorf("failed to write batch to WAL: %v", err)
|
return fmt.Errorf("failed to write batch to WAL: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Применяем операции.
|
|
||||||
if err := b.apply(); err != nil {
|
if err := b.apply(); err != nil {
|
||||||
globalBatchManager.stats.TotalFailed.Add(1)
|
globalBatchManager.stats.TotalFailed.Add(1)
|
||||||
return fmt.Errorf("batch apply failed: %v", err)
|
return fmt.Errorf("batch apply failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Также пишем в AOF для быстрого восстановления (опционально).
|
|
||||||
if globalBatchManager.aof != nil {
|
if globalBatchManager.aof != nil {
|
||||||
if err := globalBatchManager.aof.AppendBatch(b); err != nil {
|
if err := globalBatchManager.aof.AppendBatch(b); err != nil {
|
||||||
if globalBatchManager.logger != nil {
|
if globalBatchManager.logger != nil {
|
||||||
@@ -481,16 +456,11 @@ func (b *Batch) Commit() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply применяет все операции batch.
|
// apply применяет все операции batch.
|
||||||
//
|
|
||||||
// Если операция падает, мы пытаемся откатить уже применённые
|
|
||||||
// (best-effort). Полная атомарность гарантируется только на уровне
|
|
||||||
// WAL: при восстановлении batch либо весь применится, либо нет.
|
|
||||||
func (b *Batch) apply() error {
|
func (b *Batch) apply() error {
|
||||||
if globalStorage == nil {
|
if globalStorage == nil {
|
||||||
return fmt.Errorf("storage not initialized")
|
return fmt.Errorf("storage not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Собираем откат для уже применённых операций.
|
|
||||||
type appliedOp struct {
|
type appliedOp struct {
|
||||||
opType string
|
opType string
|
||||||
db string
|
db string
|
||||||
@@ -592,11 +562,7 @@ func (b *Batch) apply() error {
|
|||||||
// TIME-TRAVEL QUERIES (WAL-based)
|
// TIME-TRAVEL QUERIES (WAL-based)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// GetDocumentAtTimestamp возвращает документ на указанный момент времени,
|
// GetDocumentAtTimestamp возвращает документ на указанный момент времени.
|
||||||
// проигрывая WAL с начала до нужного LSN.
|
|
||||||
//
|
|
||||||
// Это O(N) по количеству записей WAL — для in-memory СУБД с небольшим WAL
|
|
||||||
// это приемлемо. Для больших WAL нужен индекс по времени.
|
|
||||||
func GetDocumentAtTimestamp(docID string, timestamp int64) (*Document, error) {
|
func GetDocumentAtTimestamp(docID string, timestamp int64) (*Document, error) {
|
||||||
if globalBatchManager == nil {
|
if globalBatchManager == nil {
|
||||||
return nil, fmt.Errorf("batch manager not initialized")
|
return nil, fmt.Errorf("batch manager not initialized")
|
||||||
@@ -661,8 +627,7 @@ func GetDocumentAtTimestamp(docID string, timestamp int64) (*Document, error) {
|
|||||||
return current, nil
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDocumentVersionsSince возвращает все версии документа,
|
// GetDocumentVersionsSince возвращает все версии документа после указанного времени.
|
||||||
// созданные после указанного времени.
|
|
||||||
func GetDocumentVersionsSince(docID string, since int64) ([]*Document, error) {
|
func GetDocumentVersionsSince(docID string, since int64) ([]*Document, error) {
|
||||||
if globalBatchManager == nil || globalBatchManager.wal == nil {
|
if globalBatchManager == nil || globalBatchManager.wal == nil {
|
||||||
return nil, fmt.Errorf("batch manager not initialized")
|
return nil, fmt.Errorf("batch manager not initialized")
|
||||||
@@ -747,8 +712,6 @@ type SegmentedWALManager struct {
|
|||||||
logger LoggerInterface
|
logger LoggerInterface
|
||||||
fsyncEnabled bool
|
fsyncEnabled bool
|
||||||
|
|
||||||
// ackMap — map LSN -> chan struct{} для синхронного подтверждения.
|
|
||||||
// Ключ — LSN записи. После записи в WAL закрываем канал.
|
|
||||||
ackMu sync.Mutex
|
ackMu sync.Mutex
|
||||||
ackMap map[uint64]chan struct{}
|
ackMap map[uint64]chan struct{}
|
||||||
}
|
}
|
||||||
@@ -871,13 +834,9 @@ func (wm *SegmentedWALManager) Write(record *WALRecord) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteSync — синхронная запись с гарантией fsync.
|
// WriteSync — синхронная запись с гарантией fsync.
|
||||||
//
|
|
||||||
// Используем ackMap по LSN, чтобы не было race condition
|
|
||||||
// с чужими подтверждениями.
|
|
||||||
func (wm *SegmentedWALManager) WriteSync(record *WALRecord) error {
|
func (wm *SegmentedWALManager) WriteSync(record *WALRecord) error {
|
||||||
record.Timestamp = time.Now().UnixMilli()
|
record.Timestamp = time.Now().UnixMilli()
|
||||||
|
|
||||||
// Присваиваем LSN под защитой mu.
|
|
||||||
wm.mu.Lock()
|
wm.mu.Lock()
|
||||||
lsn := wm.getCurrentLSNLocked() + 1
|
lsn := wm.getCurrentLSNLocked() + 1
|
||||||
record.LSN = lsn
|
record.LSN = lsn
|
||||||
@@ -981,7 +940,6 @@ func (wm *SegmentedWALManager) flushBatch(batch []*WALRecord) {
|
|||||||
|
|
||||||
wm.mu.Unlock()
|
wm.mu.Unlock()
|
||||||
|
|
||||||
// Подтверждаем все записи из batch.
|
|
||||||
wm.ackMu.Lock()
|
wm.ackMu.Lock()
|
||||||
for _, record := range batch {
|
for _, record := range batch {
|
||||||
if ch, ok := wm.ackMap[record.LSN]; ok {
|
if ch, ok := wm.ackMap[record.LSN]; ok {
|
||||||
@@ -1148,16 +1106,15 @@ func (im *WALIndexManager) load() error {
|
|||||||
}
|
}
|
||||||
return json.Unmarshal(data, &im.index)
|
return json.Unmarshal(data, &im.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// SAGA — ИНТЕРФЕЙСЫ ДЛЯ RAFT (без прямого импорта hashicorp/raft)
|
// SAGA — ИНТЕРФЕЙСЫ ДЛЯ RAFT
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// SagaRaftAccessor — минимальный интерфейс к Raft, необходимый
|
// SagaRaftAccessor — минимальный интерфейс к Raft, необходимый
|
||||||
// оркестратору SAGA для выборов лидера.
|
// оркестратору SAGA для выборов лидера.
|
||||||
//
|
//
|
||||||
// Реализуется в internal/cluster (RaftCoordinator), чтобы избежать
|
// Реализуется в internal/cluster (SagaLeaderBridge), чтобы storage
|
||||||
// циклических импортов и позволить storage не зависеть от raft напрямую.
|
// не зависел напрямую от hashicorp/raft.
|
||||||
type SagaRaftAccessor interface {
|
type SagaRaftAccessor interface {
|
||||||
// IsSagaLeader возвращает true, если текущий узел является лидером
|
// IsSagaLeader возвращает true, если текущий узел является лидером
|
||||||
// в Raft-группе.
|
// в Raft-группе.
|
||||||
@@ -1224,13 +1181,8 @@ type SagaStepState struct {
|
|||||||
// SagaPersistentStorage — персистентное хранилище SAGA.
|
// SagaPersistentStorage — персистентное хранилище SAGA.
|
||||||
//
|
//
|
||||||
// ДОБАВЛЕНО: executedKeys — персистентная таблица уже выполненных
|
// ДОБАВЛЕНО: executedKeys — персистентная таблица уже выполненных
|
||||||
// ExecutionID. Заполняется после успешного выполнения шага
|
// ExecutionID. Заполняется после успешного выполнения шага и после
|
||||||
// (в executeSagaInternal) и после успешной компенсации
|
// успешной компенсации. Проверяется перед выполнением/компенсацией.
|
||||||
// (в executeCompensation). Проверяется перед выполнением/компенсацией.
|
|
||||||
//
|
|
||||||
// Формат файла — JSON: { "execution_id": timestamp_ms, ... }.
|
|
||||||
// Это позволяет при рестарте быстро понять, какие шаги уже выполнены,
|
|
||||||
// и не дублировать их на удалённых узлах.
|
|
||||||
type SagaPersistentStorage struct {
|
type SagaPersistentStorage struct {
|
||||||
baseDir string
|
baseDir string
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -1241,16 +1193,11 @@ type SagaPersistentStorage struct {
|
|||||||
fsyncMaxRetries int
|
fsyncMaxRetries int
|
||||||
fsyncRetryDelay time.Duration
|
fsyncRetryDelay time.Duration
|
||||||
|
|
||||||
// executedKeys — таблица выполненных ExecutionID.
|
|
||||||
// Ключ — ExecutionID, значение — UnixMilli, когда он был выполнен.
|
|
||||||
// Хранится в памяти + на диске (saga_executed_keys.json).
|
|
||||||
executedMu sync.RWMutex
|
executedMu sync.RWMutex
|
||||||
executedKeys map[string]int64
|
executedKeys map[string]int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSagaPersistentStorageWithConfig создаёт хранилище SAGA.
|
// NewSagaPersistentStorageWithConfig создаёт хранилище SAGA.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: при создании загружаем executedKeys с диска.
|
|
||||||
func NewSagaPersistentStorageWithConfig(cfg *config.SagaConfig, logger LoggerInterface) (*SagaPersistentStorage, error) {
|
func NewSagaPersistentStorageWithConfig(cfg *config.SagaConfig, logger LoggerInterface) (*SagaPersistentStorage, error) {
|
||||||
baseDir := SagaStateDir
|
baseDir := SagaStateDir
|
||||||
maxCache := 10000
|
maxCache := 10000
|
||||||
@@ -1282,7 +1229,6 @@ func NewSagaPersistentStorageWithConfig(cfg *config.SagaConfig, logger LoggerInt
|
|||||||
executedKeys: make(map[string]int64),
|
executedKeys: make(map[string]int64),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загружаем таблицу выполненных ключей.
|
|
||||||
if err := sps.loadExecutedKeys(); err != nil {
|
if err := sps.loadExecutedKeys(); err != nil {
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
logger.Warn(fmt.Sprintf("Failed to load saga executed keys: %v", err))
|
logger.Warn(fmt.Sprintf("Failed to load saga executed keys: %v", err))
|
||||||
@@ -1325,7 +1271,6 @@ func (sps *SagaPersistentStorage) loadExecutedKeys() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// saveExecutedKeys сохраняет таблицу выполненных ключей на диск.
|
// saveExecutedKeys сохраняет таблицу выполненных ключей на диск.
|
||||||
// Вызывается под executedMu.RLock или без удержания блокировки.
|
|
||||||
func (sps *SagaPersistentStorage) saveExecutedKeys() error {
|
func (sps *SagaPersistentStorage) saveExecutedKeys() error {
|
||||||
sps.executedMu.RLock()
|
sps.executedMu.RLock()
|
||||||
data, err := json.Marshal(sps.executedKeys)
|
data, err := json.Marshal(sps.executedKeys)
|
||||||
@@ -1364,8 +1309,7 @@ func (sps *SagaPersistentStorage) IsExecuted(executionID string) bool {
|
|||||||
// MarkExecuted атомарно отмечает ExecutionID как выполненный
|
// MarkExecuted атомарно отмечает ExecutionID как выполненный
|
||||||
// и сохраняет таблицу на диск.
|
// и сохраняет таблицу на диск.
|
||||||
//
|
//
|
||||||
// Возвращает true, если ключ был добавлен впервые (т.е. операция
|
// Возвращает true, если ключ был добавлен впервые.
|
||||||
// действительно новая), и false, если ключ уже был (повтор).
|
|
||||||
func (sps *SagaPersistentStorage) MarkExecuted(executionID string) bool {
|
func (sps *SagaPersistentStorage) MarkExecuted(executionID string) bool {
|
||||||
sps.executedMu.Lock()
|
sps.executedMu.Lock()
|
||||||
if _, exists := sps.executedKeys[executionID]; exists {
|
if _, exists := sps.executedKeys[executionID]; exists {
|
||||||
@@ -1561,21 +1505,13 @@ func (sps *SagaPersistentStorage) LoadLeaderState() (*SagaLeaderState, error) {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// SagaLeaderElector — выборы лидера SAGA через Raft.
|
// SagaLeaderElector — выборы лидера SAGA через Raft.
|
||||||
//
|
|
||||||
// Если Raft доступен, IsLeader() опрашивает RaftAccessor.
|
|
||||||
// Если Raft недоступен, используется резервный режим: узел считается
|
|
||||||
// лидером, только если он один в кластере (single-node mode).
|
|
||||||
//
|
|
||||||
// Все изменения лидерства сохраняются в SagaPersistentStorage
|
|
||||||
// (файл saga_leader_state.json), чтобы после рестарта узел знал,
|
|
||||||
// был ли он лидером.
|
|
||||||
type SagaLeaderElector struct {
|
type SagaLeaderElector struct {
|
||||||
nodeID string
|
nodeID string
|
||||||
raft SagaRaftAccessor
|
raft SagaRaftAccessor
|
||||||
storage *SagaPersistentStorage
|
storage *SagaPersistentStorage
|
||||||
logger LoggerInterface
|
logger LoggerInterface
|
||||||
isLeader atomic.Bool
|
isLeader atomic.Bool
|
||||||
leaderID atomic.Value // string
|
leaderID atomic.Value
|
||||||
currentTerm atomic.Uint64
|
currentTerm atomic.Uint64
|
||||||
|
|
||||||
observersMu sync.RWMutex
|
observersMu sync.RWMutex
|
||||||
@@ -1597,7 +1533,6 @@ func NewSagaLeaderElector(nodeID string, raft SagaRaftAccessor, storage *SagaPer
|
|||||||
}
|
}
|
||||||
e.leaderID.Store("")
|
e.leaderID.Store("")
|
||||||
|
|
||||||
// Пытаемся загрузить прошлое состояние лидера.
|
|
||||||
if storage != nil {
|
if storage != nil {
|
||||||
if state, err := storage.LoadLeaderState(); err == nil && state != nil {
|
if state, err := storage.LoadLeaderState(); err == nil && state != nil {
|
||||||
e.isLeader.Store(state.IsLeader)
|
e.isLeader.Store(state.IsLeader)
|
||||||
@@ -1606,15 +1541,12 @@ func NewSagaLeaderElector(nodeID string, raft SagaRaftAccessor, storage *SagaPer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если Raft доступен, регистрируем наблюдателя, чтобы оперативно
|
|
||||||
// получать уведомления о смене лидера.
|
|
||||||
if raft != nil {
|
if raft != nil {
|
||||||
raft.RegisterSagaLeaderObserver(nodeID, func(isLeader bool, leaderID string) {
|
raft.RegisterSagaLeaderObserver(nodeID, func(isLeader bool, leaderID string) {
|
||||||
e.updateLeadership(isLeader, leaderID)
|
e.updateLeadership(isLeader, leaderID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запускаем фоновый опрос Raft (на случай, если наблюдатель не сработал).
|
|
||||||
e.wg.Add(1)
|
e.wg.Add(1)
|
||||||
go e.leaderElectionLoop()
|
go e.leaderElectionLoop()
|
||||||
|
|
||||||
@@ -1622,7 +1554,6 @@ func NewSagaLeaderElector(nodeID string, raft SagaRaftAccessor, storage *SagaPer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateLeadership — центральная точка обновления лидерства.
|
// updateLeadership — центральная точка обновления лидерства.
|
||||||
// Вызывается и наблюдателем, и фоновым опросом.
|
|
||||||
func (e *SagaLeaderElector) updateLeadership(isLeader bool, leaderID string) {
|
func (e *SagaLeaderElector) updateLeadership(isLeader bool, leaderID string) {
|
||||||
oldLeader := e.isLeader.Load()
|
oldLeader := e.isLeader.Load()
|
||||||
oldLeaderID, _ := e.leaderID.Load().(string)
|
oldLeaderID, _ := e.leaderID.Load().(string)
|
||||||
@@ -1636,7 +1567,6 @@ func (e *SagaLeaderElector) updateLeadership(isLeader bool, leaderID string) {
|
|||||||
}
|
}
|
||||||
e.currentTerm.Store(term)
|
e.currentTerm.Store(term)
|
||||||
|
|
||||||
// Сохраняем на диск.
|
|
||||||
if e.storage != nil {
|
if e.storage != nil {
|
||||||
_ = e.storage.SaveLeaderState(&SagaLeaderState{
|
_ = e.storage.SaveLeaderState(&SagaLeaderState{
|
||||||
LeaderID: leaderID,
|
LeaderID: leaderID,
|
||||||
@@ -1647,7 +1577,6 @@ func (e *SagaLeaderElector) updateLeadership(isLeader bool, leaderID string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если что-то изменилось — уведомляем наблюдателей.
|
|
||||||
if oldLeader != isLeader || oldLeaderID != leaderID {
|
if oldLeader != isLeader || oldLeaderID != leaderID {
|
||||||
e.observersMu.RLock()
|
e.observersMu.RLock()
|
||||||
obs := make([]func(isLeader bool, leaderID string), 0, len(e.observers))
|
obs := make([]func(isLeader bool, leaderID string), 0, len(e.observers))
|
||||||
@@ -1732,28 +1661,24 @@ func (e *SagaLeaderElector) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// SAGA ORCHESTRATOR (упрощённый, без мьютексов на горячем пути)
|
// SAGA ORCHESTRATOR
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// SagaOrchestrator управляет SAGA.
|
// SagaOrchestrator управляет SAGA.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: leaderElector — выборы лидера через Raft.
|
|
||||||
// isLeader больше не выставляется безусловно в true: значение
|
|
||||||
// синхронизируется с leaderElector.
|
|
||||||
type SagaOrchestrator struct {
|
type SagaOrchestrator struct {
|
||||||
storage *SagaPersistentStorage
|
storage *SagaPersistentStorage
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
logger LoggerInterface
|
logger LoggerInterface
|
||||||
nodeID string
|
nodeID string
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
isLeader atomic.Bool
|
isLeader atomic.Bool
|
||||||
leaderID string
|
leaderID string
|
||||||
config *config.SagaConfig
|
config *config.SagaConfig
|
||||||
metrics *SagaMetrics
|
metrics *SagaMetrics
|
||||||
activeSagas sync.Map // map[string]*SagaTransaction
|
activeSagas sync.Map
|
||||||
leaderElector *SagaLeaderElector
|
leaderElector *SagaLeaderElector
|
||||||
raftAccessor SagaRaftAccessor
|
raftAccessor SagaRaftAccessor
|
||||||
}
|
}
|
||||||
|
|
||||||
// SagaMetrics — метрики SAGA.
|
// SagaMetrics — метрики SAGA.
|
||||||
@@ -1765,17 +1690,11 @@ type SagaMetrics struct {
|
|||||||
TotalCompensated atomic.Uint64
|
TotalCompensated atomic.Uint64
|
||||||
ActiveCount atomic.Int64
|
ActiveCount atomic.Int64
|
||||||
RecoveryCount atomic.Uint64
|
RecoveryCount atomic.Uint64
|
||||||
IdempotentSkips atomic.Uint64 // сколько шагов было пропущено по идемпотентности
|
IdempotentSkips atomic.Uint64
|
||||||
NotLeaderRejects atomic.Uint64 // сколько SAGA было отклонено из-за отсутствия лидерства
|
NotLeaderRejects atomic.Uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSagaOrchestratorWithConfig создаёт оркестратор.
|
// NewSagaOrchestratorWithConfig создаёт оркестратор без Raft.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: второй аргумент raftAccessor может быть nil —
|
|
||||||
// тогда оркестратор работает в single-node режиме и всегда лидер.
|
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: если raftAccessor != nil, создаётся SagaLeaderElector,
|
|
||||||
// который опрашивает Raft и обновляет isLeader.
|
|
||||||
func NewSagaOrchestratorWithConfig(cfg *config.SagaConfig, nodeID string, logger LoggerInterface) (*SagaOrchestrator, error) {
|
func NewSagaOrchestratorWithConfig(cfg *config.SagaConfig, nodeID string, logger LoggerInterface) (*SagaOrchestrator, error) {
|
||||||
return NewSagaOrchestratorWithRaft(cfg, nodeID, nil, logger)
|
return NewSagaOrchestratorWithRaft(cfg, nodeID, nil, logger)
|
||||||
}
|
}
|
||||||
@@ -1814,11 +1733,8 @@ func NewSagaOrchestratorWithRaft(cfg *config.SagaConfig, nodeID string, raftAcce
|
|||||||
raftAccessor: raftAccessor,
|
raftAccessor: raftAccessor,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаём выборщик лидера.
|
|
||||||
o.leaderElector = NewSagaLeaderElector(nodeID, raftAccessor, storage, logger)
|
o.leaderElector = NewSagaLeaderElector(nodeID, raftAccessor, storage, logger)
|
||||||
|
|
||||||
// Начальное значение isLeader синхронизируем с выборщиком.
|
|
||||||
// Если Raft недоступен (nil), считаем себя лидером — single-node режим.
|
|
||||||
if raftAccessor == nil {
|
if raftAccessor == nil {
|
||||||
o.isLeader.Store(true)
|
o.isLeader.Store(true)
|
||||||
o.leaderID = nodeID
|
o.leaderID = nodeID
|
||||||
@@ -1828,7 +1744,6 @@ func NewSagaOrchestratorWithRaft(cfg *config.SagaConfig, nodeID string, raftAcce
|
|||||||
o.leaderID = o.leaderElector.GetLeaderID()
|
o.leaderID = o.leaderElector.GetLeaderID()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подписываемся на изменения лидерства.
|
|
||||||
o.leaderElector.RegisterObserver("orchestrator", func(isLeader bool, leaderID string) {
|
o.leaderElector.RegisterObserver("orchestrator", func(isLeader bool, leaderID string) {
|
||||||
o.mu.Lock()
|
o.mu.Lock()
|
||||||
o.leaderID = leaderID
|
o.leaderID = leaderID
|
||||||
@@ -1844,9 +1759,6 @@ func NewSagaOrchestratorWithRaft(cfg *config.SagaConfig, nodeID string, raftAcce
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsLeader — является ли узел лидером.
|
// IsLeader — является ли узел лидером.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: теперь значение берётся из leaderElector, а не
|
|
||||||
// из жёстко выставленного флага.
|
|
||||||
func (o *SagaOrchestrator) IsLeader() bool {
|
func (o *SagaOrchestrator) IsLeader() bool {
|
||||||
if o.leaderElector == nil {
|
if o.leaderElector == nil {
|
||||||
return true
|
return true
|
||||||
@@ -1868,7 +1780,6 @@ func (o *SagaOrchestrator) GetLeaderID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsExecuted проверяет, был ли ExecutionID уже выполнен.
|
// IsExecuted проверяет, был ли ExecutionID уже выполнен.
|
||||||
// Делегируется в storage.
|
|
||||||
func (o *SagaOrchestrator) IsExecuted(executionID string) bool {
|
func (o *SagaOrchestrator) IsExecuted(executionID string) bool {
|
||||||
if o.storage == nil {
|
if o.storage == nil {
|
||||||
return false
|
return false
|
||||||
@@ -1877,7 +1788,6 @@ func (o *SagaOrchestrator) IsExecuted(executionID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MarkExecuted помечает ExecutionID как выполненный.
|
// MarkExecuted помечает ExecutionID как выполненный.
|
||||||
// Возвращает true, если это первое выполнение.
|
|
||||||
func (o *SagaOrchestrator) MarkExecuted(executionID string) bool {
|
func (o *SagaOrchestrator) MarkExecuted(executionID string) bool {
|
||||||
if o.storage == nil {
|
if o.storage == nil {
|
||||||
return true
|
return true
|
||||||
@@ -1896,7 +1806,6 @@ func (o *SagaOrchestrator) recoveryLoop() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Восстановлением занимается только лидер.
|
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1914,7 +1823,6 @@ func (o *SagaOrchestrator) orphanCleanupLoop() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Очисткой занимается только лидер.
|
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1925,7 +1833,7 @@ func (o *SagaOrchestrator) orphanCleanupLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanupOrphanSteps — очистка "осиротевших" SAGA.
|
// cleanupOrphanSteps — очистка «осиротевших» SAGA.
|
||||||
func (o *SagaOrchestrator) cleanupOrphanSteps() {
|
func (o *SagaOrchestrator) cleanupOrphanSteps() {
|
||||||
pending, err := o.storage.ListPending()
|
pending, err := o.storage.ListPending()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1948,13 +1856,7 @@ func (o *SagaOrchestrator) cleanupOrphanSteps() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compensateOrphanSaga — компенсация "осиротевшей" SAGA.
|
// compensateOrphanSaga — компенсация «осиротевшей» SAGA.
|
||||||
//
|
|
||||||
// Проверяем nil на Compensate, сохраняем состояние после
|
|
||||||
// каждой компенсации, обрабатываем compensation_failed.
|
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: проверяем лидерство — компенсацию выполняет только лидер,
|
|
||||||
// чтобы два узла не компенсировали одну и ту же SAGA.
|
|
||||||
func (o *SagaOrchestrator) compensateOrphanSaga(state *SagaState) {
|
func (o *SagaOrchestrator) compensateOrphanSaga(state *SagaState) {
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
return
|
return
|
||||||
@@ -2015,8 +1917,6 @@ func (o *SagaOrchestrator) stateToTransaction(state *SagaState) *SagaTransaction
|
|||||||
}
|
}
|
||||||
|
|
||||||
// recoverPendingSagas — восстановление незавершённых SAGA.
|
// recoverPendingSagas — восстановление незавершённых SAGA.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: выполняется только лидером.
|
|
||||||
func (o *SagaOrchestrator) recoverPendingSagas() {
|
func (o *SagaOrchestrator) recoverPendingSagas() {
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
return
|
return
|
||||||
@@ -2050,9 +1950,6 @@ func (o *SagaOrchestrator) resumeSaga(state *SagaState) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BeginSaga — начать SAGA.
|
// BeginSaga — начать SAGA.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: если узел не лидер — возвращаем ошибку, чтобы клиент
|
|
||||||
// перенаправил запрос на лидера.
|
|
||||||
func (o *SagaOrchestrator) BeginSaga(id string) (*SagaTransaction, error) {
|
func (o *SagaOrchestrator) BeginSaga(id string) (*SagaTransaction, error) {
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
o.metrics.NotLeaderRejects.Add(1)
|
o.metrics.NotLeaderRejects.Add(1)
|
||||||
@@ -2098,8 +1995,6 @@ func (o *SagaOrchestrator) BeginSaga(id string) (*SagaTransaction, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute — выполнить SAGA.
|
// Execute — выполнить SAGA.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: только лидер выполняет SAGA.
|
|
||||||
func (o *SagaOrchestrator) Execute(saga *SagaTransaction) error {
|
func (o *SagaOrchestrator) Execute(saga *SagaTransaction) error {
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
o.metrics.NotLeaderRejects.Add(1)
|
o.metrics.NotLeaderRejects.Add(1)
|
||||||
@@ -2109,16 +2004,6 @@ func (o *SagaOrchestrator) Execute(saga *SagaTransaction) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// executeSagaInternal — внутренняя логика выполнения SAGA.
|
// executeSagaInternal — внутренняя логика выполнения SAGA.
|
||||||
//
|
|
||||||
// Компенсация идемпотентна и корректно обрабатывает ошибки.
|
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: идемпотентность через storage.IsExecuted / storage.MarkExecuted.
|
|
||||||
// Перед выполнением шага проверяем, не выполнялся ли он уже. Если да —
|
|
||||||
// пропускаем (IdempotentSkips++). После успешного выполнения отмечаем
|
|
||||||
// ExecutionID как выполненный на диске.
|
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: if !o.IsLeader() — прерываем выполнение, если узел потерял
|
|
||||||
// лидерство в процессе (защита от двух лидеров).
|
|
||||||
func (o *SagaOrchestrator) executeSagaInternal(saga *SagaTransaction) error {
|
func (o *SagaOrchestrator) executeSagaInternal(saga *SagaTransaction) error {
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
return fmt.Errorf("node is not the saga leader")
|
return fmt.Errorf("node is not the saga leader")
|
||||||
@@ -2138,8 +2023,6 @@ func (o *SagaOrchestrator) executeSagaInternal(saga *SagaTransaction) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := saga.CurrentStep; i < len(saga.Steps); i++ {
|
for i := saga.CurrentStep; i < len(saga.Steps); i++ {
|
||||||
// Проверяем лидерство на каждой итерации: если потеряли —
|
|
||||||
// прерываемся, чтобы не конфликтовать с новым лидером.
|
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
return fmt.Errorf("lost saga leadership during execution of %s", saga.ID)
|
return fmt.Errorf("lost saga leadership during execution of %s", saga.ID)
|
||||||
}
|
}
|
||||||
@@ -2155,7 +2038,6 @@ func (o *SagaOrchestrator) executeSagaInternal(saga *SagaTransaction) error {
|
|||||||
step.StartedAt = time.Now().UnixMilli()
|
step.StartedAt = time.Now().UnixMilli()
|
||||||
saga.mu.Unlock()
|
saga.mu.Unlock()
|
||||||
|
|
||||||
// ИДЕМПОТЕНТНОСТЬ: если шаг уже выполнялся (по журналу), пропускаем.
|
|
||||||
if o.IsExecuted(step.ExecutionID) || saga.HasExecuted(step.ExecutionID) {
|
if o.IsExecuted(step.ExecutionID) || saga.HasExecuted(step.ExecutionID) {
|
||||||
saga.mu.Lock()
|
saga.mu.Lock()
|
||||||
step.Status = "completed"
|
step.Status = "completed"
|
||||||
@@ -2219,8 +2101,6 @@ func (o *SagaOrchestrator) executeSagaInternal(saga *SagaTransaction) error {
|
|||||||
return fmt.Errorf("saga %s aborted at step %s: %v", saga.ID, step.Name, err)
|
return fmt.Errorf("saga %s aborted at step %s: %v", saga.ID, step.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ИДЕМПОТЕНТНОСТЬ: отмечаем шаг как выполненный на диске,
|
|
||||||
// чтобы при рестарте не выполнить его повторно.
|
|
||||||
o.MarkExecuted(step.ExecutionID)
|
o.MarkExecuted(step.ExecutionID)
|
||||||
|
|
||||||
saga.mu.Lock()
|
saga.mu.Lock()
|
||||||
@@ -2276,14 +2156,6 @@ func (o *SagaOrchestrator) saveSagaState(saga *SagaTransaction) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// executeCompensation — выполнение компенсации.
|
// executeCompensation — выполнение компенсации.
|
||||||
//
|
|
||||||
// Идемпотентность: пропускаем уже скомпенсированные шаги.
|
|
||||||
// Сохраняем состояние после каждой компенсации.
|
|
||||||
// Обрабатываем compensation_failed корректно.
|
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: проверяем лидерство в начале и на каждой итерации.
|
|
||||||
// ДОБАВЛЕНО: идемпотентность через storage.IsExecuted на
|
|
||||||
// execution_id компенсации (используем суффикс ":compensate").
|
|
||||||
func (o *SagaOrchestrator) executeCompensation(saga *SagaTransaction, failedStep int) error {
|
func (o *SagaOrchestrator) executeCompensation(saga *SagaTransaction, failedStep int) error {
|
||||||
if !o.IsLeader() {
|
if !o.IsLeader() {
|
||||||
return fmt.Errorf("node is not the saga leader")
|
return fmt.Errorf("node is not the saga leader")
|
||||||
@@ -2313,7 +2185,6 @@ func (o *SagaOrchestrator) executeCompensation(saga *SagaTransaction, failedStep
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ИДЕМПОТЕНТНОСТЬ: проверяем, не компенсировали ли уже.
|
|
||||||
compExecID := step.ExecutionID + ":compensate"
|
compExecID := step.ExecutionID + ":compensate"
|
||||||
if o.IsExecuted(compExecID) {
|
if o.IsExecuted(compExecID) {
|
||||||
saga.mu.Lock()
|
saga.mu.Lock()
|
||||||
@@ -2325,7 +2196,6 @@ func (o *SagaOrchestrator) executeCompensation(saga *SagaTransaction, failedStep
|
|||||||
}
|
}
|
||||||
|
|
||||||
if step.Compensate == nil {
|
if step.Compensate == nil {
|
||||||
// Нет функции компенсации — считаем шаг скомпенсированным.
|
|
||||||
saga.mu.Lock()
|
saga.mu.Lock()
|
||||||
step.Status = "compensated"
|
step.Status = "compensated"
|
||||||
step.CompletedAt = time.Now().UnixMilli()
|
step.CompletedAt = time.Now().UnixMilli()
|
||||||
@@ -2355,7 +2225,6 @@ func (o *SagaOrchestrator) executeCompensation(saga *SagaTransaction, failedStep
|
|||||||
o.saveSagaState(saga)
|
o.saveSagaState(saga)
|
||||||
return fmt.Errorf("compensation for step %s failed: %v", step.Name, err)
|
return fmt.Errorf("compensation for step %s failed: %v", step.Name, err)
|
||||||
}
|
}
|
||||||
// ИДЕМПОТЕНТНОСТЬ: отмечаем компенсацию как выполненную.
|
|
||||||
o.MarkExecuted(compExecID)
|
o.MarkExecuted(compExecID)
|
||||||
saga.mu.Lock()
|
saga.mu.Lock()
|
||||||
step.Status = "compensated"
|
step.Status = "compensated"
|
||||||
@@ -2387,8 +2256,6 @@ func (o *SagaOrchestrator) GetSaga(id string) (*SagaTransaction, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMetrics — метрики SAGA.
|
// GetMetrics — метрики SAGA.
|
||||||
//
|
|
||||||
// ДОБАВЛЕНО: idempotent_skips, not_leader_rejects, leader_id.
|
|
||||||
func (o *SagaOrchestrator) GetMetrics() map[string]interface{} {
|
func (o *SagaOrchestrator) GetMetrics() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"total_started": o.metrics.TotalStarted.Load(),
|
"total_started": o.metrics.TotalStarted.Load(),
|
||||||
@@ -2654,6 +2521,30 @@ func (sm *SagaManager) GetActiveSagas() []*SagaTransaction {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllSagas возвращает все SAGA транзакции — и активные, и завершённые.
|
||||||
|
//
|
||||||
|
// ДОБАВЛЕНО (2026-10): используется REPL-командой "saga list --all".
|
||||||
|
// В отличие от GetActiveSagas, читает состояние с диска через
|
||||||
|
// SagaPersistentStorage.ListAll() и конвертирует каждую запись в
|
||||||
|
// SagaTransaction через stateToTransaction.
|
||||||
|
func (sm *SagaManager) GetAllSagas() []*SagaTransaction {
|
||||||
|
result := make([]*SagaTransaction, 0)
|
||||||
|
if sm.orchestrator == nil || sm.orchestrator.storage == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
states, err := sm.orchestrator.storage.ListAll()
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, st := range states {
|
||||||
|
if st == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, sm.orchestrator.stateToTransaction(st))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Stop — остановка.
|
// Stop — остановка.
|
||||||
func (sm *SagaManager) Stop() {
|
func (sm *SagaManager) Stop() {
|
||||||
close(sm.stopChan)
|
close(sm.stopChan)
|
||||||
@@ -2721,7 +2612,7 @@ func StopBatchManager() error {
|
|||||||
// СОВМЕСТИМОСТЬ: псевдонимы для старого API
|
// СОВМЕСТИМОСТЬ: псевдонимы для старого API
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// GetTransactionStats — псевдоним для GetBatchStats (обратная совместимость).
|
// GetTransactionStats — псевдоним для GetBatchStats.
|
||||||
func GetTransactionStats() map[string]interface{} {
|
func GetTransactionStats() map[string]interface{} {
|
||||||
return GetBatchStats()
|
return GetBatchStats()
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user