Optimistic concurrency control (OCC)

Optimistic Concurrency Control (OCC) is a strategy used to manage shared resources and prevent "lost updates" or race conditions when multiple users or processes attempt to modify the same resource simultaneously.

As an example, consider systems like Google Cloud IAM, where the shared resource is an IAM Policy applied to a resource (like a Project, Bucket, or Service). To implement OCC, systems typically use a version number or an etag (entity tag) field on the resource object.

Introduction to OCC

Imagine two processes, A and B, try to update a shared resource at the same time:

  1. Process A reads the current state of the resource.

  2. Process B reads the same current state.

  3. Process A modifies its copy and writes it back to the server.

  4. Process B modifies its copy and writes it back to the server.

Because Process B overwrites the resource without knowing that Process A already changed it, Process A's updates are lost.

OCC solves this by introducing a unique fingerprint which changes every time a entity is modified. In many systems (like IAM), this is done using an etag. The server checks this tag on every write:

  1. When you read the resource, the server returns an etag (a unique fingerprint).

  2. When you send the modified resource back, you must include the original etag.

  3. If the server finds that the stored etag does not match the etag you sent (meaning someone else modified the resource since you read it), the write operation fails with an ABORTED or FAILED_PRECONDITION error.

This failure forces the client to retry the entire process—re-read the new state, re-apply the changes, and try the write again with the new etag.

Implement the OCC Loop

The core of the OCC implementation is a while loop that handles the retry logic. Set a reasonable maximum number of retries to prevent infinite loops in cases of high contention.

Steps of the Loop

Step Action Implementation Example
Read Fetch the current resource state, including the etag. Policy policy = client.getIamPolicy(resourceName);
Modify Apply the changes to the local object. policy = policy.toBuilder().addBinding(newBinding).build();
Write/Check Attempt to save the modified resource using the old etag. This action must be inside a try block. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
Success/Retry If the write succeeds, exit the loop. If it fails with a concurrency error, increment the retry counter and continue the loop (go back to the Read step).

The following file provides a runnable example of how to implement the OCC loop using an IAM policy on a Project resource as the target.

Installation

To use this example, add the following dependency to your pom.xml:

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

Example

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