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:
Proses A membaca status resource saat ini.
Proses B membaca status saat ini yang sama.
Proses A mengubah salinannya dan menuliskannya kembali ke server.
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:
Saat Anda membaca resource, server akan menampilkan
etag(sidik jari unik).Saat mengirim kembali resource yang diubah, Anda harus menyertakan
etagasli.Jika server menemukan bahwa
etagyang disimpan tidak cocok denganetagyang Anda kirim (artinya orang lain mengubah resource sejak Anda membacanya), operasi penulisan akan gagal dengan errorABORTEDatauFAILED_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;
}
}