שליטה אופטימית במקביליות (OCC) היא שיטה לניהול משאבים משותפים ולמניעת 'עדכונים שאבדו' או מרוצי תהליכים כשכמה משתמשים או תהליכים מנסים לשנות את אותו משאב בו-זמנית.
לדוגמה, במערכות כמו Google Cloud IAM, המשאב המשותף הוא מדיניות IAM שמוחלת על משאב (כמו פרויקט, קטגוריה או שירות). כדי להטמיע OCC, המערכות בדרך כלל משתמשות במספר גרסה או בשדה etag (תג ישות) באובייקט המשאב.
מבוא ל-OCC
תארו לעצמכם שני תהליכים, A ו-B, שמנסים לעדכן משאב משותף בו-זמנית:
תהליך A קורא את המצב הנוכחי של המשאב.
תהליך B קורא את המצב הנוכחי הזהה.
בתהליך A, העותק משתנה ונכתב בחזרה לשרת.
תהליך ב' משנה את העותק שלו וכותב אותו בחזרה לשרת.
מכיוון שתהליך ב' מחליף את המשאב בלי לדעת שתהליך א' כבר שינה אותו, העדכונים של תהליך א' אובדים.
כדי לפתור את הבעיה הזו, OCC מציג טביעת אצבע ייחודית שמשתנה בכל פעם שישות משתנה. במערכות רבות (כמו IAM), הפעולה הזו מתבצעת באמצעות etag. השרת בודק את התג הזה בכל פעולת כתיבה:
כשקוראים את המשאב, השרת מחזיר
etag(טביעת אצבע ייחודית).כששולחים את המשאב ששונה בחזרה, צריך לכלול את
etagהמקורי.אם השרת מגלה שהערך המאוחסן של
etagלא תואם לערך שלetagששלחתם (כלומר, מישהו אחר שינה את המשאב מאז שקראתם אותו), פעולת הכתיבה נכשלת עם שגיאהABORTEDאוFAILED_PRECONDITION.
הכשל הזה מחייב את הלקוח לנסות שוב את כל התהליך – לקרוא מחדש את המצב החדש, להחיל מחדש את השינויים ולנסות שוב את הכתיבה עם etag חדש.
הטמעה של לולאת OCC
הליבה של הטמעת OCC היא לולאת while שמטפלת בלוגיקה של הניסיון החוזר. כדאי להגדיר מספר סביר של ניסיונות חוזרים כדי למנוע לולאות אינסופיות במקרים של מחלוקות רבות.
שלבים ב-Loop
| שלב | פעולה | דוגמה להטמעה |
|---|---|---|
| קריאה | אחזור המצב הנוכחי של המשאב, כולל etag. |
Policy policy = client.getIamPolicy(resourceName); |
| שינוי | מחילים את השינויים על האובייקט המקומי. | policy = policy.toBuilder().addBinding(newBinding).build(); |
| כתיבה/בדיקה | מנסים לשמור את המשאב ששונה באמצעות etag הישן. הפעולה הזו חייבת להיות בתוך בלוק try. |
try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop } |
| הצלחה/ניסיון חוזר | אם הפעולה הצליחה, יוצאים מהלולאה. אם הפעולה נכשלת בגלל שגיאת מקבילות, מגדילים את מונה הניסיונות החוזרים וממשיכים את הלולאה (חוזרים לשלב הקריאה). |
בקובץ הבא מופיעה דוגמה להפעלה של לולאת OCC באמצעות מדיניות IAM במשאב Project כיעד.
התקנה
כדי להשתמש בדוגמה הזו, מוסיפים את יחסי התלות הבאים ל-pom.xml:
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-resourcemanager</artifactId>
<version>1.45.0</version>
</dependency>
דוגמה
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;
}
}