處理長時間執行的作業

部分 Google Cloud API 方法需要相當長的時間才能完成。這些方法不會封鎖到工作完成為止,而是會傳回代表進行中作業的 Operation 資源,讓應用程式可以輪詢其狀態和最終結果。

Rust 適用的 Cloud 用戶端程式庫提供輔助程式,可協助您處理這些長時間執行的作業 (LRO)。本文說明如何啟動 LRO、自動輪詢直到作業完成、檢查中繼進度更新,以及手動輪詢。

事前準備

本文使用 Cloud Storage 具體呈現程式碼範例,但相同概念適用於任何使用 LRO 的 Google Cloud 服務。

執行本文件中的程式碼前,請先完成下列必要條件:

  • 建立或選取已啟用計費功能的 Google Cloud 專案。
  • 請按照「開始使用 Rust」一文的說明,設定開發環境和憑證。
  • 建立已啟用階層命名空間的 Cloud Storage bucket、建立要重新命名的來源資料夾,並授予主體具備資料夾管理權限的 Identity and Access Management (IAM) 角色,例如 Storage 管理員 (roles/storage.admin)。

依附元件

將用戶端程式庫和 LRO crate 新增至 Cargo.toml 檔案:

cargo add google-cloud-storage google-cloud-lro google-cloud-longrunning anyhow

新增 tokio,用於非同步執行和計時器:

cargo add tokio --features full

啟動長時間執行的作業

