Optimistic Concurrency Control (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, yang
resource bersamanya adalah Kebijakan IAM yang diterapkan ke resource
(seperti Project, Bucket, atau Layanan). Untuk menerapkan OCC, sistem biasanya menggunakan nomor versi atau kolom etag (entity tag) 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 menulisnya kembali ke server.
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 mengatasi hal ini dengan memperkenalkan sidik jari unik yang berubah setiap kali entity diubah. Di banyak sistem (seperti IAM), hal ini dilakukan menggunakan etag. Server memeriksa tag ini pada setiap operasi tulis:
Saat Anda membaca resource, server akan menampilkan
etag(sidik jari unik).Saat Anda mengirim resource yang diubah kembali, Anda harus menyertakan
etagasli.Jika server menemukan bahwa
etagyang disimpan tidak cocok denganetagyang Anda kirim (artinya orang lain mengubah resource sejak Anda membacanya), operasi tulis akan gagal dengan errorABORTEDatauFAILED_PRECONDITION.
Kegagalan ini memaksa klien untuk mencoba lagi seluruh proses—membaca ulang status baru, menerapkan kembali perubahan, dan mencoba operasi tulis lagi dengan etag baru.
Mengimplementasikan Loop OCC
Inti dari implementasi OCC adalah loop while yang menangani logika percobaan ulang. Tetapkan jumlah percobaan ulang maksimum yang wajar untuk mencegah loop tak terbatas jika terjadi pertentangan yang tinggi.
Langkah-Langkah Loop
| Langkah | Tindakan | Contoh Implementasi |
|---|---|---|
| Membaca | Mengambil status resource saat ini, termasuk etag. |
Policy policy = client.getIamPolicy(resourceName); |
| Mengubah | Menerapkan perubahan pada objek lokal. | policy = policy.toBuilder().addBinding(newBinding).build(); |
| Menulis/Memeriksa | Mencoba menyimpan resource yang 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/Mencoba Lagi | Jika operasi tulis berhasil, keluar dari loop. Jika gagal dengan error konkurensi, tambahkan penghitung percobaan ulang dan lanjutkan loop (kembali ke langkah Membaca). |
File berikut memberikan contoh yang dapat dijalankan tentang cara mengimplementasikan loop OCC menggunakan kebijakan IAM pada resource Project sebagai target.
Penginstalan
Untuk menggunakan contoh ini, tambahkan dependensi berikut ke pom.xml:
<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;
}
}