Como trabalhar com o controle de simultaneidade otimista (OCC)

O controle de simultaneidade otimista (OCC, na sigla em inglês) é uma estratégia para gerenciar recursos compartilhados e evitar atualizações perdidas ou disputas quando vários usuários ou processos modificam o mesmo recurso ao mesmo tempo.

Por exemplo, ao trabalhar com o Identity and Access Management, uma política do IAM é um recurso compartilhado anexado a um recurso, como um projeto, bucket ou serviço. Sem o controle de simultaneidade, modificações simultâneas podem substituir mudanças recentes na política, revogando inadvertidamente o acesso legítimo ou concedendo permissões não autorizadas.

Para implementar o OCC, os sistemas normalmente usam um número de versão ou um campo etag (tag de entidade) na estrutura do recurso.

Como funciona o OCC

Suponha 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 dele e a grava de volta no servidor.
  4. O processo B modifica a cópia dele e a grava de volta no servidor.

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

O OCC resolve esse problema introduzindo uma impressão digital exclusiva que muda sempre que uma entidade é modificada. Em muitos sistemas, como o IAM, essa impressão digital é um etag. O servidor verifica essa tag em cada gravação:

  1. Quando você lê 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 etag armazenado não corresponder ao etag enviado, o que significa que outro processo modificou o recurso depois que você o leu, a operação de gravação vai falhar com um erro ABORTED ou FAILED_PRECONDITION.

Essa falha sinaliza ao cliente para repetir todo o processo: ler novamente o estado atualizado, aplicar novamente as alterações e tentar a gravação novamente com o novo etag.

Implementar o loop de OCC

O núcleo de uma implementação de OCC é um loop de repetição que lê, modifica e tenta salvar o recurso, saindo após uma gravação bem-sucedida. Para evitar loops infinitos durante alta contenção, configure um número máximo razoável de novas tentativas.

Etapas do loop

A tabela a seguir descreve cada etapa em um loop de repetição de OCC:

Etapa Ação Exemplo de implementação
Read Busque o estado atual do recurso, incluindo o etag.
let mut policy = client
    .get_iam_policy()
    .set_resource(&secret_name)
    .send()
    .await?;
Modificar Aplique as mudanças à struct local.
policy.bindings.push(new_binding);
Escrever e verificar Tente salvar o recurso modificado usando o etag atual e verifique se há códigos do erro de simultaneidade.
match client
    .set_iam_policy()
    .set_resource(&secret_name)
    .set_policy(policy)
    .send()
    .await
{
    Ok(p) => return Ok(p),
    Err(e) => { /* ... */ }
}
Sucesso ou nova tentativa Se a gravação for bem-sucedida, saia do loop. Se falhar com um erro de simultaneidade, incremente o contador de novas tentativas e continue o loop na etapa Read. -

Exemplo

O exemplo a seguir demonstra como implementar um loop de OCC que atualiza uma política do IAM em um recurso de secret do Secret Manager concedendo a um membro o papel roles/secretmanager.secretAccessor.

Para executar este exemplo, primeiro declare a dependência no arquivo Cargo.toml:

cargo add google-cloud-secretmanager-v1

Em seguida, implemente o loop de repetição do OCC:

const ROLE: &str = "roles/secretmanager.secretAccessor";
const ATTEMPTS: u32 = 5;

let secret_name = format!("projects/{project_id}/secrets/{secret_id}");
let client = SecretManagerService::builder().build().await?;
for _attempt in 0..ATTEMPTS {
    let mut current = client
        .get_iam_policy()
        .set_resource(&secret_name)
        .send()
        .await?;

    match current.bindings.iter_mut().find(|b| b.role == ROLE) {
        None => current
            .bindings
            .push(Binding::new().set_role(ROLE).set_members([member])),
        Some(b) => {
            if b.members.iter().any(|m| m == member) {
                return Ok(current);
            }
            b.members.push(member.to_string());
        }
    };
    let updated = client
        .set_iam_policy()
        .set_resource(&secret_name)
        .set_policy(current)
        .set_update_mask(FieldMask::default().set_paths(["bindings"]))
        .send()
        .await;
    match updated {
        Ok(p) => return Ok(p),
        Err(e)
            if e.status().is_some_and(|s| {
                s.code == Code::Aborted || s.code == Code::FailedPrecondition
            }) =>
        {
            continue;
        }
        Err(e) => return Err(e.into()),
    }
}
anyhow::bail!("could not set IAM policy after {ATTEMPTS} attempts")

Preencher código

O código a seguir mostra a função completa com todas as importações necessárias:

use google_cloud_gax::error::rpc::Code;
use google_cloud_iam_v1::model::Binding;
use google_cloud_iam_v1::model::Policy;
use google_cloud_secretmanager_v1::client::SecretManagerService;
use google_cloud_wkt::FieldMask;

/// Executes an Optimistic Concurrency Control (OCC) loop to safely update a resource.
///
/// This function demonstrates the core Read-Modify-Write-Retry pattern. It uses the secret manager
/// service and a hard-coded role. The principles apply to any other service or role.
///
/// # Parameters
/// * `project_id` The Google Cloud Project ID (e.g., "my-project-123").
/// * `secret_id` The Google Cloud Project ID (e.g., "my-secret").
/// * `member` The member to add (e.g., "user:user@example.com").
///
/// # Returns
/// The new IAM policy.
pub async fn sample(project_id: &str, secret_id: &str, member: &str) -> anyhow::Result<Policy> {
    const ROLE: &str = "roles/secretmanager.secretAccessor";
    const ATTEMPTS: u32 = 5;

    let secret_name = format!("projects/{project_id}/secrets/{secret_id}");
    let client = SecretManagerService::builder().build().await?;
    for _attempt in 0..ATTEMPTS {
        let mut current = client
            .get_iam_policy()
            .set_resource(&secret_name)
            .send()
            .await?;

        match current.bindings.iter_mut().find(|b| b.role == ROLE) {
            None => current
                .bindings
                .push(Binding::new().set_role(ROLE).set_members([member])),
            Some(b) => {
                if b.members.iter().any(|m| m == member) {
                    return Ok(current);
                }
                b.members.push(member.to_string());
            }
        };
        let updated = client
            .set_iam_policy()
            .set_resource(&secret_name)
            .set_policy(current)
            .set_update_mask(FieldMask::default().set_paths(["bindings"]))
            .send()
            .await;
        match updated {
            Ok(p) => return Ok(p),
            Err(e)
                if e.status().is_some_and(|s| {
                    s.code == Code::Aborted || s.code == Code::FailedPrecondition
                }) =>
            {
                continue;
            }
            Err(e) => return Err(e.into()),
        }
    }
    anyhow::bail!("could not set IAM policy after {ATTEMPTS} attempts")
}