Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.
Merged
34 changes: 34 additions & 0 deletions google/cloud/storage/_experimental/asyncio/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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.

import google_crc32c

from google.api_core import exceptions

def raise_if_no_fast_crc32c():
"""Check if the C-accelerated version of google-crc32c is available.

If not, raise an error to prevent silent performance degradation.

raises google.api_core.exceptions.FailedPrecondition: If the C extension is not available.
returns: True if the C extension is available.
rtype: bool

"""
if google_crc32c.implementation != "c":
raise exceptions.FailedPrecondition(
"The google-crc32c package is not installed with C support. "
"C extension is required for faster data integrity checks."
"For more information, see https://github.com/googleapis/python-crc32c."
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
if you want to use these Rapid Storage APIs.

"""
import time
from typing import Optional, Union

from google.api_core import exceptions
from google_crc32c import Checksum

from ._utils import raise_if_no_fast_crc32c
from google.cloud import _storage_v2
from google.cloud.storage._experimental.asyncio.async_grpc_client import (
AsyncGrpcClient,
Expand Down Expand Up @@ -100,6 +106,7 @@ def __init__(
:param write_handle: (Optional) An existing handle for writing the object.
If provided, opening the bidi-gRPC connection will be faster.
"""
raise_if_no_fast_crc32c()
self.client = client
self.bucket_name = bucket_name
self.object_name = object_name
Expand Down Expand Up @@ -191,11 +198,13 @@ async def append(self, data: bytes) -> None:
bytes_to_flush = 0
while start_idx < total_bytes:
end_idx = min(start_idx + _MAX_CHUNK_SIZE_BYTES, total_bytes)
data_chunk = data[start_idx:end_idx]
await self.write_obj_stream.send(
_storage_v2.BidiWriteObjectRequest(
write_offset=self.offset,
checksummed_data=_storage_v2.ChecksummedData(
content=data[start_idx:end_idx]
content=data_chunk,
crc32c=int.from_bytes(Checksum(data_chunk).digest(), "big"),
),
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@

from __future__ import annotations
import asyncio
import google_crc32c
from typing import List, Optional, Tuple

from google.api_core import exceptions
from google_crc32c import Checksum

from typing import List, Optional, Tuple

from ._utils import raise_if_no_fast_crc32c
from google.cloud.storage._experimental.asyncio.async_read_object_stream import (
_AsyncReadObjectStream,
)
Expand Down Expand Up @@ -160,14 +160,7 @@ def __init__(
:param read_handle: (Optional) An existing read handle.
"""

# Verify that the fast, C-accelerated version of crc32c is available.
# If not, raise an error to prevent silent performance degradation.
if google_crc32c.implementation != "c":
raise exceptions.NotFound(
"The google-crc32c package is not installed with C support. "
"Bidi reads require the C extension for data integrity checks."
"For more information, see https://github.com/googleapis/python-crc32c."
)
raise_if_no_fast_crc32c()

self.client = client
self.bucket_name = bucket_name
Expand Down
83 changes: 78 additions & 5 deletions tests/system/test_zonal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import os
import uuid
from io import BytesIO
import random

# python additional imports
import google_crc32c

import pytest

# current library imports
from google.cloud.storage._experimental.asyncio.async_grpc_client import AsyncGrpcClient
from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import (
AsyncAppendableObjectWriter,
_MAX_BUFFER_SIZE_BYTES,
)
from google.cloud.storage._experimental.asyncio.async_multi_range_downloader import (
AsyncMultiRangeDownloader,
Expand All @@ -28,6 +32,11 @@
_BYTES_TO_UPLOAD = b"dummy_bytes_to_write_read_and_delete_appendable_object"


def _get_equal_dist(a: int, b: int) -> tuple[int, int]:
step = (b - a) // 3
return a + step, a + 2 * step


async def write_one_appendable_object(
bucket_name: str,
object_name: str,
Expand Down Expand Up @@ -59,11 +68,21 @@ def appendable_object(storage_client, blobs_to_delete):


@pytest.mark.asyncio
@pytest.mark.parametrize(
"object_size",
[
256, # less than _chunk size
10 * 1024 * 1024, # less than _MAX_BUFFER_SIZE_BYTES
20 * 1024 * 1024, # greater than _MAX_BUFFER_SIZE
],
)
@pytest.mark.parametrize(
"attempt_direct_path",
[True, False],
)
async def test_basic_wrd(storage_client, blobs_to_delete, attempt_direct_path):
async def test_basic_wrd(
storage_client, blobs_to_delete, attempt_direct_path, object_size
):
object_name = f"test_basic_wrd-{str(uuid.uuid4())}"

# Client instantiation; it cannot be part of fixture because.
Expand All @@ -74,22 +93,76 @@ async def test_basic_wrd(storage_client, blobs_to_delete, attempt_direct_path):
# 2. we can keep the same event loop for entire module but that may
# create issues if tests are run in parallel and one test hogs the event
# loop slowing down other tests.
object_data = os.urandom(object_size)
object_checksum = google_crc32c.value(object_data)
grpc_client = AsyncGrpcClient(attempt_direct_path=attempt_direct_path).grpc_client

writer = AsyncAppendableObjectWriter(grpc_client, _ZONAL_BUCKET, object_name)
await writer.open()
await writer.append(_BYTES_TO_UPLOAD)
await writer.append(object_data)
object_metadata = await writer.close(finalize_on_close=True)
assert object_metadata.size == len(_BYTES_TO_UPLOAD)
assert object_metadata.size == object_size
assert int(object_metadata.checksums.crc32c) == object_checksum

mrd = AsyncMultiRangeDownloader(grpc_client, _ZONAL_BUCKET, object_name)
buffer = BytesIO()
await mrd.open()
# (0, 0) means read the whole object
await mrd.download_ranges([(0, 0, buffer)])
await mrd.close()
assert buffer.getvalue() == _BYTES_TO_UPLOAD
assert mrd.persisted_size == len(_BYTES_TO_UPLOAD)
assert buffer.getvalue() == object_data
assert mrd.persisted_size == object_size

# Clean up; use json client (i.e. `storage_client` fixture) to delete.
blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name))


@pytest.mark.asyncio
@pytest.mark.parametrize(
"object_size",
[
20 * 1024 * 1024, # greater than _MAX_BUFFER_SIZE
],
)
@pytest.mark.parametrize(
"attempt_direct_path",
[True],
Comment thread
chandra-siri marked this conversation as resolved.
Outdated
)
async def test_basic_wrd_in_slices(
storage_client, blobs_to_delete, attempt_direct_path, object_size
):
object_name = f"test_basic_wrd-{str(uuid.uuid4())}"

# Client instantiation; it cannot be part of fixture because.
# grpc_client's event loop and event loop of coroutine running it
# (i.e. this test) must be same.
# Note:
# 1. @pytest.mark.asyncio ensures new event loop for each test.
# 2. we can keep the same event loop for entire module but that may
# create issues if tests are run in parallel and one test hogs the event
# loop slowing down other tests.
object_data = os.urandom(object_size)
object_checksum = google_crc32c.value(object_data)
grpc_client = AsyncGrpcClient(attempt_direct_path=attempt_direct_path).grpc_client

writer = AsyncAppendableObjectWriter(grpc_client, _ZONAL_BUCKET, object_name)
await writer.open()
mark1, mark2 = _get_equal_dist(0, object_size)
await writer.append(object_data[0:mark1])
await writer.append(object_data[mark1:mark2])
await writer.append(object_data[mark2:])
object_metadata = await writer.close(finalize_on_close=True)
assert object_metadata.size == object_size
assert int(object_metadata.checksums.crc32c) == object_checksum

mrd = AsyncMultiRangeDownloader(grpc_client, _ZONAL_BUCKET, object_name)
buffer = BytesIO()
await mrd.open()
# (0, 0) means read the whole object
await mrd.download_ranges([(0, 0, buffer)])
await mrd.close()
assert buffer.getvalue() == object_data
assert mrd.persisted_size == object_size

# Clean up; use json client (i.e. `storage_client` fixture) to delete.
blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name))
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/asyncio/test_async_appendable_object_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import pytest
from unittest import mock

from google.api_core import exceptions
from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import (
AsyncAppendableObjectWriter,
)
Expand Down Expand Up @@ -85,6 +86,23 @@ def test_init_with_optional_args(mock_write_object_stream, mock_client):
)


@mock.patch("google.cloud.storage._experimental.asyncio._utils.google_crc32c")
@mock.patch(
"google.cloud.storage._experimental.asyncio.async_grpc_client.AsyncGrpcClient.grpc_client"
)
def test_init_raises_if_crc32c_c_extension_is_missing(
mock_grpc_client, mock_google_crc32c
):
mock_google_crc32c.implementation = "python"

with pytest.raises(exceptions.FailedPrecondition) as exc_info:
AsyncAppendableObjectWriter(mock_grpc_client, "bucket", "object")

assert "The google-crc32c package is not installed with C support" in str(
exc_info.value
)


@pytest.mark.asyncio
@mock.patch(
"google.cloud.storage._experimental.asyncio.async_appendable_object_writer._AsyncWriteObjectStream"
Expand Down
6 changes: 2 additions & 4 deletions tests/unit/asyncio/test_async_multi_range_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,7 @@ async def test_downloading_without_opening_should_throw_error(
assert str(exc.value) == "Underlying bidi-gRPC stream is not open"
assert not mrd.is_stream_open

@mock.patch(
"google.cloud.storage._experimental.asyncio.async_multi_range_downloader.google_crc32c"
)
@mock.patch("google.cloud.storage._experimental.asyncio._utils.google_crc32c")
@mock.patch(
"google.cloud.storage._experimental.asyncio.async_grpc_client.AsyncGrpcClient.grpc_client"
)
Expand All @@ -360,7 +358,7 @@ def test_init_raises_if_crc32c_c_extension_is_missing(
):
mock_google_crc32c.implementation = "python"

with pytest.raises(exceptions.NotFound) as exc_info:
with pytest.raises(exceptions.FailedPrecondition) as exc_info:
AsyncMultiRangeDownloader(mock_grpc_client, "bucket", "object")

assert "The google-crc32c package is not installed with C support" in str(
Expand Down