Skip to content

Commit c35a67f

Browse files
committed
Merge branch 'main' into samples_1_run_tests
2 parents dc3dd8b + 8d7a89c commit c35a67f

12 files changed

Lines changed: 202 additions & 43 deletions

File tree

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
google-auth==2.52.0
22
google-cloud-bigquery-storage==2.38.0
3-
google-cloud-bigquery==3.30.0; python_version <= '3.8'
4-
google-cloud-bigquery==3.41.0; python_version >= '3.9'
5-
pyarrow==12.0.1; python_version == '3.7'
6-
pyarrow==17.0.0; python_version == '3.8'
7-
pyarrow==24.0.0; python_version >= '3.9'
8-
ipython==7.31.1; python_version == '3.7'
9-
ipython==8.10.0; python_version == '3.8'
10-
ipython==8.18.1; python_version == '3.9'
11-
ipython==8.33.0; python_version == '3.10'
3+
google-cloud-bigquery==3.41.0
4+
pyarrow==24.0.0
5+
ipython===8.33.0; python_version == '3.10'
126
ipython==9.13.0; python_version >= '3.11'
137
ipywidgets==8.1.8
14-
pandas==1.3.5; python_version == '3.7'
15-
pandas==2.0.3; python_version == '3.8'
16-
pandas==2.3.3; python_version >= '3.9'
8+
pandas==2.3.3
179
tqdm==4.67.3
18-
db-dtypes==1.4.2; python_version <= '3.8'
19-
db-dtypes==1.6.0; python_version >= '3.9'
10+
db-dtypes==1.6.0

‎packages/google-cloud-bigtable/google/cloud/bigtable/batcher.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ def flush(self):
204204
* :exc:`~batcher.MutationsBatchError` if there's any error in the mutations.
205205
"""
206206
self._close_batcher()
207+
# Set atexit registration after internal batcher.
208+
atexit.unregister(self.close)
207209
self._init_batcher()
210+
atexit.register(self.close)
208211

209212
def __exit__(self, exc_type, exc_value, exc_traceback):
210213
"""Clean up resources. Flush and shutdown the ThreadPoolExecutor."""

‎packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/client.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,8 @@ def _start_background_channel_refresh(self) -> None:
416416
):
417417
# raise error if not in an event loop in async client
418418
CrossSync.verify_async_event_loop()
419-
self._channel_refresh_task = CrossSync.create_task(
419+
self._channel_refresh_task = CrossSync.create_daemon_task(
420420
self._manage_channel,
421-
sync_executor=self._executor,
422421
task_name=f"{self.__class__.__name__} channel refresh",
423422
)
424423

‎packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/mutations_batcher.py‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ def __init__(
287287
if not CrossSync.is_async
288288
else None
289289
)
290-
self._flush_timer = CrossSync.create_task(
291-
self._timer_routine, flush_interval, sync_executor=self._sync_flush_executor
290+
self._flush_timer = CrossSync.create_daemon_task(
291+
self._timer_routine, flush_interval
292292
)
293293
self._flush_jobs: set[CrossSync.Future[None]] = set()
294294
# MutationExceptionGroup reports number of successful entries along with failures
@@ -392,18 +392,25 @@ async def _flush_internal(self, new_entries: list[RowMutationEntry]):
392392
CrossSync.Future[list[FailedMutationEntryError]], list[RowMutationEntry]
393393
]
394394
] = []
395+
found_exceptions: list[FailedMutationEntryError] = []
395396
async for batch, metric in self._flow_control.add_to_flow_with_metrics(
396397
new_entries, self._target.client._metrics
397398
):
398-
batch_task = CrossSync.create_task(
399-
self._execute_mutate_rows,
400-
batch,
401-
metric,
402-
sync_executor=self._sync_rpc_executor,
403-
)
404-
in_process_requests.append((batch_task, batch))
399+
try:
400+
batch_task = CrossSync.create_task(
401+
self._execute_mutate_rows,
402+
batch,
403+
metric,
404+
sync_executor=self._sync_rpc_executor,
405+
)
406+
in_process_requests.append((batch_task, batch))
407+
except RuntimeError:
408+
# Executor is already shut down (e.g. during atexit); flush synchronously.
409+
found_exceptions.extend(await self._execute_mutate_rows(batch, metric))
405410
# wait for all inflight requests to complete
406-
found_exceptions = await self._wait_for_batch_results(*in_process_requests)
411+
found_exceptions.extend(
412+
await self._wait_for_batch_results(*in_process_requests)
413+
)
407414
# update exception data to reflect any new errors
408415
self._entries_processed_since_last_raise += len(new_entries)
409416
self._add_exceptions(found_exceptions)
@@ -545,7 +552,12 @@ async def close(self):
545552
"""
546553
self._closed.set()
547554
self._flush_timer.cancel()
548-
self._schedule_flush()
555+
entries = self._staged_entries
556+
try:
557+
self._schedule_flush()
558+
except RuntimeError:
559+
# Executor is already shut down (e.g. during atexit); flush synchronously.
560+
await self._flush_internal(entries)
549561
# shut down executors
550562
if self._sync_flush_executor:
551563
with self._sync_flush_executor:

