Skip to content

Commit 94726e7

Browse files
authored
feat: Add attempt_direct_path argument to create_channel (#583)
* feat: Add attempt_direct_path argument to create_channel * add more test cases * fix docstring * fix docstring * update docstring of attempt_direct_path arg * update docstring of target arg * Add comment for dns_prefix local variable * Set the default value of attempt_direct_path to False * simplify conditional statement * use warnings.warn instead of _LOGGER.debug * update docstring of target arg in _modify_target_for_direct_path * s/direct_path_prefix/direct_path_separator * default->google_auth_default * parametrize target in def test_create_channel_implicit * Add github issue for TODO * filter deprecation warning related to grpcio-gcp * format docstring
1 parent b72929f commit 94726e7

File tree

5 files changed

+288
-64
lines changed

5 files changed

+288
-64
lines changed

google/api_core/grpc_helpers.py

+86-11
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
# limitations under the License.
1414

1515
"""Helpers for :mod:`grpc`."""
16-
from typing import Generic, TypeVar, Iterator
16+
from typing import Generic, Iterator, Optional, TypeVar
1717

1818
import collections
1919
import functools
20-
import logging
2120
import warnings
2221

2322
import grpc
@@ -53,8 +52,6 @@
5352
# The list of gRPC Callable interfaces that return iterators.
5453
_STREAM_WRAP_CLASSES = (grpc.UnaryStreamMultiCallable, grpc.StreamStreamMultiCallable)
5554

56-
_LOGGER = logging.getLogger(__name__)
57-
5855
# denotes the proto response type for grpc calls
5956
P = TypeVar("P")
6057

@@ -271,11 +268,24 @@ def _create_composite_credentials(
271268
# Create a set of grpc.CallCredentials using the metadata plugin.
272269
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
273270

274-
if ssl_credentials is None:
275-
ssl_credentials = grpc.ssl_channel_credentials()
276-
277-
# Combine the ssl credentials and the authorization credentials.
278-
return grpc.composite_channel_credentials(ssl_credentials, google_auth_credentials)
271+
# if `ssl_credentials` is set, use `grpc.composite_channel_credentials` instead of
272+
# `grpc.compute_engine_channel_credentials` as the former supports passing
273+
# `ssl_credentials` via `channel_credentials` which is needed for mTLS.
274+
if ssl_credentials:
275+
# Combine the ssl credentials and the authorization credentials.
276+
# See https://grpc.github.io/grpc/python/grpc.html#grpc.composite_channel_credentials
277+
return grpc.composite_channel_credentials(
278+
ssl_credentials, google_auth_credentials
279+
)
280+
else:
281+
# Use grpc.compute_engine_channel_credentials in order to support Direct Path.
282+
# See https://grpc.github.io/grpc/python/grpc.html#grpc.compute_engine_channel_credentials
283+
# TODO(https://github.com/googleapis/python-api-core/issues/598):
284+
# Although `grpc.compute_engine_channel_credentials` returns channel credentials
285+
# outside of a Google Compute Engine environment (GCE), we should determine if
286+
# there is a way to reliably detect a GCE environment so that
287+
# `grpc.compute_engine_channel_credentials` is not called outside of GCE.
288+
return grpc.compute_engine_channel_credentials(google_auth_credentials)
279289

280290

281291
def create_channel(
@@ -288,6 +298,7 @@ def create_channel(
288298
default_scopes=None,
289299
default_host=None,
290300
compression=None,
301+
attempt_direct_path: Optional[bool] = False,
291302
**kwargs,
292303
):
293304
"""Create a secure channel with credentials.
@@ -311,6 +322,22 @@ def create_channel(
311322
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
312323
compression (grpc.Compression): An optional value indicating the
313324
compression method to be used over the lifetime of the channel.
325+
attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted
326+
when the request is made. Direct Path is only available within a Google
327+
Compute Engine (GCE) environment and provides a proxyless connection
328+
which increases the available throughput, reduces latency, and increases
329+
reliability. Note:
330+
331+
- This argument should only be set in a GCE environment and for Services
332+
that are known to support Direct Path.
333+
- If this argument is set outside of GCE, then this request will fail
334+
unless the back-end service happens to have configured fall-back to DNS.
335+
- If the request causes a `ServiceUnavailable` response, it is recommended
336+
that the client repeat the request with `attempt_direct_path` set to
337+
`False` as the Service may not support Direct Path.
338+
- Using `ssl_credentials` with `attempt_direct_path` set to `True` will
339+
result in `ValueError` as this combination is not yet supported.
340+
314341
kwargs: Additional key-word args passed to
315342
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
316343
Note: `grpc_gcp` is only supported in environments with protobuf < 4.0.0.
@@ -320,8 +347,15 @@ def create_channel(
320347
321348
Raises:
322349
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
350+
ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`.
323351
"""
324352

353+
# If `ssl_credentials` is set and `attempt_direct_path` is set to `True`,
354+
# raise ValueError as this is not yet supported.
355+
# See https://github.com/googleapis/python-api-core/issues/590
356+
if ssl_credentials and attempt_direct_path:
357+
raise ValueError("Using ssl_credentials with Direct Path is not supported")
358+
325359
composite_credentials = _create_composite_credentials(
326360
credentials=credentials,
327361
credentials_file=credentials_file,
@@ -332,17 +366,58 @@ def create_channel(
332366
default_host=default_host,
333367
)
334368

369+
# Note that grpcio-gcp is deprecated
335370
if HAS_GRPC_GCP: # pragma: NO COVER
336371
if compression is not None and compression != grpc.Compression.NoCompression:
337-
_LOGGER.debug(
338-
"Compression argument is being ignored for grpc_gcp.secure_channel creation."
372+
warnings.warn(
373+
"The `compression` argument is ignored for grpc_gcp.secure_channel creation.",
374+
DeprecationWarning,
375+
)
376+
if attempt_direct_path:
377+
warnings.warn(
378+
"""The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation.""",
379+
DeprecationWarning,
339380
)
340381
return grpc_gcp.secure_channel(target, composite_credentials, **kwargs)
382+
383+
if attempt_direct_path:
384+
target = _modify_target_for_direct_path(target)
385+
341386
return grpc.secure_channel(
342387
target, composite_credentials, compression=compression, **kwargs
343388
)
344389

345390

391+
def _modify_target_for_direct_path(target: str) -> str:
392+
"""
393+
Given a target, return a modified version which is compatible with Direct Path.
394+
395+
Args:
396+
target (str): The target service address in the format 'hostname[:port]' or
397+
'dns://hostname[:port]'.
398+
399+
Returns:
400+
target (str): The target service address which is converted into a format compatible with Direct Path.
401+
If the target contains `dns:///` or does not contain `:///`, the target will be converted in
402+
a format compatible with Direct Path; otherwise the original target will be returned as the
403+
original target may already denote Direct Path.
404+
"""
405+
406+
# A DNS prefix may be included with the target to indicate the endpoint is living in the Internet,
407+
# outside of Google Cloud Platform.
408+
dns_prefix = "dns:///"
409+
# Remove "dns:///" if `attempt_direct_path` is set to True as
410+
# the Direct Path prefix `google-c2p:///` will be used instead.
411+
target = target.replace(dns_prefix, "")
412+
413+
direct_path_separator = ":///"
414+
if direct_path_separator not in target:
415+
target_without_port = target.split(":")[0]
416+
# Modify the target to use Direct Path by adding the `google-c2p:///` prefix
417+
target = f"google-c2p{direct_path_separator}{target_without_port}"
418+
return target
419+
420+
346421
_MethodCall = collections.namedtuple(
347422
"_MethodCall", ("request", "timeout", "metadata", "credentials", "compression")
348423
)

google/api_core/grpc_helpers_async.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import asyncio
2222
import functools
2323

24-
from typing import Generic, Iterator, AsyncGenerator, TypeVar
24+
from typing import AsyncGenerator, Generic, Iterator, Optional, TypeVar
2525

2626
import grpc
2727
from grpc import aio
@@ -223,6 +223,7 @@ def create_channel(
223223
default_scopes=None,
224224
default_host=None,
225225
compression=None,
226+
attempt_direct_path: Optional[bool] = False,
226227
**kwargs
227228
):
228229
"""Create an AsyncIO secure channel with credentials.
@@ -246,15 +247,38 @@ def create_channel(
246247
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
247248
compression (grpc.Compression): An optional value indicating the
248249
compression method to be used over the lifetime of the channel.
250+
attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted
251+
when the request is made. Direct Path is only available within a Google
252+
Compute Engine (GCE) environment and provides a proxyless connection
253+
which increases the available throughput, reduces latency, and increases
254+
reliability. Note:
255+
256+
- This argument should only be set in a GCE environment and for Services
257+
that are known to support Direct Path.
258+
- If this argument is set outside of GCE, then this request will fail
259+
unless the back-end service happens to have configured fall-back to DNS.
260+
- If the request causes a `ServiceUnavailable` response, it is recommended
261+
that the client repeat the request with `attempt_direct_path` set to
262+
`False` as the Service may not support Direct Path.
263+
- Using `ssl_credentials` with `attempt_direct_path` set to `True` will
264+
result in `ValueError` as this combination is not yet supported.
265+
249266
kwargs: Additional key-word args passed to :func:`aio.secure_channel`.
250267
251268
Returns:
252269
aio.Channel: The created channel.
253270
254271
Raises:
255272
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
273+
ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`.
256274
"""
257275

276+
# If `ssl_credentials` is set and `attempt_direct_path` is set to `True`,
277+
# raise ValueError as this is not yet supported.
278+
# See https://github.com/googleapis/python-api-core/issues/590
279+
if ssl_credentials and attempt_direct_path:
280+
raise ValueError("Using ssl_credentials with Direct Path is not supported")
281+
258282
composite_credentials = grpc_helpers._create_composite_credentials(
259283
credentials=credentials,
260284
credentials_file=credentials_file,
@@ -265,6 +289,9 @@ def create_channel(
265289
default_host=default_host,
266290
)
267291

292+
if attempt_direct_path:
293+
target = grpc_helpers._modify_target_for_direct_path(target)
294+
268295
return aio.secure_channel(
269296
target, composite_credentials, compression=compression, **kwargs
270297
)

pytest.ini

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ filterwarnings =
1212
# Remove once support for grpcio-gcp is deprecated
1313
# See https://github.com/googleapis/python-api-core/blob/42e8b6e6f426cab749b34906529e8aaf3f133d75/google/api_core/grpc_helpers.py#L39-L45
1414
ignore:.*Support for grpcio-gcp is deprecated:DeprecationWarning
15-
# Remove once https://github.com/googleapis/python-api-common-protos/pull/187/files is merged
15+
ignore: The `compression` argument is ignored for grpc_gcp.secure_channel creation:DeprecationWarning
16+
ignore:The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation:DeprecationWarning
17+
# Remove once the minimum supported version of googleapis-common-protos is 1.62.0
1618
ignore:.*pkg_resources.declare_namespace:DeprecationWarning
1719
ignore:.*pkg_resources is deprecated as an API:DeprecationWarning
18-
# Remove once release PR https://github.com/googleapis/proto-plus-python/pull/391 is merged
19-
ignore:datetime.datetime.utcfromtimestamp\(\) is deprecated:DeprecationWarning:proto.datetime_helpers
20-
# Remove once https://github.com/grpc/grpc/issues/35086 is fixed
20+
# Remove once https://github.com/grpc/grpc/issues/35086 is fixed (and version newer than 1.60.0 is published)
2121
ignore:There is no current event loop:DeprecationWarning

0 commit comments

Comments
 (0)