Contrôle de simultanéité optimiste (OCC)

Le contrôle d'accès simultané optimiste (OCC, Optimistic Concurrency Control) est une stratégie utilisée pour gérer les ressources partagées et éviter les "mises à jour perdues" ou les conditions de concurrence lorsque plusieurs utilisateurs ou processus tentent de modifier la même ressource simultanément.

Par exemple, considérez les systèmes comme Google Cloud IAM, où la ressource partagée est une stratégie IAM appliquée à une ressource (comme un projet, un bucket ou un service). Pour implémenter l'OCC, les systèmes utilisent généralement un numéro de version ou un champ etag (balise d'entité) sur l'objet de ressource.

Présentation de l'OCC

Imaginez que deux processus, A et B, tentent de mettre à jour une ressource partagée en même temps :

  1. Le processus A lit l'état actuel de la ressource.

  2. Le processus B lit le même état actuel.

  3. Le processus A modifie sa copie et la réécrit sur le serveur.

  4. Le processus B modifie sa copie et la réécrit sur le serveur.

Étant donné que le processus B écrase la ressource sans savoir que le processus A l'a déjà modifiée, les mises à jour du processus A sont perdues.

L'OCC résout ce problème en introduisant une empreinte unique qui change chaque fois qu'une entité est modifiée. Dans de nombreux systèmes (comme IAM), cela se fait à l'aide d'un etag. Le serveur vérifie cette balise à chaque écriture :

  1. Lorsque vous lisez la ressource, le serveur renvoie un etag (une empreinte unique).

  2. Lorsque vous renvoyez la ressource modifiée, vous devez inclure l'etag d'origine.

  3. Si le serveur constate que l'etag stocké ne correspond pas à l'etag que vous avez envoyé (ce qui signifie que quelqu'un d'autre a modifié la ressource depuis que vous l'avez lue), l'opération d'écriture échoue avec une erreur ABORTED ou FAILED_PRECONDITION.

Cet échec oblige le client à réessayer l'ensemble du processus : relire le nouvel état, réappliquer les modifications et réessayer l'écriture avec le nouvel etag.

Implémenter la boucle OCC

Le cœur de l'implémentation de l'OCC est une boucle while qui gère la logique de nouvelle tentative. Définissez un nombre maximal raisonnable de nouvelles tentatives pour éviter les boucles infinies en cas de forte contention.

Étapes de la boucle

Step Action Exemple d'implémentation
Read (Lire) Récupérez l'état actuel de la ressource, y compris l'etag. Policy policy = client.getIamPolicy(resourceName);
Modifier Appliquez les modifications à l'objet local. policy = policy.toBuilder().addBinding(newBinding).build();
Write/Check (Écrire/Vérifier) Tentez d'enregistrer la ressource modifiée à l'aide de l'ancien etag. Cette action doit se trouver dans un bloc try. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
Success/Retry (Succès/Nouvelle tentative) Si l'écriture réussit, quittez la boucle. En cas d'échec avec une erreur de simultanéité, incrémentez le compteur de nouvelles tentatives et poursuivez la boucle (revenez à l'étape de lecture).

Le fichier suivant fournit un exemple exécutable d'implémentation de la boucle OCC à l'aide d'une stratégie IAM sur une ressource de projet comme cible.

Installation

Pour utiliser cet exemple, ajoutez la dépendance suivante à votre fichier pom.xml :

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

Exemple

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;
    }
}