v5.0.63: instance-level mutex to prevent parallel operations on same resource
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -36,6 +37,19 @@ type UniversalClient struct {
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
// instanceMutexes — глобальная карта мьютексов для сериализации операций на одном инстансе.
|
||||
// Terraform параллелит ресурсы (до 10), но API Nubes не поддерживает параллельные операции
|
||||
// на одном инстансе (orchestrator error, state_out inconsistency).
|
||||
var instanceMutexes sync.Map
|
||||
|
||||
// LockInstance блокирует мьютекс для указанного instanceUid.
|
||||
// Возвращает функцию unlock, которую нужно вызывать через defer.
|
||||
func (c *UniversalClient) LockInstance(instanceUid string) func() {
|
||||
mu, _ := instanceMutexes.LoadOrStore(instanceUid, &sync.Mutex{})
|
||||
mu.(*sync.Mutex).Lock()
|
||||
return func() { mu.(*sync.Mutex).Unlock() }
|
||||
}
|
||||
|
||||
// isProxyAPI returns true if ApiEndpoint uses legacy ?endpoint= proxy pattern (contains "index.cfm").
|
||||
func (c *UniversalClient) isProxyAPI() bool {
|
||||
return strings.Contains(c.ApiEndpoint, "index.cfm")
|
||||
|
||||
@@ -51,6 +51,9 @@ func UpdateResourceWithTimeout(ctx context.Context, client *core.UniversalClient
|
||||
if strings.TrimSpace(instanceID) == "" {
|
||||
return fmt.Errorf("missing instance id for modify")
|
||||
}
|
||||
unlock := client.LockInstance(instanceID)
|
||||
defer unlock()
|
||||
|
||||
ctxWithTimeout, err := core.WithOperationTimeout(ctx, operationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -70,6 +73,11 @@ func DeleteResourceWithTimeout(ctx context.Context, client *core.UniversalClient
|
||||
mode = "state_only"
|
||||
}
|
||||
|
||||
if mode != "state_only" && mode != "detach" {
|
||||
unlock := client.LockInstance(instanceID)
|
||||
defer unlock()
|
||||
}
|
||||
|
||||
ctxWithTimeout, err := core.WithOperationTimeout(ctx, operationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -121,6 +129,9 @@ func RunOperationByCodeWithTimeout(ctx context.Context, client *core.UniversalCl
|
||||
if client == nil {
|
||||
return fmt.Errorf("missing client for operation")
|
||||
}
|
||||
unlock := client.LockInstance(instanceID)
|
||||
defer unlock()
|
||||
|
||||
ctxWithTimeout, err := core.WithOperationTimeout(ctx, operationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -143,10 +154,13 @@ func adoptExistingInstanceOnCreate(ctx context.Context, client *core.UniversalCl
|
||||
switch state {
|
||||
case StateNotCreated:
|
||||
// P0.2: авто-cleanup orphan-инстанса при adopt_existing_on_create=true.
|
||||
unlock := client.LockInstance(existing.InstanceUid)
|
||||
if err := client.RunInstanceOperationUniversal(ctx, existing.InstanceUid, "delete", nil); err != nil {
|
||||
unlock()
|
||||
return "", fmt.Errorf("не удалось авто-удалить orphan-инстанс %s (статус: %s): %w. %s",
|
||||
existing.InstanceUid, statusText, err, formatInstanceDetails(existing, serviceID, displayName))
|
||||
}
|
||||
unlock()
|
||||
return client.CreateGenericInstanceUniversalV6(ctx, serviceID, displayName, params)
|
||||
|
||||
case StateRunning, StateRunningPending:
|
||||
@@ -167,9 +181,12 @@ func adoptExistingInstanceOnCreate(ctx context.Context, client *core.UniversalCl
|
||||
if len(mismatches) > 0 {
|
||||
return "", fmt.Errorf("required params mismatch for resource_name %s: %s. %s", displayName, FormatRequiredParamMismatchMessage(mismatches), formatInstanceDetails(existing, serviceID, displayName))
|
||||
}
|
||||
unlock := client.LockInstance(existing.InstanceUid)
|
||||
if err := client.RunInstanceOperationUniversal(ctx, existing.InstanceUid, "resume", nil); err != nil {
|
||||
unlock()
|
||||
return "", err
|
||||
}
|
||||
unlock()
|
||||
resumed, err := client.GetInstanceState(ctx, existing.InstanceUid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -83,6 +83,9 @@ func (r *ServiceOperationResource) Create(ctx context.Context, req resource.Crea
|
||||
return
|
||||
}
|
||||
|
||||
unlock := r.client.LockInstance(instanceUID)
|
||||
defer unlock()
|
||||
|
||||
if err := r.client.RunInstanceOperationUniversalByCode(ctx, instanceUID, operation, params); err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||
return
|
||||
@@ -127,6 +130,9 @@ func (r *ServiceOperationResource) Update(ctx context.Context, req resource.Upda
|
||||
return
|
||||
}
|
||||
|
||||
unlock := r.client.LockInstance(instanceUID)
|
||||
defer unlock()
|
||||
|
||||
if err := r.client.RunInstanceOperationUniversalByCode(ctx, instanceUID, operation, params); err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user