Controle de simultaneidade otimista (OCC)

O controle de simultaneidade otimista (OCC, na sigla em inglês) é uma estratégia usada para gerenciar recursos compartilhados e evitar "atualizações perdidas" ou condições de disputa quando vários usuários ou processos tentam modificar o mesmo recurso simultaneamente.

Por exemplo, considere sistemas como o IAM Google Cloud , em que o recurso compartilhado é uma política do IAM aplicada a um recurso (como um projeto, bucket ou serviço). Para implementar o OCC, os sistemas geralmente usam um número de versão ou um campo etag (tag de entidade) no objeto de recurso.

Introdução ao OCC

Imagine que dois processos, A e B, tentem atualizar um recurso compartilhado ao mesmo tempo:

  1. O processo A lê o estado atual do recurso.

  2. O processo B lê o mesmo estado atual.

  3. O processo A modifica a cópia e a grava de volta no servidor.

  4. O processo B modifica a cópia e a grava de volta no servidor.

Como o processo B substitui o recurso sem saber que o processo A já o mudou, as atualizações do processo A são perdidas.

O OCC resolve isso introduzindo uma impressão digital exclusiva que muda sempre que uma entidade é modificada. Em muitos sistemas (como o IAM), isso é feito usando um etag. O servidor verifica essa tag em todas as gravações:

  1. Ao ler o recurso, o servidor retorna um etag (uma impressão digital exclusiva).

  2. Ao enviar o recurso modificado de volta, inclua o etag original.

  3. Se o servidor descobrir que o etag armazenado não corresponde ao etag que você enviou (ou seja, outra pessoa modificou o recurso desde que você o leu), a operação de gravação vai falhar com um erro ABORTED ou FAILED_PRECONDITION.

Essa falha força o cliente a tentar novamente todo o processo: ler novamente o novo estado, reaplicar as mudanças e tentar gravar de novo com o novo etag.

Implementar o loop de OCC

O núcleo da implementação do OCC é um loop while que processa a lógica de nova tentativa. Defina um número máximo razoável de novas tentativas para evitar loops infinitos em casos de alta disputa.

Etapas do loop

Etapa Ação Exemplo de implementação
Read Busque o estado atual do recurso, incluindo o etag. Policy policy = client.getIamPolicy(resourceName);
Modificar Aplique as mudanças ao objeto local. policy = policy.toBuilder().addBinding(newBinding).build();
Gravar/Verificar Tente salvar o recurso modificado usando o etag antigo. Essa ação precisa estar dentro de um bloco try. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
Sucesso/Tentar de novo Se a gravação for bem-sucedida, saia do loop. Se houver uma falha com um erro de simultaneidade, incremente o contador de novas tentativas e continue o loop (volte para a etapa de leitura).

O arquivo a seguir fornece um exemplo executável de como implementar o loop da OCC usando uma política do IAM em um recurso de projeto como destino.

Instalação

Para usar este exemplo, adicione a seguinte dependência ao seu pom.xml:

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

Exemplo

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