[commit-keeper sha=488f11b] Отсутствие атомарности при создании snapshot-версии #2

Open
opened 2026-04-27 09:13:38 +00:00 by commit-keeper · 0 comments

Severity: major
Commit: 488f11b — alpha
Затронутые места:

  • service/version.go строки 135–156

В чём проблема

Отсутствие атомарности при создании snapshot-версии. Операции CreateVersion и UpdateLatestVersion выполняются без транзакции. Если вторая операция завершится ошибкой, в базе останется 'сиротская' версия, а документ будет ссылаться на старую версию, что нарушает консистентность данных.

Цитата кода

if s.snapshotInterval > 0 && !isSnapshot {
			// считаем количество версий от этого snapshot'а до новой версии
			// простая реализация: пройти вверх по родителям и посчитать
			count := 0
			curr := &newVer
			for {
				count++
				if curr.ParentID == nil {
					break
			}
				parent, err := s.storage.GetParentVersion(curr)
				if err != nil {
					break
			}
				if parent == nil {
					break
			}
				curr = parent
				if curr.IsSnapshot {
						break
			}
			}
			if count >= s.snapshotInterval {
				// делаем snapshot: восстанавливаем полную версию и сохраняем snapshot-версию
				full, err := s.ReconstructVersion(newVer.ID)
				if err == nil {
					snapVer := models.Version{
						ID:         uuid.New(),
					DocumentID: docID,
					ParentID:   &newVer.ID,
					IsSnapshot: true,
					Snapshot:   full,
					CreatedAt:  time.Now(),
				}
					// сохраняем snapshot-версию и обновляем latest_version
					_ = s.storage.CreateVersion(&snapVer)
					_ = s.storage.UpdateLatestVersion(docID, snapVer.ID)
				}
			}
	}

Как исправить

Вариант 1 — Обернуть операции в транзакцию на уровне хранилища или сервиса.

Необходимо добавить метод в IVersionStorage для выполнения операций в рамках одной транзакции, либо передавать объект транзакции в методы.

// В IVersionStorage добавить метод
func (s *GormStorage) WithTransaction(fn func(tx *GormStorage) error) error {
	return s.db.Transaction(func(tx *gorm.DB) error {
		return fn(&GormStorage{db: tx})
	})
}

// В service/version.go
if count >= s.snapshotInterval {
	full, err := s.ReconstructVersion(newVer.ID)
	if err == nil {
		err := s.storage.WithTransaction(func(txStore *store.IVersionStorage) error {
			snapVer := models.Version{
				ID:         uuid.New(),
			DocumentID: docID,
			ParentID:   &newVer.ID,
			IsSnapshot: true,
			Snapshot:   full,
			CreatedAt:  time.Now(),
		}
			if err := txStore.CreateVersion(&snapVer); err != nil {
			return err
		}
			return txStore.UpdateLatestVersion(docID, snapVer.ID)
	})
		if err != nil {
			// log error
		}
	}
	}
}

Автоматическая проверка commit-keeper · модель gemma4:31b · sha=488f11b

**Severity:** major **Commit:** [`488f11b`](https://git.gm6.ru/icewind/seadoc/commit/488f11bd56db4f7e17b4b4bd9bd6e6f4f4a394c5) — alpha **Затронутые места:** - `service/version.go` строки 135–156 ## В чём проблема Отсутствие атомарности при создании snapshot-версии. Операции CreateVersion и UpdateLatestVersion выполняются без транзакции. Если вторая операция завершится ошибкой, в базе останется 'сиротская' версия, а документ будет ссылаться на старую версию, что нарушает консистентность данных. ## Цитата кода ``` if s.snapshotInterval > 0 && !isSnapshot { // считаем количество версий от этого snapshot'а до новой версии // простая реализация: пройти вверх по родителям и посчитать count := 0 curr := &newVer for { count++ if curr.ParentID == nil { break } parent, err := s.storage.GetParentVersion(curr) if err != nil { break } if parent == nil { break } curr = parent if curr.IsSnapshot { break } } if count >= s.snapshotInterval { // делаем snapshot: восстанавливаем полную версию и сохраняем snapshot-версию full, err := s.ReconstructVersion(newVer.ID) if err == nil { snapVer := models.Version{ ID: uuid.New(), DocumentID: docID, ParentID: &newVer.ID, IsSnapshot: true, Snapshot: full, CreatedAt: time.Now(), } // сохраняем snapshot-версию и обновляем latest_version _ = s.storage.CreateVersion(&snapVer) _ = s.storage.UpdateLatestVersion(docID, snapVer.ID) } } } ``` ## Как исправить ### Вариант 1 — Обернуть операции в транзакцию на уровне хранилища или сервиса. Необходимо добавить метод в IVersionStorage для выполнения операций в рамках одной транзакции, либо передавать объект транзакции в методы. ``` // В IVersionStorage добавить метод func (s *GormStorage) WithTransaction(fn func(tx *GormStorage) error) error { return s.db.Transaction(func(tx *gorm.DB) error { return fn(&GormStorage{db: tx}) }) } // В service/version.go if count >= s.snapshotInterval { full, err := s.ReconstructVersion(newVer.ID) if err == nil { err := s.storage.WithTransaction(func(txStore *store.IVersionStorage) error { snapVer := models.Version{ ID: uuid.New(), DocumentID: docID, ParentID: &newVer.ID, IsSnapshot: true, Snapshot: full, CreatedAt: time.Now(), } if err := txStore.CreateVersion(&snapVer); err != nil { return err } return txStore.UpdateLatestVersion(docID, snapVer.ID) }) if err != nil { // log error } } } } ``` --- <sub><sup>Автоматическая проверка commit-keeper · модель `gemma4:31b` · `sha=488f11b`</sup></sub> <!-- commit-keeper -->
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: icewind/seadoc#2
No description provided.