עבודה עם בקרת בו-זמניות אופטימית (OCC)

בקרת בו-זמניות אופטימית (OCC) היא אסטרטגיה לניהול משאבים משותפים ולמניעת אובדן עדכונים או מרוצי תהליכים, כשכמה משתמשים או תהליכים משנים את אותו משאב בו-זמנית.

לדוגמה, כשעובדים עם ניהול זהויות והרשאות גישה (IAM), מדיניות IAM היא משאב משותף שמצורף למשאב כמו פרויקט, קטגוריה או שירות. בלי בקרת בו-זמניות, שינויים שמתבצעים בו-זמנית יכולים להחליף שינויים שבוצעו לאחרונה במדיניות, וכך לבטל בטעות גישה לגיטימית או להעניק הרשאות לא מורשות.

כדי להטמיע OCC, מערכות בדרך כלל משתמשות במספר גרסה או בשדה etag (תג ישות) במבנה המשאב.

איך פועלת אופטימיזציית המרות אונליין (OCC)

נניח ששני תהליכים, א' וב', מנסים לעדכן משאב משותף בו-זמנית:

  1. תהליך א' קורא את המצב הנוכחי של המשאב.
  2. תהליך ב' קורא את אותו מצב נוכחי.
  3. תהליך א' משנה את העותק שלו וכותב אותו בחזרה לשרת.
  4. תהליך ב' משנה את העותק שלו וכותב אותו בחזרה לשרת.

מכיוון שתהליך ב' מחליף את המשאב בלי לדעת שתהליך א' כבר שינה אותו, העדכונים של תהליך א' אובדים.

כדי לפתור את הבעיה הזו, ב-OCC משתמשים בטביעת אצבע ייחודית שמשתנה בכל פעם שמשנים ישות. במערכות רבות, כמו IAM, טביעת האצבע הזו היא etag. השרת בודק את התג הזה בכל כתיבה:

  1. כשקוראים את המשאב, השרת מחזיר etag (טביעת אצבע ייחודית).
  2. כששולחים בחזרה את המשאב ששונה, צריך לכלול את ה-etag המקורי.
  3. אם הערך של etag ששמור לא תואם לערך של etag ששלחתם, כלומר תהליך אחר שינה את המשאב אחרי שקראתם אותו, פעולת הכתיבה תיכשל ותופיע השגיאה ABORTED או FAILED_PRECONDITION.

השגיאה הזו מאותתת ללקוח לנסות שוב את כל התהליך: לקרוא מחדש את המצב המעודכן, להחיל מחדש את השינויים ולנסות שוב את הכתיבה עם ה-etag החדש.

הטמעת לולאת ה-OCC

הליבה של הטמעת OCC היא לולאת ניסיון חוזר שקוראת, משנה ומנסה לשמור את המשאב, ויוצאת אחרי כתיבה מוצלחת. כדי למנוע לולאות אינסופיות בזמן תחרות גבוהה, הגדירו מספר מקסימלי סביר של ניסיונות חוזרים.

שלבי הלולאה

בטבלה הבאה מתואר כל שלב בלולאת הניסיונות החוזרים של OCC:

שלב פעולה דוגמה של הטמעה
קריאה מאחזרים את מצב המשאב הנוכחי, כולל ה-etag.
let mut policy = client
    .get_iam_policy()
    .set_resource(&secret_name)
    .send()
    .await?;
שינוי מחילים את השינויים על ה-struct המקומי.
policy.bindings.push(new_binding);
כתיבה ובדיקה מנסים לשמור את המשאב ששונה באמצעות ה-etag הקיים, ובודקים אם יש קודי שגיאה של בו-זמניות.
match client
    .set_iam_policy()
    .set_resource(&secret_name)
    .set_policy(policy)
    .send()
    .await
{
    Ok(p) => return Ok(p),
    Err(e) => { /* ... */ }
}
הצלחה או ניסיון חוזר אם הכתיבה מצליחה, יוצאים מהלולאה. אם הפעולה נכשלת עם שגיאת בו-זמניות, מגדילים את מונה הניסיונות החוזרים וממשיכים את הלולאה מהשלב Read. -

דוגמה

