Skip to content

Commit 50474ec

Browse files
committed
fix(storage): add sync context manager fallback and prevent trace state leakage across unit tests
1 parent 1ed8293 commit 50474ec

6 files changed

Lines changed: 80 additions & 29 deletions

File tree

‎packages/google-cloud-storage/google/cloud/storage/_bucket_metadata_cache.py‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def get_or_queue_fetch(self, bucket_name):
6161
else:
6262
# fire a background fetch and get bucket metadata.
6363
self._inflight_fetches.add(bucket_name)
64-
if hasattr(self._client, "grpc_client"):
64+
if getattr(self._client, "_is_async_grpc_client", False) is True:
6565
try:
6666
loop = asyncio.get_running_loop()
6767
loop.create_task(self._fetch_background_async(bucket_name))
@@ -81,7 +81,7 @@ def check_and_evict(self, bucket_name):
8181
if bucket_name in self._inflight_checks:
8282
return
8383
self._inflight_checks.add(bucket_name)
84-
if hasattr(self._client, "grpc_client"):
84+
if getattr(self._client, "_is_async_grpc_client", False) is True:
8585
try:
8686
loop = asyncio.get_running_loop()
8787
loop.create_task(
@@ -178,25 +178,31 @@ def update_from_bucket(self, bucket, bucket_name=None):
178178
if not bucket:
179179
return
180180
name = bucket_name or getattr(bucket, "name", None)
181-
if not name:
181+
if not name or not isinstance(name, str):
182182
return
183183
if name.startswith("projects/") and "/buckets/" in name:
184184
name = name.split("/buckets/", 1)[1]
185185

186186
project_number = getattr(bucket, "project_number", None)
187187
if not project_number:
188-
project = getattr(bucket, "project", None)
189-
if project:
190-
proj_str = str(project)
188+
proj_attr = getattr(bucket, "project", None)
189+
if isinstance(proj_attr, (str, int)):
190+
proj_str = str(proj_attr)
191191
if proj_str.startswith("projects/"):
192192
project_number = proj_str.split("projects/", 1)[1]
193-
else:
193+
elif proj_str:
194194
project_number = proj_str
195195

196-
location = getattr(bucket, "location", None) or "global"
197-
location = location.lower()
198-
location_type = getattr(bucket, "location_type", None) or "region"
199-
location_type = location_type.lower()
196+
loc_attr = getattr(bucket, "location", None)
197+
location = (
198+
loc_attr.lower() if isinstance(loc_attr, str) and loc_attr else "global"
199+
)
200+
loc_type_attr = getattr(bucket, "location_type", None)
201+
location_type = (
202+
loc_type_attr.lower()
203+
if isinstance(loc_type_attr, str) and loc_type_attr
204+
else "region"
205+
)
200206

201207
if location_type in ("multi-region", "dual-region"):
202208
location = "global"

‎packages/google-cloud-storage/google/cloud/storage/_helpers.py‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import base64
2121
import datetime
22+
import inspect
2223
import logging
2324
import os
2425
import secrets
@@ -32,18 +33,15 @@
3233
from google.auth import environment_vars
3334
from google.cloud.exceptions import NotFound
3435

35-
from google.cloud.storage._opentelemetry_tracing import (
36-
_is_bucket_metadata_disabled,
37-
)
38-
from google.cloud.storage._opentelemetry_tracing import (
39-
create_trace_span as _base_create_trace_span,
40-
)
36+
from google.cloud.storage import _opentelemetry_tracing
4137
from google.cloud.storage.constants import _DEFAULT_TIMEOUT
4238
from google.cloud.storage.retry import (
4339
DEFAULT_RETRY,
4440
DEFAULT_RETRY_IF_METAGENERATION_SPECIFIED,
4541
)
4642

43+
_base_create_trace_span = None
44+
4745
_logger = logging.getLogger(__name__)
4846

4947
STORAGE_EMULATOR_ENV_VAR = "STORAGE_EMULATOR_HOST" # Despite name, includes scheme.
@@ -172,7 +170,8 @@ def _prepare_base_cm(self):
172170
and client
173171
and hasattr(client, "_bucket_metadata_cache")
174172
and client._bucket_metadata_cache
175-
and not _is_bucket_metadata_disabled()
173+
and _opentelemetry_tracing._is_otel_traces_enabled()
174+
and not _opentelemetry_tracing._is_bucket_metadata_disabled()
176175
):
177176
try:
178177
if name in (
@@ -202,7 +201,12 @@ def _prepare_base_cm(self):
202201
if "client" not in kwargs and client:
203202
kwargs["client"] = client
204203

205-
self._base_cm = _base_create_trace_span(name, attributes=span_attrs, **kwargs)
204+
create_span_fn = (
205+
_base_create_trace_span
206+
if _base_create_trace_span is not None
207+
else _opentelemetry_tracing.create_trace_span
208+
)
209+
self._base_cm = create_span_fn(name, attributes=span_attrs, **kwargs)
206210
return self._base_cm
207211

208212
def _handle_not_found(self):
@@ -212,6 +216,7 @@ def _handle_not_found(self):
212216
and self.client
213217
and hasattr(self.client, "_bucket_metadata_cache")
214218
and self.client._bucket_metadata_cache
219+
and _opentelemetry_tracing._is_otel_traces_enabled()
215220
):
216221
try:
217222
self.client._bucket_metadata_cache.check_and_evict(self.bucket_name)
@@ -235,15 +240,21 @@ def __exit__(self, exc_type, exc_val, exc_tb):
235240

