Kontrol konkurensi optimis (OCC)

Kontrol Persamaan Optimis (OCC) adalah strategi yang digunakan untuk mengelola resource bersama dan mencegah "update yang hilang" atau kondisi race saat beberapa pengguna atau proses mencoba mengubah resource yang sama secara bersamaan.

Sebagai contoh, pertimbangkan sistem seperti Google Cloud IAM, di mana resource yang dibagikan adalah Kebijakan IAM yang diterapkan ke resource (seperti Project, Bucket, atau Layanan). Untuk menerapkan OCC, sistem biasanya menggunakan nomor versi atau kolom etag (tag entitas) pada objek resource.

Pengantar OCC

Bayangkan 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 menuliskannya kembali ke server.

  4. Proses B mengubah salinannya dan menuliskannya kembali ke server.

Karena Proses B menggantikan resource tanpa mengetahui bahwa Proses A telah mengubahnya, pembaruan Proses A akan hilang.

OCC mengatasi hal ini dengan memperkenalkan sidik jari unik yang berubah setiap kali entitas dimodifikasi. Di banyak sistem (seperti IAM), hal ini dilakukan menggunakan 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 server menemukan bahwa etag yang disimpan tidak cocok dengan etag yang Anda kirim (artinya orang lain mengubah resource sejak Anda membacanya), operasi penulisan akan gagal dengan error ABORTED atau FAILED_PRECONDITION.

Kegagalan ini memaksa klien untuk mencoba lagi seluruh proses—membaca ulang status baru, menerapkan ulang perubahan, dan mencoba penulisan lagi dengan etag baru.

Menerapkan Loop OCC

Inti penerapan OCC adalah loop while yang menangani logika coba lagi. Tetapkan jumlah maksimum percobaan ulang yang wajar untuk mencegah loop tanpa henti jika terjadi persaingan yang tinggi.

Langkah-Langkah Loop

Langkah Tindakan Contoh Penerapan
Melihat Ambil status resource saat ini, termasuk etag. Policy policy = client.getIamPolicy(resourceName);
Mengubah Terapkan perubahan pada objek lokal. policy = policy.toBuilder().addBinding(newBinding).build();
Menulis/Memeriksa Mencoba menyimpan resource yang telah diubah menggunakan etag lama. Tindakan ini harus berada di dalam blok try. try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop }
Berhasil/Coba lagi Jika penulisan berhasil, keluar dari loop. Jika gagal dengan error serentak, naikkan penghitung percobaan ulang dan lanjutkan loop (kembali ke langkah Baca).

File berikut memberikan contoh yang dapat dijalankan tentang cara menerapkan loop OCC menggunakan kebijakan IAM pada resource Project sebagai target.

Penginstalan

Untuk menggunakan contoh ini, tambahkan dependensi berikut ke pom.xml Anda:

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

Contoh

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