Optimistische Nebenläufigkeitserkennung (Optimistic Concurrency Control, OCC)

Die optimistische Gleichzeitigkeitserkennung (Optimistic Concurrency Control, OCC) ist eine Strategie, die verwendet wird, um gemeinsam genutzte Ressourcen zu verwalten und „verlorene Aktualisierungen“ oder Race-Bedingungen zu verhindern, wenn mehrere Nutzer oder Prozesse versuchen, dieselbe Ressource gleichzeitig zu ändern.

Ein Beispiel dafür sind Systeme wie Google Cloud IAM, bei denen die freigegebene Ressource eine IAM-Richtlinie ist, die auf eine Ressource (z. B. ein Projekt, ein Bucket oder ein Dienst) angewendet wird. Zur Implementierung von OCC verwenden Systeme in der Regel eine Versionsnummer oder ein etag-Feld (Entity-Tag) für das Ressourcenobjekt.

Einführung in OCC

Stellen Sie sich vor, zwei Prozesse, A und B, versuchen, eine freigegebene Ressource gleichzeitig zu aktualisieren:

  1. Bei Prozess A wird der aktuelle Status der Ressource gelesen.

  2. Prozess B liest den gleichen aktuellen Status.

  3. Prozess A ändert seine Kopie und schreibt sie zurück auf den Server.

  4. Prozess B ändert seine Kopie und schreibt sie zurück auf den Server.

Da Prozess B die Ressource überschreibt, ohne zu wissen, dass Prozess A sie bereits geändert hat, gehen die Aktualisierungen von Prozess A verloren.

OCC löst dieses Problem, indem ein eindeutiger Fingerabdruck eingeführt wird, der sich jedes Mal ändert, wenn eine Einheit geändert wird. In vielen Systemen (z. B. IAM) erfolgt dies über eine etag. Der Server prüft dieses Tag bei jedem Schreibvorgang:

  1. Wenn Sie die Ressource lesen, gibt der Server einen etag (einen eindeutigen Fingerabdruck) zurück.

  2. Wenn Sie die geänderte Ressource zurücksenden, müssen Sie die ursprüngliche etag angeben.

  3. Wenn der Server feststellt, dass die gespeicherte etag nicht mit der von Ihnen gesendeten etag übereinstimmt (d. h., dass jemand anderes die Ressource geändert hat, seit Sie sie gelesen haben), schlägt der Schreibvorgang mit einem ABORTED- oder FAILED_PRECONDITION-Fehler fehl.

Dieser Fehler zwingt den Client, den gesamten Vorgang zu wiederholen: Er muss den neuen Status noch einmal lesen, die Änderungen noch einmal anwenden und den Schreibvorgang mit dem neuen etag noch einmal versuchen.

OCC-Schleife implementieren

Das Herzstück der OCC-Implementierung ist eine while-Schleife, die die Logik für Wiederholungsversuche verarbeitet. Legen Sie eine angemessene maximale Anzahl von Wiederholungsversuchen fest, um Endlosschleifen bei hoher Auslastung zu vermeiden.

Schritte des Loops

Step Aktion Beispiel für die Implementierung
Lesen Rufen Sie den aktuellen Ressourcenstatus ab, einschließlich des etag. Policy policy = client.getIamPolicy(resourceName);
Ändern Wenden Sie die Änderungen auf das lokale Objekt an. policy = policy.toBuilder().addBinding(newBinding).build();
Schreiben/Prüfen Versuchen Sie, die geänderte Ressource mit dem alten etag zu speichern. Diese Aktion muss sich in einem try-Block befinden. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
Erfolg/Wiederholen Wenn der Schreibvorgang erfolgreich ist, beenden Sie die Schleife. Wenn ein Fehler aufgrund von Parallelität auftritt, erhöhen Sie den Zähler für Wiederholungsversuche und setzen Sie die Schleife fort (gehen Sie zurück zum Schritt „Lesen“).

Die folgende Datei enthält ein ausführbares Beispiel für die Implementierung des OCC-Loops mit einer IAM-Richtlinie für eine Projektressource als Ziel.

Installation

Wenn Sie dieses Beispiel verwenden möchten, fügen Sie Ihrer pom.xml die folgende Abhängigkeit hinzu:

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

Beispiel

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