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:
El proceso A lee el estado actual del recurso.
El proceso B lee el mismo estado actual.
El proceso A modifica su copia y la vuelve a escribir en el servidor.
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:
Cuando lees el recurso, el servidor devuelve un
etag(una huella digital única).Cuando envíes el recurso modificado, debes incluir el
etagoriginal.Si el servidor detecta que el
etagalmacenado no coincide con eletagque enviaste (lo que significa que otra persona modificó el recurso desde que lo leíste), la operación de escritura falla con un errorABORTEDoFAILED_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;
}
}