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