Controllo della contemporaneità ottimistico (OCC)

Il controllo della concorrenza ottimistico (OCC) è una strategia utilizzata per gestire le risorse condivise e impedire "aggiornamenti persi" o condizioni di competizione quando più utenti o processi tentano di modificare la stessa risorsa contemporaneamente.

Ad esempio, considera sistemi come Google Cloud IAM, in cui la risorsa condivisa è un criterio IAM applicato a una risorsa (come un progetto, un bucket o un servizio). Per implementare OCC, i sistemi in genere utilizzano un numero di versione o un campo etag (tag entità) nell'oggetto risorsa.

Introduzione a OCC

Immagina che due processi, A e B, tentino di aggiornare una risorsa condivisa contemporaneamente:

  1. Il processo A legge lo stato attuale della risorsa.

  2. Il processo B legge lo stesso stato attuale.

  3. Il processo A modifica la propria copia e la riscrive sul server.

  4. Il processo B modifica la propria copia e la riscrive sul server.

Poiché il processo B sovrascrive la risorsa senza sapere che il processo A l'ha già modificata, gli aggiornamenti del processo A vengono persi.

OCC risolve questo problema introducendo un'impronta univoca che cambia ogni volta che viene modificata un'entità. In molti sistemi (come IAM), questa operazione viene eseguita utilizzando un etag. Il server controlla questo tag a ogni scrittura:

  1. Quando leggi la risorsa, il server restituisce un etag (un'impronta univoca).

  2. Quando restituisci la risorsa modificata, devi includere l'etag originale.

  3. Se il server rileva che il etag memorizzato non corrisponde al etag che hai inviato (il che significa che qualcun altro ha modificato la risorsa dopo che l'hai letta), l'operazione di scrittura non va a buon fine e viene restituito un errore ABORTED o FAILED_PRECONDITION.

Questo errore costringe il client a riprovare l'intera procedura: rileggere lo stato nuovo, riapplicare le modifiche e riprovare a scrivere con il nuovo etag.

Implementa il ciclo OCC

Il fulcro dell'implementazione di OCC è un ciclo while che gestisce la logica di ripetizione. Imposta un numero massimo ragionevole di tentativi per evitare loop infiniti in caso di contesa elevata.

Passaggi del loop

Step Azione Esempio di implementazione
Leggi Recupera lo stato attuale della risorsa, incluso etag. Policy policy = client.getIamPolicy(resourceName);
Modifica Applica le modifiche all'oggetto locale. policy = policy.toBuilder().addBinding(newBinding).build();
Scrittura/Controllo Tentativo di salvare la risorsa modificata utilizzando il vecchio etag. Questa azione deve trovarsi all'interno di un blocco try. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
Riuscito/Riprova Se la scrittura ha esito positivo, esci dal ciclo. Se non riesce e restituisce un errore di concorrenza, incrementa il contatore dei tentativi e continua il ciclo (torna al passaggio Read).

Il seguente file fornisce un esempio eseguibile di come implementare il ciclo OCC utilizzando un criterio IAM su una risorsa Project come target.

Installazione

Per utilizzare questo esempio, aggiungi la seguente dipendenza al tuo pom.xml:

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

Esempio

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