Controllo della contemporaneità ottimistico (OCC)

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

Ad esempio, considera i sistemi come Google Cloud IAM, dove 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 di entità) nell'oggetto risorsa.

Introduzione a OCC

Supponiamo 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 copia e la riscrive sul server.

  4. Il processo B modifica la 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 digitale univoca che cambia ogni volta che un'entità viene modificata. In molti sistemi (come IAM), questa operazione viene eseguita utilizzando un etag. Il server controlla questo tag su ogni scrittura:

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

  2. Quando invii di nuovo la risorsa modificata, devi includere l'etag originale.

  3. Se il server rileva che l'etag memorizzato non corrisponde all'etag inviato (il che significa che qualcun altro ha modificato la risorsa da quando l'hai letta), l'operazione di scrittura non riesce e viene restituito un errore ABORTED o FAILED_PRECONDITION.

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

Implementare il ciclo OCC

Il nucleo 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 elevata contesa.

Passaggi del ciclo

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

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

Installazione

Per utilizzare questo esempio, aggiungi la seguente dipendenza a 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;
    }
}