Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 49 additions & 11 deletions management/server/store/sql_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,7 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met
conns = runtime.NumCPU()
}

switch storeEngine {
case types.MysqlStoreEngine:
if err := db.Exec("SET GLOBAL FOREIGN_KEY_CHECKS = 0").Error; err != nil {
return nil, err
}
case types.SqliteStoreEngine:
if storeEngine == types.SqliteStoreEngine {
if err == nil {
log.WithContext(ctx).Warnf("setting NB_SQL_MAX_OPEN_CONNS is not supported for sqlite, using default value 1")
}
Expand Down Expand Up @@ -162,7 +157,7 @@ func (s *SqlStore) SaveAccount(ctx context.Context, account *types.Account) erro
group.StoreGroupPeers()
}

err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(func(tx *gorm.DB) error {
result := tx.Select(clause.Associations).Delete(account.Policies, "account_id = ?", account.Id)
if result.Error != nil {
return result.Error
Expand Down Expand Up @@ -257,7 +252,7 @@ func (s *SqlStore) checkAccountDomainBeforeSave(ctx context.Context, accountID,
func (s *SqlStore) DeleteAccount(ctx context.Context, account *types.Account) error {
start := time.Now()

err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(func(tx *gorm.DB) error {
result := tx.Select(clause.Associations).Delete(account.Policies, "account_id = ?", account.Id)
if result.Error != nil {
return result.Error
Expand Down Expand Up @@ -307,7 +302,7 @@ func (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.
peerCopy := peer.Copy()
peerCopy.AccountID = accountID

err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(func(tx *gorm.DB) error {
// check if peer exists before saving
var peerID string
result := tx.Model(&nbpeer.Peer{}).Select("id").Take(&peerID, accountAndIDQueryCondition, accountID, peer.ID)
Expand Down Expand Up @@ -596,7 +591,7 @@ func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStre
}

func (s *SqlStore) DeleteUser(ctx context.Context, accountID, userID string) error {
err := s.db.Transaction(func(tx *gorm.DB) error {
err := s.transaction(func(tx *gorm.DB) error {
result := tx.Delete(&types.PersonalAccessToken{}, "user_id = ?", userID)
if result.Error != nil {
return result.Error
Expand Down Expand Up @@ -1735,13 +1730,31 @@ func (s *SqlStore) ExecuteInTransaction(ctx context.Context, operation func(stor
if tx.Error != nil {
return tx.Error
}

// For MySQL, disable FK checks within this transaction to avoid deadlocks
// This is session-scoped and doesn't require SUPER privileges
if s.storeEngine == types.MysqlStoreEngine {
if err := tx.Exec("SET FOREIGN_KEY_CHECKS = 0").Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to disable FK checks: %w", err)
}
}

repo := s.withTx(tx)
err := operation(repo)
if err != nil {
tx.Rollback()
return err
}

// Re-enable FK checks before commit (optional, as transaction end resets it)
if s.storeEngine == types.MysqlStoreEngine {
if err := tx.Exec("SET FOREIGN_KEY_CHECKS = 1").Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to re-enable FK checks: %w", err)
}
}

err = tx.Commit().Error

log.WithContext(ctx).Tracef("transaction took %v", time.Since(startTime))
Expand All @@ -1759,6 +1772,31 @@ func (s *SqlStore) withTx(tx *gorm.DB) Store {
}
}

// transaction wraps a GORM transaction with MySQL-specific FK checks handling
// Use this instead of db.Transaction() directly to avoid deadlocks on MySQL/Aurora
func (s *SqlStore) transaction(fn func(*gorm.DB) error) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// For MySQL, disable FK checks within this transaction to avoid deadlocks
// This is session-scoped and doesn't require SUPER privileges
if s.storeEngine == types.MysqlStoreEngine {
if err := tx.Exec("SET FOREIGN_KEY_CHECKS = 0").Error; err != nil {
return fmt.Errorf("failed to disable FK checks: %w", err)
}
}

err := fn(tx)

// Re-enable FK checks before commit (optional, as transaction end resets it)
if s.storeEngine == types.MysqlStoreEngine && err == nil {
if fkErr := tx.Exec("SET FOREIGN_KEY_CHECKS = 1").Error; fkErr != nil {
return fmt.Errorf("failed to re-enable FK checks: %w", fkErr)
}
}

return err
})
}

func (s *SqlStore) GetDB() *gorm.DB {
return s.db
}
Expand Down Expand Up @@ -2015,7 +2053,7 @@ func (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error {
}

func (s *SqlStore) DeletePolicy(ctx context.Context, accountID, policyID string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.transaction(func(tx *gorm.DB) error {
if err := tx.Where("policy_id = ?", policyID).Delete(&types.PolicyRule{}).Error; err != nil {
return fmt.Errorf("delete policy rules: %w", err)
}
Expand Down
Loading