236241
async def __aenter__(self):
237242
self._prepare_base_cm()
238-
return await self._base_cm.__aenter__()
243+
if hasattr(self._base_cm, "__aenter__"):
244+
res = self._base_cm.__aenter__()
245+
return await res if inspect.isawaitable(res) else res
246+
return self._base_cm.__enter__()
239247

240248
async def __aexit__(self, exc_type, exc_val, exc_tb):
241249
if exc_val is not None and isinstance(
242250
exc_val, (NotFound, api_exceptions.NotFound)
243251
):
244252
self._handle_not_found()
245253
if self._base_cm is not None:
246-
return await self._base_cm.__aexit__(exc_type, exc_val, exc_tb)
254+
if hasattr(self._base_cm, "__aexit__"):
255+
res = self._base_cm.__aexit__(exc_type, exc_val, exc_tb)
256+
return await res if inspect.isawaitable(res) else res
257+
return self._base_cm.__exit__(exc_type, exc_val, exc_tb)
247258
return False
248259

249260

‎packages/google-cloud-storage/google/cloud/storage/_opentelemetry_tracing.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ def _is_bucket_metadata_disabled() -> bool:
4343
enable_otel_traces = _parse_bool_env(
4444
ENABLE_OTEL_TRACES_ENV_VAR, _DEFAULT_ENABLE_OTEL_TRACES_VALUE
4545
)
46+
47+
48+
def _is_otel_traces_enabled() -> bool:
49+
if not HAS_OPENTELEMETRY or not enable_otel_traces:
50+
return False
51+
return _parse_bool_env(
52+
ENABLE_OTEL_TRACES_ENV_VAR, _DEFAULT_ENABLE_OTEL_TRACES_VALUE
53+
)
54+
55+
4656
logger = logging.getLogger(__name__)
4757

4858

