feat(delivery): enforce instance name uniqueness at task creation
Wrap task creation in a transaction that locks the business line row (SELECT ... FOR UPDATE) so concurrent requests cannot both pass the availability check. A name is considered occupied by in-flight or retained tasks (pending through registering, finished, register_failed, canceling, rollback_pending/rolling_back/rollback_failed) and by active deployment results; it becomes reusable only after rollback or explicit cleanup releases it.
This commit is contained in:
@@ -552,7 +552,18 @@ func (s *DeliveryService) CreateTask(ctx context.Context, userID uint64, isAdmin
|
||||
PayloadHash: hex.EncodeToString(digest[:]),
|
||||
IdempotencyKey: idempotencyKey,
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Create(&task).Error; err != nil {
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// Serialize name allocation per business line so concurrent requests cannot
|
||||
// pass the availability check and create the same instance identity.
|
||||
var businessLine model.BusinessLine
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&businessLine, input.BusinessLineID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureInstanceNameAvailable(tx, input.BusinessLineID, task.Component, input.InstanceName); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&task).Error
|
||||
}); err != nil {
|
||||
if lookupErr := s.db.WithContext(ctx).Where("idempotency_key = ?", idempotencyKey).First(&existing).Error; lookupErr == nil {
|
||||
return &existing, true, nil
|
||||
}
|
||||
@@ -567,6 +578,45 @@ func (s *DeliveryService) CreateTask(ctx context.Context, userID uint64, isAdmin
|
||||
return &task, false, nil
|
||||
}
|
||||
|
||||
// ensureInstanceNameAvailable treats in-flight tasks, retained failed deployments,
|
||||
// and active results as name reservations. A name is reusable only after rollback
|
||||
// or explicit cleanup has released the corresponding task/result.
|
||||
func ensureInstanceNameAvailable(tx *gorm.DB, businessLineID uint64, component, instanceName string) error {
|
||||
occupiedTaskStatuses := []string{
|
||||
model.TaskPending,
|
||||
model.TaskValidating,
|
||||
model.TaskDispatching,
|
||||
model.TaskRunning,
|
||||
model.TaskRegistering,
|
||||
model.TaskFinished,
|
||||
model.TaskRegisterFailed,
|
||||
model.TaskCanceling,
|
||||
model.TaskRollbackPending,
|
||||
model.TaskRollingBack,
|
||||
model.TaskRollbackFailed,
|
||||
}
|
||||
var taskCount int64
|
||||
if err := tx.Model(&model.DeliveryTask{}).
|
||||
Where("business_line_id = ? AND component = ? AND instance_name = ? AND status IN ?", businessLineID, component, instanceName, occupiedTaskStatuses).
|
||||
Count(&taskCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if taskCount > 0 {
|
||||
return fmt.Errorf("instance_name %q is already in use by an active or pending delivery", instanceName)
|
||||
}
|
||||
|
||||
var resultCount int64
|
||||
if err := tx.Model(&model.DeploymentResult{}).
|
||||
Where("business_line_id = ? AND component = ? AND instance_name = ? AND status = ?", businessLineID, component, instanceName, "active").
|
||||
Count(&resultCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if resultCount > 0 {
|
||||
return fmt.Errorf("instance_name %q is already in use by a delivered instance", instanceName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 版本白名单与 playbook 的 mysql_package_map 保持同步:
|
||||
// 8.0 走 Ubuntu 自带源,8.4 走 MySQL 官方 APT 源;5.6/5.7 已 EOL 且无 noble 包,不支持。
|
||||
var supportedMySQLVersions = map[string]bool{"8.0": true, "8.4": true}
|
||||
|
||||
Reference in New Issue
Block a user