‎packages/google-cloud-bigtable/google/cloud/bigtable/data/_cross_sync/cross_sync.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,19 @@ def create_task(
205205
task.set_name(task_name)
206206
return task
207207

208+
@staticmethod
209+
def create_daemon_task(
210+
fn: Callable[..., Coroutine[Any, Any, T]],
211+
*fn_args,
212+
task_name: str | None = None,
213+
**fn_kwargs,
214+
) -> CrossSync.Task[T]:
215+
"""
216+
Abstraction over asyncio.create_task for long-lived background tasks.
217+
Sync version runs on a dedicated daemon thread instead of a ThreadPoolExecutor.
218+
"""
219+
return CrossSync.create_task(fn, *fn_args, task_name=task_name, **fn_kwargs)
220+
208221
@staticmethod
209222
async def yield_to_event_loop() -> None:
210223
"""
@@ -319,6 +332,30 @@ def create_task(
319332
raise ValueError("sync_executor is required for sync version")
320333
return sync_executor.submit(fn, *fn_args, **fn_kwargs)
321334

335+
@staticmethod
336+
def create_daemon_task(
337+
fn: Callable[..., T],
338+
*fn_args,
339+
task_name: str | None = None,
340+
**fn_kwargs,
341+
) -> CrossSync._Sync_Impl.Task[T]:
342+
"""
343+
Run a long-lived background task on a dedicated daemon thread and
344+
return a Future representing its completion.
345+
"""
346+
future: concurrent.futures.Future[T] = concurrent.futures.Future()
347+
348+
def _run() -> None:
349+
if not future.set_running_or_notify_cancel(): # pragma: NO COVER
350+
return
351+
try:
352+
future.set_result(fn(*fn_args, **fn_kwargs))
353+
except BaseException as exc:
354+
future.set_exception(exc)
355+
356+
threading.Thread(target=_run, name=task_name, daemon=True).start()
357+
return future
358+
322359
@staticmethod
323360
def yield_to_event_loop() -> None:
324361
"""

‎packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/client.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,8 @@ def _start_background_channel_refresh(self) -> None:
306306
and (not self._disable_background_refresh)
307307
):
308308
CrossSync._Sync_Impl.verify_async_event_loop()
309-
self._channel_refresh_task = CrossSync._Sync_Impl.create_task(
309+
self._channel_refresh_task = CrossSync._Sync_Impl.create_daemon_task(
310310
self._manage_channel,
311-
sync_executor=self._executor,
312311
task_name=f"{self.__class__.__name__} channel refresh",
313312
)
314313

‎packages/google-cloud-bigtable/google/cloud/bigtable/data/_sync_autogen/mutations_batcher.py‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ def __init__(
254254
if not CrossSync._Sync_Impl.is_async
255255
else None
256256
)
257-
self._flush_timer = CrossSync._Sync_Impl.create_task(
258-
self._timer_routine, flush_interval, sync_executor=self._sync_flush_executor
257+
self._flush_timer = CrossSync._Sync_Impl.create_daemon_task(
258+
self._timer_routine, flush_interval
259259
)
260260
self._flush_jobs: set[CrossSync._Sync_Impl.Future[None]] = set()
261261
self._entries_processed_since_last_raise: int = 0
@@ -340,17 +340,21 @@ def _flush_internal(self, new_entries: list[RowMutationEntry]):
340340
list[RowMutationEntry],
341341
]
342342
] = []
343+
found_exceptions: list[FailedMutationEntryError] = []
343344
for batch, metric in self._flow_control.add_to_flow_with_metrics(
344345
new_entries, self._target.client._metrics
345346
):
346-
batch_task = CrossSync._Sync_Impl.create_task(
347-
self._execute_mutate_rows,
348-
batch,
349-
metric,
350-
sync_executor=self._sync_rpc_executor,
351-
)
352-
in_process_requests.append((batch_task, batch))
353-
found_exceptions = self._wait_for_batch_results(*in_process_requests)
347+
try:
348+
batch_task = CrossSync._Sync_Impl.create_task(
349+
self._execute_mutate_rows,
350+
batch,
351+
metric,
352+
sync_executor=self._sync_rpc_executor,
353+
)
354+
in_process_requests.append((batch_task, batch))
355+
except RuntimeError:
356+
found_exceptions.extend(self._execute_mutate_rows(batch, metric))
357+
found_exceptions.extend(self._wait_for_batch_results(*in_process_requests))
354358
self._entries_processed_since_last_raise += len(new_entries)
355359
self._add_exceptions(found_exceptions)
356360