בדוגמה הבאה מוסבר איך להטמיע לולאת OCC שמעדכנת מדיניות IAM במשאב סודי של Secret Manager, על ידי הקצאת התפקיד roles/secretmanager.secretAccessor לחבר.

כדי להריץ את הדוגמה הזו, קודם צריך להצהיר על התלות בקובץ Cargo.toml:

cargo add google-cloud-secretmanager-v1

לאחר מכן, מטמיעים את לולאת הניסיון החוזר של OCC:

const ROLE: &str = "roles/secretmanager.secretAccessor";
const ATTEMPTS: u32 = 5;

let secret_name = format!("projects/{project_id}/secrets/{secret_id}");
let client = SecretManagerService::builder().build().await?;
for _attempt in 0..ATTEMPTS {
    let mut current = client
        .get_iam_policy()
        .set_resource(&secret_name)
        .send()
        .await?;

    match current.bindings.iter_mut().find(|b| b.role == ROLE) {
        None => current
            .bindings
            .push(Binding::new().set_role(ROLE).set_members([member])),
        Some(b) => {
            if b.members.iter().any(|m| m == member) {
                return Ok(current);
            }
            b.members.push(member.to_string());
        }
    };
    let updated = client
        .set_iam_policy()
        .set_resource(&secret_name)
        .set_policy(current)
        .set_update_mask(FieldMask::default().set_paths(["bindings"]))
        .send()
        .await;
    match updated {
        Ok(p) => return Ok(p),
        Err(e)
            if e.status().is_some_and(|s| {
                s.code == Code::Aborted || s.code == Code::FailedPrecondition
            }) =>
        {
            continue;
        }
        Err(e) => return Err(e.into()),
    }
}
anyhow::bail!("could not set IAM policy after {ATTEMPTS} attempts")

השלמת הקוד

הקוד הבא מציג את הפונקציה המלאה עם כל הייבואים הנדרשים:

use google_cloud_gax::error::rpc::Code;
use google_cloud_iam_v1::model::Binding;
use google_cloud_iam_v1::model::Policy;
use google_cloud_secretmanager_v1::client::SecretManagerService;
use google_cloud_wkt::FieldMask;

/// Executes an Optimistic Concurrency Control (OCC) loop to safely update a resource.
///
/// This function demonstrates the core Read-Modify-Write-Retry pattern. It uses the secret manager
/// service and a hard-coded role. The principles apply to any other service or role.
///
/// # Parameters
/// * `project_id` The Google Cloud Project ID (e.g., "my-project-123").
/// * `secret_id` The Google Cloud Project ID (e.g., "my-secret").
/// * `member` The member to add (e.g., "user:user@example.com").
///
/// # Returns
/// The new IAM policy.
pub async fn sample(project_id: &str, secret_id: &str, member: &str) -> anyhow::Result<Policy> {
    const ROLE: &str = "roles/secretmanager.secretAccessor";
    const ATTEMPTS: u32 = 5;

    let secret_name = format!("projects/{project_id}/secrets/{secret_id}");
    let client = SecretManagerService::builder().build().await?;
    for _attempt in 0..ATTEMPTS {
        let mut current = client
            .get_iam_policy()
            .set_resource(&secret_name)
            .send()
            .await?;

        match current.bindings.iter_mut().find(|b| b.role == ROLE) {
            None => current
                .bindings
                .push(Binding::new().set_role(ROLE).set_members([member])),
            Some(b) => {
                if b.members.iter().any(|m| m == member) {
                    return Ok(current);
                }
                b.members.push(member.to_string());
            }
        };
        let updated = client
            .set_iam_policy()
            .set_resource(&secret_name)
            .set_policy(current)
            .set_update_mask(FieldMask::default().set_paths(["bindings"]))
            .send()
            .await;
        match updated {
            Ok(p) => return Ok(p),
            Err(e)
                if e.status().is_some_and(|s| {
                    s.code == Code::Aborted || s.code == Code::FailedPrecondition
                }) =>
            {
                continue;
            }
            Err(e) => return Err(e.into()),
        }
    }
    anyhow::bail!("could not set IAM policy after {ATTEMPTS} attempts")
}