Skip to content

Commit 8feb1b8

Browse files
authored
tests(bigquery): implement robust wait loop for socket leak tests (#17688)
## Intent This PR addresses flakiness in the `google-cloud-bigquery` system tests that verify system resource cleanup (specifically open sockets). ## The Problem Some system tests in `google-cloud-bigquery` were performing instantaneous assertions on the number of open sockets immediately after client closure or garbage collection. These assertions are fragile because underlying transport layers (like gRPC) and OS socket reclamation can occur asynchronously in background threads. This leads to intermittent failures (flakiness) especially under varied environmental loads or parallel execution. ## Solution Refactored the fragile instantaneous checks in `test_client.py` and `test_magics.py` to use a **Robust Wait-Loop** pattern. - The tests now wait up to 3 seconds, checking periodically, to allow asynchronous cleanup to complete before asserting. - This pattern is already proven effective in other packages like `google-cloud-pubsub`. - Added explicit `gc.collect()` to `test_magics.py` to align with best practices for resource verification tests. > [!note] > Also includes some linting updates. Sorry.
1 parent e5f7fef commit 8feb1b8

2 files changed

Lines changed: 71 additions & 54 deletions

File tree

packages/google-cloud-bigquery/tests/system/test_client.py

Lines changed: 59 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,36 @@
2222
import operator
2323
import os
2424
import pathlib
25+
import random
26+
import string
2527
import time
2628
import unittest
2729
import uuid
28-
import random
29-
import string
3030
from typing import Optional
3131

32-
from google.api_core.exceptions import PreconditionFailed
33-
from google.api_core.exceptions import BadRequest
34-
from google.api_core.exceptions import ClientError
35-
from google.api_core.exceptions import Conflict
36-
from google.api_core.exceptions import GoogleAPICallError
37-
from google.api_core.exceptions import NotFound
38-
from google.api_core.exceptions import InternalServerError
39-
from google.api_core.exceptions import ServiceUnavailable
40-
from google.api_core.exceptions import TooManyRequests
41-
from google.cloud import bigquery
42-
from google.cloud.bigquery.dataset import Dataset
43-
from google.cloud.bigquery.dataset import DatasetReference
44-
from google.cloud.bigquery.table import Table
32+
import psutil
33+
import pytest
34+
from google.api_core.exceptions import (
35+
BadRequest,
36+
ClientError,
37+
Conflict,
38+
GoogleAPICallError,
39+
InternalServerError,
40+
NotFound,
41+
PreconditionFailed,
42+
ServiceUnavailable,
43+
TooManyRequests,
44+
)
45+
from google.cloud import bigquery, storage
4546
from google.cloud._helpers import UTC
4647
from google.cloud.bigquery import dbapi, enums
47-
from google.cloud import storage
48-
from google.cloud.datacatalog_v1 import types as datacatalog_types
48+
from google.cloud.bigquery.dataset import Dataset, DatasetReference
49+
from google.cloud.bigquery.table import Table
4950
from google.cloud.datacatalog_v1 import PolicyTagManagerClient
50-
from google.cloud.resourcemanager_v3 import types as resourcemanager_types
51+
from google.cloud.datacatalog_v1 import types as datacatalog_types
5152
from google.cloud.resourcemanager_v3 import TagKeysClient, TagValuesClient
52-
import psutil
53-
import pytest
54-
from test_utils.retry import RetryErrors
55-
from test_utils.retry import RetryInstanceState
56-
from test_utils.retry import RetryResult
53+
from google.cloud.resourcemanager_v3 import types as resourcemanager_types
54+
from test_utils.retry import RetryErrors, RetryInstanceState, RetryResult
5755
from test_utils.system import unique_resource_id
5856

5957
from . import helpers
@@ -254,8 +252,13 @@ def test_close_releases_open_sockets(self):
254252
import gc
255253

256254
gc.collect()
257-
conn_end = current_process.net_connections()
258-
conn_count_end = len(conn_end)
255+
for _ in range(30): # Wait up to 3 seconds
256+
conn_end = current_process.net_connections()
257+
conn_count_end = len(conn_end)
258+
if conn_count_end <= conn_count_start:
259+
break
260+
time.sleep(0.1)
261+
259262
self.assertLessEqual(conn_count_end, conn_count_start)
260263

261264
def test_create_dataset(self):
@@ -635,8 +638,7 @@ def test_create_table_with_default_value_expression(self):
635638
self.assertEqual("FOO", row_2.get("username"))
636639

637640
def test_create_table_w_time_partitioning_w_clustering_fields(self):
638-
from google.cloud.bigquery.table import TimePartitioning
639-
from google.cloud.bigquery.table import TimePartitioningType
641+
from google.cloud.bigquery.table import TimePartitioning, TimePartitioningType
640642

641643
dataset = self.temp_dataset(_make_dataset_id("create_table_tp_cf"))
642644
table_id = "test_table"
@@ -964,12 +966,12 @@ def test_update_table_clustering_configuration(self):
964966
self.assertIsNone(table3.clustering_fields, None)
965967

966968
def test_update_table_constraints(self):
967-
from google.cloud.bigquery.table import TableConstraints
968969
from google.cloud.bigquery.table import (
969-
PrimaryKey,
970+
ColumnReference,
970971
ForeignKey,
972+
PrimaryKey,
973+
TableConstraints,
971974
TableReference,
972-
ColumnReference,
973975
)
974976

975977
dataset = self.temp_dataset(_make_dataset_id("update_table"))
@@ -1128,8 +1130,7 @@ def test_insert_rows_then_dump_table(self):
11281130
self.assertEqual(sorted(row_tuples, key=by_age), sorted(ROWS, key=by_age))
11291131

11301132
def test_load_table_from_local_avro_file_then_dump_table(self):
1131-
from google.cloud.bigquery.job import SourceFormat
1132-
from google.cloud.bigquery.job import WriteDisposition
1133+
from google.cloud.bigquery.job import SourceFormat, WriteDisposition
11331134

11341135
TABLE_NAME = "test_table_avro"
11351136
ROWS = [
@@ -1169,8 +1170,7 @@ def test_load_table_from_local_avro_file_then_dump_table(self):
11691170

11701171
def test_load_table_from_local_parquet_file_decimal_types(self):
11711172
from google.cloud.bigquery.enums import DecimalTargetType
1172-
from google.cloud.bigquery.job import SourceFormat
1173-
from google.cloud.bigquery.job import WriteDisposition
1173+
from google.cloud.bigquery.job import SourceFormat, WriteDisposition
11741174

11751175
TABLE_NAME = "test_table_parquet"
11761176

@@ -1359,9 +1359,11 @@ def test_load_table_from_csv_w_picosecond_timestamp(self):
13591359
self.assertEqual(table.num_rows, 3)
13601360

13611361
def test_load_avro_from_uri_then_dump_table(self):
1362-
from google.cloud.bigquery.job import CreateDisposition
1363-
from google.cloud.bigquery.job import SourceFormat
1364-
from google.cloud.bigquery.job import WriteDisposition
1362+
from google.cloud.bigquery.job import (
1363+
CreateDisposition,
1364+
SourceFormat,
1365+
WriteDisposition,
1366+
)
13651367

13661368
table_name = "test_table"
13671369
rows = [
@@ -1399,9 +1401,11 @@ def test_load_avro_from_uri_then_dump_table(self):
13991401
)
14001402

14011403
def test_load_table_from_uri_then_dump_table(self):
1402-
from google.cloud.bigquery.job import CreateDisposition
1403-
from google.cloud.bigquery.job import SourceFormat
1404-
from google.cloud.bigquery.job import WriteDisposition
1404+
from google.cloud.bigquery.job import (
1405+
CreateDisposition,
1406+
SourceFormat,
1407+
WriteDisposition,
1408+
)
14051409

14061410
TABLE_ID = "test_table"
14071411
GS_URL = self._write_csv_to_storage(
@@ -2202,15 +2206,22 @@ def test_dbapi_connection_does_not_leak_sockets(self):
22022206
import gc
22032207

22042208
gc.collect()
2205-
conn_end = current_process.net_connections()
2206-
conn_count_end = len(conn_end)
2209+
for _ in range(30): # Wait up to 3 seconds
2210+
conn_end = current_process.net_connections()
2211+
conn_count_end = len(conn_end)
2212+
if conn_count_end <= conn_count_start:
2213+
break
2214+
time.sleep(0.1)
2215+
22072216
self.assertLessEqual(conn_count_end, conn_count_start)
22082217

22092218
def _load_table_for_dml(self, rows, dataset_id, table_id):
22102219
from google.cloud._testing import _NamedTemporaryFile
2211-
from google.cloud.bigquery.job import CreateDisposition
2212-
from google.cloud.bigquery.job import SourceFormat
2213-
from google.cloud.bigquery.job import WriteDisposition
2220+
from google.cloud.bigquery.job import (
2221+
CreateDisposition,
2222+
SourceFormat,
2223+
WriteDisposition,
2224+
)
22142225

22152226
dataset = self.temp_dataset(dataset_id)
22162227
greeting = bigquery.SchemaField("greeting", "STRING", mode="NULLABLE")
@@ -2732,8 +2743,7 @@ def test_nested_table_to_arrow(self):
27322743
bigquery_storage = pytest.importorskip("google.cloud.bigquery_storage")
27332744
pyarrow = pytest.importorskip("pyarrow")
27342745
pyarrow.types = pytest.importorskip("pyarrow.types")
2735-
from google.cloud.bigquery.job import SourceFormat
2736-
from google.cloud.bigquery.job import WriteDisposition
2746+
from google.cloud.bigquery.job import SourceFormat, WriteDisposition
27372747

27382748
SF = bigquery.SchemaField
27392749
schema = [
@@ -2865,8 +2875,7 @@ def test_parameterized_types_round_trip(dataset_id: str):
28652875

28662876

28672877
def test_table_snapshots(dataset_id: str):
2868-
from google.cloud.bigquery import CopyJobConfig
2869-
from google.cloud.bigquery import OperationType
2878+
from google.cloud.bigquery import CopyJobConfig, OperationType
28702879

28712880
client = Config.CLIENT
28722881

@@ -2936,8 +2945,7 @@ def test_table_snapshots(dataset_id: str):
29362945

29372946

29382947
def test_table_clones(dataset_id: str):
2939-
from google.cloud.bigquery import CopyJobConfig
2940-
from google.cloud.bigquery import OperationType
2948+
from google.cloud.bigquery import CopyJobConfig, OperationType
29412949

29422950
client = Config.CLIENT
29432951

packages/google-cloud-bigquery/tests/system/test_magics.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
"""System tests for Jupyter/IPython connector."""
1616

1717
import re
18+
import time
1819

19-
import pytest
2020
import psutil
21+
import pytest
2122

2223
from . import helpers
2324

@@ -71,8 +72,16 @@ def test_bigquery_magic(ipython_interactive):
7172
with io.capture_output() as captured:
7273
result = ip.run_cell_magic("bigquery", "--use_rest_api", sql)
7374

74-
conn_end = current_process.net_connections()
75-
conn_count_end = len(conn_end)
75+
import gc
76+
77+
gc.collect()
78+
79+
for _ in range(30): # Wait up to 3 seconds
80+
conn_end = current_process.net_connections()
81+
conn_count_end = len(conn_end)
82+
if conn_count_end <= conn_count_start:
83+
break
84+
time.sleep(0.1)
7685

7786
lines = re.split("\n|\r", captured.stdout)
7887
# Removes blanks & terminal code (result of display clearing)

0 commit comments

Comments
 (0)