@@ -462,7 +466,11 @@ def close(self):
462466
"""Flush queue and clean up resources"""
463467
self._closed.set()
464468
self._flush_timer.cancel()
465-
self._schedule_flush()
469+
entries = self._staged_entries
470+
try:
471+
self._schedule_flush()
472+
except RuntimeError:
473+
self._flush_internal(entries)
466474
if self._sync_flush_executor:
467475
with self._sync_flush_executor:
468476
self._sync_flush_executor.shutdown(wait=True)
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
apache-beam==2.60.0; python_version == '3.8'
2-
apache-beam==2.69.0; python_version == '3.9'
3-
apache-beam==2.71.0; python_version >= '3.10'
1+
apache-beam==2.71.0
42
google-cloud-bigtable
53
google-cloud-core==2.5.0

‎packages/google-cloud-bigtable/tests/unit/data/_async/test_mutations_batcher.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,22 @@ async def test_close(self):
11591159
assert flush_mock.call_count == 1
11601160
assert raise_mock.call_count == 1
11611161

1162+
@CrossSync.pytest
1163+
async def test_close_executor_shutdown_fallback(self):
1164+
"""If create_task raises RuntimeError during close (e.g. atexit), flush synchronously"""
1165+
async with self._make_one() as instance:
1166+
mutation = self._make_mutation(count=1, size=1)
1167+
instance._staged_entries = [mutation]
1168+
with mock.patch.object(
1169+
CrossSync, "create_task", side_effect=RuntimeError("shutdown")
1170+
):
1171+
with mock.patch.object(
1172+
instance, "_execute_mutate_rows", return_value=[]
1173+
) as exec_mock:
1174+
await instance.close()
1175+
assert exec_mock.call_count == 1
1176+
assert instance._staged_entries == []
1177+
11621178
@CrossSync.pytest
11631179
async def test_close_w_exceptions(self):
11641180
"""Raise exceptions on close"""

‎packages/google-cloud-bigtable/tests/unit/data/_cross_sync/test_cross_sync.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,45 @@ def test_create_task_no_executor(self, cs_sync):
450450
cs_sync.create_task(lambda: None)
451451
assert "sync_executor is required" in str(e.value)
452452

453+
def test_create_daemon_task(self, cs_sync):
454+
"""
455+
create_daemon_task should run on a daemon thread and return a Future
456+
"""
457+
result = cs_sync.create_daemon_task(
458+
lambda x, y: (x + y, threading.current_thread().daemon),
459+
2,
460+
y=3,
461+
task_name="test-daemon",
462+
)
463+
assert isinstance(result, cs_sync.Task)
464+
assert result.result(timeout=5) == (5, True)
465+
466+
def test_create_daemon_task_exception(self, cs_sync):
467+
"""
468+
create_daemon_task should propagate exceptions onto the returned Future
469+
"""
470+
471+
def fail():
472+
raise ValueError("boom")
473+
474+
result = cs_sync.create_daemon_task(fail)
475+
with pytest.raises(ValueError, match="boom"):
476+
result.result(timeout=5)
477+
478+
@pytest.mark.asyncio
479+
async def test_create_daemon_task_async(self, cs_async):
480+
"""
481+
Async create_daemon_task should create an asyncio.Task
482+
"""
483+
484+
async def coro_fn(x, y):
485+
return x + y
486+
487+
result = cs_async.create_daemon_task(coro_fn, 2, y=3, task_name="async-daemon")
488+
assert isinstance(result, asyncio.Task)
489+
assert result.get_name() == "async-daemon"
490+
assert await result == 5
491+
453492
@pytest.mark.asyncio
454493
async def test_create_task_async(self, cs_async):
455494
"""

0 commit comments

Comments
 (0)