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:
Il processo A legge lo stato attuale della risorsa.
Il processo B legge lo stesso stato attuale.
Il processo A modifica la propria copia e la riscrive sul server.
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:
Quando leggi la risorsa, il server restituisce un
etag(un'impronta univoca).Quando restituisci la risorsa modificata, devi includere l'
etagoriginale.Se il server rileva che il
etagmemorizzato non corrisponde aletagche 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 erroreABORTEDoFAILED_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;
}
}