Control de simultaneidad optimista (OCC)

El control de simultaneidad optimista (OCC) es una estrategia que se usa para administrar recursos compartidos y evitar "actualizaciones perdidas" o condiciones de carrera cuando varios usuarios o procesos intentan modificar el mismo recurso de forma simultánea.

Por ejemplo, considera sistemas como Google Cloud IAM, en los que el recurso compartido es una política de IAM aplicada a un recurso (como un proyecto, un bucket o un servicio). Para implementar OCC, los sistemas suelen usar un número de versión o un campo etag (etiqueta de entidad) en el objeto de recurso.

Introducción al OCC

Imagina que dos procesos, A y B, intentan actualizar un recurso compartido al mismo tiempo:

  1. El proceso A lee el estado actual del recurso.

  2. El proceso B lee el mismo estado actual.

  3. El proceso A modifica su copia y la vuelve a escribir en el servidor.

  4. El proceso B modifica su copia y la vuelve a escribir en el servidor.

Como el proceso B sobrescribe el recurso sin saber que el proceso A ya lo cambió, las actualizaciones del proceso A se pierden.

OCC resuelve este problema introduciendo una huella digital única que cambia cada vez que se modifica una entidad. En muchos sistemas (como IAM), esto se hace con un etag. El servidor verifica esta etiqueta en cada escritura:

  1. Cuando lees el recurso, el servidor devuelve un etag (una huella digital única).

  2. Cuando envíes el recurso modificado, debes incluir el etag original.

  3. Si el servidor detecta que el etag almacenado no coincide con el etag que enviaste (lo que significa que otra persona modificó el recurso desde que lo leíste), la operación de escritura falla con un error ABORTED o FAILED_PRECONDITION.

Este error obliga al cliente a volver a intentar todo el proceso: volver a leer el estado nuevo, volver a aplicar los cambios y volver a intentar la escritura con el nuevo etag.

Implementa el bucle de OCC

El núcleo de la implementación de OCC es un bucle while que controla la lógica de reintentos. Establece una cantidad máxima razonable de reintentos para evitar bucles infinitos en casos de alta contención.

Pasos del bucle

Step Acción Ejemplo de implementación
Lectura Recupera el estado actual del recurso, incluido el etag. Policy policy = client.getIamPolicy(resourceName);
Modificar Aplica los cambios al objeto local. policy = policy.toBuilder().addBinding(newBinding).build();
Escribir/Verificar Intenta guardar el recurso modificado con el etag anterior. Esta acción debe estar dentro de un bloque try. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
Éxito/Reintento Si la escritura se realiza correctamente, sal del bucle. Si falla con un error de simultaneidad, incrementa el contador de reintentos y continúa el bucle (vuelve al paso de lectura).

En el siguiente archivo, se proporciona un ejemplo ejecutable de cómo implementar el bucle de OCC con una política de IAM en un recurso de proyecto como destino.

Instalación

Para usar este ejemplo, agrega la siguiente dependencia a tu pom.xml:

<dependency>
  <groupId>com.google.cloud</groupId>
  <artifactId>google-cloud-resourcemanager</artifactId>
  <version>1.45.0</version>
</dependency>

Ejemplo

import com.google.api.gax.rpc.AbortedException;
import com.google.api.gax.rpc.FailedPreconditionException;
import com.google.cloud.resourcemanager.v3.ProjectName;
import com.google.cloud.resourcemanager.v3.ProjectsClient;
import com.google.iam.v1.Binding;
import com.google.iam.v1.GetIamPolicyRequest;
import com.google.iam.v1.Policy;
import com.google.iam.v1.SetIamPolicyRequest;
import java.util.ArrayList;
import java.util.List;

public class IamOccExample {

    /**
     * Executes an Optimistic Concurrency Control (OCC) loop to safely update a resource.
     *
     * This method demonstrates the core Read-Modify-Write-Retry pattern.
     *
     * @param projectId  The Google Cloud Project ID (e.g., "my-project-123").
     * @param role       The IAM role to grant (e.g., "roles/storage.objectAdmin").
     * @param member     The member to add (e.g., "user:user@example.com").
     * @param maxRetries The maximum number of times to retry the update.
     * @return The successfully updated IAM policy (or null on failure).
     */
    public static Policy updateIamPolicyWithOcc(
            String projectId,
            String role,
            String member,
            int maxRetries
    ) throws Exception {
        // Setup Client
        try (ProjectsClient projectsClient = ProjectsClient.create()) {
            String projectName = ProjectName.of(projectId).toString();
            int retries = 0;

            // START OCC LOOP (Read-Modify-Write-Retry)
            while (retries < maxRetries) {
                try {
                    // READ: Get the current policy. This includes the current etag.
                    System.out.printf("Attempt %d: Reading current IAM policy for %s...%n", retries, projectName);
                    GetIamPolicyRequest getIamPolicyRequest = GetIamPolicyRequest.newBuilder()
                            .setResource(projectName)
                            .build();
                    Policy policy = projectsClient.getIamPolicy(getIamPolicyRequest);

                    // MODIFY: Apply the desired changes to the local Policy object.
                    List<Binding> bindings = new ArrayList<>(policy.getBindingsList());
                    Binding targetBinding = null;
                    int bindingIndex = -1;

                    for (int i = 0; i < bindings.size(); i++) {
                        if (bindings.get(i).getRole().equals(role)) {
                            targetBinding = bindings.get(i);
                            bindingIndex = i;
                            break;
                        }
                    }

                    if (targetBinding != null) {
                        if (targetBinding.getMembersList().contains(member)) {
                            System.out.printf("Policy for role %s and member %s exists already!%n", role, member);
                            return policy;
                        }
                        // Create a new binding based on existing one to add the member
                        Binding updatedBinding = targetBinding.toBuilder()
                                .addMembers(member)
                                .build();
                        bindings.set(bindingIndex, updatedBinding);
                    } else {
                        // Role not found, create a new binding
                        Binding newBinding = Binding.newBuilder()
                                .setRole(role)
                                .addMembers(member)
                                .build();
                        bindings.add(newBinding);
                    }

                    // The policy builder now contains the modified bindings AND the original etag.
                    Policy updatedPolicy = policy.toBuilder()
                            .clearBindings()
                            .addAllBindings(bindings)
                            .build();

                    // WRITE/CHECK: Attempt to write the modified policy.
                    System.out.printf("Attempt %d: Setting modified IAM policy...%n", retries);
                    SetIamPolicyRequest setIamPolicyRequest = SetIamPolicyRequest.newBuilder()
                            .setResource(projectName)
                            .setPolicy(updatedPolicy)
                            .build();
                    Policy resultPolicy = projectsClient.setIamPolicy(setIamPolicyRequest);

                    // SUCCESS: If the call succeeds, return the new policy and exit the loop.
                    System.out.printf("Successfully updated IAM policy in attempt %d.%n", retries);
                    return resultPolicy;

                } catch (AbortedException | FailedPreconditionException e) {
                    // If the etag is stale (concurrency conflict), this will throw a retryable exception.
                    retries++;
                    System.out.printf("Concurrency conflict detected (etag mismatch). Retrying... (%d/%d)%n",
                            retries, maxRetries);

                    // Exponential backoff (100ms * retry count)
                    Thread.sleep(100L * retries);
                }
            }
            // END OCC LOOP
        }

        System.out.printf("Failed to update IAM policy after %d attempts due to persistent concurrency conflicts.%n", maxRetries);
        return null;
    }
}