Introduction
The Google Cloud Client Libraries for Rust is a collection of Rust crates to interact with Google Cloud Services.
This guide is organized as a series of small tutorials showing how to perform specific actions with the client libraries. Most Google Cloud services follow a series of guidelines, collectively known as the AIPs. This makes the client libraries more consistent from one service to the next. The functions to delete or list resources almost always have the same interface.
Audience
This guide is intended for Rust developers who are familiar with the language and the Rust ecosystem. We will assume you know how to use Rust and its supporting toolchain.
At the risk of being repetitive, most of the guides do not assume you have used any Google Service or client library before (in Rust or other language). However, the guides will refer you to service specific tutorials to initialize their projects and services.
Service specific documentation
These guides are not intended as tutorials for each service or as extended guides on how to design Rust applications to work on Google Cloud. They are starting points to get you productive with the client libraries for Rust.
We recommend you read the service documentation at https://cloud.google.com to learn more about each service. If you need guidance on how to design your application for Google Cloud, the Cloud Architecture Center may have what you are looking for.
Reporting bugs
We welcome bugs about the client libraries or their documentation. Please use GitHub Issues.
License
The client libraries source and their documentation are release under the Apache License, Version 2.0.
Setting up your development environment
Prepare your environment for Rust app development and deployment on Google Cloud by installing the following tools.
Install Rust
-
To install Rust, see Getting Started.
-
Confirm that you have the most recent version of Rust installed:
cargo --version
Install an editor
The Getting Started guide links popular editor plugins and IDEs, which provide the following features:
- Fully integrated debugging capabilities
- Syntax highlighting
- Code completion
Install the Google Cloud CLI
The Google Cloud CLI is a set of tools for Google Cloud. It contains the
gcloud
and
bq
command-line
tools used to access Compute Engine, Cloud Storage, BigQuery, and other services
from the command line. You can run these tools interactively or in your
automated scripts.
To install the gcloud CLI, see Installing the gcloud CLI.
Install the Cloud Client Libraries for Rust in a new project
The Cloud Client Libraries for Rust is the idiomatic way for Rust developers to integrate with Google Cloud services, such as Secret Manager and Workflows.
For example, to use the package for an individual API, such as the Secret Manager API, do the following:
-
Create a new Rust project:
cargo new my-project
-
Change your directory to the new project:
cd my-project
-
Add the Secret Manager client library to the new project:
cargo add google-cloud-secretmanager-v1
If you haven't already enabled the Secret Manager API, enable it in APIs and services or by running the following command:
gcloud services enable secretmanager.googleapis.com
-
Add the google-cloud-gax crate to the new project:
cargo add google-cloud-gax
-
Add the tokio crate to the new project:
cargo add tokio --features macros
-
Edit
src/main.rs
in your project to use the Secret Manager client library:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use google_cloud_gax::paginator::ItemPaginator as _;
use google_cloud_secretmanager_v1::client::SecretManagerService;
let project_id = std::env::args().nth(1).unwrap();
let client = SecretManagerService::builder().build().await?;
let mut items = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item();
while let Some(item) = items.next().await {
println!("{}", item?.name);
}
Ok(())
}
-
Build your program:
cargo build
The program should build without errors.
Note: The source of the Cloud Client Libraries for Rust is on GitHub.
Running the program
-
To use the Cloud Client Libraries in a local development environment, set up Application Default Credentials.
gcloud auth application-default login
For more information, see Authenticate for using client libraries.
-
Run your program, supplying your Google Cloud Platform project's ID:
PROJECT_ID=$(gcloud config get project) cargo run ${PROJECT_ID}
The program will print the secrets associated with your project ID. If you don't see any secrets, you might not have any in Secret Manager. You can create a secret and rerun the program, and you should see the secret printed in the output.
What's next
- Explore authentication methods at Google.
- Browse the documentation for Google Cloud products.
Setting up Rust on Cloud Shell
Cloud Shell is a great environment to run small examples and tests. This guide shows you how to configure Rust and install one of the Cloud Client Libraries in Cloud Shell.
Start up Cloud Shell
-
In the Google Cloud console project selector, select a project.
-
Open https://shell.cloud.google.com to start a new shell. You might be prompted to authorize Cloud Shell to use your credentials for Google Cloud API calls.
Configure Rust
-
Cloud Shell comes with rustup pre-installed. You can use it to install and configure the default version of Rust:
rustup default stable
-
Confirm that you have the most recent version of Rust installed:
cargo --version
Install Rust client libraries in Cloud Shell
-
Create a new Rust project:
cargo new my-project
-
Change your directory to the new project:
cd my-project
-
Add the Secret Manager client library to the new project:
cargo add google-cloud-secretmanager-v1
-
Add the google-cloud-gax crate to the new project:
cargo add google-cloud-gax
-
Add the tokio crate to the new project:
cargo add tokio --features macros
-
Edit
src/main.rs
in your project to use the Secret Manager client library:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use google_cloud_gax::paginator::ItemPaginator as _;
use google_cloud_secretmanager_v1::client::SecretManagerService;
let project_id = std::env::args().nth(1).unwrap();
let client = SecretManagerService::builder().build().await?;
let mut items = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item();
while let Some(item) = items.next().await {
println!("{}", item?.name);
}
Ok(())
}
-
Run your program, supplying your Google Cloud Platform project's ID:
PROJECT_ID=$(gcloud config get project) cargo run ${PROJECT_ID}
The program will print the secrets associated with your project ID. If you don't see any secrets, you might not have any in Secret Manager. You can create a secret and rerun the program, and you should see the secret printed in the output.
How to initialize a client
The Google Cloud Client Libraries for Rust use clients as the main abstraction to interface with specific services. Clients are implemented as Rust structs, with methods corresponding to each RPC offered by the service. To use a Google Cloud service using the Rust client libraries, you need to first initialize a client.
Prerequisites
In this guide you'll initialize a client and then use the client to make a simple RPC. For this tutorial, you'll use the Secret Manager API. The same structure applies to any other service in Google Cloud.
We recommend that you follow one of the Secret Manager getting started guides, such as how to Create a secret, before attempting to use the client library. These guides cover service specific concepts in more detail, and provide detailed guidance on project prerequisites.
We also recommend that you follow the instructions in the Authenticate for using client libraries guide. This guide will show you how to log in to configure the Application Default Credentials used in this guide.
Dependencies
As usual with Rust, you must declare the dependency in your Cargo.toml
file:
cargo add google-cloud-secretmanager-v1
To initialize a client, you first call Client::builder()
to obtain an
appropriate ClientBuilder
and then call build()
on
that builder to create a client.
The following creates a client with the default configuration, which is designed to meet requirements for most use cases.
let client = SecretManagerService::builder().build().await?;
Once the client is successfully initialized, you can use it to make RPCs:
use google_cloud_gax::paginator::Paginator as _;
let mut items = client
.list_locations()
.set_name(format!("projects/{project_id}"))
.by_page();
while let Some(page) = items.next().await {
let page = page?;
for location in page.locations {
println!("{}", location.name);
}
}
This example shows a call to list_locations
, which returns information about
the supported locations for the service (in this case, Secret Manager). The
output of the example should look something like this:
projects/123456789012/locations/europe-west8
projects/123456789012/locations/europe-west9
projects/123456789012/locations/us-east5
...
Full program
Putting all this code together into a full program looks as follows:
pub type Result = std::result::Result<(), Box<dyn std::error::Error>>;
pub async fn initialize_client(project_id: &str) -> Result {
use google_cloud_secretmanager_v1::client::SecretManagerService;
// Initialize a client with the default configuration. This is an
// asynchronous operation that may fail, as it requires acquiring an an
// access token.
let client = SecretManagerService::builder().build().await?;
// Once initialized, use the client to make requests.
use google_cloud_gax::paginator::Paginator as _;
let mut items = client
.list_locations()
.set_name(format!("projects/{project_id}"))
.by_page();
while let Some(page) = items.next().await {
let page = page?;
for location in page.locations {
println!("{}", location.name);
}
}
Ok(())
}
What's next
This guide showed you how to initialize a client using the Google Cloud Client Libraries for Rust. For a more complex example of working with a service, check out Generate text using the Vertex AI Gemini API.
Generate text using the Vertex AI Gemini API
In this guide, you send a text prompt request, and then a multimodal prompt and image request to the Vertex AI Gemini API and view the responses.
Prerequisites
To complete this guide, you must have a Google Cloud project with the Vertex AI API enabled. You can use the Vertex AI setup guide to complete these steps.
Add the Vertex AI client library as a dependency
The Vertex AI client library includes many features. Compiling all of them is relatively slow. To speed up compilation, you can just enable the features you need:
cargo add google-cloud-aiplatform-v1 --no-default-features --features prediction-service
Send a prompt to the Vertex AI Gemini API
First, initialize the client using the default settings:
use google_cloud_aiplatform_v1 as vertexai;
let client = vertexai::client::PredictionService::builder()
.build()
.await?;
Then build the model name. For simplicity, this example receives the project ID
as an argument and uses a fixed location (global
) and model id
(gemini-2.0-flash-001
).
const MODEL: &str = "gemini-2.0-flash-001";
let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");
If you want to run this function in your own code, use the project id (without
any projects/
prefix) of the project you selected while going through the
prerequisites.
With the client initialized you can send the request:
let response = client
.generate_content().set_model(&model)
.set_contents([vertexai::model::Content::new().set_role("user").set_parts([
vertexai::model::Part::new().set_text("What's a good name for a flower shop that specializes in selling bouquets of dried flowers?"),
])])
.send()
.await;
And then print the response. You can use the :#?
format specifier to prettify
the nested response objects:
println!("RESPONSE = {response:#?}");
See below for the complete code.
Send a prompt and an image to the Vertex AI Gemini API
As in the previous example, initialize the client using the default settings:
use google_cloud_aiplatform_v1 as vertexai;
let client = vertexai::client::PredictionService::builder()
.build()
.await?;
And then build the model name:
const MODEL: &str = "gemini-2.0-flash-001";
let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");
The new request includes an image part:
vertexai::model::Part::new().set_file_data(
vertexai::model::FileData::new()
.set_mime_type("image/jpeg")
.set_file_uri("gs://generativeai-downloads/images/scones.jpg"),
),
And the prompt part:
vertexai::model::Part::new().set_text("Describe this picture."),
Send the full request:
let response = client
.generate_content()
.set_model(&model)
.set_contents(
[vertexai::model::Content::new().set_role("user").set_parts([
vertexai::model::Part::new().set_file_data(
vertexai::model::FileData::new()
.set_mime_type("image/jpeg")
.set_file_uri("gs://generativeai-downloads/images/scones.jpg"),
),
vertexai::model::Part::new().set_text("Describe this picture."),
])],
)
.send()
.await;
As in the previous example, print the full response:
println!("RESPONSE = {response:#?}");
See below for the complete code.
Text prompt: complete code
pub async fn text_prompt(project_id: &str) -> crate::Result<()> {
use google_cloud_aiplatform_v1 as vertexai;
let client = vertexai::client::PredictionService::builder()
.build()
.await?;
const MODEL: &str = "gemini-2.0-flash-001";
let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");
let response = client
.generate_content().set_model(&model)
.set_contents([vertexai::model::Content::new().set_role("user").set_parts([
vertexai::model::Part::new().set_text("What's a good name for a flower shop that specializes in selling bouquets of dried flowers?"),
])])
.send()
.await;
println!("RESPONSE = {response:#?}");
Ok(())
}
Prompt and image: complete code
pub async fn prompt_and_image(project_id: &str) -> crate::Result<()> {
use google_cloud_aiplatform_v1 as vertexai;
let client = vertexai::client::PredictionService::builder()
.build()
.await?;
const MODEL: &str = "gemini-2.0-flash-001";
let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");
let response = client
.generate_content()
.set_model(&model)
.set_contents(
[vertexai::model::Content::new().set_role("user").set_parts([
vertexai::model::Part::new().set_file_data(
vertexai::model::FileData::new()
.set_mime_type("image/jpeg")
.set_file_uri("gs://generativeai-downloads/images/scones.jpg"),
),
vertexai::model::Part::new().set_text("Describe this picture."),
])],
)
.send()
.await;
println!("RESPONSE = {response:#?}");
Ok(())
}
Using Google Cloud Storage
Google Cloud Storage is a managed service for storing unstructured data.
The Rust client library provides an idiomatic API to access this service. The client library resumes interrupted downloads and uploads, and automatically performs integrity checks on the data. For metadata operations, the client library can retry failed requests, and automatically poll long-running operations.
Quickstart
This guide will show you how to create a Cloud Storage bucket, upload an object to this bucket, and then read the object back.
Prerequisites
The guide assumes you have an existing Google Cloud project with billing enabled.
Add the client library as a dependency
cargo add google-cloud-storage
Create a storage bucket
The client to perform operations on buckets and object metadata is called
StorageControl
:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
To create a bucket you must provide the project name and the desired bucket id:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
You can also provide other attributes for the bucket. For example, if you want all objects in the bucket to use the same permissions, you can enable Uniform bucket-level access:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
Then send this request and wait for the response:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
Upload an object
The client to perform operations on object data is called Storage
:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
In this case we will create an object called hello.txt
, with the traditional
greeting for a programming tutorial:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
Download an object
To download the contents of an object use read_object()
:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
Cleanup
Finally we remove the object and bucket to cleanup all the resources used in this guide:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
Next Steps
Full program
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
use google_cloud_storage as gcs;
use google_cloud_storage::client::StorageControl;
let control = StorageControl::builder().build().await?;
let bucket = control
.create_bucket()
.set_parent("projects/_")
.set_bucket_id(bucket_id)
.set_bucket(
gcs::model::Bucket::new()
.set_project(format!("projects/{project_id}"))
.set_iam_config(
gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
.set_enabled(true),
),
),
)
.send()
.await?;
println!("bucket successfully created {bucket:?}");
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let object = client
.write_object(&bucket.name, "hello.txt", "Hello World!")
.send_buffered()
.await?;
println!("object successfully uploaded {object:?}");
let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await.transpose()? {
contents.extend_from_slice(&chunk);
}
println!(
"object contents successfully downloaded {:?}",
bytes::Bytes::from_owner(contents)
);
control
.delete_object()
.set_bucket(&bucket.name)
.set_object(&object.name)
.set_generation(object.generation)
.send()
.await?;
control
.delete_bucket()
.set_name(&bucket.name)
.send()
.await?;
Ok(())
}
Push data on object writes
The client API to write Cloud Storage objects pulls the payload from a type provided by the application. Some applications generate the payload in a thread and would rather "push" the object payload to the service.
This guide shows you how to write an object to Cloud Storage using a push data source.
Prerequisites
The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.
Add the client library as a dependency
cargo add google-cloud-storage
Convert a queue to a StreamingSource
The key idea is to use a queue to separate the task pushing new data from the task pulling the payload. This tutorial uses a Tokio mpsc queue, but you can use any queue that integrates with Tokio's async runtime.
First wrap the receiver in our own type:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Then implement the trait required by the Google Cloud client library:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
In this tutorial you write the rest of the code in a function that accepts the bucket and object name as parameters:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
// ... code goes here ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Initialize a client:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Create a queue, obtaining the receiver and sender:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Use the client to write an object with the data received from this queue. Note
that we do not await
the future created in the write_object()
method.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Create a task to process the queue and write the data in the background:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
In the main task, send some data to write:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Once you have finished sending the data, drop the sender to close the sending side of the queue:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Now you can wait for the task to finish and extract the result:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Full program
Putting all these steps together you get:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
type Error = std::convert::Infallible;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0.recv().await.map(Ok)
}
}
pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
let upload = client
.write_object(bucket_name, object_name, QueueSource(receiver))
.send_buffered();
let task = tokio::spawn(upload);
for _ in 0..1000 {
let line = "I will not write funny examples in class\n";
sender
.send(bytes::Bytes::from_static(line.as_bytes()))
.await?;
}
drop(sender);
let object = task.await??;
println!("object successfully uploaded {object:?}");
Ok(())
}
Rewriting objects
Rewriting a Cloud Storage object can require multiple client requests,
depending on the details of the operation. In such cases, the service will
return a response representing its progress, along with a rewrite_token
which
the client must use to continue the operation.
This guide will show you how to fully execute the rewrite loop for a Cloud Storage object.
Prerequisites
The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.
Add the client library as a dependency
cargo add google-cloud-storage
Rewriting an object
Prepare client
First, create a client.
The service recommends an overall timeout of at least 30 seconds. In this
example, we use a RetryPolicy
that does not set any timeout on the operation.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
Prepare builder
Next we prepare the request builder.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
Optionally, we can limit the maximum amount of bytes written per call, before the service responds with a progress report. Setting this option is an alternative to increasing the attempt timeout.
Note that the value used in this example is intentionally small to force the rewrite loop to take multiple iterations. In practice, you would likely use a larger value.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
Rewriting an object allows you to copy its data to a different bucket, copy its data to a different object in the same bucket, change its encryption key, and/or change its storage class. The rewrite loop is identical for all these transformations. We will change the storage class to illustrate the code.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
Note that there is a minimum storage duration associated with the new storage
class. While the object used in this example (3 MiB) incurs less than $0.001
of cost, the billing may be noticeable for larger objects.
Introduce rewrite loop helpers
Next, we introduce a helper function to perform one iteration of the rewrite loop.
We send the request and process the response. We log the progress made.
If the operation is done
, we return the object metadata, otherwise we return
the rewrite token.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
Execute rewrite loop
Now we are ready to perform the rewrite loop until the operation is done.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
Note how if the operation is incomplete, we supply the rewrite token returned by the server to the next request.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
Also note that the rewrite token can be used to continue the operation from another process. Rewrite tokens are valid for up to one week.
Full program
Putting all these steps together you get:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;
pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
let source_object = upload(bucket_name).await?;
let control = StorageControl::builder()
.with_retry_policy(RetryableErrors.with_attempt_limit(5))
.build()
.await?;
let mut builder = control
.rewrite_object()
.set_source_bucket(bucket_name)
.set_source_object(&source_object.name)
.set_destination_bucket(bucket_name)
.set_destination_name("rewrite-object-clone");
// Optionally limit the max bytes written per request.
builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);
// Optionally change the storage class to force GCS to copy bytes
builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));
let dest_object = loop {
let progress = make_one_request(builder.clone()).await?;
match progress {
RewriteProgress::Incomplete(rewrite_token) => {
builder = builder.set_rewrite_token(rewrite_token);
}
RewriteProgress::Done(object) => break object,
};
};
println!("dest_object={dest_object:?}");
cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
Ok(())
}
enum RewriteProgress {
// This holds the rewrite token
Incomplete(String),
Done(Box<Object>),
}
async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
let resp = builder.send().await?;
if resp.done {
println!(
"DONE: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
return Ok(RewriteProgress::Done(Box::new(
resp.resource
.expect("A `done` response must have an object."),
)));
}
println!(
"PROGRESS: total_bytes_rewritten={}; object_size={}",
resp.total_bytes_rewritten, resp.object_size
);
Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}
// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
let storage = gcs::client::Storage::builder().build().await?;
// We need the size to exceed 1MiB to exercise the rewrite token logic.
let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
let object = storage
.write_object(bucket_name, "rewrite-object-source", payload)
.send_unbuffered()
.await?;
Ok(object)
}
// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o1)
.send()
.await;
let _ = control
.delete_object()
.set_bucket(bucket_name)
.set_object(o2)
.send()
.await;
let _ = control.delete_bucket().set_name(bucket_name).send().await;
}
We should see output similar to:
PROGRESS: total_bytes_rewritten=1048576; object_size=3145728
PROGRESS: total_bytes_rewritten=2097152; object_size=3145728
DONE: total_bytes_rewritten=3145728; object_size=3145728
dest_object=Object { name: "rewrite-object-clone", ... }
Speed up large object downloads
In this tutorial you will learn how to use striped downloads to speed up downloads of large Cloud Storage objects.
Prerequisites
The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.
You will create some large objects during this tutorial, remember to clean up any resources to avoid excessive billing.
The tutorial assumes you are familiar with the basics of using the client library. If not, read the quickstart guide.
Add the client library as a dependency
cargo add google-cloud-storage
Create source data
To run this tutorial you will need some large objects in Cloud Storage. You can create such objects by seeding a smaller object and then repeatedly composing it to create objects of the desired size.
You can put all the code for seeding the data in its own function. This function will receive the storage and storage control clients as parameters. For information on how to create these clients, consult the quickstart guide:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
// ... details omitted ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
As usual, the function starts with some use declarations to simplify the code:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Using the storage client, you create a 1MiB object:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Then you use the storage control client to concatenate 32 copies of this object into a larger object. This operation does not require transferring any object data to the client, it is performed by the service:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
You can repeat the operation to create larger and larger objects:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Striped downloads
Again, write a function to perform the striped download:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
// ... details below ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Use the storage control client to query the object metadata. This metadata includes the object size and the current generation:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
We will split the download of each stripe to a separate function. You will see
the details of this function in a moment, for now just note that it is async
,
so it returns a Future
:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
// ... details below ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
You can compute the size of each stripe and then call write_stripe()
to
download each of these stripes. Then collect the results into a vector:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
You can use the standard Rust facilities to concurrently await all these futures:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Once they complete, the file is downloaded.
Now you should complete writing the write_stripe()
function. First, duplicate
the write object and prepare to write starting at the desired offset:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Start a download from Cloud Storage:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
To restrict the download to the desired stripe, use .with_read_offset()
and
.with_read_limit()
:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
You may also want to restrict the download to the right object generation. This will avoid race conditions where another process writes over the object and you get inconsistent reads:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Then you read the data and write it to the local file:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Next steps
- Consider optimizing the case where the last stripe only has a few bytes
Expected performance
The performance of these downloads depends on:
- The I/O subsystem: if your local storage is not fast enough the downloads will be throttled by the writes to disk.
- The configuration of your VM: if you do not have enough CPUs, the downloads will be throttled on trying to decrypt the on data, as Cloud Storage and the client library always encrypt the data in transit.
- The location of the bucket and the particular object: the bucket may store all of the objects (or some objects) in a region different from your VM's location. In this case, you may be throttled by the wide-area network capacity.
With a large enough VM, using SSD for disk, and with a bucket in the same region as the VM you should get close to 1,000 MiB/s of effective throughput.
Full program
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;
async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::model::compose_object_request::SourceObject;
let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
let seed = client
.write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
.send_unbuffered()
.await?;
println!(
"Uploaded object {}, size={}KiB",
seed.name,
seed.size / 1024
);
let seed_32 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed.name)
.set_generation(seed.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
let seed_1024 = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
.set_source_objects((0..32).map(|_| {
SourceObject::new()
.set_name(&seed_32.name)
.set_generation(seed_32.generation)
}))
.send()
.await?;
println!(
"Created object {}, size={}MiB",
seed.name,
seed.size / (1024 * 1024)
);
for s in [2, 4, 8, 16, 32] {
let name = format!("{s}GiB.txt");
let target = control
.compose_object()
.set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
.set_source_objects((0..s).map(|_| {
SourceObject::new()
.set_name(&seed_1024.name)
.set_generation(seed_1024.generation)
}))
.send()
.await?;
println!(
"Created object {} size={} MiB",
target.name,
target.size / (1024 * 1024)
);
}
Ok(())
}
async fn download(
client: Storage,
control: StorageControl,
bucket_name: &str,
object_name: &str,
stripe_size: usize,
destination: &str,
) -> anyhow::Result<()> {
let metadata = control
.get_object()
.set_bucket(bucket_name)
.set_object(object_name)
.send()
.await?;
let file = tokio::fs::File::create(destination).await?;
let start = std::time::Instant::now();
let size = metadata.size as u64;
let limit = stripe_size as u64;
let count = size / limit;
let mut stripes = (0..count)
.map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
.collect::<Vec<_>>();
if size % limit != 0 {
stripes.push(write_stripe(
client.clone(),
&file,
count * limit,
limit,
&metadata,
))
}
futures::future::join_all(stripes)
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let elapsed = std::time::Instant::now() - start;
let mib = metadata.size as f64 / (1024.0 * 1024.0);
let bw = mib / elapsed.as_secs_f64();
println!(
"Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
);
Ok(())
}
async fn write_stripe(
client: Storage,
file: &tokio::fs::File,
offset: u64,
limit: u64,
metadata: &Object,
) -> anyhow::Result<()> {
use google_cloud_storage::model_ext::ReadRange;
use tokio::io::AsyncSeekExt;
let mut writer = file.try_clone().await?;
writer.seek(std::io::SeekFrom::Start(offset)).await?;
let mut reader = client
.read_object(&metadata.bucket, &metadata.name)
.set_generation(metadata.generation)
.set_read_range(ReadRange::segment(offset, limit))
.send()
.await?;
while let Some(b) = reader.next().await.transpose()? {
use tokio::io::AsyncWriteExt;
writer.write_all(&b).await?;
}
Ok(())
}
pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
const MB: usize = 1024 * 1024;
let client = Storage::builder().build().await?;
let control = StorageControl::builder().build().await?;
seed(client.clone(), control.clone(), bucket_name).await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
32 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32MiB.txt",
MB,
destination,
)
.await?;
#[cfg(feature = "run-large-downloads")]
{
let destination = "/dev/shm/output";
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"32GiB.txt",
256 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
1024 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"1GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"4GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"8GiB.txt",
512 * MB,
destination,
)
.await?;
download(
client.clone(),
control.clone(),
bucket_name,
"16GiB.txt",
512 * MB,
destination,
)
.await?;
}
Ok(())
}
Use errors to terminate object writes
In this guide you will learn how to use errors and custom data sources to terminate an object write before it is finalized. This is useful when applications want to stop the client library from finalizing the object creation if there is some error condition.
Prerequisites
The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.
The tutorial assumes you are familiar with the basics of using the client library. If not, you may want to read the quickstart guide first.
Add the client library as a dependency
cargo add google-cloud-storage
Overview
The client library creates objects from any type implementing the
StreamingSource
trait. The client library pulls data from implementations of
the trait. The library terminates the object write on the first error.
In this guide you will build a custom implementation of StreamingSource
that
returns some data and then stops on an error. You will verify that an object
write using this custom data source returns an error.
Create a custom error type
To terminate an object write without finalizing it, your StreamingSource must return an error. In this example you will create a simple error type, in your application code you can use any existing error type:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
The client library requires that your custom error type implements the standard Error trait:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
As you may recall, that requires implementing Display too:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
Create a custom StreamingSource
Create a type that generates the data for your object. In this example you will use synthetic data using a counter:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
Implement the streaming source trait for your type:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
// ... more details below ...
}
Define the error type:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
And implement the main function in this trait. Note how this function will (eventually) return the error type you defined above:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
Create the object
You will need a client to interact with Cloud Storage:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
Use the custom type to create the object:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
As expected, this object write fails. You can inspect the error details:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
Next Steps
Full Program
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(Debug)]
pub enum MyError {
ExpectedProblem,
OhNoes,
}
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectedProblem => write!(f, "this kind of thing happens"),
Self::OhNoes => write!(f, "oh noes! something terrible happened"),
}
}
}
#[derive(Debug, Default)]
struct MySource(u32);
impl google_cloud_storage::streaming_source::StreamingSource for MySource {
type Error = MyError;
async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
self.0 += 1;
match self.0 {
42 => Some(Err(MyError::ExpectedProblem)),
n if n > 42 => None,
n => Some(Ok(bytes::Bytes::from_owner(format!(
"test data for the example {n}\n"
)))),
}
}
}
pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
use google_cloud_storage::client::Storage;
let client = Storage::builder().build().await?;
let upload = client
.write_object(bucket_name, "expect-error", MySource::default())
.send_buffered()
.await;
println!("Upload result {upload:?}");
let err = upload.expect_err("the source is supposed to terminate the upload");
assert!(err.is_serialization(), "{err:?}");
use std::error::Error as _;
assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
Ok(())
}
Update a resource using a field mask
This guide shows you how to update a resource using a field mask, so that you can control which fields on the resource will be updated. The guide uses a secret from Secret Manager as a resource, but the concepts apply to other resources and services as well.
Prerequisites
To complete this tutorial, you need a Rust development environment with the following dependencies installed:
- The Secret Manager client library
- Tokio
To get set up, follow the steps in Setting up your development environment.
Install well known types
The google_cloud_wkt crate contains well known types for Google Cloud APIs.
These types typically have custom JSON encoding, and may provide conversion
functions to and from native or commonly used Rust types. google_cloud_wkt
contains the field mask type, FieldMask
, so you'll need to add the crate as a
dependency:
cargo add google-cloud-wkt
FieldMask
A FieldMask represents a set of symbolic field paths. Field masks are used to specify a subset of fields that should be returned by a get operation or modified by an update operation.
A field mask in an update operation specifies which fields of the targeted resource should be updated. The API is required to change only the values of the fields specified in the mask and leave the others untouched. If a resource is passed in to describe the updated values, the API ignores the values of all fields not covered by the mask. If a field mask is not present on update, the operation applies to all fields (as if a field mask of all fields had been specified).
In order to reset a field to the default value, you must include the field in the mask and set the default value in the provided resource. Thus, in order to reset all fields of a resource, provide a default instance of the resource and set all fields in the mask, or don't provide a mask.
Update fields on a resource
First, initialize a Secret Manager client and create a secret:
let client = SecretManagerService::builder().build().await?;
let secret = client
.create_secret()
.set_parent(format!("projects/{project_id}"))
.set_secret_id("your-secret")
.set_secret(model::Secret::new().set_replication(
model::Replication::new().set_automatic(model::replication::Automatic::new()),
))
.send()
.await?;
println!("CREATE = {secret:?}");
If you examine the output from the create operation, you'll see that both the
labels
and annotations
fields are empty.
The following code updates the labels
and annotations
fields:
let tag = |mut labels: HashMap<_, _>, msg: &str| {
labels.insert("updated".to_string(), msg.to_string());
labels
};
let update = client
.update_secret()
.set_secret(
model::Secret::new()
.set_name(&secret.name)
.set_etag(secret.etag)
.set_labels(tag(secret.labels, "your-label"))
.set_annotations(tag(secret.annotations, "your-annotations")),
)
.set_update_mask(
google_cloud_wkt::FieldMask::default().set_paths(["annotations", "labels"]),
)
.send()
.await?;
println!("UPDATE = {update:?}");
The set_etag
method lets you set an etag on the secret, which prevents
overwriting concurrent updates.
Having set labels and annotations on the updated secret, you pass a field mask
to set_update_mask
specifying the field paths to be updated:
.set_update_mask(
google_cloud_wkt::FieldMask::default().set_paths(["annotations", "labels"]),
)
In the output from the update operation, you can see that the fields have been updated:
labels: {"updated": "your-label"},
...
annotations: {"updated": "your-annotations"},
See below for the complete code.
What's next
In this guide, you updated a resource using a field mask. The sample code uses the Secret Manager API, but you can use field masks with other clients too. Try one of the other Cloud Client Libraries for Rust:
- Generate text using the Vertex AI Gemini API
- Using Google Cloud Storage: Push data on object uploads
Update field: complete code
pub async fn update_field(project_id: &str) -> anyhow::Result<()> {
use google_cloud_secretmanager_v1::client::SecretManagerService;
use std::collections::HashMap;
let client = SecretManagerService::builder().build().await?;
let secret = client
.create_secret()
.set_parent(format!("projects/{project_id}"))
.set_secret_id("your-secret")
.set_secret(model::Secret::new().set_replication(
model::Replication::new().set_automatic(model::replication::Automatic::new()),
))
.send()
.await?;
println!("CREATE = {secret:?}");
let tag = |mut labels: HashMap<_, _>, msg: &str| {
labels.insert("updated".to_string(), msg.to_string());
labels
};
let update = client
.update_secret()
.set_secret(
model::Secret::new()
.set_name(&secret.name)
.set_etag(secret.etag)
.set_labels(tag(secret.labels, "your-label"))
.set_annotations(tag(secret.annotations, "your-annotations")),
)
.set_update_mask(
google_cloud_wkt::FieldMask::default().set_paths(["annotations", "labels"]),
)
.send()
.await?;
println!("UPDATE = {update:?}");
Ok(())
}
Configuring Retry Policies
The Google Cloud client libraries for Rust can automatically retry operations that fail due to transient errors.
This guide shows you how to customize the retry loop. First you'll learn how to enable a common retry policy for all requests in a client, and then how to override this default for a specific request.
Prerequisites
The guide uses the Secret Manager service. That makes the examples more concrete and therefore easier to follow. With that said, the same ideas work for any other service.
You may want to follow the service quickstart. This guide will walk you through the steps necessary to enable the service, ensure you have logged in, and that your account has the necessary permissions.
Dependencies
As usual with Rust, you must declare dependencies in your Cargo.toml
file:
cargo add google-cloud-secretmanager-v1
Configuring the default retry policy
This example uses the Aip194Strict
policy. This policy is based on the
guidelines in AIP-194
, which documents the conditions under which a Google
API client should automatically retry a request. The policy is fairly
conservative, and will not retry any error that indicates the request may have
reached the service, unless the request is idempotent. As such, the policy
is safe to use as a default. The only downside may be additional requests to the
service, consuming some quota and billing.
To make this the default policy for the service, set the policy during the client initialization:
let client = secret_manager::client::SecretManagerService::builder()
.with_retry_policy(Aip194Strict)
.build()
.await?;
Then use the service as usual:
let mut list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item();
while let Some(secret) = list.next().await {
let secret = secret?;
println!(" secret={}", secret.name);
}
See below for the complete code.
Configuring the default retry policy with limits
The Aip194Strict
policy does not limit the number of retry attempts or the
time spent retrying requests. However, it can be decorated to set such limits.
For example, you can limit both the number of attempts and the time spent in
the retry loop using:
let client = secret_manager::client::SecretManagerService::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(15)),
)
.build()
.await?;
Requests work as usual too:
let mut list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item();
while let Some(secret) = list.next().await {
let secret = secret?;
println!(" secret={}", secret.name);
}
See below for the complete code.
Override the retry policy for one request
Sometimes applications need to override the retry policy for a specific request. For example, the application developer may know specific details of the service or application and determine it is safe to tolerate more errors.
For example, deleting a secret is idempotent, because it can only succeed once. But the client library assumes all delete operations are unsafe. The application can override the policy for one request:
client
.delete_secret()
.set_name(format!("projects/{project_id}/secrets/{secret_id}"))
.with_retry_policy(
AlwaysRetry
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(15)),
)
.send()
.await?;
See below for the complete code.
Configuring the default retry policy: complete code
pub async fn client_retry(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::paginator::ItemPaginator as _;
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_secretmanager_v1 as secret_manager;
let client = secret_manager::client::SecretManagerService::builder()
.with_retry_policy(Aip194Strict)
.build()
.await?;
let mut list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item();
while let Some(secret) = list.next().await {
let secret = secret?;
println!(" secret={}", secret.name);
}
Ok(())
}
Configuring the default retry policy with limits: complete code
pub async fn client_retry_full(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::paginator::ItemPaginator as _;
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use google_cloud_secretmanager_v1 as secret_manager;
use std::time::Duration;
let client = secret_manager::client::SecretManagerService::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(15)),
)
.build()
.await?;
let mut list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item();
while let Some(secret) = list.next().await {
let secret = secret?;
println!(" secret={}", secret.name);
}
Ok(())
}
Override the retry policy for one request: complete code
use google_cloud_secretmanager_v1 as secret_manager;
pub async fn request_retry(
client: &secret_manager::client::SecretManagerService,
project_id: &str,
secret_id: &str,
) -> crate::Result<()> {
use google_cloud_gax::options::RequestOptionsBuilder;
use google_cloud_gax::retry_policy::AlwaysRetry;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
client
.delete_secret()
.set_name(format!("projects/{project_id}/secrets/{secret_id}"))
.with_retry_policy(
AlwaysRetry
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(15)),
)
.send()
.await?;
Ok(())
}
Error handling
Sometimes applications need to branch based on the type and details of the error returned by the client library. This guide shows you how to write code to handle such errors.
Retryable errors: One of the most common reasons to handle errors in distributed systems is to retry requests that fail due to transient errors. The Google Cloud Client Libraries for Rust implement a policy-based retry loop. You only need to configure the policies to enable the retry loop, and the libraries implement common retry policies. Consult the Configuring Retry Policies section before implementing your own retry loop.
Prerequisites
The guide uses the Secret Manager service to demonstrate error handling, but the concepts apply to other services as well.
You may want to follow the service quickstart, which shows you how to enable the service and ensure that you've logged in and that your account has the necessary permissions.
For complete setup instructions for the Rust libraries, see Setting up your development environment.
Dependencies
Add the Secret Manager library to your Cargo.toml
file:
cargo add google-cloud-secretmanager-v1
In addition, this guide uses crc32c
to calculate the checksum:
cargo add crc32c
Motivation
In this guide you'll create a new secret version. Secret versions are contained in secrets. You must create the secret before adding secret versions. A common pattern in cloud services is to use a resource as if the container for it existed, and only create the container if there is an error. If the container exists most of the time, such an approach is more efficient than checking if the container exists before making the request. Checking if the container exists consumes more quota, results in more RPC charges, and is slower when the container already exists.
Handling the error
This section walks you through a function to update a secret. For the full code
sample, see update_secret
.
First make an attempt to create a new secret version:
match update_attempt(&client, project_id, secret_id, data.clone()).await {
If update_attempt
succeeds, you can just print the
successful result and return:
Ok(version) => {
println!("new version is {}", version.name);
Ok(version)
}
The request may have failed for many reasons: because the connection dropped before the request was fully sent, or the connection dropped before the response was received, or because it was impossible to create the authentication tokens.
The retry policies can deal with most of these errors. Here we are interested only in errors returned by the service:
Err(e) => {
if let Some(status) = e.status() {
and then only in errors that correspond to a missing secret:
use gax::error::rpc::Code;
if status.code == Code::NotFound {
If this is a "not found" error, you can try to create the secret. This simply returns on failure:
let _ = create_secret(&client, project_id, secret_id).await?;
Assuming create_secret
is successful, you can try to add the
secret version again, this time just returning an error if anything fails:
let version = update_attempt(&client, project_id, secret_id, data).await?;
println!("new version is {}", version.name);
return Ok(version);
What's next
Learn more about error handling:
Code samples
update_secret
pub async fn update_secret(
project_id: &str,
secret_id: &str,
data: Vec<u8>,
) -> crate::Result<sm::model::SecretVersion> {
let client = sm::client::SecretManagerService::builder().build().await?;
match update_attempt(&client, project_id, secret_id, data.clone()).await {
Ok(version) => {
println!("new version is {}", version.name);
Ok(version)
}
Err(e) => {
if let Some(status) = e.status() {
use gax::error::rpc::Code;
if status.code == Code::NotFound {
let _ = create_secret(&client, project_id, secret_id).await?;
let version = update_attempt(&client, project_id, secret_id, data).await?;
println!("new version is {}", version.name);
return Ok(version);
}
}
Err(e.into())
}
}
}
update_attempt
async fn update_attempt(
client: &sm::client::SecretManagerService,
project_id: &str,
secret_id: &str,
data: Vec<u8>,
) -> gax::Result<sm::model::SecretVersion> {
let checksum = crc32c::crc32c(&data) as i64;
client
.add_secret_version()
.set_parent(format!("projects/{project_id}/secrets/{secret_id}"))
.set_payload(
sm::model::SecretPayload::new()
.set_data(data)
.set_data_crc32c(checksum),
)
.send()
.await
}
create_secret
pub async fn create_secret(
client: &sm::client::SecretManagerService,
project_id: &str,
secret_id: &str,
) -> gax::Result<sm::model::Secret> {
use google_cloud_gax::options::RequestOptionsBuilder;
use google_cloud_gax::retry_policy::AlwaysRetry;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
client
.create_secret()
.set_parent(format!("projects/{project_id}"))
.with_retry_policy(
AlwaysRetry
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(15)),
)
.set_secret_id(secret_id)
.set_secret(
sm::model::Secret::new()
.set_replication(sm::model::Replication::new().set_replication(
sm::model::replication::Replication::Automatic(
sm::model::replication::Automatic::new().into(),
),
))
.set_labels([("integration-test", "true")]),
)
.send()
.await
}
Examine error details
Some Google Cloud services include additional error details when requests fail.
To help with any troubleshooting, the Google Cloud client libraries for Rust
always include these details when errors are formatted using
std::fmt::Display
. Some applications may want to examine these details and
change their behavior based on their contents.
This guide shows you how to examine the error details returned by Google Cloud services.
Prerequisites
This guide uses the Cloud Natural Language API to show error details, but the concepts apply to other services as well.
You may want to follow the service quickstart, which shows you how to enable the service and set up authentication.
For complete setup instructions for the Rust libraries, see Setting up your development environment.
Dependencies
As usual with Rust, you must declare the dependency in your Cargo.toml
file:
cargo add google-cloud-language-v2
Examining error details
You'll create a request that intentionally results in an error, and then examine the error contents. First, create a client:
use google_cloud_language_v2 as lang;
let client = lang::client::LanguageService::builder().build().await?;
Then send a request. In this case, a key field is missing:
let result = client
.analyze_sentiment()
.set_document(
lang::model::Document::new()
// Missing document contents
// .set_content("Hello World!")
.set_type(lang::model::document::Type::PlainText),
)
.send()
.await;
Extract the error from the result, using standard Rust functions. The error type prints all the error details in human-readable form:
let err = result.expect_err("the request should have failed");
println!("\nrequest failed with error {err:#?}");
This should produce output similar to:
request failed with error Error {
kind: Service {
status_code: Some(
400,
),
headers: Some(
{
"vary": "X-Origin",
"vary": "Referer",
"vary": "Origin,Accept-Encoding",
"content-type": "application/json; charset=UTF-8",
"date": "Sat, 24 May 2025 17:19:49 GMT",
"server": "scaffolding on HTTPServer2",
"x-xss-protection": "0",
"x-frame-options": "SAMEORIGIN",
"x-content-type-options": "nosniff",
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
"accept-ranges": "none",
"transfer-encoding": "chunked",
},
),
status: Status {
code: InvalidArgument,
message: "One of content, or gcs_content_uri must be set.",
details: [
BadRequest(
BadRequest {
field_violations: [
FieldViolation {
field: "document.content",
description: "Must have some text content to annotate.",
reason: "",
localized_message: None,
_unknown_fields: {},
},
],
_unknown_fields: {},
},
),
],
},
},
}
Programmatically examining the error details
Sometimes you may need to examine the error details programmatically. The rest of the example traverses the data structure and prints the most relevant fields.
Only errors returned by the service contain detailed information, so first query the error to see if it contains the correct error type. If it does, you can break down some top-level information about the error:
if let Some(status) = err.status() {
println!(
" status.code={}, status.message={}",
status.code, status.message,
);
And then iterate over all the details:
for detail in status.details.iter() {
use google_cloud_gax::error::rpc::StatusDetails;
match detail {
The client libraries return a StatusDetails
enum with the different types of
error details. This example only examines BadRequest
errors:
StatusDetails::BadRequest(bad) => {
A BadRequest
contains a list of fields that are in violation. You can iterate
and print the details for each:
for f in bad.field_violations.iter() {
println!(
" the request field {} has a problem: \"{}\"",
f.field, f.description
);
}
Such information can be useful during development. Other branches of
StatusDetails
such as QuotaFailure
may be useful at runtime to throttle an
application.
Expected output
Typically the output from the error details will look like so:
status.code=400, status.message=One of content, or gcs_content_uri must be set., status.status=Some("INVALID_ARGUMENT")
the request field document.content has a problem: "Must have some text content to annotate."
What's next
- Learn how to handle binding errors that occur when a client library can't find a URI to match an HTTP request.
- Learn how to work with List operations.
Examining error details: complete code
pub async fn examine_error_details() -> crate::Result<()> {
use google_cloud_language_v2 as lang;
let client = lang::client::LanguageService::builder().build().await?;
let result = client
.analyze_sentiment()
.set_document(
lang::model::Document::new()
// Missing document contents
// .set_content("Hello World!")
.set_type(lang::model::document::Type::PlainText),
)
.send()
.await;
let err = result.expect_err("the request should have failed");
println!("\nrequest failed with error {err:#?}");
if let Some(status) = err.status() {
println!(
" status.code={}, status.message={}",
status.code, status.message,
);
for detail in status.details.iter() {
use google_cloud_gax::error::rpc::StatusDetails;
match detail {
StatusDetails::BadRequest(bad) => {
for f in bad.field_violations.iter() {
println!(
" the request field {} has a problem: \"{}\"",
f.field, f.description
);
}
}
_ => {
println!(" additional error details: {detail:?}");
}
}
}
}
Ok(())
}
Handling binding errors
You might have tried to make a request and run into an error that looks like this:
Error: cannot find a matching binding to send the request: at least one of the
conditions must be met: (1) field `name` needs to be set and match the template:
'projects/*/secrets/*' OR (2) field `name` needs to be set and match the
template: 'projects/*/locations/*/secrets/*'
This is a binding error, and this guide explains how to troubleshoot binding errors.
What causes a binding error
The Google Cloud Client Libraries for Rust primarily use HTTP to send requests to Google Cloud services. An HTTP request uses a Uniform Resource Identifier (URI) to specify a resource.
Some RPCs correspond to multiple URIs. The contents of the request determine which URI is used.
The client library considers all possible URIs, and only returns a binding error if no URIs work. Typically this happens when a field is either missing or in an invalid format.
The example error above was produced by trying to get a resource without naming
the resource. Specifically, the name
field on a GetSecretRequest
was
required but not set.
let secret = client
.get_secret()
//.set_name("projects/my-project/secrets/my-secret")
.send()
.await;
How to fix it
In this case, to fix the error you'd set the name
field to something matching
one of the templates shown in the error message:
'projects/*/secrets/*'
'projects/*/locations/*/secrets/*'
Either allows the client library to make a request to the server:
let secret = client
.get_secret()
.set_name("projects/my-project/secrets/my-secret")
.send()
.await;
or
let secret = client
.get_secret()
.set_name("projects/my-project/locations/us-central1/secrets/my-secret")
.send()
.await;
Interpreting templates
The error message for a binding error includes a number of template strings
showing possible values for the request fields. Most template strings include
*
and **
as wildcards to match the field values.
Single wildcard
The *
wildcard alone means a non-empty string without a /
. It can be thought
of as the regex [^/]+
.
Here are some examples:
Template | Input | Match? |
---|---|---|
"*" | "simple-string-123" | true |
"projects/*" | "projects/p" | true |
"projects/*/locations" | "projects/p/locations" | true |
"projects/*/locations/*" | "projects/p/locations/l" | true |
"*" | "" (empty) | false |
"*" | "string/with/slashes" | false |
"projects/*" | "projects/" (empty) | false |
"projects/*" | "projects/p/" (extra slash) | false |
"projects/*" | "projects/p/locations/l" | false |
"projects/*/locations" | "projects/p" | false |
"projects/*/locations" | "projects/p/locations/l" | false |
Double wildcard
Less common is the **
wildcard, which means any string. The string can be
empty or contain any number of /
's. It can be thought of as the regex .*
.
Also, when a template ends in /**
, that initial slash is optionally included.
Template | Input | Match? |
---|---|---|
"**" | "" | true |
"**" | "simple-string-123" | true |
"**" | "string/with/slashes" | true |
"projects/*/**" | "projects/p" | true |
"projects/*/**" | "projects/p/locations" | true |
"projects/*/**" | "projects/p/locations/l" | true |
"projects/*/**" | "locations/l" | false |
"projects/*/**" | "projects//locations/l" | false |
Inspecting the error
If you need to inspect the error programmatically, you can do so by checking
that it is a binding error, then downcasting it to a BindingError
.
let secret = client
.get_secret()
//.set_name("projects/my-project/secrets/my-secret")
.send()
.await;
use gax::error::binding::BindingError;
let e = secret.unwrap_err();
assert!(e.is_binding(), "{e:?}");
assert!(e.source().is_some(), "{e:?}");
let _ = e
.source()
.and_then(|e| e.downcast_ref::<BindingError>())
.expect("should be a BindingError");
Working with list operations
Some services return potentially large lists of items, such as rows or resource
descriptions. To keep CPU and memory usage under control, services return these
resources in pages
: smaller subsets of the items with a continuation token to
request the next subset.
Iterating over items by page can be tedious. The client libraries provide adapters to convert the pages into asynchronous iterators. This guide shows you how to work with these adapters.
Prerequisites
This guide uses the Secret Manager service to demonstrate list operations, but the concepts apply to other services as well.
You may want to follow the service quickstart, which shows you how to enable the service and ensure that you've logged in and that your account has the necessary permissions.
For complete setup instructions for the Rust libraries, see Setting up your development environment.
Dependencies
Add the Secret Manager library to your Cargo.toml
file:
cargo add google-cloud-secretmanager-v1
Iterating list methods
To help iterate the items in a list method, the APIs return an implementation of
the ItemPaginator
trait. Introduce it into scope via a use
declaration:
use google_cloud_gax::paginator::ItemPaginator as _;
To iterate the items, use the by_item
function.
let mut list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item();
while let Some(secret) = list.next().await {
let secret = secret?;
println!(" secret={}", secret.name)
}
In rare cases, pages might contain extra information that you need access to. Or you may need to checkpoint your progress across processes. In these cases, you can iterate over full pages instead of individual items.
First introduce Paginator
into scope via a use
declaration:
use google_cloud_gax::paginator::Paginator as _;
Then iterate over the pages using by_page
:
let mut list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_page();
while let Some(page) = list.next().await {
let page = page?;
println!(" next_page_token={}", page.next_page_token);
page.secrets
.into_iter()
.for_each(|secret| println!(" secret={}", secret.name));
}
Working with futures::Stream
You may want to use these APIs in the larger Rust ecosystem of asynchronous
streams, such as tokio::Stream
. This is readily done, but you must first
enable the unstable-streams
feature in the google_cloud_gax
crate:
cargo add google-cloud-gax --features unstable-stream
The name of this feature is intended to convey that we consider these APIs
unstable, because they are! You should only use them if you are prepared to deal
with any breaks that result from incompatible changes to the
futures::Stream
trait.
The following examples also use the futures::stream::StreamExt
trait, which
you enable by adding the futures
crate.
cargo add futures
Add the required use
declarations:
use futures::stream::StreamExt;
use google_cloud_gax::paginator::ItemPaginator as _;
Then use the into_stream
function to convert ItemPaginator
into a
futures::Stream
of items.
let list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_item()
.into_stream();
list.map(|secret| -> gax::Result<()> {
println!(" secret={}", secret?.name);
Ok(())
})
.fold(Ok(()), async |acc, result| -> gax::Result<()> {
acc.and(result)
})
.await?;
Similarly, you can use the into_stream
function to convert Paginator
into a
futures::Stream
of pages.
let list = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.by_page()
.into_stream();
list.enumerate()
.map(|(index, page)| -> gax::Result<()> {
println!("page={}, next_page_token={}", index, page?.next_page_token);
Ok(())
})
.fold(Ok(()), async |acc, result| -> gax::Result<()> {
acc.and(result)
})
.await?;
Resuming list methods by setting next page token
In some cases, such as an interrupted list operation, you can set the next page token to resume paginating from a specific page.
let page = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.send()
.await;
let page = page?;
let mut next_page_token = page.next_page_token.clone();
page.secrets
.into_iter()
.for_each(|secret| println!(" secret={}", secret.name));
while !next_page_token.is_empty() {
println!(" next_page_token={next_page_token}");
let page = client
.list_secrets()
.set_parent(format!("projects/{project_id}"))
.set_page_token(next_page_token)
.send()
.await;
let page = page?;
next_page_token = page.next_page_token.clone();
page.secrets
.into_iter()
.for_each(|secret| println!(" secret={}", secret.name));
}
Additional paginator technical details
The standard Google API List method follows the pagination guideline defined
by AIP-158. Each call to a List
method for a resource returns a page of
resource items (e.g. secrets) along with a next-page token that can be passed to
the List
method to retrieve the next page.
The Google Cloud Client Libraries for Rust provide an adapter to convert the list RPCs as defined by AIP-4233 into a streams that can be iterated over in an async fashion.
What's next
Learn more about working with the Cloud Client Libraries for Rust:
Working with long-running operations
Occasionally, an API may need to expose a method that takes a significant amount of time to complete. In these situations, it's often a poor user experience to simply block while the task runs. It's usually better to return some kind of promise to the user and allow the user to check back later.
The Google Cloud Client Libraries for Rust provide helpers to work with these long-running operations (LROs). This guide shows you how to start LROs and wait for their completion.
Prerequisites
The guide uses the Speech-To-Text V2 service to keep the code snippets concrete. The same ideas work for any other service using LROs.
We recommend that you first follow one of the service guides, such as Transcribe speech to text by using the command line. These guides cover critical topics such as ensuring your project has the API enabled, your account has the right permissions, and how to set up billing for your project (if needed). Skipping the service guides may result in problems that are hard to diagnose.
For complete setup instructions for the Rust libraries, see Setting up your development environment.
Dependencies
Declare Google Cloud dependencies in your Cargo.toml
file:
cargo add google-cloud-speech-v2 google-cloud-lro google-cloud-longrunning
You'll also need several tokio
features:
cargo add tokio --features full,macros
Starting a long-running operation
To start a long-running operation, you'll initialize a client and then make the RPC. But first, add some use declarations to avoid the long package names:
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
Now create the client:
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
You'll use batch recognize for this example. While this is designed for long audio files, it works well with small files too.
In the Rust client libraries, each request is represented by a method that
returns a request builder. First, call the right method on the client to create
the request builder. You'll use the default recognizer (_
) in the global
region.
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
Then initialize the request to use a publicly available audio file:
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
Configure the request to return the transcripts inline:
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
Then configure the service to transcribe to US English, using the short model and some other default configuration:
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
Make the request and wait for an Operation to
be returned. This Operation
acts as a promise for the result of the
long-running request:
.send()
.await?;
Finally, poll the promise until it completes:
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
You'll examine the manually_poll_lro()
function in the
Manually polling a long-running operation section.
You can find the full function below.
Automatically polling a long-running operation
To configure automatic polling, you prepare the request just like you did to
start a long-running operation. The difference comes at the end, where instead
of sending the request to get the Operation
promise:
.send()
.await?;
... you create a Poller
and wait until it is done:
.poller()
.until_done()
.await?;
Let's review the code step-by-step.
First, introduce the trait in scope via a use
declaration:
use google_cloud_lro::Poller;
Then initialize the client and prepare the request as before:
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
And then poll until the operation is completed and print the result:
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
You can find the full function below.
Polling a long-running operation
While .until_done()
is convenient, it omits some information: long-running
operations may report partial progress via a "metadata" attribute. If your
application requires such information, you need to use the poller directly:
let mut poller = client
.batch_recognize(/* stuff */)
/* more stuff */
.poller();
Then use the poller in a loop:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
Note how this loop explicitly waits before polling again. The polling period depends on the specific operation and its payload. You should consult the service documentation and/or experiment with your own data to determine a good value.
The poller uses a policy to determine what polling errors are transient and may resolve themselves. The Configuring polling policies chapter covers this topic in detail.
You can find the full function below.
Manually polling a long-running operation
In general, we recommend that you use the previous two approaches in your application. Alternatively, you can manually poll a long-running operation, but this can be quite tedious, and it is easy to get the types wrong. If you do need to manually poll a long-running operation, this section walks you through the required steps. You may want to read the Operation message reference documentation, as some of the fields and types are used below.
Recall that you started the long-running operation using the client:
let mut operation = client
.batch_recognize(/* stuff */)
/* more stuff */
.send()
.await?;
You are going to start a loop to poll the operation
, and you need to check if
the operation completed immediately (this is rare but does happen). The done
field indicates if the operation completed:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
In most cases, if the operation is done it contains a result. However, the field
is optional because the service could return done
as true and no result: maybe
the operation deletes resources and a successful completion has no return value.
In this example using the Speech-to-Text service, you can treat this as an
error:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
Starting a long-running operation successfully does not guarantee that it will complete successfully. The result may be an error or a valid response. You need to check for both. First check for errors:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
The error type is a Status message type. This does not
implement the standard Error
interface. You need to manually convert it to a
valid error. You can use Error::service to perform this conversion.
Assuming the result is successful, you need to extract the response type. You can find this type in the documentation for the LRO method, or by reading the service API documentation:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
Note that extraction of the value may fail if the type does not match what the service sent.
All types in Google Cloud may add fields and branches in the future. While this
is unlikely for a common type such as Operation
, it happens frequently for
most service messages. The Google Cloud Client Libraries for Rust mark all
structs and enums as #[non_exhaustive]
to signal that such changes are
possible. In this case, you must handle this unexpected case:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
If the operation has not completed, then it may contain some metadata. Some services just include initial information about the request, while other services include partial progress reports. You can choose to extract and report this metadata:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
As the operation has not completed, you need to wait before polling again. Consider adjusting the polling period, maybe using a form of truncated exponential backoff. This example simply polls every 500ms:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
Then you can poll the operation to get its new status:
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
For simplicity, the example ignores all errors. In your application you may choose to treat only a subset of the errors as non-recoverable, and may want to limit the number of polling attempts if these fail.
You can find the full function below.
What's next
- To learn about customizing error handling and backoff periods for LROs, see Configuring polling policies.
- To learn how to simulate LROs in your unit tests, see How to write tests for long-running operations.
Starting a long-running operation: complete code
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
Automatically polling a long-running operation: complete code
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
Polling a long-running operation: complete code
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
Manually polling a long-running operation: complete code
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
pub async fn start(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let operation = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.send()
.await?;
println!("LRO started, response={operation:?}");
let response = manually_poll_lro(client, operation).await;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn automatic(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
pub async fn polling(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::retry_policy::Aip194Strict;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
use google_cloud_lro::{Poller, PollingResult};
let client = speech::client::Speech::builder()
.with_retry_policy(
Aip194Strict
.with_attempt_limit(5)
.with_time_limit(Duration::from_secs(30)),
)
.build()
.await?;
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.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(())
}
pub async fn manually_poll_lro(
client: speech::client::Speech,
operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
let mut operation = operation;
loop {
if operation.done {
match &operation.result {
None => {
return Err("missing result for finished operation".into());
}
Some(r) => {
return match r {
longrunning::model::operation::Result::Error(e) => {
Err(format!("{e:?}").into())
}
longrunning::model::operation::Result::Response(any) => {
let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
Ok(response)
}
_ => Err(format!("unexpected result branch {r:?}").into()),
};
}
}
}
if let Some(any) = &operation.metadata {
let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
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;
}
}
}
Configuring polling policies
The Google Cloud Client Libraries for Rust provide helper functions to simplify waiting and monitoring the progress of LROs (Long-Running Operations). These helpers use policies to configure the polling frequency and to determine what polling errors are transient and may be ignored until the next polling event.
This guide will walk you through the configuration of these policies for all the long-running operations started by a client, or just for one specific request.
There are two different policies controlling the behavior of the LRO loops:
- The polling backoff policy controls how long the loop waits before polling the status of a LRO that is still in progress.
- The polling error policy controls what to do on an polling error. Some polling errors are unrecoverable, and indicate that the operation was aborted or the caller has no permissions to check the status of the LRO. Other polling errors are transient, and indicate a temporary problem in the client network or the service.
Each one of these policies can be set independently, and each one can be set for all the LROs started on a client or changed for just one request.
Prerequisites
The guide uses the Speech-To-Text V2 service to keep the code snippets concrete. The same ideas work for any other service using LROs.
We recommend you first follow one of the service guides, such as Transcribe speech to text by using the command line. These guides will cover critical topics such as ensuring your project has the API enabled, your account has the right permissions, and how to set up billing for your project (if needed). Skipping the service guides may result in problems that are hard to diagnose.
Dependencies
As it is usual with Rust, you must declare the dependency in your Cargo.toml
file. We use:
cargo add google-cloud-speech-v2 google-cloud-lro
Configuring the polling frequency for all requests in a client
If you are planning to use the same polling backoff policy for all (or even most) requests with the same client then consider setting this as a client option.
To configure the polling frequency you use a type implementing the PollingBackoffPolicy trait. The client libraries provide ExponentialBackoff:
use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
Then initialize the client with the configuration you want:
let client = speech::client::Speech::builder()
.with_polling_backoff_policy(
ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_millis(250))
.with_maximum_delay(Duration::from_secs(10))
.build()?,
)
.build()
.await?;
Unless you override the policy with a per-request setting this policy will be in effect for any long-running operation started with the client. In this example, if you make a call such as:
let mut operation = client
.batch_recognize(/* stuff */)
/* more stuff */
.send()
.await?;
The client library will first wait for 500ms, after the first polling attempt, then for 1,000ms (or 1s) for the second attempt, and sub-sequent attempts will wait 2s, 4s, 8s and then all attempts will wait 10s.
See below for the complete code.
Configuring the polling frequency for a specific request
As described in the previous section. We need a type implementing the PollingBackoffPolicy trait to configure the polling frequency. We will also use ExponentialBackoff in this example:
use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
use std::time::Duration;
The configuration of the request will require bringing a trait within scope:
use google_cloud_gax::options::RequestOptionsBuilder;
You create the request builder as usual:
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
And then configure the polling backoff policy:
.with_polling_backoff_policy(
ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_millis(250))
.with_maximum_delay(Duration::from_secs(10))
.build()?,
)
You can issue this request as usual. For example:
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
See below for the complete code.
Configuring the retryable polling errors for all requests in a client
To configure the retryable errors we need to use a type implementing the PollingErrorPolicy trait. The client libraries provide a number of them, a conservative choice is Aip194Strict:
use google_cloud_gax::polling_error_policy::Aip194Strict;
use google_cloud_gax::polling_error_policy::PollingErrorPolicyExt;
use google_cloud_gax::retry_policy;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
If you are planning to use the same polling policy for all (or even most) requests with the same client then consider setting this as a client option.
Add the polling policies that you will use for all long running operations:
let builder = speech::client::Speech::builder().with_polling_error_policy(
Aip194Strict
.with_attempt_limit(100)
.with_time_limit(Duration::from_secs(300)),
);
You can also add retry policies to handle errors in the initial request:
let client = builder
.with_retry_policy(
retry_policy::Aip194Strict
.with_attempt_limit(100)
.with_time_limit(Duration::from_secs(300)),
)
.build()
.await?;
Unless you override the policy with a per-request setting this policy will be in effect for any long-running operation started with the client. In this example, if you make a call such as:
let mut operation = client
.batch_recognize(/* stuff */)
/* more stuff */
.send()
.await?;
The client library will only treat UNAVAILABLE
(see AIP-194) as a retryable
error, and will stop polling after 100 attempts or 300 seconds, whichever comes
first.
See below for the complete code.
Configuring the retryable polling errors for a specific request
To configure the retryable errors we need to use a type implementing the PollingErrorPolicy trait. The client libraries provide a number of them, a conservative choice is Aip194Strict:
use google_cloud_gax::polling_error_policy::Aip194Strict;
use google_cloud_gax::polling_error_policy::PollingErrorPolicyExt;
use google_cloud_gax::retry_policy;
use google_cloud_gax::retry_policy::RetryPolicyExt;
use std::time::Duration;
The configuration of the request will require bringing a trait within scope:
use google_cloud_gax::options::RequestOptionsBuilder;
You create the request builder as usual:
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
And then configure the polling backoff policy:
.with_polling_error_policy(
Aip194Strict
.with_attempt_limit(100)
.with_time_limit(Duration::from_secs(300)),
)
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
You can issue this request as usual. For example:
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Consider adding a retry policy in case the initial request to start the LRO fails:
let client = speech::client::Speech::builder()
.with_retry_policy(
retry_policy::Aip194Strict
.with_attempt_limit(100)
.with_time_limit(Duration::from_secs(300)),
)
.build()
.await?;
See below for the complete code.
Configuring the polling frequency for all requests in a client: complete code
pub async fn client_backoff(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
use google_cloud_lro::Poller;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_polling_backoff_policy(
ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_millis(250))
.with_maximum_delay(Duration::from_secs(10))
.build()?,
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
Configuring the polling frequency for a specific request: complete code
pub async fn rpc_backoff(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
use std::time::Duration;
use google_cloud_gax::options::RequestOptionsBuilder;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder().build().await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.with_polling_backoff_policy(
ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_millis(250))
.with_maximum_delay(Duration::from_secs(10))
.build()?,
)
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
Configuring the retryable polling errors for all requests in a client: complete code
pub async fn client_backoff(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
use google_cloud_lro::Poller;
use std::time::Duration;
let client = speech::client::Speech::builder()
.with_polling_backoff_policy(
ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_millis(250))
.with_maximum_delay(Duration::from_secs(10))
.build()?,
)
.build()
.await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
Configuring the retryable polling errors for a specific request: complete code
pub async fn rpc_backoff(project_id: &str) -> crate::Result<()> {
use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
use std::time::Duration;
use google_cloud_gax::options::RequestOptionsBuilder;
use google_cloud_lro::Poller;
let client = speech::client::Speech::builder().build().await?;
let response = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.with_polling_backoff_policy(
ExponentialBackoffBuilder::new()
.with_initial_delay(Duration::from_millis(250))
.with_maximum_delay(Duration::from_secs(10))
.build()?,
)
.set_files([speech::model::BatchRecognizeFileMetadata::new()
.set_uri("gs://cloud-samples-data/speech/hello.wav")])
.set_recognition_output_config(
speech::model::RecognitionOutputConfig::new()
.set_inline_response_config(speech::model::InlineOutputConfig::new()),
)
.set_processing_strategy(
speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
)
.set_config(
speech::model::RecognitionConfig::new()
.set_language_codes(["en-US"])
.set_model("short")
.set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
)
.poller()
.until_done()
.await?;
println!("LRO completed, response={response:?}");
Ok(())
}
How to write tests using a client
The Google Cloud Client Libraries for Rust provide a way to stub out the real client implementations, so a mock can be injected for testing.
Applications can use mocks to write controlled, reliable unit tests that do not involve network calls, and do not incur billing.
This guide shows how.
Dependencies
There are several mocking frameworks in Rust. This guide uses mockall
,
which seems to be the most popular.
cargo add --dev mockall
This guide will use a Speech
client. Note that the same ideas
in this guide apply to all of the clients, not just the Speech
client.
We declare the dependency in our Cargo.toml
. Yours will be similar, but
without the custom path
.
cargo add google-cloud-speech-v2 google-cloud-lro
Mocking a client
First, some use
declarations to simplify the code:
use google_cloud_gax as gax;
use google_cloud_speech_v2 as speech;
Let's assume our application has a function that uses the Speech
client to
make an RPC, and process the response from the server.
// An example application function.
//
// It makes an RPC, setting some field. In this case, it is the `GetRecognizer`
// RPC, setting the name field.
//
// It processes the response from the server. In this case, it extracts the
// display name of the recognizer.
async fn my_application_function(client: &speech::client::Speech) -> gax::Result<String> {
client
.get_recognizer()
.set_name("invalid-test-recognizer")
.send()
.await
.map(|r| r.display_name)
}
We want to test how our code handles different responses from the service.
First we will define the mock class. This class implements the
speech::stub::Speech
trait.
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn get_recognizer(&self, req: speech::model::GetRecognizerRequest, _options: gax::options::RequestOptions) -> gax::Result<gax::response::Response<speech::model::Recognizer>>;
}
}
Next, we create an instance of the mock. Note that the
mockall::mock!
macro prepends a Mock
prefix to the name of our
struct from above.
let mut mock = MockSpeech::new();
Next we will set expectations on the mock. We expect GetRecognizer
to be
called, with a particular name.
If that happens, we will simulate a successful response from the service.
mock.expect_get_recognizer()
.withf(move |r, _|
// Optionally, verify fields in the request.
r.name == "invalid-test-recognizer")
.return_once(|_, _| {
Ok(gax::response::Response::from(
speech::model::Recognizer::new().set_display_name("test-display-name"),
))
});
Now we are ready to create a Speech
client with our mock.
let client = speech::client::Speech::from_stub(mock);
Finally, we are ready to call our function...
let display_name = my_application_function(&client).await?;
... and verify the results.
assert_eq!(display_name, "test-display-name");
Simulating errors
Simulating errors is no different than simulating successes. We just need to modify the result returned by our mock.
mock.expect_get_recognizer().return_once(|_, _| {
// This time, return an error.
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::NotFound)
.set_message("Resource not found");
Err(Error::service(status))
});
Note that a client built from_stub()
does not have an internal retry loop. It
returns all errors from the stub directly to the application.
Full program
Putting all this code together into a full program looks as follows:
#[cfg(test)]
mod tests {
use google_cloud_gax as gax;
use google_cloud_speech_v2 as speech;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
// An example application function.
//
// It makes an RPC, setting some field. In this case, it is the `GetRecognizer`
// RPC, setting the name field.
//
// It processes the response from the server. In this case, it extracts the
// display name of the recognizer.
async fn my_application_function(client: &speech::client::Speech) -> gax::Result<String> {
client
.get_recognizer()
.set_name("invalid-test-recognizer")
.send()
.await
.map(|r| r.display_name)
}
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn get_recognizer(&self, req: speech::model::GetRecognizerRequest, _options: gax::options::RequestOptions) -> gax::Result<gax::response::Response<speech::model::Recognizer>>;
}
}
#[tokio::test]
async fn basic_success() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_get_recognizer()
.withf(move |r, _|
// Optionally, verify fields in the request.
r.name == "invalid-test-recognizer")
.return_once(|_, _| {
Ok(gax::response::Response::from(
speech::model::Recognizer::new().set_display_name("test-display-name"),
))
});
// Create a client, implemented by the mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function.
let display_name = my_application_function(&client).await?;
// Verify the final result of the RPC.
assert_eq!(display_name, "test-display-name");
Ok(())
}
#[tokio::test]
async fn basic_fail() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_get_recognizer().return_once(|_, _| {
// This time, return an error.
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::NotFound)
.set_message("Resource not found");
Err(Error::service(status))
});
// Create a client, implemented by the mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function.
let display_name = my_application_function(&client).await;
// Verify the final result of the RPC.
assert!(display_name.is_err());
Ok(())
}
}
How to write tests for long-running operations
The Google Cloud client libraries for Rust have helpers that simplify interaction with long-running operations (henceforth, LROs).
Simulating the behavior of LROs in tests involves understanding the details these helpers hide. This guide shows how to do that.
Prerequisites
This guide assumes you are familiar with the previous chapters:
Tests for automatic polling
Let's say our application code awaits lro::Poller::until_done()
. In previous
sections, we called this "automatic polling".
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
Note that our application only cares about the final result of the LRO. We do not need to test how it handles intermediate results from polling the LRO. Our tests can simply return the final result of the LRO from the mock.
Creating the longrunning::model::Operation
Let's say we want our call to result in the following response.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
You may have noticed that the stub returns a longrunning::model::Operation
,
not a BatchRecognizeResponse
. We need to pack our desired response into the
Operation::result
.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
Note also that we set the done
field to true
. This indicates to the Poller
that the operation has completed, thus ending the polling loop.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
Test code
Now we are ready to write our test.
First we define our mock class, which implements the
speech::stub::Speech
trait.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
Now in our test we create our mock, and set expectations on it.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
Finally, we create a client from the mock, call our function, and verify the response.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
Tests for manual polling with intermediate metadata
Let's say our application code manually polls, and does some processing on partial updates.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(
response: &BatchRecognizeResponse,
) -> Result<gax::response::Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
#[tokio::test]
async fn manual_polling_with_metadata() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert_eq!(result.billed_duration?, expected_duration());
Ok(())
}
}
We want to simulate how our application acts when it receives intermediate metadata. We can achieve this by returning in-progress operations from our mock.
Creating the longrunning::model::Operation
The BatchRecognize
RPC returns partial results in the form of a
speech::model::OperationMetadata
. Like before, we will need to pack this into
the returned longrunning::model::Operation
, but this time into the
Operation::metadata
field.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(
response: &BatchRecognizeResponse,
) -> Result<gax::response::Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
#[tokio::test]
async fn manual_polling_with_metadata() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert_eq!(result.billed_duration?, expected_duration());
Ok(())
}
}
Test code
First we define our mock class, which implements the
speech::stub::Speech
trait. Note that we override
get_operation()
. We will see why shortly.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(
response: &BatchRecognizeResponse,
) -> Result<gax::response::Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
#[tokio::test]
async fn manual_polling_with_metadata() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert_eq!(result.billed_duration?, expected_duration());
Ok(())
}
}
Now in our test we create our mock, and set expectations on it.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(
response: &BatchRecognizeResponse,
) -> Result<gax::response::Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
#[tokio::test]
async fn manual_polling_with_metadata() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert_eq!(result.billed_duration?, expected_duration());
Ok(())
}
}
These expectations will return partial results (25%, 50%, 75%), then return our desired final outcome.
Now a few things you probably noticed.
-
The first expectation is set on
batch_recognize()
, whereas all subsequent expectations are set onget_operation()
.The initial
BatchRecognize
RPC starts the LRO on the server-side. The server returns some identifier for the LRO. This is thename
field which is omitted from the test code, for simplicity.From then on, the client library just polls the status of that LRO. It does this using the
GetOperation
RPC.That is why we set expectations on different RPCs for the initial response vs. all subsequent responses.
-
Expectations are set in a sequence.
This allows
mockall
to verify the order of the calls. It is also necessary to determine whichexpect_get_operation
is matched.
Finally, we create a client from the mock, call our function, and verify the response.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(
response: &BatchRecognizeResponse,
) -> Result<gax::response::Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
#[tokio::test]
async fn manual_polling_with_metadata() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert_eq!(result.billed_duration?, expected_duration());
Ok(())
}
}
Simulating errors
Errors can arise in an LRO from a few places.
If your application uses automatic polling, the following cases are all
equivalent: until_done()
returns the error in the Result
, regardless of
where it originated.
Simulating an error starting an LRO will
yield the simplest test.
Note that the stubbed out client does not have a retry or polling policy. In all cases, the polling loop will terminate on the first error, even if the error is typically considered transient.
Simulating an error starting an LRO
The simplest way to simulate an error is to have the initial request fail with an error.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
For manual polling, an error starting an LRO is returned via the completed branch. This ends the polling loop.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
Simulating an LRO resulting in an error
If you need to simulate an LRO resulting in an error, after intermediate
metadata is returned, we need to return the error in the final
longrunning::model::Operation
.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
We set our expectations to return the Operation
from get_operation
as
before.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
An LRO ending in an error will be returned via the completed branch. This ends the polling loop.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
Simulating a polling error
Polling loops can also exit because the polling policy has been exhausted. When this happens, the client library can not say definitively whether the LRO has completed or not.
If your application has custom logic to deal with this case, we can exercise it
by returning an error from the get_operation
expectation.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
An LRO ending with a polling error will be returned via the polling error branch.
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
Automatic polling - Full test
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};
// Example application code that is under test
mod my_application {
use super::*;
// An example application function that automatically polls.
//
// It starts an LRO, awaits the result, and processes it.
pub async fn my_automatic_poller(
client: &speech::client::Speech,
project_id: &str,
) -> Result<Option<wkt::Duration>> {
use google_cloud_lro::Poller;
client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller()
.until_done()
.await
.map(|r| r.total_billed_duration)
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn automatic_polling() -> Result<()> {
// Create a mock, and set expectations on it.
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.return_once(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which automatically polls.
let billed_duration = my_automatic_poller(&client, "my-project").await?;
// Verify the final result of the LRO.
assert_eq!(billed_duration, expected_duration());
Ok(())
}
}
Manual polling with intermediate metadata - Full test
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn expected_duration() -> Option<wkt::Duration> {
Some(wkt::Duration::clamp(100, 0))
}
fn expected_response() -> BatchRecognizeResponse {
BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
}
fn make_finished_operation(
response: &BatchRecognizeResponse,
) -> Result<gax::response::Response<Operation>> {
let any = wkt::Any::from_msg(response).expect("test message should succeed");
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Response(any.into()));
Ok(Response::from(operation))
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
#[tokio::test]
async fn manual_polling_with_metadata() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_finished_operation(&expected_response()));
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert_eq!(result.billed_duration?, expected_duration());
Ok(())
}
}
Simulating errors - Full tests
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Examples showing how to simulate LROs in tests.
use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};
// Example application code that is under test
mod my_application {
use super::*;
pub struct BatchRecognizeResult {
pub progress_updates: Vec<i32>,
pub billed_duration: Result<Option<wkt::Duration>>,
}
// An example application function that manually polls.
//
// It starts an LRO. It consolidates the polling results, whether full or
// partial.
//
// In this case, it is the `BatchRecognize` RPC. If we get a partial update,
// we extract the `progress_percent` field. If we get a final result, we
// extract the `total_billed_duration` field.
pub async fn my_manual_poller(
client: &speech::client::Speech,
project_id: &str,
) -> BatchRecognizeResult {
use google_cloud_lro::{Poller, PollingResult};
let mut progress_updates = Vec::new();
let mut poller = client
.batch_recognize()
.set_recognizer(format!(
"projects/{project_id}/locations/global/recognizers/_"
))
.poller();
while let Some(p) = poller.poll().await {
match p {
PollingResult::Completed(r) => {
let billed_duration = r.map(|r| r.total_billed_duration);
return BatchRecognizeResult {
progress_updates,
billed_duration,
};
}
PollingResult::InProgress(m) => {
if let Some(metadata) = m {
// This is a silly application. Your application likely
// performs some task immediately with the partial
// update, instead of storing it for after the operation
// has completed.
progress_updates.push(metadata.progress_percent);
}
}
PollingResult::PollingError(e) => {
return BatchRecognizeResult {
progress_updates,
billed_duration: Err(e),
};
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// We can only get here if `poll()` returns `None`, but it only returns
// `None` after it returned `PollingResult::Completed`. Therefore this
// is never reached.
unreachable!("loop should exit via the `Completed` branch.");
}
}
#[cfg(test)]
mod tests {
use super::my_application::*;
use super::*;
mockall::mock! {
#[derive(Debug)]
Speech {}
impl speech::stub::Speech for Speech {
async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
}
}
fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
let metadata = OperationMetadata::new().set_progress_percent(progress);
let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
let operation = Operation::new().set_metadata(any);
Ok(Response::from(operation))
}
fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
let operation = Operation::new()
.set_done(true)
.set_result(OperationResult::Error(status.into()));
Ok(Response::from(operation))
}
#[tokio::test]
async fn error_starting_lro() -> Result<()> {
let mut mock = MockSpeech::new();
mock.expect_batch_recognize().return_once(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Resource exhausted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the the final result.
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn lro_ending_in_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(50));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(75));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
// This is a common error for `Create*` RPCs, which are often
// LROs. It is less applicable to `BatchRecognize` in practice.
let status = rpc::model::Status::default()
.set_code(gax::error::rpc::Code::AlreadyExists as i32)
.set_message("resource already exists");
make_failed_operation(status)
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25, 50, 75]);
assert!(result.billed_duration.is_err());
Ok(())
}
#[tokio::test]
async fn polling_loop_error() -> Result<()> {
let mut seq = mockall::Sequence::new();
let mut mock = MockSpeech::new();
mock.expect_batch_recognize()
.once()
.in_sequence(&mut seq)
.returning(|_, _| make_partial_operation(25));
mock.expect_get_operation()
.once()
.in_sequence(&mut seq)
.returning(|_, _| {
use gax::error::Error;
use gax::error::rpc::{Code, Status};
let status = Status::default()
.set_code(Code::Aborted)
.set_message("Operation was aborted");
Err(Error::service(status))
});
// Create a client, implemented by our mock.
let client = speech::client::Speech::from_stub(mock);
// Call our function which manually polls.
let result = my_manual_poller(&client, "my-project").await;
// Verify the partial metadata updates, and the final result.
assert_eq!(result.progress_updates, [25]);
assert!(result.billed_duration.is_err());
Ok(())
}
}
Working with enums
This guide will show you how to use enumerations in the Google Cloud client libraries for Rust, including working with enumeration values introduced after the library was released.
Background
Google Cloud services use enumerations for fields that only accept or provide a discrete and limited set of values. While at any point in time the set of allowed values is known, this list may change over time.
The client libraries are prepared to receive and send enumerations, and support working with values introduced after the library release.
Prerequisites
This guide does not make any calls to Google Cloud services. You can run the examples without having a project or account in Google Cloud. The guide will use the client library for Secret Manager. The same principles apply to any other enumeration in any other client library.
Dependencies
As it is usual with Rust, you must declare the dependency in your Cargo.toml
file. We use:
cargo add google-cloud-secretmanager-v1 serde_json
Handling known values
When using known values, you can use the enumeration as usual:
use google_cloud_secretmanager_v1::model::secret_version::State;
let enabled = State::Enabled;
println!("State::Enabled = {enabled}");
assert_eq!(enabled.value(), Some(1));
assert_eq!(enabled.name(), Some("ENABLED"));
let state = State::from(1);
println!("state = {state}");
assert_eq!(state.value(), Some(1));
assert_eq!(state.name(), Some("ENABLED"));
let state = State::from("ENABLED");
println!("state = {state}");
assert_eq!(state.value(), Some(1));
assert_eq!(state.name(), Some("ENABLED"));
println!("json = {}", serde_json::to_value(&state)?);
Handling unknown values
When using unknown string values, the .value()
function return None
, but
everything else works as normal:
use google_cloud_secretmanager_v1::model::secret_version::State;
use serde_json::json;
let state = State::from("STATE_NAME_FROM_THE_FUTURE");
println!("state = {state}");
assert_eq!(state.value(), None);
assert_eq!(state.name(), Some("STATE_NAME_FROM_THE_FUTURE"));
println!("json = {}", serde_json::to_value(&state)?);
let u = serde_json::from_value::<State>(json!("STATE_NAME_FROM_THE_FUTURE"))?;
assert_eq!(state, u);
The same principle applies to unknown integer values:
use google_cloud_secretmanager_v1::model::secret_version::State;
use serde_json::json;
let state = State::from("STATE_NAME_FROM_THE_FUTURE");
println!("state = {state}");
assert_eq!(state.value(), None);
assert_eq!(state.name(), Some("STATE_NAME_FROM_THE_FUTURE"));
println!("json = {}", serde_json::to_value(&state)?);
let u = serde_json::from_value::<State>(json!("STATE_NAME_FROM_THE_FUTURE"))?;
assert_eq!(state, u);
Preparing for upgrades
As mentioned above, the Rust enumerations in the client libraries may gain new
variants in future releases. To avoid breaking applications we mark these
enumerations as #[non_exhaustive]
.
If you use a match expression for non-exhaustive enumerations then you must include the wildcard pattern in your match. This will prevent compilation problems when new variants are included in the enumeration.
use google_cloud_secretmanager_v1::model::secret_version::State;
fn match_with_wildcard(state: State) -> anyhow::Result<()> {
use anyhow::Error;
match state {
State::Unspecified => {
return Err(Error::msg("the documentation says this is never used"));
}
State::Enabled => println!("the secret is enabled and can be accessed"),
State::Disabled => {
println!("the secret version is not accessible until it is enabled")
}
State::Destroyed => {
println!("the secret is destroyed, the data is no longer accessible")
}
State::UnknownValue(u) => {
println!("unknown State variant ({u:?}) time to update the library")
}
_ => return Err(Error::msg("unexpected value, update this code")),
};
Ok(())
}
Nevertheless, you may want a warning or error if new variants appear, at least
so you can examine the code and decide if it must be updated. If that is the
case, consider using the wildcard_enum_match_arm
clippy warning:
use google_cloud_secretmanager_v1::model::secret_version::State;
fn match_with_warnings(state: State) -> anyhow::Result<()> {
use anyhow::Error;
#[warn(clippy::wildcard_enum_match_arm)]
match state {
State::Unspecified => {
return Err(Error::msg("the documentation says this is never used"));
}
State::Enabled => println!("the secret is enabled and can be accessed"),
State::Disabled => {
println!("the secret version is not accessible until it is enabled")
}
State::Destroyed => {
println!("the secret is destroyed, the data is no longer accessible")
}
State::UnknownValue(u) => {
println!("unknown State variant ({u:?}) time to update the library")
}
_ => {
// *If* your CI includes treating clippy warnings as errors,
// consider using `unreachable!()`.
return Err(Error::msg("unexpected value, update this code"));
}
};
Ok(())
}
You may also consider the (currently unstable)
non_exhaustive_omitted_patterns
lint.