Bekerja dengan kontrol konkurensi optimis (OCC)

Kontrol konkurensi optimis (OCC) adalah strategi untuk mengelola resource bersama dan mencegah update yang hilang atau kondisi race saat beberapa pengguna atau proses mengubah resource yang sama secara bersamaan.

Misalnya, saat menggunakan Identity and Access Management, kebijakan IAM adalah resource bersama yang dilampirkan ke resource seperti project, bucket, atau layanan. Tanpa kontrol konkurensi, modifikasi simultan dapat menimpa perubahan kebijakan terbaru, sehingga secara tidak sengaja mencabut akses yang sah atau memberikan izin yang tidak sah.

Untuk menerapkan OCC, sistem biasanya menggunakan nomor versi atau kolom etag (tag entitas) pada struct resource.

Cara kerja OCC

Misalkan dua proses, A dan B, mencoba memperbarui resource bersama secara bersamaan:

  1. Proses A membaca status resource saat ini.
  2. Proses B membaca status saat ini yang sama.
  3. Proses A mengubah salinannya dan menulisnya kembali ke server.
  4. Proses B mengubah salinannya dan menulisnya kembali ke server.

Karena Proses B menimpa resource tanpa mengetahui bahwa Proses A telah mengubahnya, update Proses A akan hilang.

OCC memecahkan masalah ini dengan memperkenalkan sidik jari unik yang berubah setiap kali entity diubah. Di banyak sistem, seperti IAM, sidik jari ini adalah etag. Server memeriksa tag ini pada setiap penulisan:

  1. Saat Anda membaca resource, server akan menampilkan etag (sidik jari unik).
  2. Saat mengirim kembali resource yang diubah, Anda harus menyertakan etag asli.
  3. Jika etag yang disimpan tidak cocok dengan etag yang Anda kirim—artinya proses lain mengubah resource setelah Anda membacanya—operasi tulis akan gagal dengan error ABORTED atau FAILED_PRECONDITION.

Kegagalan ini memberi sinyal kepada klien untuk mencoba kembali seluruh proses: membaca ulang status yang diperbarui, menerapkan kembali perubahan, dan mencoba menulis lagi dengan etag baru.

Mengimplementasikan loop OCC

Inti dari implementasi OCC adalah loop percobaan ulang yang membaca, mengubah, dan mencoba menyimpan resource, lalu keluar setelah penulisan berhasil. Untuk mencegah loop tak terbatas selama pertentangan tinggi, konfigurasikan jumlah maksimum upaya percobaan ulang yang wajar.

Langkah-langkah loop

Tabel berikut menjelaskan setiap langkah dalam loop percobaan ulang OCC:

Langkah Tindakan Contoh penerapan
Melihat Ambil status resource saat ini, termasuk etag.
let mut policy = client
    .get_iam_policy()
    .set_resource(&secret_name)
    .send()
    .await?;
Mengubah Terapkan perubahan pada struct lokal.
policy.bindings.push(new_binding);
Menulis dan memeriksa Mencoba menyimpan resource yang diubah menggunakan etag yang ada, dan memeriksa kode error serentak.
match client
    .set_iam_policy()
    .set_resource(&secret_name)
    .set_policy(policy)
    .send()
    .await
{
    Ok(p) => return Ok(p),
    Err(e) => { /* ... */ }
}
Berhasil atau coba lagi Jika penulisan berhasil, keluar dari loop. Jika gagal dengan error konkurensi, tambahkan penghitung percobaan ulang dan lanjutkan loop dari langkah Read. -

Contoh

Contoh berikut menunjukkan cara menerapkan loop OCC yang memperbarui kebijakan IAM pada resource secret Secret Manager dengan memberikan peran roles/secretmanager.secretAccessor kepada anggota.

Untuk menjalankan contoh ini, deklarasikan dependensi terlebih dahulu dalam file Cargo.toml Anda:

cargo add google-cloud-secretmanager-v1

Kemudian, terapkan loop percobaan ulang 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")

Kode lengkap

Kode berikut menunjukkan fungsi lengkap dengan semua impor yang diperlukan:

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