オプティミスティック同時実行制御(OCC)は、共有リソースを管理し、複数のユーザーまたはプロセスが同じリソースを同時に変更しようとした場合に発生する「更新の損失」や競合状態を防ぐために使用される戦略です。
たとえば、 Google Cloud IAM のようなシステムを考えてみましょう。
共有リソースは、リソース
(プロジェクト、バケット、サービスなど)に適用される IAM ポリシー です。OCC を実装するために、システムは通常、リソース オブジェクトのバージョン番号または etag(エンティティ
タグ)フィールドを使用します。
OCC の概要
2 つのプロセス A と B が同時に共有リソースを更新しようとしているとします。
プロセス A がリソースの現在の状態を読み取ります。
プロセス B が同じ現在の状態を読み取ります。
プロセス A がコピーを変更してサーバーに書き戻します。
プロセス B がコピーを変更してサーバーに書き戻します。
プロセス B は、プロセス A がすでにリソースを変更したことを知らずにリソースを上書きするため、プロセス A の更新は 失われます。
OCC は、 エンティティが変更されるたびに変化する一意のフィンガープリントを導入することで、この問題を解決します。多くのシステム(IAM
など)では、etag を使用してこれを行います。サーバーは書き込みごとにこのタグを確認します。
リソースを読み取ると、サーバーは
etag(一意のフィンガープリント)を返します。変更したリソースを返送する場合は、元の
etagを含める必要があります。保存されている
etagが送信したetagと一致しない場合(読み取り後に他のユーザーがリソースを変更した場合)、書き込みオペレーションはABORTEDまたはFAILED_PRECONDITIONエラーで失敗します。
このエラーにより、クライアントはプロセス全体を再試行 する必要があります。つまり、新しい状態を読み取り、変更を再適用し、新しい etag
で書き込みを再度試みます。
OCC ループを実装する
OCC 実装の中核は、再試行ロジックを処理する while
ループです。競合が多い場合に無限ループが発生しないように、適切な最大再試行回数を設定します。
ループのステップ
| Step | 操作 | 実装例 |
|---|---|---|
| 既読 | 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 } |
| 成功/再試行 | 書き込みが成功した場合は、ループを終了します。同時実行エラーで失敗した場合は、再試行カウンタをインクリメントしてループを続行します(読み取りステップに戻ります)。 |
次のファイルは、ターゲットとしてプロジェクト リソースの IAM ポリシーを使用して OCC ループを実装する方法の実行可能な例を示しています。
インストール
この例を使用するには、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;
}
}