本文件中的範例會呼叫 Cloud Storage 重新命名資料夾作業,並傳回 LRO。如要直接啟動 LRO 並取得初始 Operation 資源,請完成下列步驟:

  1. 新增 use 宣告,縮短類型路徑:

    use anyhow::anyhow;
    use google_cloud_longrunning as longrunning;
    use google_cloud_storage::client::StorageControl;

  2. 定義 async 函式,接受 bucket ID、來源資料夾 ID 和目的地資料夾 ID:

    pub async fn manual(bucket: &str, folder: &str, dest: &str) -> anyhow::Result<()> {

  3. 初始化 StorageControl 用戶端:

    let client = StorageControl::builder().build().await?;

  4. 建立 rename_folder 要求建構工具:

    let operation = client
        .rename_folder()
        .set_name(format!("projects/_/buckets/{bucket}/folders/{folder}"))
        .set_destination_folder_id(dest)

  5. 傳送要求並等待初始 Operation 資源,代表背景作業:

    .send()
    .await?;

啟動作業後,請等待作業完成,再擷取最終結果。

自動輪詢長時間執行的作業

在大多數用途中,請使用 Poller 特徵的 until_done 方法,而非直接呼叫 send。輪詢器會啟動 LRO,並根據用戶端的輪詢和錯誤政策進行輪詢,然後解析為最終回應:

  1. 將 Poller 特徵帶入範圍:

    use google_cloud_lro::Poller;

  2. 初始化 StorageControl 用戶端,並設定 rename_folder 要求建構工具:

    let response = client
        .rename_folder()
        .set_name(format!("projects/_/buckets/{bucket}/folders/{folder}"))
        .set_destination_folder_id(dest)

  3. 在要求建構工具上呼叫 poller 和 until_done,等待完成並輸出結果:

        .poller()
        .until_done()
        .await?;
    
    println!("LRO completed, response={response:?}");

完成自動輪詢程式碼

以下函式顯示完整的自動輪詢範例:

pub async fn automatic(bucket: &str, folder: &str, dest: &str) -> anyhow::Result<()> {
    use google_cloud_lro::Poller;

    let client = StorageControl::builder().build().await?;

    let response = client
        .rename_folder()
        .set_name(format!("projects/_/buckets/{bucket}/folders/{folder}"))
        .set_destination_folder_id(dest)
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

輪詢長時間執行的作業,並取得中繼結果

until_done 方法只會傳回最終結果,並省略中繼中繼資料更新。如果應用程式需要在 LRO 執行期間更新進度,請使用 poller 取得輪詢器,並在迴圈中呼叫 poll:

  1. 匯入 Poller 和 PollingResult:

    use google_cloud_lro::{Poller, PollingResult};

  2. 使用 poller 將要求建構工具轉換為輪詢器:

    .poller();

  3. 在迴圈中呼叫 poller.poll().await,處理 InProgress、PollingError 和 Completed 狀態:

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

直接呼叫 poll 時,迴圈會控制輪詢嘗試之間的延遲時間。請根據預期的作業時間,選擇合適的延遲時間。

使用中繼結果輪詢的完整程式碼

以下函式顯示完整的 poll 迴圈:

pub async fn polling(bucket: &str, folder: &str, dest: &str) -> anyhow::Result<()> {
    use google_cloud_lro::{Poller, PollingResult};

    let client = StorageControl::builder().build().await?;

    let mut poller = client
        .rename_folder()
        .set_name(format!("projects/_/buckets/{bucket}/folders/{folder}"))
        .set_destination_folder_id(dest)
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

手動輪詢長時間執行的作業

雖然 Poller 特徵可處理幾乎所有用途,但您也可以使用 get_operation 手動輪詢 Operation:

  1. 使用 send 啟動 LRO 後,請進入迴圈並檢查 done 欄位:

    let response: anyhow::Result<Folder> = loop {
        if operation.done {

  2. 檢查 operation.result。result 欄位是 Option,因為某些作業 (例如刪除) 完成時不會傳回酬載。如果是 rename_folder,服務會傳回 Folder 資源:

    match &operation.result {
        None => {
            break Err(anyhow!("missing result for finished operation"));
        }

  3. 檢查作業是否完成,但發生錯誤。錯誤變體包含 Status 訊息,下列範例會將其格式化為錯誤,您也可以使用 Error::service 將其轉換為用戶端錯誤:

    Some(r) => {
        break match r {
            longrunning::model::operation::Result::Error(s) => {
                Err(anyhow!("operation completed with error {s:?}"))
            }

  4. 如果作業成功,請將 Any 酬載解壓縮為預期的 Folder 回應類型:

    longrunning::model::operation::Result::Response(any) => {
        let response = any.to_msg::<Folder>()?;
        Ok(response)
    }

  5. 新增萬用字元分支,因為 Rust 適用的 Cloud 用戶端程式庫中的列舉會標示為 #[non_exhaustive]:

    _ => Err(anyhow!("unexpected result branch {r:?}")),

  6. 選用:operation.done 處於 false 狀態時,請解壓縮 operation.metadata,檢查進度更新:

    if let Some(any) = &operation.metadata {
        let metadata = any.to_msg::<RenameFolderMetadata>()?;
        println!("LRO in progress, metadata={metadata:?}");
    }

  7. 請稍後再重新輪詢。為求簡潔,這個範例會固定休眠 500 毫秒;在實際工作環境程式碼中,請使用截斷的指數輪詢:

    tokio::time::sleep(std::time::Duration::from_millis(500)).await;

  8. 使用 operation.name 呼叫 get_operation,重新整理作業狀態:

    if let Ok(attempt) = client
        .get_operation()
        .set_name(&operation.name)
        .send()
        .await
    {
        operation = attempt;
    }

完成手動輪詢程式碼

下列函式結合了所有手動輪詢步驟:

pub async fn manual(bucket: &str, folder: &str, dest: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::Folder;
    use google_cloud_storage::model::RenameFolderMetadata;

    let client = StorageControl::builder().build().await?;

    let operation = client
        .rename_folder()
        .set_name(format!("projects/_/buckets/{bucket}/folders/{folder}"))
        .set_destination_folder_id(dest)
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let mut operation = operation;
    let response: anyhow::Result<Folder> = loop {
        if operation.done {
            match &operation.result {
                None => {
                    break Err(anyhow!("missing result for finished operation"));
                }
                Some(r) => {
                    break match r {
                        longrunning::model::operation::Result::Error(s) => {
                            Err(anyhow!("operation completed with error {s:?}"))
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<Folder>()?;
                            Ok(response)
                        }
                        _ => Err(anyhow!("unexpected result branch {r:?}")),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<RenameFolderMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    };
    println!("LRO completed, response={response:?}");

    Ok(())
}