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 a 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.
Debido a que 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 mediante la introducción de una huella digital única que cambia cada vez que se modifica una entidad. En muchos sistemas (como IAM), esto se hace con una etag. El servidor verifica esta etiqueta en cada escritura:
Cuando lees el recurso, el servidor muestra una
etag(una huella digital única).Cuando envías el recurso modificado, debes incluir la
etagoriginal.Si el servidor descubre que la
etagalmacenada no coincide con laetagque 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.
Esta falla 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 la nueva etag.
Implementa el bucle OCC
El núcleo de la implementación de OCC es un bucle while que controla la lógica de reintento. 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, incluida la etag. |
Policy policy = client.getIamPolicy(resourceName); |
| Modificar | Aplica los cambios al objeto local. | policy = policy.toBuilder().addBinding(newBinding).build(); |
| Escritura/Verificación | Intenta guardar el recurso modificado con la 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 con el bucle (vuelve al paso de lectura). |
En el siguiente archivo, se proporciona un ejemplo ejecutable de cómo implementar el bucle 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;
}
}