v5.0.63: instance-level mutex to prevent parallel operations on same resource

This commit is contained in:
“Naeel”
2026-07-07 08:27:42 +04:00
parent d9f2c60e90
commit 4ac21d9367
9 changed files with 184 additions and 9 deletions
+14
View File
@@ -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")
+17
View File
@@ -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