fix: undo locks its history entries and marks them only once (WR-11)
Two concurrent undos of one entry both passed the already-undone check and both restored. Undo and UndoBatch now read the entries FOR UPDATE, and the final mark is conditional on undone_at IS NULL, refusing with already_undone when another undo got there first.
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// The undo refusals (D-13, T-32-27).
|
||||
@@ -68,8 +69,11 @@ func Undoable(r models.Revision) bool {
|
||||
// later change touched the same values (undo_stale, naming who and when),
|
||||
// when the restore would create a redirect loop (undo_loop) or when the
|
||||
// record it belongs to is gone (undo_gone).
|
||||
//
|
||||
// The entry is locked (FOR UPDATE) until tx ends, so a second undo of the
|
||||
// same entry waits and is then refused as already undone (WR-11).
|
||||
func (s *Service) Undo(ctx context.Context, tx *gorm.DB, id uint) ([]uint, error) {
|
||||
rev, err := LoadRevision(tx, id)
|
||||
rev, err := LoadRevision(tx.Clauses(clause.Locking{Strength: "UPDATE"}), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -87,7 +91,7 @@ func (s *Service) Undo(ctx context.Context, tx *gorm.DB, id uint) ([]uint, error
|
||||
// has nothing left to undo.
|
||||
func (s *Service) UndoBatch(ctx context.Context, tx *gorm.DB, batch string) ([]uint, error) {
|
||||
var revs []models.Revision
|
||||
if err := tx.Where("batch_id = ? AND undone_at IS NULL", batch).Order("id DESC").Find(&revs).Error; err != nil {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("batch_id = ? AND undone_at IS NULL", batch).Order("id DESC").Find(&revs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(revs) == 0 {
|
||||
@@ -339,7 +343,14 @@ func (s *Service) undoOne(ctx context.Context, tx *gorm.DB, rev models.Revision)
|
||||
} else if p, ok := mcpPrincipal(ctx); ok && p.ID != 0 {
|
||||
updates["undone_by_id"] = p.ID
|
||||
}
|
||||
return tx.Model(&models.Revision{}).Where("id = ?", rev.ID).Updates(updates).Error
|
||||
marked := tx.Model(&models.Revision{}).Where("id = ? AND undone_at IS NULL", rev.ID).Updates(updates)
|
||||
if marked.Error != nil {
|
||||
return marked.Error
|
||||
}
|
||||
if marked.RowsAffected == 0 {
|
||||
return &UndoRefusal{Code: UndoDone, Key: "golem15.seo::lang.history.undo_done"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// kindLabels are the phrase keys of the history subjects in the undo_gone
|
||||
|
||||
Reference in New Issue
Block a user