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,449 @@
|
|
|
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
|
+
|
|
16
|
+
# This file is automatically generated by CrossSync. Do not edit manually.
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
from typing import Sequence, TYPE_CHECKING, cast
|
|
20
|
+
import atexit
|
|
21
|
+
import warnings
|
|
22
|
+
from collections import deque
|
|
23
|
+
import concurrent.futures
|
|
24
|
+
from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
|
|
25
|
+
from google.cloud.bigtable.data.exceptions import FailedMutationEntryError
|
|
26
|
+
from google.cloud.bigtable.data._helpers import _get_retryable_errors
|
|
27
|
+
from google.cloud.bigtable.data._helpers import _get_timeouts
|
|
28
|
+
from google.cloud.bigtable.data._helpers import TABLE_DEFAULT
|
|
29
|
+
from google.cloud.bigtable.data.mutations import _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
|
|
30
|
+
from google.cloud.bigtable.data.mutations import Mutation
|
|
31
|
+
from google.cloud.bigtable.data._cross_sync import CrossSync
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from google.cloud.bigtable.data.mutations import RowMutationEntry
|
|
35
|
+
from google.cloud.bigtable.data._sync_autogen.client import Table as TableType
|
|
36
|
+
_MB_SIZE = 1024 * 1024
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@CrossSync._Sync_Impl.add_mapping_decorator("_FlowControl")
|
|
40
|
+
class _FlowControl:
|
|
41
|
+
"""
|
|
42
|
+
Manages flow control for batched mutations. Mutations are registered against
|
|
43
|
+
the FlowControl object before being sent, which will block if size or count
|
|
44
|
+
limits have reached capacity. As mutations completed, they are removed from
|
|
45
|
+
the FlowControl object, which will notify any blocked requests that there
|
|
46
|
+
is additional capacity.
|
|
47
|
+
|
|
48
|
+
Flow limits are not hard limits. If a single mutation exceeds the configured
|
|
49
|
+
limits, it will be allowed as a single batch when the capacity is available.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
max_mutation_count: maximum number of mutations to send in a single rpc.
|
|
53
|
+
This corresponds to individual mutations in a single RowMutationEntry.
|
|
54
|
+
max_mutation_bytes: maximum number of bytes to send in a single rpc.
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: if max_mutation_count or max_mutation_bytes is less than 0
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, max_mutation_count: int, max_mutation_bytes: int):
|
|
60
|
+
self._max_mutation_count = max_mutation_count
|
|
61
|
+
self._max_mutation_bytes = max_mutation_bytes
|
|
62
|
+
if self._max_mutation_count < 1:
|
|
63
|
+
raise ValueError("max_mutation_count must be greater than 0")
|
|
64
|
+
if self._max_mutation_bytes < 1:
|
|
65
|
+
raise ValueError("max_mutation_bytes must be greater than 0")
|
|
66
|
+
self._capacity_condition = CrossSync._Sync_Impl.Condition()
|
|
67
|
+
self._in_flight_mutation_count = 0
|
|
68
|
+
self._in_flight_mutation_bytes = 0
|
|
69
|
+
|
|
70
|
+
def _has_capacity(self, additional_count: int, additional_size: int) -> bool:
|
|
71
|
+
"""Checks if there is capacity to send a new entry with the given size and count
|
|
72
|
+
|
|
73
|
+
FlowControl limits are not hard limits. If a single mutation exceeds
|
|
74
|
+
the configured flow limits, it will be sent in a single batch when
|
|
75
|
+
previous batches have completed.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
additional_count: number of mutations in the pending entry
|
|
79
|
+
additional_size: size of the pending entry
|
|
80
|
+
Returns:
|
|
81
|
+
bool: True if there is capacity to send the pending entry, False otherwise
|
|
82
|
+
"""
|
|
83
|
+
acceptable_size = max(self._max_mutation_bytes, additional_size)
|
|
84
|
+
acceptable_count = max(self._max_mutation_count, additional_count)
|
|
85
|
+
new_size = self._in_flight_mutation_bytes + additional_size
|
|
86
|
+
new_count = self._in_flight_mutation_count + additional_count
|
|
87
|
+
return new_size <= acceptable_size and new_count <= acceptable_count
|
|
88
|
+
|
|
89
|
+
def remove_from_flow(
|
|
90
|
+
self, mutations: RowMutationEntry | list[RowMutationEntry]
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Removes mutations from flow control. This method should be called once
|
|
93
|
+
for each mutation that was sent to add_to_flow, after the corresponding
|
|
94
|
+
operation is complete.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
mutations: mutation or list of mutations to remove from flow control"""
|
|
98
|
+
if not isinstance(mutations, list):
|
|
99
|
+
mutations = [mutations]
|
|
100
|
+
total_count = sum((len(entry.mutations) for entry in mutations))
|
|
101
|
+
total_size = sum((entry.size() for entry in mutations))
|
|
102
|
+
self._in_flight_mutation_count -= total_count
|
|
103
|
+
self._in_flight_mutation_bytes -= total_size
|
|
104
|
+
with self._capacity_condition:
|
|
105
|
+
self._capacity_condition.notify_all()
|
|
106
|
+
|
|
107
|
+
def add_to_flow(self, mutations: RowMutationEntry | list[RowMutationEntry]):
|
|
108
|
+
"""Generator function that registers mutations with flow control. As mutations
|
|
109
|
+
are accepted into the flow control, they are yielded back to the caller,
|
|
110
|
+
to be sent in a batch. If the flow control is at capacity, the generator
|
|
111
|
+
will block until there is capacity available.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
mutations: list mutations to break up into batches
|
|
115
|
+
Yields:
|
|
116
|
+
list[RowMutationEntry]:
|
|
117
|
+
list of mutations that have reserved space in the flow control.
|
|
118
|
+
Each batch contains at least one mutation."""
|
|
119
|
+
if not isinstance(mutations, list):
|
|
120
|
+
mutations = [mutations]
|
|
121
|
+
start_idx = 0
|
|
122
|
+
end_idx = 0
|
|
123
|
+
while end_idx < len(mutations):
|
|
124
|
+
start_idx = end_idx
|
|
125
|
+
batch_mutation_count = 0
|
|
126
|
+
with self._capacity_condition:
|
|
127
|
+
while end_idx < len(mutations):
|
|
128
|
+
next_entry = mutations[end_idx]
|
|
129
|
+
next_size = next_entry.size()
|
|
130
|
+
next_count = len(next_entry.mutations)
|
|
131
|
+
if (
|
|
132
|
+
self._has_capacity(next_count, next_size)
|
|
133
|
+
and batch_mutation_count + next_count
|
|
134
|
+
<= _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
|
|
135
|
+
):
|
|
136
|
+
end_idx += 1
|
|
137
|
+
batch_mutation_count += next_count
|
|
138
|
+
self._in_flight_mutation_bytes += next_size
|
|
139
|
+
self._in_flight_mutation_count += next_count
|
|
140
|
+
elif start_idx != end_idx:
|
|
141
|
+
break
|
|
142
|
+
else:
|
|
143
|
+
self._capacity_condition.wait_for(
|
|
144
|
+
lambda: self._has_capacity(next_count, next_size)
|
|
145
|
+
)
|
|
146
|
+
yield mutations[start_idx:end_idx]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class MutationsBatcher:
|
|
150
|
+
"""
|
|
151
|
+
Allows users to send batches using context manager API:
|
|
152
|
+
|
|
153
|
+
Runs mutate_row, mutate_rows, and check_and_mutate_row internally, combining
|
|
154
|
+
to use as few network requests as required
|
|
155
|
+
|
|
156
|
+
Will automatically flush the batcher:
|
|
157
|
+
- every flush_interval seconds
|
|
158
|
+
- after queue size reaches flush_limit_mutation_count
|
|
159
|
+
- after queue reaches flush_limit_bytes
|
|
160
|
+
- when batcher is closed or destroyed
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
table: Table to preform rpc calls
|
|
164
|
+
flush_interval: Automatically flush every flush_interval seconds.
|
|
165
|
+
If None, no time-based flushing is performed.
|
|
166
|
+
flush_limit_mutation_count: Flush immediately after flush_limit_mutation_count
|
|
167
|
+
mutations are added across all entries. If None, this limit is ignored.
|
|
168
|
+
flush_limit_bytes: Flush immediately after flush_limit_bytes bytes are added.
|
|
169
|
+
flow_control_max_mutation_count: Maximum number of inflight mutations.
|
|
170
|
+
flow_control_max_bytes: Maximum number of inflight bytes.
|
|
171
|
+
batch_operation_timeout: timeout for each mutate_rows operation, in seconds.
|
|
172
|
+
If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_operation_timeout.
|
|
173
|
+
batch_attempt_timeout: timeout for each individual request, in seconds.
|
|
174
|
+
If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_attempt_timeout.
|
|
175
|
+
If None, defaults to batch_operation_timeout.
|
|
176
|
+
batch_retryable_errors: a list of errors that will be retried if encountered.
|
|
177
|
+
Defaults to the Table's default_mutate_rows_retryable_errors.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
table: TableType,
|
|
183
|
+
*,
|
|
184
|
+
flush_interval: float | None = 5,
|
|
185
|
+
flush_limit_mutation_count: int | None = 1000,
|
|
186
|
+
flush_limit_bytes: int = 20 * _MB_SIZE,
|
|
187
|
+
flow_control_max_mutation_count: int = 100000,
|
|
188
|
+
flow_control_max_bytes: int = 100 * _MB_SIZE,
|
|
189
|
+
batch_operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
190
|
+
batch_attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
191
|
+
batch_retryable_errors: Sequence[type[Exception]]
|
|
192
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
193
|
+
):
|
|
194
|
+
(self._operation_timeout, self._attempt_timeout) = _get_timeouts(
|
|
195
|
+
batch_operation_timeout, batch_attempt_timeout, table
|
|
196
|
+
)
|
|
197
|
+
self._retryable_errors: list[type[Exception]] = _get_retryable_errors(
|
|
198
|
+
batch_retryable_errors, table
|
|
199
|
+
)
|
|
200
|
+
self._closed = CrossSync._Sync_Impl.Event()
|
|
201
|
+
self._table = table
|
|
202
|
+
self._staged_entries: list[RowMutationEntry] = []
|
|
203
|
+
(self._staged_count, self._staged_bytes) = (0, 0)
|
|
204
|
+
self._flow_control = CrossSync._Sync_Impl._FlowControl(
|
|
205
|
+
flow_control_max_mutation_count, flow_control_max_bytes
|
|
206
|
+
)
|
|
207
|
+
self._flush_limit_bytes = flush_limit_bytes
|
|
208
|
+
self._flush_limit_count = (
|
|
209
|
+
flush_limit_mutation_count
|
|
210
|
+
if flush_limit_mutation_count is not None
|
|
211
|
+
else float("inf")
|
|
212
|
+
)
|
|
213
|
+
self._sync_rpc_executor = (
|
|
214
|
+
concurrent.futures.ThreadPoolExecutor(max_workers=8)
|
|
215
|
+
if not CrossSync._Sync_Impl.is_async
|
|
216
|
+
else None
|
|
217
|
+
)
|
|
218
|
+
self._sync_flush_executor = (
|
|
219
|
+
concurrent.futures.ThreadPoolExecutor(max_workers=4)
|
|
220
|
+
if not CrossSync._Sync_Impl.is_async
|
|
221
|
+
else None
|
|
222
|
+
)
|
|
223
|
+
self._flush_timer = CrossSync._Sync_Impl.create_task(
|
|
224
|
+
self._timer_routine, flush_interval, sync_executor=self._sync_flush_executor
|
|
225
|
+
)
|
|
226
|
+
self._flush_jobs: set[CrossSync._Sync_Impl.Future[None]] = set()
|
|
227
|
+
self._entries_processed_since_last_raise: int = 0
|
|
228
|
+
self._exceptions_since_last_raise: int = 0
|
|
229
|
+
self._exception_list_limit: int = 10
|
|
230
|
+
self._oldest_exceptions: list[Exception] = []
|
|
231
|
+
self._newest_exceptions: deque[Exception] = deque(
|
|
232
|
+
maxlen=self._exception_list_limit
|
|
233
|
+
)
|
|
234
|
+
atexit.register(self._on_exit)
|
|
235
|
+
|
|
236
|
+
def _timer_routine(self, interval: float | None) -> None:
|
|
237
|
+
"""Set up a background task to flush the batcher every interval seconds
|
|
238
|
+
|
|
239
|
+
If interval is None, an empty future is returned
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
flush_interval: Automatically flush every flush_interval seconds.
|
|
243
|
+
If None, no time-based flushing is performed."""
|
|
244
|
+
if not interval or interval <= 0:
|
|
245
|
+
return None
|
|
246
|
+
while not self._closed.is_set():
|
|
247
|
+
CrossSync._Sync_Impl.event_wait(
|
|
248
|
+
self._closed, timeout=interval, async_break_early=False
|
|
249
|
+
)
|
|
250
|
+
if not self._closed.is_set() and self._staged_entries:
|
|
251
|
+
self._schedule_flush()
|
|
252
|
+
|
|
253
|
+
def append(self, mutation_entry: RowMutationEntry):
|
|
254
|
+
"""Add a new set of mutations to the internal queue
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
mutation_entry: new entry to add to flush queue
|
|
258
|
+
Raises:
|
|
259
|
+
RuntimeError: if batcher is closed
|
|
260
|
+
ValueError: if an invalid mutation type is added"""
|
|
261
|
+
if self._closed.is_set():
|
|
262
|
+
raise RuntimeError("Cannot append to closed MutationsBatcher")
|
|
263
|
+
if isinstance(cast(Mutation, mutation_entry), Mutation):
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"invalid mutation type: {type(mutation_entry).__name__}. Only RowMutationEntry objects are supported by batcher"
|
|
266
|
+
)
|
|
267
|
+
self._staged_entries.append(mutation_entry)
|
|
268
|
+
self._staged_count += len(mutation_entry.mutations)
|
|
269
|
+
self._staged_bytes += mutation_entry.size()
|
|
270
|
+
if (
|
|
271
|
+
self._staged_count >= self._flush_limit_count
|
|
272
|
+
or self._staged_bytes >= self._flush_limit_bytes
|
|
273
|
+
):
|
|
274
|
+
self._schedule_flush()
|
|
275
|
+
CrossSync._Sync_Impl.yield_to_event_loop()
|
|
276
|
+
|
|
277
|
+
def _schedule_flush(self) -> CrossSync._Sync_Impl.Future[None] | None:
|
|
278
|
+
"""Update the flush task to include the latest staged entries
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Future[None] | None:
|
|
282
|
+
future representing the background task, if started"""
|
|
283
|
+
if self._staged_entries:
|
|
284
|
+
(entries, self._staged_entries) = (self._staged_entries, [])
|
|
285
|
+
(self._staged_count, self._staged_bytes) = (0, 0)
|
|
286
|
+
new_task = CrossSync._Sync_Impl.create_task(
|
|
287
|
+
self._flush_internal, entries, sync_executor=self._sync_flush_executor
|
|
288
|
+
)
|
|
289
|
+
if not new_task.done():
|
|
290
|
+
self._flush_jobs.add(new_task)
|
|
291
|
+
new_task.add_done_callback(self._flush_jobs.remove)
|
|
292
|
+
return new_task
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def _flush_internal(self, new_entries: list[RowMutationEntry]):
|
|
296
|
+
"""Flushes a set of mutations to the server, and updates internal state
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
new_entries list of RowMutationEntry objects to flush"""
|
|
300
|
+
in_process_requests: list[
|
|
301
|
+
CrossSync._Sync_Impl.Future[list[FailedMutationEntryError]]
|
|
302
|
+
] = []
|
|
303
|
+
for batch in self._flow_control.add_to_flow(new_entries):
|
|
304
|
+
batch_task = CrossSync._Sync_Impl.create_task(
|
|
305
|
+
self._execute_mutate_rows, batch, sync_executor=self._sync_rpc_executor
|
|
306
|
+
)
|
|
307
|
+
in_process_requests.append(batch_task)
|
|
308
|
+
found_exceptions = self._wait_for_batch_results(*in_process_requests)
|
|
309
|
+
self._entries_processed_since_last_raise += len(new_entries)
|
|
310
|
+
self._add_exceptions(found_exceptions)
|
|
311
|
+
|
|
312
|
+
def _execute_mutate_rows(
|
|
313
|
+
self, batch: list[RowMutationEntry]
|
|
314
|
+
) -> list[FailedMutationEntryError]:
|
|
315
|
+
"""Helper to execute mutation operation on a batch
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
batch: list of RowMutationEntry objects to send to server
|
|
319
|
+
timeout: timeout in seconds. Used as operation_timeout and attempt_timeout.
|
|
320
|
+
If not given, will use table defaults
|
|
321
|
+
Returns:
|
|
322
|
+
list[FailedMutationEntryError]:
|
|
323
|
+
list of FailedMutationEntryError objects for mutations that failed.
|
|
324
|
+
FailedMutationEntryError objects will not contain index information"""
|
|
325
|
+
try:
|
|
326
|
+
operation = CrossSync._Sync_Impl._MutateRowsOperation(
|
|
327
|
+
self._table.client._gapic_client,
|
|
328
|
+
self._table,
|
|
329
|
+
batch,
|
|
330
|
+
operation_timeout=self._operation_timeout,
|
|
331
|
+
attempt_timeout=self._attempt_timeout,
|
|
332
|
+
retryable_exceptions=self._retryable_errors,
|
|
333
|
+
)
|
|
334
|
+
operation.start()
|
|
335
|
+
except MutationsExceptionGroup as e:
|
|
336
|
+
for subexc in e.exceptions:
|
|
337
|
+
subexc.index = None
|
|
338
|
+
return list(e.exceptions)
|
|
339
|
+
finally:
|
|
340
|
+
self._flow_control.remove_from_flow(batch)
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
def _add_exceptions(self, excs: list[Exception]):
|
|
344
|
+
"""Add new list of exceptions to internal store. To avoid unbounded memory,
|
|
345
|
+
the batcher will store the first and last _exception_list_limit exceptions,
|
|
346
|
+
and discard any in between.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
excs: list of exceptions to add to the internal store"""
|
|
350
|
+
self._exceptions_since_last_raise += len(excs)
|
|
351
|
+
if excs and len(self._oldest_exceptions) < self._exception_list_limit:
|
|
352
|
+
addition_count = self._exception_list_limit - len(self._oldest_exceptions)
|
|
353
|
+
self._oldest_exceptions.extend(excs[:addition_count])
|
|
354
|
+
excs = excs[addition_count:]
|
|
355
|
+
if excs:
|
|
356
|
+
self._newest_exceptions.extend(excs[-self._exception_list_limit :])
|
|
357
|
+
|
|
358
|
+
def _raise_exceptions(self):
|
|
359
|
+
"""Raise any unreported exceptions from background flush operations
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
MutationsExceptionGroup: exception group with all unreported exceptions"""
|
|
363
|
+
if self._oldest_exceptions or self._newest_exceptions:
|
|
364
|
+
(oldest, self._oldest_exceptions) = (self._oldest_exceptions, [])
|
|
365
|
+
newest = list(self._newest_exceptions)
|
|
366
|
+
self._newest_exceptions.clear()
|
|
367
|
+
(entry_count, self._entries_processed_since_last_raise) = (
|
|
368
|
+
self._entries_processed_since_last_raise,
|
|
369
|
+
0,
|
|
370
|
+
)
|
|
371
|
+
(exc_count, self._exceptions_since_last_raise) = (
|
|
372
|
+
self._exceptions_since_last_raise,
|
|
373
|
+
0,
|
|
374
|
+
)
|
|
375
|
+
raise MutationsExceptionGroup.from_truncated_lists(
|
|
376
|
+
first_list=oldest,
|
|
377
|
+
last_list=newest,
|
|
378
|
+
total_excs=exc_count,
|
|
379
|
+
entry_count=entry_count,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def __enter__(self):
|
|
383
|
+
"""Allow use of context manager API"""
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def __exit__(self, exc_type, exc, tb):
|
|
387
|
+
"""Allow use of context manager API.
|
|
388
|
+
|
|
389
|
+
Flushes the batcher and cleans up resources."""
|
|
390
|
+
self.close()
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def closed(self) -> bool:
|
|
394
|
+
"""Returns:
|
|
395
|
+
- True if the batcher is closed, False otherwise"""
|
|
396
|
+
return self._closed.is_set()
|
|
397
|
+
|
|
398
|
+
def close(self):
|
|
399
|
+
"""Flush queue and clean up resources"""
|
|
400
|
+
self._closed.set()
|
|
401
|
+
self._flush_timer.cancel()
|
|
402
|
+
self._schedule_flush()
|
|
403
|
+
if self._sync_flush_executor:
|
|
404
|
+
with self._sync_flush_executor:
|
|
405
|
+
self._sync_flush_executor.shutdown(wait=True)
|
|
406
|
+
if self._sync_rpc_executor:
|
|
407
|
+
with self._sync_rpc_executor:
|
|
408
|
+
self._sync_rpc_executor.shutdown(wait=True)
|
|
409
|
+
CrossSync._Sync_Impl.wait([*self._flush_jobs, self._flush_timer])
|
|
410
|
+
atexit.unregister(self._on_exit)
|
|
411
|
+
self._raise_exceptions()
|
|
412
|
+
|
|
413
|
+
def _on_exit(self):
|
|
414
|
+
"""Called when program is exited. Raises warning if unflushed mutations remain"""
|
|
415
|
+
if not self._closed.is_set() and self._staged_entries:
|
|
416
|
+
warnings.warn(
|
|
417
|
+
f"MutationsBatcher for table {self._table.table_name} was not closed. {len(self._staged_entries)} Unflushed mutations will not be sent to the server."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
@staticmethod
|
|
421
|
+
def _wait_for_batch_results(
|
|
422
|
+
*tasks: CrossSync._Sync_Impl.Future[list[FailedMutationEntryError]]
|
|
423
|
+
| CrossSync._Sync_Impl.Future[None],
|
|
424
|
+
) -> list[Exception]:
|
|
425
|
+
"""Takes in a list of futures representing _execute_mutate_rows tasks,
|
|
426
|
+
waits for them to complete, and returns a list of errors encountered.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
*tasks: futures representing _execute_mutate_rows or _flush_internal tasks
|
|
430
|
+
Returns:
|
|
431
|
+
list[Exception]:
|
|
432
|
+
list of Exceptions encountered by any of the tasks. Errors are expected
|
|
433
|
+
to be FailedMutationEntryError, representing a failed mutation operation.
|
|
434
|
+
If a task fails with a different exception, it will be included in the
|
|
435
|
+
output list. Successful tasks will not be represented in the output list.
|
|
436
|
+
"""
|
|
437
|
+
if not tasks:
|
|
438
|
+
return []
|
|
439
|
+
exceptions: list[Exception] = []
|
|
440
|
+
for task in tasks:
|
|
441
|
+
try:
|
|
442
|
+
exc_list = task.result()
|
|
443
|
+
if exc_list:
|
|
444
|
+
for exc in exc_list:
|
|
445
|
+
exc.index = None
|
|
446
|
+
exceptions.extend(exc_list)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
exceptions.append(e)
|
|
449
|
+
return exceptions
|