google-cloud-bigtable 2.30.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/cloud/bigtable/__init__.py +25 -0
- google/cloud/bigtable/app_profile.py +377 -0
- google/cloud/bigtable/backup.py +490 -0
- google/cloud/bigtable/batcher.py +414 -0
- google/cloud/bigtable/client.py +475 -0
- google/cloud/bigtable/cluster.py +541 -0
- google/cloud/bigtable/column_family.py +362 -0
- google/cloud/bigtable/data/__init__.py +103 -0
- google/cloud/bigtable/data/_async/__init__.py +25 -0
- google/cloud/bigtable/data/_async/_mutate_rows.py +226 -0
- google/cloud/bigtable/data/_async/_read_rows.py +363 -0
- google/cloud/bigtable/data/_async/client.py +1522 -0
- google/cloud/bigtable/data/_async/mutations_batcher.py +534 -0
- google/cloud/bigtable/data/_cross_sync/__init__.py +20 -0
- google/cloud/bigtable/data/_cross_sync/_decorators.py +441 -0
- google/cloud/bigtable/data/_cross_sync/_mapping_meta.py +64 -0
- google/cloud/bigtable/data/_cross_sync/cross_sync.py +334 -0
- google/cloud/bigtable/data/_helpers.py +249 -0
- google/cloud/bigtable/data/_sync_autogen/_mutate_rows.py +182 -0
- google/cloud/bigtable/data/_sync_autogen/_read_rows.py +304 -0
- google/cloud/bigtable/data/_sync_autogen/client.py +1293 -0
- google/cloud/bigtable/data/_sync_autogen/mutations_batcher.py +449 -0
- google/cloud/bigtable/data/exceptions.py +340 -0
- google/cloud/bigtable/data/execute_query/__init__.py +43 -0
- google/cloud/bigtable/data/execute_query/_async/__init__.py +13 -0
- google/cloud/bigtable/data/execute_query/_async/execute_query_iterator.py +291 -0
- google/cloud/bigtable/data/execute_query/_byte_cursor.py +123 -0
- google/cloud/bigtable/data/execute_query/_checksum.py +43 -0
- google/cloud/bigtable/data/execute_query/_parameters_formatting.py +155 -0
- google/cloud/bigtable/data/execute_query/_query_result_parsing_utils.py +134 -0
- google/cloud/bigtable/data/execute_query/_reader.py +122 -0
- google/cloud/bigtable/data/execute_query/_sync_autogen/execute_query_iterator.py +239 -0
- google/cloud/bigtable/data/execute_query/metadata.py +399 -0
- google/cloud/bigtable/data/execute_query/values.py +123 -0
- google/cloud/bigtable/data/mutations.py +380 -0
- google/cloud/bigtable/data/read_modify_write_rules.py +112 -0
- google/cloud/bigtable/data/read_rows_query.py +536 -0
- google/cloud/bigtable/data/row.py +535 -0
- google/cloud/bigtable/data/row_filters.py +968 -0
- google/cloud/bigtable/encryption_info.py +64 -0
- google/cloud/bigtable/enums.py +223 -0
- google/cloud/bigtable/error.py +64 -0
- google/cloud/bigtable/gapic_version.py +16 -0
- google/cloud/bigtable/helpers.py +31 -0
- google/cloud/bigtable/instance.py +789 -0
- google/cloud/bigtable/policy.py +255 -0
- google/cloud/bigtable/row.py +1267 -0
- google/cloud/bigtable/row_data.py +380 -0
- google/cloud/bigtable/row_filters.py +838 -0
- google/cloud/bigtable/row_merger.py +250 -0
- google/cloud/bigtable/row_set.py +213 -0
- google/cloud/bigtable/table.py +1409 -0
- google/cloud/bigtable_admin/__init__.py +410 -0
- google/cloud/bigtable_admin/gapic_version.py +16 -0
- google/cloud/bigtable_admin/py.typed +2 -0
- google/cloud/bigtable_admin_v2/__init__.py +250 -0
- google/cloud/bigtable_admin_v2/gapic_metadata.json +962 -0
- google/cloud/bigtable_admin_v2/gapic_version.py +16 -0
- google/cloud/bigtable_admin_v2/py.typed +2 -0
- google/cloud/bigtable_admin_v2/services/__init__.py +15 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/__init__.py +22 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py +3465 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py +3944 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/pagers.py +681 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/__init__.py +40 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py +751 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py +1249 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py +1579 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py +6820 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest_base.py +1746 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/__init__.py +22 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py +3472 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py +3949 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/pagers.py +669 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/__init__.py +40 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py +708 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py +1257 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py +1549 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py +6609 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest_base.py +1714 -0
- google/cloud/bigtable_admin_v2/types/__init__.py +248 -0
- google/cloud/bigtable_admin_v2/types/bigtable_instance_admin.py +1364 -0
- google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py +1715 -0
- google/cloud/bigtable_admin_v2/types/common.py +81 -0
- google/cloud/bigtable_admin_v2/types/instance.py +819 -0
- google/cloud/bigtable_admin_v2/types/table.py +1028 -0
- google/cloud/bigtable_admin_v2/types/types.py +776 -0
- google/cloud/bigtable_v2/__init__.py +136 -0
- google/cloud/bigtable_v2/gapic_metadata.json +193 -0
- google/cloud/bigtable_v2/gapic_version.py +16 -0
- google/cloud/bigtable_v2/py.typed +2 -0
- google/cloud/bigtable_v2/services/__init__.py +15 -0
- google/cloud/bigtable_v2/services/bigtable/__init__.py +22 -0
- google/cloud/bigtable_v2/services/bigtable/async_client.py +1741 -0
- google/cloud/bigtable_v2/services/bigtable/client.py +2194 -0
- google/cloud/bigtable_v2/services/bigtable/transports/__init__.py +38 -0
- google/cloud/bigtable_v2/services/bigtable/transports/base.py +343 -0
- google/cloud/bigtable_v2/services/bigtable/transports/grpc.py +660 -0
- google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py +762 -0
- google/cloud/bigtable_v2/services/bigtable/transports/rest.py +2489 -0
- google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py +711 -0
- google/cloud/bigtable_v2/types/__init__.py +138 -0
- google/cloud/bigtable_v2/types/bigtable.py +1531 -0
- google/cloud/bigtable_v2/types/data.py +1612 -0
- google/cloud/bigtable_v2/types/feature_flags.py +119 -0
- google/cloud/bigtable_v2/types/request_stats.py +171 -0
- google/cloud/bigtable_v2/types/response_params.py +64 -0
- google/cloud/bigtable_v2/types/types.py +579 -0
- google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_admin_v2_keywords.py +233 -0
- google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_v2_keywords.py +186 -0
- google_cloud_bigtable-2.30.0.dist-info/LICENSE +202 -0
- google_cloud_bigtable-2.30.0.dist-info/METADATA +154 -0
- google_cloud_bigtable-2.30.0.dist-info/RECORD +115 -0
- google_cloud_bigtable-2.30.0.dist-info/WHEEL +5 -0
- google_cloud_bigtable-2.30.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
# Copyright 2024 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Sequence, TYPE_CHECKING, cast
|
|
18
|
+
import atexit
|
|
19
|
+
import warnings
|
|
20
|
+
from collections import deque
|
|
21
|
+
import concurrent.futures
|
|
22
|
+
|
|
23
|
+
from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
|
|
24
|
+
from google.cloud.bigtable.data.exceptions import FailedMutationEntryError
|
|
25
|
+
from google.cloud.bigtable.data._helpers import _get_retryable_errors
|
|
26
|
+
from google.cloud.bigtable.data._helpers import _get_timeouts
|
|
27
|
+
from google.cloud.bigtable.data._helpers import TABLE_DEFAULT
|
|
28
|
+
|
|
29
|
+
from google.cloud.bigtable.data.mutations import (
|
|
30
|
+
_MUTATE_ROWS_REQUEST_MUTATION_LIMIT,
|
|
31
|
+
)
|
|
32
|
+
from google.cloud.bigtable.data.mutations import Mutation
|
|
33
|
+
|
|
34
|
+
from google.cloud.bigtable.data._cross_sync import CrossSync
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from google.cloud.bigtable.data.mutations import RowMutationEntry
|
|
38
|
+
|
|
39
|
+
if CrossSync.is_async:
|
|
40
|
+
from google.cloud.bigtable.data._async.client import TableAsync as TableType
|
|
41
|
+
else:
|
|
42
|
+
from google.cloud.bigtable.data._sync_autogen.client import Table as TableType # type: ignore
|
|
43
|
+
|
|
44
|
+
__CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.mutations_batcher"
|
|
45
|
+
|
|
46
|
+
# used to make more readable default values
|
|
47
|
+
_MB_SIZE = 1024 * 1024
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@CrossSync.convert_class(sync_name="_FlowControl", add_mapping_for_name="_FlowControl")
|
|
51
|
+
class _FlowControlAsync:
|
|
52
|
+
"""
|
|
53
|
+
Manages flow control for batched mutations. Mutations are registered against
|
|
54
|
+
the FlowControl object before being sent, which will block if size or count
|
|
55
|
+
limits have reached capacity. As mutations completed, they are removed from
|
|
56
|
+
the FlowControl object, which will notify any blocked requests that there
|
|
57
|
+
is additional capacity.
|
|
58
|
+
|
|
59
|
+
Flow limits are not hard limits. If a single mutation exceeds the configured
|
|
60
|
+
limits, it will be allowed as a single batch when the capacity is available.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
max_mutation_count: maximum number of mutations to send in a single rpc.
|
|
64
|
+
This corresponds to individual mutations in a single RowMutationEntry.
|
|
65
|
+
max_mutation_bytes: maximum number of bytes to send in a single rpc.
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: if max_mutation_count or max_mutation_bytes is less than 0
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
max_mutation_count: int,
|
|
73
|
+
max_mutation_bytes: int,
|
|
74
|
+
):
|
|
75
|
+
self._max_mutation_count = max_mutation_count
|
|
76
|
+
self._max_mutation_bytes = max_mutation_bytes
|
|
77
|
+
if self._max_mutation_count < 1:
|
|
78
|
+
raise ValueError("max_mutation_count must be greater than 0")
|
|
79
|
+
if self._max_mutation_bytes < 1:
|
|
80
|
+
raise ValueError("max_mutation_bytes must be greater than 0")
|
|
81
|
+
self._capacity_condition = CrossSync.Condition()
|
|
82
|
+
self._in_flight_mutation_count = 0
|
|
83
|
+
self._in_flight_mutation_bytes = 0
|
|
84
|
+
|
|
85
|
+
def _has_capacity(self, additional_count: int, additional_size: int) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Checks if there is capacity to send a new entry with the given size and count
|
|
88
|
+
|
|
89
|
+
FlowControl limits are not hard limits. If a single mutation exceeds
|
|
90
|
+
the configured flow limits, it will be sent in a single batch when
|
|
91
|
+
previous batches have completed.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
additional_count: number of mutations in the pending entry
|
|
95
|
+
additional_size: size of the pending entry
|
|
96
|
+
Returns:
|
|
97
|
+
bool: True if there is capacity to send the pending entry, False otherwise
|
|
98
|
+
"""
|
|
99
|
+
# adjust limits to allow overly large mutations
|
|
100
|
+
acceptable_size = max(self._max_mutation_bytes, additional_size)
|
|
101
|
+
acceptable_count = max(self._max_mutation_count, additional_count)
|
|
102
|
+
# check if we have capacity for new mutation
|
|
103
|
+
new_size = self._in_flight_mutation_bytes + additional_size
|
|
104
|
+
new_count = self._in_flight_mutation_count + additional_count
|
|
105
|
+
return new_size <= acceptable_size and new_count <= acceptable_count
|
|
106
|
+
|
|
107
|
+
@CrossSync.convert
|
|
108
|
+
async def remove_from_flow(
|
|
109
|
+
self, mutations: RowMutationEntry | list[RowMutationEntry]
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Removes mutations from flow control. This method should be called once
|
|
113
|
+
for each mutation that was sent to add_to_flow, after the corresponding
|
|
114
|
+
operation is complete.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
mutations: mutation or list of mutations to remove from flow control
|
|
118
|
+
"""
|
|
119
|
+
if not isinstance(mutations, list):
|
|
120
|
+
mutations = [mutations]
|
|
121
|
+
total_count = sum(len(entry.mutations) for entry in mutations)
|
|
122
|
+
total_size = sum(entry.size() for entry in mutations)
|
|
123
|
+
self._in_flight_mutation_count -= total_count
|
|
124
|
+
self._in_flight_mutation_bytes -= total_size
|
|
125
|
+
# notify any blocked requests that there is additional capacity
|
|
126
|
+
async with self._capacity_condition:
|
|
127
|
+
self._capacity_condition.notify_all()
|
|
128
|
+
|
|
129
|
+
@CrossSync.convert
|
|
130
|
+
async def add_to_flow(self, mutations: RowMutationEntry | list[RowMutationEntry]):
|
|
131
|
+
"""
|
|
132
|
+
Generator function that registers mutations with flow control. As mutations
|
|
133
|
+
are accepted into the flow control, they are yielded back to the caller,
|
|
134
|
+
to be sent in a batch. If the flow control is at capacity, the generator
|
|
135
|
+
will block until there is capacity available.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
mutations: list mutations to break up into batches
|
|
139
|
+
Yields:
|
|
140
|
+
list[RowMutationEntry]:
|
|
141
|
+
list of mutations that have reserved space in the flow control.
|
|
142
|
+
Each batch contains at least one mutation.
|
|
143
|
+
"""
|
|
144
|
+
if not isinstance(mutations, list):
|
|
145
|
+
mutations = [mutations]
|
|
146
|
+
start_idx = 0
|
|
147
|
+
end_idx = 0
|
|
148
|
+
while end_idx < len(mutations):
|
|
149
|
+
start_idx = end_idx
|
|
150
|
+
batch_mutation_count = 0
|
|
151
|
+
# fill up batch until we hit capacity
|
|
152
|
+
async with self._capacity_condition:
|
|
153
|
+
while end_idx < len(mutations):
|
|
154
|
+
next_entry = mutations[end_idx]
|
|
155
|
+
next_size = next_entry.size()
|
|
156
|
+
next_count = len(next_entry.mutations)
|
|
157
|
+
if (
|
|
158
|
+
self._has_capacity(next_count, next_size)
|
|
159
|
+
# make sure not to exceed per-request mutation count limits
|
|
160
|
+
and (batch_mutation_count + next_count)
|
|
161
|
+
<= _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
|
|
162
|
+
):
|
|
163
|
+
# room for new mutation; add to batch
|
|
164
|
+
end_idx += 1
|
|
165
|
+
batch_mutation_count += next_count
|
|
166
|
+
self._in_flight_mutation_bytes += next_size
|
|
167
|
+
self._in_flight_mutation_count += next_count
|
|
168
|
+
elif start_idx != end_idx:
|
|
169
|
+
# we have at least one mutation in the batch, so send it
|
|
170
|
+
break
|
|
171
|
+
else:
|
|
172
|
+
# batch is empty. Block until we have capacity
|
|
173
|
+
await self._capacity_condition.wait_for(
|
|
174
|
+
lambda: self._has_capacity(next_count, next_size)
|
|
175
|
+
)
|
|
176
|
+
yield mutations[start_idx:end_idx]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@CrossSync.convert_class(sync_name="MutationsBatcher")
|
|
180
|
+
class MutationsBatcherAsync:
|
|
181
|
+
"""
|
|
182
|
+
Allows users to send batches using context manager API:
|
|
183
|
+
|
|
184
|
+
Runs mutate_row, mutate_rows, and check_and_mutate_row internally, combining
|
|
185
|
+
to use as few network requests as required
|
|
186
|
+
|
|
187
|
+
Will automatically flush the batcher:
|
|
188
|
+
- every flush_interval seconds
|
|
189
|
+
- after queue size reaches flush_limit_mutation_count
|
|
190
|
+
- after queue reaches flush_limit_bytes
|
|
191
|
+
- when batcher is closed or destroyed
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
table: Table to preform rpc calls
|
|
195
|
+
flush_interval: Automatically flush every flush_interval seconds.
|
|
196
|
+
If None, no time-based flushing is performed.
|
|
197
|
+
flush_limit_mutation_count: Flush immediately after flush_limit_mutation_count
|
|
198
|
+
mutations are added across all entries. If None, this limit is ignored.
|
|
199
|
+
flush_limit_bytes: Flush immediately after flush_limit_bytes bytes are added.
|
|
200
|
+
flow_control_max_mutation_count: Maximum number of inflight mutations.
|
|
201
|
+
flow_control_max_bytes: Maximum number of inflight bytes.
|
|
202
|
+
batch_operation_timeout: timeout for each mutate_rows operation, in seconds.
|
|
203
|
+
If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_operation_timeout.
|
|
204
|
+
batch_attempt_timeout: timeout for each individual request, in seconds.
|
|
205
|
+
If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_attempt_timeout.
|
|
206
|
+
If None, defaults to batch_operation_timeout.
|
|
207
|
+
batch_retryable_errors: a list of errors that will be retried if encountered.
|
|
208
|
+
Defaults to the Table's default_mutate_rows_retryable_errors.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
table: TableType,
|
|
214
|
+
*,
|
|
215
|
+
flush_interval: float | None = 5,
|
|
216
|
+
flush_limit_mutation_count: int | None = 1000,
|
|
217
|
+
flush_limit_bytes: int = 20 * _MB_SIZE,
|
|
218
|
+
flow_control_max_mutation_count: int = 100_000,
|
|
219
|
+
flow_control_max_bytes: int = 100 * _MB_SIZE,
|
|
220
|
+
batch_operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
221
|
+
batch_attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
222
|
+
batch_retryable_errors: Sequence[type[Exception]]
|
|
223
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
224
|
+
):
|
|
225
|
+
self._operation_timeout, self._attempt_timeout = _get_timeouts(
|
|
226
|
+
batch_operation_timeout, batch_attempt_timeout, table
|
|
227
|
+
)
|
|
228
|
+
self._retryable_errors: list[type[Exception]] = _get_retryable_errors(
|
|
229
|
+
batch_retryable_errors, table
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
self._closed = CrossSync.Event()
|
|
233
|
+
self._table = table
|
|
234
|
+
self._staged_entries: list[RowMutationEntry] = []
|
|
235
|
+
self._staged_count, self._staged_bytes = 0, 0
|
|
236
|
+
self._flow_control = CrossSync._FlowControl(
|
|
237
|
+
flow_control_max_mutation_count, flow_control_max_bytes
|
|
238
|
+
)
|
|
239
|
+
self._flush_limit_bytes = flush_limit_bytes
|
|
240
|
+
self._flush_limit_count = (
|
|
241
|
+
flush_limit_mutation_count
|
|
242
|
+
if flush_limit_mutation_count is not None
|
|
243
|
+
else float("inf")
|
|
244
|
+
)
|
|
245
|
+
# used by sync class to run mutate_rows operations
|
|
246
|
+
self._sync_rpc_executor = (
|
|
247
|
+
concurrent.futures.ThreadPoolExecutor(max_workers=8)
|
|
248
|
+
if not CrossSync.is_async
|
|
249
|
+
else None
|
|
250
|
+
)
|
|
251
|
+
# used by sync class to manage flush_internal tasks
|
|
252
|
+
self._sync_flush_executor = (
|
|
253
|
+
concurrent.futures.ThreadPoolExecutor(max_workers=4)
|
|
254
|
+
if not CrossSync.is_async
|
|
255
|
+
else None
|
|
256
|
+
)
|
|
257
|
+
self._flush_timer = CrossSync.create_task(
|
|
258
|
+
self._timer_routine, flush_interval, sync_executor=self._sync_flush_executor
|
|
259
|
+
)
|
|
260
|
+
self._flush_jobs: set[CrossSync.Future[None]] = set()
|
|
261
|
+
# MutationExceptionGroup reports number of successful entries along with failures
|
|
262
|
+
self._entries_processed_since_last_raise: int = 0
|
|
263
|
+
self._exceptions_since_last_raise: int = 0
|
|
264
|
+
# keep track of the first and last _exception_list_limit exceptions
|
|
265
|
+
self._exception_list_limit: int = 10
|
|
266
|
+
self._oldest_exceptions: list[Exception] = []
|
|
267
|
+
self._newest_exceptions: deque[Exception] = deque(
|
|
268
|
+
maxlen=self._exception_list_limit
|
|
269
|
+
)
|
|
270
|
+
# clean up on program exit
|
|
271
|
+
atexit.register(self._on_exit)
|
|
272
|
+
|
|
273
|
+
@CrossSync.convert
|
|
274
|
+
async def _timer_routine(self, interval: float | None) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Set up a background task to flush the batcher every interval seconds
|
|
277
|
+
|
|
278
|
+
If interval is None, an empty future is returned
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
flush_interval: Automatically flush every flush_interval seconds.
|
|
282
|
+
If None, no time-based flushing is performed.
|
|
283
|
+
"""
|
|
284
|
+
if not interval or interval <= 0:
|
|
285
|
+
return None
|
|
286
|
+
while not self._closed.is_set():
|
|
287
|
+
# wait until interval has passed, or until closed
|
|
288
|
+
await CrossSync.event_wait(
|
|
289
|
+
self._closed, timeout=interval, async_break_early=False
|
|
290
|
+
)
|
|
291
|
+
if not self._closed.is_set() and self._staged_entries:
|
|
292
|
+
self._schedule_flush()
|
|
293
|
+
|
|
294
|
+
@CrossSync.convert
|
|
295
|
+
async def append(self, mutation_entry: RowMutationEntry):
|
|
296
|
+
"""
|
|
297
|
+
Add a new set of mutations to the internal queue
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
mutation_entry: new entry to add to flush queue
|
|
301
|
+
Raises:
|
|
302
|
+
RuntimeError: if batcher is closed
|
|
303
|
+
ValueError: if an invalid mutation type is added
|
|
304
|
+
"""
|
|
305
|
+
# TODO: return a future to track completion of this entry
|
|
306
|
+
if self._closed.is_set():
|
|
307
|
+
raise RuntimeError("Cannot append to closed MutationsBatcher")
|
|
308
|
+
if isinstance(cast(Mutation, mutation_entry), Mutation):
|
|
309
|
+
raise ValueError(
|
|
310
|
+
f"invalid mutation type: {type(mutation_entry).__name__}. Only RowMutationEntry objects are supported by batcher"
|
|
311
|
+
)
|
|
312
|
+
self._staged_entries.append(mutation_entry)
|
|
313
|
+
# start a new flush task if limits exceeded
|
|
314
|
+
self._staged_count += len(mutation_entry.mutations)
|
|
315
|
+
self._staged_bytes += mutation_entry.size()
|
|
316
|
+
if (
|
|
317
|
+
self._staged_count >= self._flush_limit_count
|
|
318
|
+
or self._staged_bytes >= self._flush_limit_bytes
|
|
319
|
+
):
|
|
320
|
+
self._schedule_flush()
|
|
321
|
+
# yield to the event loop to allow flush to run
|
|
322
|
+
await CrossSync.yield_to_event_loop()
|
|
323
|
+
|
|
324
|
+
def _schedule_flush(self) -> CrossSync.Future[None] | None:
|
|
325
|
+
"""
|
|
326
|
+
Update the flush task to include the latest staged entries
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Future[None] | None:
|
|
330
|
+
future representing the background task, if started
|
|
331
|
+
"""
|
|
332
|
+
if self._staged_entries:
|
|
333
|
+
entries, self._staged_entries = self._staged_entries, []
|
|
334
|
+
self._staged_count, self._staged_bytes = 0, 0
|
|
335
|
+
new_task = CrossSync.create_task(
|
|
336
|
+
self._flush_internal, entries, sync_executor=self._sync_flush_executor
|
|
337
|
+
)
|
|
338
|
+
if not new_task.done():
|
|
339
|
+
self._flush_jobs.add(new_task)
|
|
340
|
+
new_task.add_done_callback(self._flush_jobs.remove)
|
|
341
|
+
return new_task
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
@CrossSync.convert
|
|
345
|
+
async def _flush_internal(self, new_entries: list[RowMutationEntry]):
|
|
346
|
+
"""
|
|
347
|
+
Flushes a set of mutations to the server, and updates internal state
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
new_entries list of RowMutationEntry objects to flush
|
|
351
|
+
"""
|
|
352
|
+
# flush new entries
|
|
353
|
+
in_process_requests: list[CrossSync.Future[list[FailedMutationEntryError]]] = []
|
|
354
|
+
async for batch in self._flow_control.add_to_flow(new_entries):
|
|
355
|
+
batch_task = CrossSync.create_task(
|
|
356
|
+
self._execute_mutate_rows, batch, sync_executor=self._sync_rpc_executor
|
|
357
|
+
)
|
|
358
|
+
in_process_requests.append(batch_task)
|
|
359
|
+
# wait for all inflight requests to complete
|
|
360
|
+
found_exceptions = await self._wait_for_batch_results(*in_process_requests)
|
|
361
|
+
# update exception data to reflect any new errors
|
|
362
|
+
self._entries_processed_since_last_raise += len(new_entries)
|
|
363
|
+
self._add_exceptions(found_exceptions)
|
|
364
|
+
|
|
365
|
+
@CrossSync.convert
|
|
366
|
+
async def _execute_mutate_rows(
|
|
367
|
+
self, batch: list[RowMutationEntry]
|
|
368
|
+
) -> list[FailedMutationEntryError]:
|
|
369
|
+
"""
|
|
370
|
+
Helper to execute mutation operation on a batch
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
batch: list of RowMutationEntry objects to send to server
|
|
374
|
+
timeout: timeout in seconds. Used as operation_timeout and attempt_timeout.
|
|
375
|
+
If not given, will use table defaults
|
|
376
|
+
Returns:
|
|
377
|
+
list[FailedMutationEntryError]:
|
|
378
|
+
list of FailedMutationEntryError objects for mutations that failed.
|
|
379
|
+
FailedMutationEntryError objects will not contain index information
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
operation = CrossSync._MutateRowsOperation(
|
|
383
|
+
self._table.client._gapic_client,
|
|
384
|
+
self._table,
|
|
385
|
+
batch,
|
|
386
|
+
operation_timeout=self._operation_timeout,
|
|
387
|
+
attempt_timeout=self._attempt_timeout,
|
|
388
|
+
retryable_exceptions=self._retryable_errors,
|
|
389
|
+
)
|
|
390
|
+
await operation.start()
|
|
391
|
+
except MutationsExceptionGroup as e:
|
|
392
|
+
# strip index information from exceptions, since it is not useful in a batch context
|
|
393
|
+
for subexc in e.exceptions:
|
|
394
|
+
subexc.index = None
|
|
395
|
+
return list(e.exceptions)
|
|
396
|
+
finally:
|
|
397
|
+
# mark batch as complete in flow control
|
|
398
|
+
await self._flow_control.remove_from_flow(batch)
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
def _add_exceptions(self, excs: list[Exception]):
|
|
402
|
+
"""
|
|
403
|
+
Add new list of exceptions to internal store. To avoid unbounded memory,
|
|
404
|
+
the batcher will store the first and last _exception_list_limit exceptions,
|
|
405
|
+
and discard any in between.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
excs: list of exceptions to add to the internal store
|
|
409
|
+
"""
|
|
410
|
+
self._exceptions_since_last_raise += len(excs)
|
|
411
|
+
if excs and len(self._oldest_exceptions) < self._exception_list_limit:
|
|
412
|
+
# populate oldest_exceptions with found_exceptions
|
|
413
|
+
addition_count = self._exception_list_limit - len(self._oldest_exceptions)
|
|
414
|
+
self._oldest_exceptions.extend(excs[:addition_count])
|
|
415
|
+
excs = excs[addition_count:]
|
|
416
|
+
if excs:
|
|
417
|
+
# populate newest_exceptions with remaining found_exceptions
|
|
418
|
+
self._newest_exceptions.extend(excs[-self._exception_list_limit :])
|
|
419
|
+
|
|
420
|
+
def _raise_exceptions(self):
|
|
421
|
+
"""
|
|
422
|
+
Raise any unreported exceptions from background flush operations
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
MutationsExceptionGroup: exception group with all unreported exceptions
|
|
426
|
+
"""
|
|
427
|
+
if self._oldest_exceptions or self._newest_exceptions:
|
|
428
|
+
oldest, self._oldest_exceptions = self._oldest_exceptions, []
|
|
429
|
+
newest = list(self._newest_exceptions)
|
|
430
|
+
self._newest_exceptions.clear()
|
|
431
|
+
entry_count, self._entries_processed_since_last_raise = (
|
|
432
|
+
self._entries_processed_since_last_raise,
|
|
433
|
+
0,
|
|
434
|
+
)
|
|
435
|
+
exc_count, self._exceptions_since_last_raise = (
|
|
436
|
+
self._exceptions_since_last_raise,
|
|
437
|
+
0,
|
|
438
|
+
)
|
|
439
|
+
raise MutationsExceptionGroup.from_truncated_lists(
|
|
440
|
+
first_list=oldest,
|
|
441
|
+
last_list=newest,
|
|
442
|
+
total_excs=exc_count,
|
|
443
|
+
entry_count=entry_count,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
@CrossSync.convert(sync_name="__enter__")
|
|
447
|
+
async def __aenter__(self):
|
|
448
|
+
"""Allow use of context manager API"""
|
|
449
|
+
return self
|
|
450
|
+
|
|
451
|
+
@CrossSync.convert(sync_name="__exit__")
|
|
452
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
453
|
+
"""
|
|
454
|
+
Allow use of context manager API.
|
|
455
|
+
|
|
456
|
+
Flushes the batcher and cleans up resources.
|
|
457
|
+
"""
|
|
458
|
+
await self.close()
|
|
459
|
+
|
|
460
|
+
@property
|
|
461
|
+
def closed(self) -> bool:
|
|
462
|
+
"""
|
|
463
|
+
Returns:
|
|
464
|
+
- True if the batcher is closed, False otherwise
|
|
465
|
+
"""
|
|
466
|
+
return self._closed.is_set()
|
|
467
|
+
|
|
468
|
+
@CrossSync.convert
|
|
469
|
+
async def close(self):
|
|
470
|
+
"""
|
|
471
|
+
Flush queue and clean up resources
|
|
472
|
+
"""
|
|
473
|
+
self._closed.set()
|
|
474
|
+
self._flush_timer.cancel()
|
|
475
|
+
self._schedule_flush()
|
|
476
|
+
# shut down executors
|
|
477
|
+
if self._sync_flush_executor:
|
|
478
|
+
with self._sync_flush_executor:
|
|
479
|
+
self._sync_flush_executor.shutdown(wait=True)
|
|
480
|
+
if self._sync_rpc_executor:
|
|
481
|
+
with self._sync_rpc_executor:
|
|
482
|
+
self._sync_rpc_executor.shutdown(wait=True)
|
|
483
|
+
await CrossSync.wait([*self._flush_jobs, self._flush_timer])
|
|
484
|
+
atexit.unregister(self._on_exit)
|
|
485
|
+
# raise unreported exceptions
|
|
486
|
+
self._raise_exceptions()
|
|
487
|
+
|
|
488
|
+
def _on_exit(self):
|
|
489
|
+
"""
|
|
490
|
+
Called when program is exited. Raises warning if unflushed mutations remain
|
|
491
|
+
"""
|
|
492
|
+
if not self._closed.is_set() and self._staged_entries:
|
|
493
|
+
warnings.warn(
|
|
494
|
+
f"MutationsBatcher for table {self._table.table_name} was not closed. "
|
|
495
|
+
f"{len(self._staged_entries)} Unflushed mutations will not be sent to the server."
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
@CrossSync.convert
|
|
500
|
+
async def _wait_for_batch_results(
|
|
501
|
+
*tasks: CrossSync.Future[list[FailedMutationEntryError]]
|
|
502
|
+
| CrossSync.Future[None],
|
|
503
|
+
) -> list[Exception]:
|
|
504
|
+
"""
|
|
505
|
+
Takes in a list of futures representing _execute_mutate_rows tasks,
|
|
506
|
+
waits for them to complete, and returns a list of errors encountered.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
*tasks: futures representing _execute_mutate_rows or _flush_internal tasks
|
|
510
|
+
Returns:
|
|
511
|
+
list[Exception]:
|
|
512
|
+
list of Exceptions encountered by any of the tasks. Errors are expected
|
|
513
|
+
to be FailedMutationEntryError, representing a failed mutation operation.
|
|
514
|
+
If a task fails with a different exception, it will be included in the
|
|
515
|
+
output list. Successful tasks will not be represented in the output list.
|
|
516
|
+
"""
|
|
517
|
+
if not tasks:
|
|
518
|
+
return []
|
|
519
|
+
exceptions: list[Exception] = []
|
|
520
|
+
for task in tasks:
|
|
521
|
+
if CrossSync.is_async:
|
|
522
|
+
# futures don't need to be awaited in sync mode
|
|
523
|
+
await task
|
|
524
|
+
try:
|
|
525
|
+
exc_list = task.result()
|
|
526
|
+
if exc_list:
|
|
527
|
+
# expect a list of FailedMutationEntryError objects
|
|
528
|
+
for exc in exc_list:
|
|
529
|
+
# strip index information
|
|
530
|
+
exc.index = None
|
|
531
|
+
exceptions.extend(exc_list)
|
|
532
|
+
except Exception as e:
|
|
533
|
+
exceptions.append(e)
|
|
534
|
+
return exceptions
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2024 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from .cross_sync import CrossSync
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CrossSync",
|
|
20
|
+
]
|