| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -24,6 +24,7 @@ | |||
| 24 | 24 | from typing import Optional, Union | |
| 25 | 25 | ||
| 26 | 26 | from google_crc32c import Checksum | |
| 27 | + from google.api_core import exceptions | ||
| 27 | 28 | ||
| 28 | 29 | from ._utils import raise_if_no_fast_crc32c | |
| 29 | 30 | from google.cloud import _storage_v2 | |
@@ -36,7 +37,7 @@ | |||
| 36 | 37 | ||
| 37 | 38 | ||
| 38 | 39 | _MAX_CHUNK_SIZE_BYTES = 2 * 1024 * 1024 # 2 MiB | |
| 39 | - _MAX_BUFFER_SIZE_BYTES = 16 * 1024 * 1024 # 16 MiB | ||
| 40 | + _DEFAULT_FLUSH_INTERVAL_BYTES = 16 * 1024 * 1024 # 16 MiB | ||
| 40 | 41 | ||
| 41 | 42 | ||
| 42 | 43 | class AsyncAppendableObjectWriter: | |
@@ -49,6 +50,7 @@ def __init__( | |||
| 49 | 50 | object_name: str, | |
| 50 | 51 | generation=None, | |
| 51 | 52 | write_handle=None, | |
| 53 | + writer_options: Optional[dict] = None, | ||
| 52 | 54 | ): | |
| 53 | 55 | """ | |
| 54 | 56 | Class for appending data to a GCS Appendable Object. | |
@@ -125,6 +127,21 @@ def __init__( | |||
| 125 | 127 | # Please note: `offset` and `persisted_size` are same when the stream is | |
| 126 | 128 | # opened. | |
| 127 | 129 | self.persisted_size: Optional[int] = None | |
| 130 | + if writer_options is None: | ||
| 131 | + writer_options = {} | ||
| 132 | + self.flush_interval = writer_options.get( | ||
| 133 | + "FLUSH_INTERVAL_BYTES", _DEFAULT_FLUSH_INTERVAL_BYTES | ||
| 134 | + ) | ||
| 135 | + # TODO: add test case for this. | ||
| 136 | + if self.flush_interval < _MAX_CHUNK_SIZE_BYTES: | ||
| 137 | + raise exceptions.OutOfRange( | ||
| 138 | + f"flush_interval must be >= {_MAX_CHUNK_SIZE_BYTES} , but provided {self.flush_interval}" | ||
| 139 | + ) | ||
| 140 | + if self.flush_interval % _MAX_CHUNK_SIZE_BYTES != 0: | ||
| 141 | + raise exceptions.OutOfRange( | ||
| 142 | + f"flush_interval must be a multiple of {_MAX_CHUNK_SIZE_BYTES}, but provided {self.flush_interval}" | ||
| 143 | + ) | ||
| 144 | + self.bytes_appended_since_last_flush = 0 | ||
| 128 | 145 | ||
| 129 | 146 | async def state_lookup(self) -> int: | |
| 130 | 147 | """Returns the persisted_size | |
@@ -193,7 +210,6 @@ async def append(self, data: bytes) -> None: | |||
| 193 | 210 | self.offset = self.persisted_size | |
| 194 | 211 | ||
| 195 | 212 | start_idx = 0 | |
| 196 | - bytes_to_flush = 0 | ||
| 197 | 213 | while start_idx < total_bytes: | |
| 198 | 214 | end_idx = min(start_idx + _MAX_CHUNK_SIZE_BYTES, total_bytes) | |
| 199 | 215 | data_chunk = data[start_idx:end_idx] | |
@@ -208,10 +224,10 @@ async def append(self, data: bytes) -> None: | |||
| 208 | 224 | ) | |
| 209 | 225 | chunk_size = end_idx - start_idx | |
| 210 | 226 | self.offset += chunk_size | |
| 211 | - bytes_to_flush += chunk_size | ||
| 212 | - if bytes_to_flush >= _MAX_BUFFER_SIZE_BYTES: | ||
| 227 | + self.bytes_appended_since_last_flush += chunk_size | ||
| 228 | + if self.bytes_appended_since_last_flush >= self.flush_interval: | ||
| 213 | 229 | await self.simple_flush() | |
| 214 | - bytes_to_flush = 0 | ||
| 230 | + self.bytes_appended_since_last_flush = 0 | ||
| 215 | 231 | start_idx = end_idx | |
| 216 | 232 | ||
| 217 | 233 | async def simple_flush(self) -> None: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,6 +13,7 @@ | |||
| 13 | 13 | from google.cloud.storage._experimental.asyncio.async_grpc_client import AsyncGrpcClient | |
| 14 | 14 | from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import ( | |
| 15 | 15 | AsyncAppendableObjectWriter, | |
| 16 | + _DEFAULT_FLUSH_INTERVAL_BYTES, | ||
| 16 | 17 | ) | |
| 17 | 18 | from google.cloud.storage._experimental.asyncio.async_multi_range_downloader import ( | |
| 18 | 19 | AsyncMultiRangeDownloader, | |
@@ -162,6 +163,59 @@ async def test_basic_wrd_in_slices(storage_client, blobs_to_delete, object_size) | |||
| 162 | 163 | blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name)) | |
| 163 | 164 | ||
| 164 | 165 | ||
| 166 | + @pytest.mark.asyncio | ||
| 167 | + @pytest.mark.parametrize( | ||
| 168 | + "flush_interval", | ||
| 169 | + [2 * 1024 * 1024, 4 * 1024 * 1024, 8 * 1024 * 1024, _DEFAULT_FLUSH_INTERVAL_BYTES], | ||
| 170 | + ) | ||
| 171 | + async def test_wrd_with_non_default_flush_interval( | ||
| 172 | + storage_client, | ||
| 173 | + blobs_to_delete, | ||
| 174 | + flush_interval, | ||
| 175 | + ): | ||
| 176 | + object_name = f"test_basic_wrd-{str(uuid.uuid4())}" | ||
| 177 | + object_size = 9 * 1024 * 1024 | ||
| 178 | + | ||
| 179 | + # Client instantiation; it cannot be part of fixture because. | ||
| 180 | + # grpc_client's event loop and event loop of coroutine running it | ||
| 181 | + # (i.e. this test) must be same. | ||
| 182 | + # Note: | ||
| 183 | + # 1. @pytest.mark.asyncio ensures new event loop for each test. | ||
| 184 | + # 2. we can keep the same event loop for entire module but that may | ||
| 185 | + # create issues if tests are run in parallel and one test hogs the event | ||
| 186 | + # loop slowing down other tests. | ||
| 187 | + object_data = os.urandom(object_size) | ||
| 188 | + object_checksum = google_crc32c.value(object_data) | ||
| 189 | + grpc_client = AsyncGrpcClient().grpc_client | ||
| 190 | + | ||
| 191 | + writer = AsyncAppendableObjectWriter( | ||
| 192 | + grpc_client, | ||
| 193 | + _ZONAL_BUCKET, | ||
| 194 | + object_name, | ||
| 195 | + writer_options={"FLUSH_INTERVAL_BYTES": flush_interval}, | ||
| 196 | + ) | ||
| 197 | + await writer.open() | ||
| 198 | + mark1, mark2 = _get_equal_dist(0, object_size) | ||
| 199 | + await writer.append(object_data[0:mark1]) | ||
| 200 | + await writer.append(object_data[mark1:mark2]) | ||
| 201 | + await writer.append(object_data[mark2:]) | ||
| 202 | + object_metadata = await writer.close(finalize_on_close=True) | ||
| 203 | + assert object_metadata.size == object_size | ||
| 204 | + assert int(object_metadata.checksums.crc32c) == object_checksum | ||
| 205 | + | ||
| 206 | + mrd = AsyncMultiRangeDownloader(grpc_client, _ZONAL_BUCKET, object_name) | ||
| 207 | + buffer = BytesIO() | ||
| 208 | + await mrd.open() | ||
| 209 | + # (0, 0) means read the whole object | ||
| 210 | + await mrd.download_ranges([(0, 0, buffer)]) | ||
| 211 | + await mrd.close() | ||
| 212 | + assert buffer.getvalue() == object_data | ||
| 213 | + assert mrd.persisted_size == object_size | ||
| 214 | + | ||
| 215 | + # Clean up; use json client (i.e. `storage_client` fixture) to delete. | ||
| 216 | + blobs_to_delete.append(storage_client.bucket(_ZONAL_BUCKET).blob(object_name)) | ||
| 217 | + | ||
| 218 | + | ||
| 165 | 219 | @pytest.mark.asyncio | |
| 166 | 220 | async def test_read_unfinalized_appendable_object(storage_client, blobs_to_delete): | |
| 167 | 221 | object_name = f"read_unfinalized_appendable_object-{str(uuid.uuid4())[:4]}" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,6 +21,9 @@ | |||
| 21 | 21 | from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import ( | |
| 22 | 22 | AsyncAppendableObjectWriter, | |
| 23 | 23 | ) | |
| 24 | + from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import ( | ||
| 25 | + _MAX_CHUNK_SIZE_BYTES, | ||
| 26 | + ) | ||
| 24 | 27 | from google.cloud import _storage_v2 | |
| 25 | 28 | ||
| 26 | 29 | ||
@@ -29,6 +32,7 @@ | |||
| 29 | 32 | GENERATION = 123 | |
| 30 | 33 | WRITE_HANDLE = b"test-write-handle" | |
| 31 | 34 | PERSISTED_SIZE = 456 | |
| 35 | + EIGHT_MIB = 8 * 1024 * 1024 | ||
| 32 | 36 | ||
| 33 | 37 | ||
| 34 | 38 | @pytest.fixture | |
@@ -52,6 +56,7 @@ def test_init(mock_write_object_stream, mock_client): | |||
| 52 | 56 | assert not writer._is_stream_open | |
| 53 | 57 | assert writer.offset is None | |
| 54 | 58 | assert writer.persisted_size is None | |
| 59 | + assert writer.bytes_appended_since_last_flush == 0 | ||
| 55 | 60 | ||
| 56 | 61 | mock_write_object_stream.assert_called_once_with( | |
| 57 | 62 | client=mock_client, | |
@@ -78,6 +83,7 @@ def test_init_with_optional_args(mock_write_object_stream, mock_client): | |||
| 78 | 83 | ||
| 79 | 84 | assert writer.generation == GENERATION | |
| 80 | 85 | assert writer.write_handle == WRITE_HANDLE | |
| 86 | + assert writer.bytes_appended_since_last_flush == 0 | ||
| 81 | 87 | ||
| 82 | 88 | mock_write_object_stream.assert_called_once_with( | |
| 83 | 89 | client=mock_client, | |
@@ -88,6 +94,60 @@ def test_init_with_optional_args(mock_write_object_stream, mock_client): | |||
| 88 | 94 | ) | |
| 89 | 95 | ||
| 90 | 96 | ||
| 97 | + @mock.patch( | ||
| 98 | + "google.cloud.storage._experimental.asyncio.async_appendable_object_writer._AsyncWriteObjectStream" | ||
| 99 | + ) | ||
| 100 | + def test_init_with_writer_options(mock_write_object_stream, mock_client): | ||
| 101 | + """Test the constructor with optional arguments.""" | ||
| 102 | + writer = AsyncAppendableObjectWriter( | ||
| 103 | + mock_client, | ||
| 104 | + BUCKET, | ||
| 105 | + OBJECT, | ||
| 106 | + writer_options={"FLUSH_INTERVAL_BYTES": EIGHT_MIB}, | ||
| 107 | + ) | ||
| 108 | + | ||
| 109 | + assert writer.flush_interval == EIGHT_MIB | ||
| 110 | + assert writer.bytes_appended_since_last_flush == 0 | ||
| 111 | + | ||
| 112 | + mock_write_object_stream.assert_called_once_with( | ||
| 113 | + client=mock_client, | ||
| 114 | + bucket_name=BUCKET, | ||
| 115 | + object_name=OBJECT, | ||
| 116 | + generation_number=None, | ||
| 117 | + write_handle=None, | ||
| 118 | + ) | ||
| 119 | + | ||
| 120 | + | ||
| 121 | + @mock.patch( | ||
| 122 | + "google.cloud.storage._experimental.asyncio.async_appendable_object_writer._AsyncWriteObjectStream" | ||
| 123 | + ) | ||
| 124 | + def test_init_with_flush_interval_less_than_chunk_size_raises_error(mock_client): | ||
| 125 | + """Test that an OutOfRange error is raised if flush_interval is less than the chunk size.""" | ||
| 126 | + | ||
| 127 | + with pytest.raises(exceptions.OutOfRange): | ||
| 128 | + AsyncAppendableObjectWriter( | ||
| 129 | + mock_client, | ||
| 130 | + BUCKET, | ||
| 131 | + OBJECT, | ||
| 132 | + writer_options={"FLUSH_INTERVAL_BYTES": _MAX_CHUNK_SIZE_BYTES - 1}, | ||
| 133 | + ) | ||
| 134 | + | ||
| 135 | + | ||
| 136 | + @mock.patch( | ||
| 137 | + "google.cloud.storage._experimental.asyncio.async_appendable_object_writer._AsyncWriteObjectStream" | ||
| 138 | + ) | ||
| 139 | + def test_init_with_flush_interval_not_multiple_of_chunk_size_raises_error(mock_client): | ||
| 140 | + """Test that an OutOfRange error is raised if flush_interval is not a multiple of the chunk size.""" | ||
| 141 | + | ||
| 142 | + with pytest.raises(exceptions.OutOfRange): | ||
| 143 | + AsyncAppendableObjectWriter( | ||
| 144 | + mock_client, | ||
| 145 | + BUCKET, | ||
| 146 | + OBJECT, | ||
| 147 | + writer_options={"FLUSH_INTERVAL_BYTES": _MAX_CHUNK_SIZE_BYTES + 1}, | ||
| 148 | + ) | ||
| 149 | + | ||
| 150 | + | ||
| 91 | 151 | @mock.patch("google.cloud.storage._experimental.asyncio._utils.google_crc32c") | |
| 92 | 152 | @mock.patch( | |
| 93 | 153 | "google.cloud.storage._experimental.asyncio.async_grpc_client.AsyncGrpcClient.grpc_client" | |
@@ -477,7 +537,7 @@ async def test_append_flushes_when_buffer_is_full( | |||
| 477 | 537 | ): | |
| 478 | 538 | """Test that append flushes the stream when the buffer size is reached.""" | |
| 479 | 539 | from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import ( | |
| 480 | - _MAX_BUFFER_SIZE_BYTES, | ||
| 540 | + _DEFAULT_FLUSH_INTERVAL_BYTES, | ||
| 481 | 541 | ) | |
| 482 | 542 | ||
| 483 | 543 | writer = AsyncAppendableObjectWriter(mock_client, BUCKET, OBJECT) | |
@@ -487,7 +547,7 @@ async def test_append_flushes_when_buffer_is_full( | |||
| 487 | 547 | mock_stream.send = mock.AsyncMock() | |
| 488 | 548 | writer.simple_flush = mock.AsyncMock() | |
| 489 | 549 | ||
| 490 | - data = b"a" * _MAX_BUFFER_SIZE_BYTES | ||
| 550 | + data = b"a" * _DEFAULT_FLUSH_INTERVAL_BYTES | ||
| 491 | 551 | await writer.append(data) | |
| 492 | 552 | ||
| 493 | 553 | writer.simple_flush.assert_awaited_once() | |
@@ -500,7 +560,7 @@ async def test_append_flushes_when_buffer_is_full( | |||
| 500 | 560 | async def test_append_handles_large_data(mock_write_object_stream, mock_client): | |
| 501 | 561 | """Test that append handles data larger than the buffer size.""" | |
| 502 | 562 | from google.cloud.storage._experimental.asyncio.async_appendable_object_writer import ( | |
| 503 | - _MAX_BUFFER_SIZE_BYTES, | ||
| 563 | + _DEFAULT_FLUSH_INTERVAL_BYTES, | ||
| 504 | 564 | ) | |
| 505 | 565 | ||
| 506 | 566 | writer = AsyncAppendableObjectWriter(mock_client, BUCKET, OBJECT) | |
@@ -510,7 +570,7 @@ async def test_append_handles_large_data(mock_write_object_stream, mock_client): | |||
| 510 | 570 | mock_stream.send = mock.AsyncMock() | |
| 511 | 571 | writer.simple_flush = mock.AsyncMock() | |
| 512 | 572 | ||
| 513 | - data = b"a" * (_MAX_BUFFER_SIZE_BYTES * 2 + 1) | ||
| 573 | + data = b"a" * (_DEFAULT_FLUSH_INTERVAL_BYTES * 2 + 1) | ||
| 514 | 574 | await writer.append(data) | |
| 515 | 575 | ||
| 516 | 576 | assert writer.simple_flush.await_count == 2 | |
| Back | FazBrowse Home | New Git URL |
0 commit comments