Utilizzo del controllo della contemporaneità ottimistica (OCC)

Controllo della contemporaneità ottimistico (OCC) è una strategia per la gestione delle risorse condivise e la prevenzione di aggiornamenti persi o race condition quando più utenti o processi modificano la stessa risorsa contemporaneamente.

Ad esempio, quando utilizzi Identity and Access Management, una policy IAM è una risorsa condivisa collegata a una risorsa come un progetto, un bucket o un servizio. Senza controllo della contemporaneità, le modifiche simultanee possono sovrascrivere le modifiche recenti alle policy, revocando inavvertitamente l'accesso legittimo o concedendo autorizzazioni non autorizzate.

Per implementare l'OCC, i sistemi in genere utilizzano un numero di versione o un campo etag (tag di entità) nella struttura della risorsa.

Come funziona OCC

Supponiamo che due processi, A e B, tentino di aggiornare una risorsa condivisa contemporaneamente:

  1. Il processo A legge lo stato attuale della risorsa.
  2. Il processo B legge lo stesso stato attuale.
  3. Il processo A modifica la sua copia e la riscrive sul server.
  4. Il processo B modifica la sua 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 vanno persi.

L'OCC risolve questo problema introducendo una fingerprint univoca che cambia ogni volta che un'entità viene modificata. In molti sistemi, come IAM, questa fingerprint è un etag. Il server controlla questo tag a ogni scrittura:

  1. Quando leggi la risorsa, il server restituisce un etag (un fingerprint univoco).
  2. Quando invii la risorsa modificata, devi includere l'etag originale.
  3. Se il valore etag archiviato non corrisponde al valore etag che hai inviato, ovvero un altro processo ha modificato la risorsa dopo che l'hai letta, l'operazione di scrittura non va a buon fine e viene restituito un errore ABORTED o FAILED_PRECONDITION.

Questo errore indica al client di riprovare l'intero processo: rileggere lo stato aggiornato, riapplicare le modifiche e riprovare a scrivere con il nuovo etag.

Implementa il ciclo OCC

Il nucleo di un'implementazione OCC è un ciclo di nuovi tentativi che legge, modifica e tenta di salvare la risorsa, uscendo dopo una scrittura riuscita. Per evitare loop infiniti durante un'elevata contesa, configura un numero massimo ragionevole di tentativi di ripetizione.

Passaggi del loop

La tabella seguente descrive ogni passaggio di un ciclo di nuovi tentativi OCC:

Passaggio Azione Esempio di implementazione
Leggi Recupera lo stato attuale della risorsa, incluso etag.
let mut policy = client
    .get_iam_policy()
    .set_resource(&secret_name)
    .send()
    .await?;
Modifica Applica le modifiche alla struttura locale.
policy.bindings.push(new_binding);
Scrivi e controlla Prova a salvare la risorsa modificata utilizzando l'etag esistente e verifica la presenza di codici di errore di concorrenza.
match client
    .set_iam_policy()
    .set_resource(&secret_name)
    .set_policy(policy)
    .send()
    .await
{
    Ok(p) => return Ok(p),
    Err(e) => { /* ... */ }
}
Operazione riuscita o riprova Se la scrittura va a buon fine, esci dal loop. Se l'operazione non riesce e restituisce un errore di concorrenza, incrementa il contatore dei nuovi tentativi e continua il ciclo dal passaggio Leggi. -

Esempio

L'esempio seguente mostra come implementare un ciclo OCC che aggiorna una policy IAM su una risorsa secret di Secret Manager concedendo a un membro il ruolo roles/secretmanager.secretAccessor.

Per eseguire questo esempio, dichiara prima la dipendenza nel file Cargo.toml:

cargo add google-cloud-secretmanager-v1

Quindi, implementa il loop di ripetizione 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")

Completamento codice

Il codice seguente mostra la funzione completa con tutte le importazioni richieste:

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")
}