@@ -96,7 +106,7 @@ def __init__(
96106
self._span = None
97107

98108
def __enter__(self):
99-
if not HAS_OPENTELEMETRY or not enable_otel_traces:
109+
if not _is_otel_traces_enabled():
100110
return None
101111

102112
tracer = trace.get_tracer(__name__)

‎packages/google-cloud-storage/google/cloud/storage/asyncio/_utils.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@ def update_write_handle_if_exists(obj, response):
4545
def inject_traceparent_to_metadata(metadata=None):
4646
"""Inject W3C traceparent (and tracestate) into gRPC metadata tuple or list."""
4747
meta_list = list(metadata) if metadata else []
48-
if (
49-
not _opentelemetry_tracing.HAS_OPENTELEMETRY
50-
or not _opentelemetry_tracing.enable_otel_traces
51-
):
48+
if not _opentelemetry_tracing._is_otel_traces_enabled():
5249
return (
5350
tuple(meta_list)
5451
if isinstance(metadata, tuple) or metadata is None

‎packages/google-cloud-storage/google/cloud/storage/asyncio/async_grpc_client.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __init__(
7676
*,
7777
attempt_direct_path=True,
7878
):
79+
self._is_async_grpc_client = True
7980
self._bucket_metadata_cache = BucketMetadataCache(self)
8081
if isinstance(credentials, auth_credentials.AnonymousCredentials):
8182
if client_options is None or client_options.api_endpoint is None:

‎packages/google-cloud-storage/tests/unit/asyncio/test_zonal_observability.py‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def exporter(monkeypatch):
4747
monkeypatch.delenv("DISABLE_GCS_PYTHON_CLIENT_OTEL_BUCKET_METADATA", raising=False)
4848
importlib.reload(_opentelemetry_tracing)
4949

50+
if hasattr(trace_api, "_TRACER_PROVIDER_SET_ONCE"):
51+
trace_api._TRACER_PROVIDER_SET_ONCE._done = False
52+
trace_api._TRACER_PROVIDER = None
53+
5054
tracer_provider = TracerProvider()
5155
memory_exporter = InMemorySpanExporter()
5256
span_processor = export.SimpleSpanProcessor(memory_exporter)
@@ -55,6 +59,8 @@ def exporter(monkeypatch):
5559

5660
yield memory_exporter
5761
memory_exporter.clear()
62+
monkeypatch.setenv("ENABLE_GCS_PYTHON_CLIENT_OTEL_TRACES", "false")
63+
importlib.reload(_opentelemetry_tracing)
5864

5965

6066
@pytest.fixture
@@ -75,8 +81,7 @@ def mock_client():
7581

7682

7783
def test_inject_traceparent_to_metadata_when_disabled(monkeypatch):
78-
monkeypatch.setenv("ENABLE_GCS_PYTHON_CLIENT_OTEL_TRACES", "false")
79-
importlib.reload(_opentelemetry_tracing)
84+
monkeypatch.setattr(_opentelemetry_tracing, "enable_otel_traces", False)
8085

8186
orig = (("x-goog-request-params", "bucket=foo"),)
8287
result = _utils.inject_traceparent_to_metadata(orig)
@@ -195,7 +200,8 @@ async def test_async_multi_range_downloader_spans(exporter, mock_client):
195200
mock_stream_cls.return_value = mock_stream
196201

197202
mock_mux = mock.AsyncMock()
198-
mock_mux.register.return_value = mock.AsyncMock()
203+
mock_mux.register = mock.Mock(return_value=mock.AsyncMock())
204+
mock_mux.unregister = mock.Mock()
199205
mock_mux_cls.return_value = mock_mux
200206

201207
mock_retry_mgr = mock.AsyncMock()
@@ -250,3 +256,23 @@ async def test_bucket_metadata_cache_async_grpc_fetch(mock_client):
250256
"projects/987654321/buckets/new-zonal-bucket",
251257
"us-west1-b",
252258
)
259+
260+
261+
@pytest.mark.asyncio
262+
async def test_create_trace_span_helper_with_sync_mock_context_manager(mock_client):
263+
"""Verify fallback when _base_create_trace_span returns a synchronous context manager."""
264+
from contextlib import contextmanager
265+
from google.cloud.storage import _helpers
266+
267+
fake_span = object()
268+
269+
@contextmanager
270+
def sync_cm(*args, **kwargs):
271+
yield fake_span
272+
273+
with mock.patch.object(_helpers, "_base_create_trace_span", side_effect=sync_cm):
274+
async with _helpers.create_trace_span_helper(
275+
mock_client, "my-zonal-bucket", "Test.SyncFallback"
276+
) as span:
277+
assert span is fake_span
278+

0 commit comments

Comments
 (0)