aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/python
diff options
context:
space:
mode:
Diffstat (limited to 'src/python')
-rw-r--r--src/python/grpcio/grpc/__init__.py36
-rw-r--r--src/python/grpcio/grpc/_channel.py40
-rw-r--r--src/python/grpcio/grpc/_cython/BUILD.bazel47
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/_hooks.pyx.pxi15
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/channel.pyx.pxi27
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/channelz.pyx.pxi8
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/completion_queue.pyx.pxi2
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi3
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pxd.pxi20
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pyx.pxi20
-rw-r--r--src/python/grpcio/grpc/_cython/_cygrpc/server.pyx.pxi11
-rw-r--r--src/python/grpcio/grpc/_cython/cygrpc.pxd1
-rw-r--r--src/python/grpcio/grpc/_cython/cygrpc.pyx1
-rw-r--r--src/python/grpcio/grpc/_server.py182
-rw-r--r--src/python/grpcio/grpc_core_dependencies.py2
-rw-r--r--src/python/grpcio_channelz/grpc_channelz/v1/channelz.py3
-rw-r--r--src/python/grpcio_status/.gitignore3
-rw-r--r--src/python/grpcio_status/MANIFEST.in4
-rw-r--r--src/python/grpcio_status/README.rst9
-rw-r--r--src/python/grpcio_status/grpc_status/BUILD.bazel14
-rw-r--r--src/python/grpcio_status/grpc_status/__init__.py13
-rw-r--r--src/python/grpcio_status/grpc_status/rpc_status.py92
-rw-r--r--src/python/grpcio_status/grpc_version.py17
-rw-r--r--src/python/grpcio_status/setup.py93
-rw-r--r--src/python/grpcio_status/status_commands.py39
-rw-r--r--src/python/grpcio_testing/grpc_testing/_server/_servicer_context.py3
-rw-r--r--src/python/grpcio_tests/commands.py9
-rw-r--r--src/python/grpcio_tests/setup.py13
-rw-r--r--src/python/grpcio_tests/tests/channelz/_channelz_servicer_test.py3
-rw-r--r--src/python/grpcio_tests/tests/status/BUILD.bazel19
-rw-r--r--src/python/grpcio_tests/tests/status/__init__.py13
-rw-r--r--src/python/grpcio_tests/tests/status/_grpc_status_test.py173
-rw-r--r--src/python/grpcio_tests/tests/tests.json3
-rw-r--r--src/python/grpcio_tests/tests/unit/BUILD.bazel8
-rw-r--r--src/python/grpcio_tests/tests/unit/_abort_test.py124
-rw-r--r--src/python/grpcio_tests/tests/unit/_api_test.py1
-rw-r--r--src/python/grpcio_tests/tests/unit/_exit_test.py15
-rw-r--r--src/python/grpcio_tests/tests/unit/_server_shutdown_scenarios.py97
-rw-r--r--src/python/grpcio_tests/tests/unit/_server_shutdown_test.py90
39 files changed, 1136 insertions, 137 deletions
diff --git a/src/python/grpcio/grpc/__init__.py b/src/python/grpcio/grpc/__init__.py
index 6022fc3ef2..daf869b156 100644
--- a/src/python/grpcio/grpc/__init__.py
+++ b/src/python/grpcio/grpc/__init__.py
@@ -266,6 +266,22 @@ class StatusCode(enum.Enum):
UNAUTHENTICATED = (_cygrpc.StatusCode.unauthenticated, 'unauthenticated')
+############################# gRPC Status ################################
+
+
+class Status(six.with_metaclass(abc.ABCMeta)):
+ """Describes the status of an RPC.
+
+ This is an EXPERIMENTAL API.
+
+ Attributes:
+ code: A StatusCode object to be sent to the client.
+ details: An ASCII-encodable string to be sent to the client upon
+ termination of the RPC.
+ trailing_metadata: The trailing :term:`metadata` in the RPC.
+ """
+
+
############################# gRPC Exceptions ################################
@@ -1119,6 +1135,25 @@ class ServicerContext(six.with_metaclass(abc.ABCMeta, RpcContext)):
raise NotImplementedError()
@abc.abstractmethod
+ def abort_with_status(self, status):
+ """Raises an exception to terminate the RPC with a non-OK status.
+
+ The status passed as argument will supercede any existing status code,
+ status message and trailing metadata.
+
+ This is an EXPERIMENTAL API.
+
+ Args:
+ status: A grpc.Status object. The status code in it must not be
+ StatusCode.OK.
+
+ Raises:
+ Exception: An exception is always raised to signal the abortion the
+ RPC to the gRPC runtime.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
def set_code(self, code):
"""Sets the value to be used as status code upon RPC completion.
@@ -1747,6 +1782,7 @@ __all__ = (
'Future',
'ChannelConnectivity',
'StatusCode',
+ 'Status',
'RpcError',
'RpcContext',
'Call',
diff --git a/src/python/grpcio/grpc/_channel.py b/src/python/grpcio/grpc/_channel.py
index 951c6f33ff..8051fb306c 100644
--- a/src/python/grpcio/grpc/_channel.py
+++ b/src/python/grpcio/grpc/_channel.py
@@ -499,6 +499,7 @@ class _UnaryUnaryMultiCallable(grpc.UnaryUnaryMultiCallable):
self._method = method
self._request_serializer = request_serializer
self._response_deserializer = response_deserializer
+ self._context = cygrpc.build_context()
def _prepare(self, request, timeout, metadata, wait_for_ready):
deadline, serialized_request, rendezvous = _start_unary_request(
@@ -528,11 +529,12 @@ class _UnaryUnaryMultiCallable(grpc.UnaryUnaryMultiCallable):
raise rendezvous # pylint: disable-msg=raising-bad-type
else:
call = self._channel.segregated_call(
- 0, self._method, None, deadline, metadata, None
+ cygrpc.PropagationConstants.GRPC_PROPAGATE_DEFAULTS,
+ self._method, None, deadline, metadata, None
if credentials is None else credentials._credentials, ((
operations,
None,
- ),))
+ ),), self._context)
event = call.next_event()
_handle_event(event, state, self._response_deserializer)
return state, call
@@ -570,9 +572,10 @@ class _UnaryUnaryMultiCallable(grpc.UnaryUnaryMultiCallable):
else:
event_handler = _event_handler(state, self._response_deserializer)
call = self._managed_call(
- 0, self._method, None, deadline, metadata, None
+ cygrpc.PropagationConstants.GRPC_PROPAGATE_DEFAULTS,
+ self._method, None, deadline, metadata, None
if credentials is None else credentials._credentials,
- (operations,), event_handler)
+ (operations,), event_handler, self._context)
return _Rendezvous(state, call, self._response_deserializer,
deadline)
@@ -587,6 +590,7 @@ class _UnaryStreamMultiCallable(grpc.UnaryStreamMultiCallable):
self._method = method
self._request_serializer = request_serializer
self._response_deserializer = response_deserializer
+ self._context = cygrpc.build_context()
def __call__(self,
request,
@@ -615,9 +619,10 @@ class _UnaryStreamMultiCallable(grpc.UnaryStreamMultiCallable):
)
event_handler = _event_handler(state, self._response_deserializer)
call = self._managed_call(
- 0, self._method, None, deadline, metadata, None
+ cygrpc.PropagationConstants.GRPC_PROPAGATE_DEFAULTS,
+ self._method, None, deadline, metadata, None
if credentials is None else credentials._credentials,
- operationses, event_handler)
+ operationses, event_handler, self._context)
return _Rendezvous(state, call, self._response_deserializer,
deadline)
@@ -632,6 +637,7 @@ class _StreamUnaryMultiCallable(grpc.StreamUnaryMultiCallable):
self._method = method
self._request_serializer = request_serializer
self._response_deserializer = response_deserializer
+ self._context = cygrpc.build_context()
def _blocking(self, request_iterator, timeout, metadata, credentials,
wait_for_ready):
@@ -640,10 +646,11 @@ class _StreamUnaryMultiCallable(grpc.StreamUnaryMultiCallable):
initial_metadata_flags = _InitialMetadataFlags().with_wait_for_ready(
wait_for_ready)
call = self._channel.segregated_call(
- 0, self._method, None, deadline, metadata, None
+ cygrpc.PropagationConstants.GRPC_PROPAGATE_DEFAULTS, self._method,
+ None, deadline, metadata, None
if credentials is None else credentials._credentials,
_stream_unary_invocation_operationses_and_tags(
- metadata, initial_metadata_flags))
+ metadata, initial_metadata_flags), self._context)
_consume_request_iterator(request_iterator, state, call,
self._request_serializer, None)
while True:
@@ -687,10 +694,11 @@ class _StreamUnaryMultiCallable(grpc.StreamUnaryMultiCallable):
initial_metadata_flags = _InitialMetadataFlags().with_wait_for_ready(
wait_for_ready)
call = self._managed_call(
- 0, self._method, None, deadline, metadata, None
+ cygrpc.PropagationConstants.GRPC_PROPAGATE_DEFAULTS, self._method,
+ None, deadline, metadata, None
if credentials is None else credentials._credentials,
_stream_unary_invocation_operationses(
- metadata, initial_metadata_flags), event_handler)
+ metadata, initial_metadata_flags), event_handler, self._context)
_consume_request_iterator(request_iterator, state, call,
self._request_serializer, event_handler)
return _Rendezvous(state, call, self._response_deserializer, deadline)
@@ -706,6 +714,7 @@ class _StreamStreamMultiCallable(grpc.StreamStreamMultiCallable):
self._method = method
self._request_serializer = request_serializer
self._response_deserializer = response_deserializer
+ self._context = cygrpc.build_context()
def __call__(self,
request_iterator,
@@ -727,9 +736,10 @@ class _StreamStreamMultiCallable(grpc.StreamStreamMultiCallable):
)
event_handler = _event_handler(state, self._response_deserializer)
call = self._managed_call(
- 0, self._method, None, deadline, metadata, None
+ cygrpc.PropagationConstants.GRPC_PROPAGATE_DEFAULTS, self._method,
+ None, deadline, metadata, None
if credentials is None else credentials._credentials, operationses,
- event_handler)
+ event_handler, self._context)
_consume_request_iterator(request_iterator, state, call,
self._request_serializer, event_handler)
return _Rendezvous(state, call, self._response_deserializer, deadline)
@@ -789,7 +799,7 @@ def _channel_managed_call_management(state):
# pylint: disable=too-many-arguments
def create(flags, method, host, deadline, metadata, credentials,
- operationses, event_handler):
+ operationses, event_handler, context):
"""Creates a cygrpc.IntegratedCall.
Args:
@@ -804,7 +814,7 @@ def _channel_managed_call_management(state):
started on the call.
event_handler: A behavior to call to handle the events resultant from
the operations on the call.
-
+ context: Context object for distributed tracing.
Returns:
A cygrpc.IntegratedCall with which to conduct an RPC.
"""
@@ -815,7 +825,7 @@ def _channel_managed_call_management(state):
with state.lock:
call = state.channel.integrated_call(flags, method, host, deadline,
metadata, credentials,
- operationses_and_tags)
+ operationses_and_tags, context)
if state.managed_calls == 0:
state.managed_calls = 1
_run_channel_spin_thread(state)
diff --git a/src/python/grpcio/grpc/_cython/BUILD.bazel b/src/python/grpcio/grpc/_cython/BUILD.bazel
index e318298d0a..42db7b8721 100644
--- a/src/python/grpcio/grpc/_cython/BUILD.bazel
+++ b/src/python/grpcio/grpc/_cython/BUILD.bazel
@@ -6,46 +6,47 @@ pyx_library(
name = "cygrpc",
srcs = [
"__init__.py",
- "cygrpc.pxd",
- "cygrpc.pyx",
+ "_cygrpc/_hooks.pxd.pxi",
"_cygrpc/_hooks.pyx.pxi",
- "_cygrpc/grpc_string.pyx.pxi",
+ "_cygrpc/arguments.pxd.pxi",
"_cygrpc/arguments.pyx.pxi",
+ "_cygrpc/call.pxd.pxi",
"_cygrpc/call.pyx.pxi",
- "_cygrpc/channelz.pyx.pxi",
+ "_cygrpc/channel.pxd.pxi",
"_cygrpc/channel.pyx.pxi",
- "_cygrpc/credentials.pyx.pxi",
+ "_cygrpc/channelz.pyx.pxi",
+ "_cygrpc/completion_queue.pxd.pxi",
"_cygrpc/completion_queue.pyx.pxi",
- "_cygrpc/event.pyx.pxi",
- "_cygrpc/fork_posix.pyx.pxi",
- "_cygrpc/metadata.pyx.pxi",
- "_cygrpc/operation.pyx.pxi",
- "_cygrpc/records.pyx.pxi",
- "_cygrpc/security.pyx.pxi",
- "_cygrpc/server.pyx.pxi",
- "_cygrpc/tag.pyx.pxi",
- "_cygrpc/time.pyx.pxi",
- "_cygrpc/grpc_gevent.pyx.pxi",
- "_cygrpc/grpc.pxi",
- "_cygrpc/_hooks.pxd.pxi",
- "_cygrpc/arguments.pxd.pxi",
- "_cygrpc/call.pxd.pxi",
- "_cygrpc/channel.pxd.pxi",
"_cygrpc/credentials.pxd.pxi",
- "_cygrpc/completion_queue.pxd.pxi",
+ "_cygrpc/credentials.pyx.pxi",
"_cygrpc/event.pxd.pxi",
+ "_cygrpc/event.pyx.pxi",
"_cygrpc/fork_posix.pxd.pxi",
+ "_cygrpc/fork_posix.pyx.pxi",
+ "_cygrpc/grpc.pxi",
+ "_cygrpc/grpc_gevent.pxd.pxi",
+ "_cygrpc/grpc_gevent.pyx.pxi",
+ "_cygrpc/grpc_string.pyx.pxi",
"_cygrpc/metadata.pxd.pxi",
+ "_cygrpc/metadata.pyx.pxi",
"_cygrpc/operation.pxd.pxi",
+ "_cygrpc/operation.pyx.pxi",
+ "_cygrpc/propagation_bits.pxd.pxi",
+ "_cygrpc/propagation_bits.pyx.pxi",
"_cygrpc/records.pxd.pxi",
+ "_cygrpc/records.pyx.pxi",
"_cygrpc/security.pxd.pxi",
+ "_cygrpc/security.pyx.pxi",
"_cygrpc/server.pxd.pxi",
+ "_cygrpc/server.pyx.pxi",
"_cygrpc/tag.pxd.pxi",
+ "_cygrpc/tag.pyx.pxi",
"_cygrpc/time.pxd.pxi",
- "_cygrpc/grpc_gevent.pxd.pxi",
+ "_cygrpc/time.pyx.pxi",
+ "cygrpc.pxd",
+ "cygrpc.pyx",
],
deps = [
"//:grpc",
],
)
-
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/_hooks.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/_hooks.pyx.pxi
index 38cf629dc2..cd4a51a635 100644
--- a/src/python/grpcio/grpc/_cython/_cygrpc/_hooks.pyx.pxi
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/_hooks.pyx.pxi
@@ -15,3 +15,18 @@
cdef object _custom_op_on_c_call(int op, grpc_call *call):
raise NotImplementedError("No custom hooks are implemented")
+
+def install_census_context_from_call(Call call):
+ pass
+
+def uninstall_context():
+ pass
+
+def build_context():
+ pass
+
+cdef class CensusContext:
+ pass
+
+def set_census_context_on_call(_CallState call_state, CensusContext census_ctx):
+ pass
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/channel.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/channel.pyx.pxi
index a81ff4d823..135d224095 100644
--- a/src/python/grpcio/grpc/_cython/_cygrpc/channel.pyx.pxi
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/channel.pyx.pxi
@@ -159,7 +159,8 @@ cdef void _call(
_ChannelState channel_state, _CallState call_state,
grpc_completion_queue *c_completion_queue, on_success, int flags, method,
host, object deadline, CallCredentials credentials,
- object operationses_and_user_tags, object metadata) except *:
+ object operationses_and_user_tags, object metadata,
+ object context) except *:
"""Invokes an RPC.
Args:
@@ -185,6 +186,7 @@ cdef void _call(
which is an object to be used as a tag. A SendInitialMetadataOperation
must be present in the first element of this value.
metadata: The metadata for this call.
+ context: Context object for distributed tracing.
"""
cdef grpc_slice method_slice
cdef grpc_slice host_slice
@@ -208,6 +210,8 @@ cdef void _call(
grpc_slice_unref(method_slice)
if host_slice_ptr:
grpc_slice_unref(host_slice)
+ if context is not None:
+ set_census_context_on_call(call_state, context)
if credentials is not None:
c_call_credentials = credentials.c()
c_call_error = grpc_call_set_credentials(
@@ -257,7 +261,8 @@ cdef class IntegratedCall:
cdef IntegratedCall _integrated_call(
_ChannelState state, int flags, method, host, object deadline,
- object metadata, CallCredentials credentials, operationses_and_user_tags):
+ object metadata, CallCredentials credentials, operationses_and_user_tags,
+ object context):
call_state = _CallState()
def on_success(started_tags):
@@ -266,7 +271,7 @@ cdef IntegratedCall _integrated_call(
_call(
state, call_state, state.c_call_completion_queue, on_success, flags,
- method, host, deadline, credentials, operationses_and_user_tags, metadata)
+ method, host, deadline, credentials, operationses_and_user_tags, metadata, context)
return IntegratedCall(state, call_state)
@@ -308,7 +313,8 @@ cdef class SegregatedCall:
cdef SegregatedCall _segregated_call(
_ChannelState state, int flags, method, host, object deadline,
- object metadata, CallCredentials credentials, operationses_and_user_tags):
+ object metadata, CallCredentials credentials, operationses_and_user_tags,
+ object context):
cdef _CallState call_state = _CallState()
cdef SegregatedCall segregated_call
cdef grpc_completion_queue *c_completion_queue
@@ -325,7 +331,8 @@ cdef SegregatedCall _segregated_call(
try:
_call(
state, call_state, c_completion_queue, on_success, flags, method, host,
- deadline, credentials, operationses_and_user_tags, metadata)
+ deadline, credentials, operationses_and_user_tags, metadata,
+ context)
except:
_destroy_c_completion_queue(c_completion_queue)
raise
@@ -443,10 +450,11 @@ cdef class Channel:
def integrated_call(
self, int flags, method, host, object deadline, object metadata,
- CallCredentials credentials, operationses_and_tags):
+ CallCredentials credentials, operationses_and_tags,
+ object context = None):
return _integrated_call(
self._state, flags, method, host, deadline, metadata, credentials,
- operationses_and_tags)
+ operationses_and_tags, context)
def next_call_event(self):
def on_success(tag):
@@ -461,10 +469,11 @@ cdef class Channel:
def segregated_call(
self, int flags, method, host, object deadline, object metadata,
- CallCredentials credentials, operationses_and_tags):
+ CallCredentials credentials, operationses_and_tags,
+ object context = None):
return _segregated_call(
self._state, flags, method, host, deadline, metadata, credentials,
- operationses_and_tags)
+ operationses_and_tags, context)
def check_connectivity_state(self, bint try_to_connect):
with self._state.condition:
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/channelz.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/channelz.pyx.pxi
index 113f7976dd..36c8cd121c 100644
--- a/src/python/grpcio/grpc/_cython/_cygrpc/channelz.pyx.pxi
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/channelz.pyx.pxi
@@ -36,15 +36,17 @@ def channelz_get_server(server_id):
' server_id==%s is valid' % server_id)
return c_returned_str
-def channelz_get_server_sockets(server_id, start_socket_id):
+def channelz_get_server_sockets(server_id, start_socket_id, max_results):
cdef char *c_returned_str = grpc_channelz_get_server_sockets(
server_id,
start_socket_id,
+ max_results,
)
if c_returned_str == NULL:
raise ValueError('Failed to get server sockets, please ensure your' \
- ' server_id==%s and start_socket_id==%s is valid' %
- (server_id, start_socket_id))
+ ' server_id==%s and start_socket_id==%s and' \
+ ' max_results==%s is valid' %
+ (server_id, start_socket_id, max_results))
return c_returned_str
def channelz_get_channel(channel_id):
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/completion_queue.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/completion_queue.pyx.pxi
index 141116df5d..3c33b46dbb 100644
--- a/src/python/grpcio/grpc/_cython/_cygrpc/completion_queue.pyx.pxi
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/completion_queue.pyx.pxi
@@ -49,7 +49,7 @@ cdef grpc_event _next(grpc_completion_queue *c_completion_queue, deadline):
cdef _interpret_event(grpc_event c_event):
cdef _Tag tag
if c_event.type == GRPC_QUEUE_TIMEOUT:
- # NOTE(nathaniel): For now we coopt ConnectivityEvent here.
+ # TODO(ericgribkoff) Do not coopt ConnectivityEvent here.
return None, ConnectivityEvent(GRPC_QUEUE_TIMEOUT, False, None)
elif c_event.type == GRPC_QUEUE_SHUTDOWN:
# NOTE(nathaniel): For now we coopt ConnectivityEvent here.
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi
index 5bbc10af25..fc7a9ba439 100644
--- a/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi
@@ -389,7 +389,8 @@ cdef extern from "grpc/grpc.h":
char* grpc_channelz_get_servers(intptr_t start_server_id)
char* grpc_channelz_get_server(intptr_t server_id)
char* grpc_channelz_get_server_sockets(intptr_t server_id,
- intptr_t start_socket_id)
+ intptr_t start_socket_id,
+ intptr_t max_results)
char* grpc_channelz_get_channel(intptr_t channel_id)
char* grpc_channelz_get_subchannel(intptr_t subchannel_id)
char* grpc_channelz_get_socket(intptr_t socket_id)
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pxd.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pxd.pxi
new file mode 100644
index 0000000000..cd6e94c816
--- /dev/null
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pxd.pxi
@@ -0,0 +1,20 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+cdef extern from "grpc/impl/codegen/propagation_bits.h":
+ cdef int _GRPC_PROPAGATE_DEADLINE "GRPC_PROPAGATE_DEADLINE"
+ cdef int _GRPC_PROPAGATE_CENSUS_STATS_CONTEXT "GRPC_PROPAGATE_CENSUS_STATS_CONTEXT"
+ cdef int _GRPC_PROPAGATE_CENSUS_TRACING_CONTEXT "GRPC_PROPAGATE_CENSUS_TRACING_CONTEXT"
+ cdef int _GRPC_PROPAGATE_CANCELLATION "GRPC_PROPAGATE_CANCELLATION"
+ cdef int _GRPC_PROPAGATE_DEFAULTS "GRPC_PROPAGATE_DEFAULTS"
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pyx.pxi
new file mode 100644
index 0000000000..2dcc76a2db
--- /dev/null
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/propagation_bits.pyx.pxi
@@ -0,0 +1,20 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class PropagationConstants:
+ GRPC_PROPAGATE_DEADLINE = _GRPC_PROPAGATE_DEADLINE
+ GRPC_PROPAGATE_CENSUS_STATS_CONTEXT = _GRPC_PROPAGATE_CENSUS_STATS_CONTEXT
+ GRPC_PROPAGATE_CENSUS_TRACING_CONTEXT = _GRPC_PROPAGATE_CENSUS_TRACING_CONTEXT
+ GRPC_PROPAGATE_CANCELLATION = _GRPC_PROPAGATE_CANCELLATION
+ GRPC_PROPAGATE_DEFAULTS = _GRPC_PROPAGATE_DEFAULTS
diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/server.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/server.pyx.pxi
index ce701724fd..e89e02b171 100644
--- a/src/python/grpcio/grpc/_cython/_cygrpc/server.pyx.pxi
+++ b/src/python/grpcio/grpc/_cython/_cygrpc/server.pyx.pxi
@@ -128,7 +128,10 @@ cdef class Server:
with nogil:
grpc_server_cancel_all_calls(self.c_server)
- def __dealloc__(self):
+ # TODO(https://github.com/grpc/grpc/issues/17515) Determine what, if any,
+ # portion of this is safe to call from __dealloc__, and potentially remove
+ # backup_shutdown_queue.
+ def destroy(self):
if self.c_server != NULL:
if not self.is_started:
pass
@@ -146,4 +149,8 @@ cdef class Server:
while not self.is_shutdown:
time.sleep(0)
grpc_server_destroy(self.c_server)
- grpc_shutdown()
+ self.c_server = NULL
+
+ def __dealloc(self):
+ if self.c_server == NULL:
+ grpc_shutdown()
diff --git a/src/python/grpcio/grpc/_cython/cygrpc.pxd b/src/python/grpcio/grpc/_cython/cygrpc.pxd
index 8258b857bc..64cae6b34d 100644
--- a/src/python/grpcio/grpc/_cython/cygrpc.pxd
+++ b/src/python/grpcio/grpc/_cython/cygrpc.pxd
@@ -29,6 +29,7 @@ include "_cygrpc/server.pxd.pxi"
include "_cygrpc/tag.pxd.pxi"
include "_cygrpc/time.pxd.pxi"
include "_cygrpc/_hooks.pxd.pxi"
+include "_cygrpc/propagation_bits.pxd.pxi"
include "_cygrpc/grpc_gevent.pxd.pxi"
diff --git a/src/python/grpcio/grpc/_cython/cygrpc.pyx b/src/python/grpcio/grpc/_cython/cygrpc.pyx
index 9ab919375c..ce98fa3a8e 100644
--- a/src/python/grpcio/grpc/_cython/cygrpc.pyx
+++ b/src/python/grpcio/grpc/_cython/cygrpc.pyx
@@ -36,6 +36,7 @@ include "_cygrpc/tag.pyx.pxi"
include "_cygrpc/time.pyx.pxi"
include "_cygrpc/_hooks.pyx.pxi"
include "_cygrpc/channelz.pyx.pxi"
+include "_cygrpc/propagation_bits.pyx.pxi"
include "_cygrpc/grpc_gevent.pyx.pxi"
diff --git a/src/python/grpcio/grpc/_server.py b/src/python/grpcio/grpc/_server.py
index 7276a7fd90..eb750ef1a8 100644
--- a/src/python/grpcio/grpc/_server.py
+++ b/src/python/grpcio/grpc/_server.py
@@ -48,7 +48,7 @@ _CANCELLED = 'cancelled'
_EMPTY_FLAGS = 0
-_UNEXPECTED_EXIT_SERVER_GRACE = 1.0
+_DEALLOCATED_SERVER_CHECK_PERIOD_S = 1.0
def _serialized_request(request_event):
@@ -291,6 +291,10 @@ class _Context(grpc.ServicerContext):
self._state.abortion = Exception()
raise self._state.abortion
+ def abort_with_status(self, status):
+ self._state.trailing_metadata = status.trailing_metadata
+ self.abort(status.code, status.details)
+
def set_code(self, code):
with self._state.condition:
self._state.code = code
@@ -480,43 +484,51 @@ def _status(rpc_event, state, serialized_response):
def _unary_response_in_pool(rpc_event, state, behavior, argument_thunk,
request_deserializer, response_serializer):
- argument = argument_thunk()
- if argument is not None:
- response, proceed = _call_behavior(rpc_event, state, behavior, argument,
- request_deserializer)
- if proceed:
- serialized_response = _serialize_response(
- rpc_event, state, response, response_serializer)
- if serialized_response is not None:
- _status(rpc_event, state, serialized_response)
+ cygrpc.install_census_context_from_call(rpc_event.call)
+ try:
+ argument = argument_thunk()
+ if argument is not None:
+ response, proceed = _call_behavior(rpc_event, state, behavior,
+ argument, request_deserializer)
+ if proceed:
+ serialized_response = _serialize_response(
+ rpc_event, state, response, response_serializer)
+ if serialized_response is not None:
+ _status(rpc_event, state, serialized_response)
+ finally:
+ cygrpc.uninstall_context()
def _stream_response_in_pool(rpc_event, state, behavior, argument_thunk,
request_deserializer, response_serializer):
- argument = argument_thunk()
- if argument is not None:
- response_iterator, proceed = _call_behavior(
- rpc_event, state, behavior, argument, request_deserializer)
- if proceed:
- while True:
- response, proceed = _take_response_from_response_iterator(
- rpc_event, state, response_iterator)
- if proceed:
- if response is None:
- _status(rpc_event, state, None)
- break
- else:
- serialized_response = _serialize_response(
- rpc_event, state, response, response_serializer)
- if serialized_response is not None:
- proceed = _send_response(rpc_event, state,
- serialized_response)
- if not proceed:
- break
- else:
+ cygrpc.install_census_context_from_call(rpc_event.call)
+ try:
+ argument = argument_thunk()
+ if argument is not None:
+ response_iterator, proceed = _call_behavior(
+ rpc_event, state, behavior, argument, request_deserializer)
+ if proceed:
+ while True:
+ response, proceed = _take_response_from_response_iterator(
+ rpc_event, state, response_iterator)
+ if proceed:
+ if response is None:
+ _status(rpc_event, state, None)
break
- else:
- break
+ else:
+ serialized_response = _serialize_response(
+ rpc_event, state, response, response_serializer)
+ if serialized_response is not None:
+ proceed = _send_response(
+ rpc_event, state, serialized_response)
+ if not proceed:
+ break
+ else:
+ break
+ else:
+ break
+ finally:
+ cygrpc.uninstall_context()
def _handle_unary_unary(rpc_event, state, method_handler, thread_pool):
@@ -664,6 +676,9 @@ class _ServerState(object):
self.rpc_states = set()
self.due = set()
+ # A "volatile" flag to interrupt the daemon serving thread
+ self.server_deallocated = False
+
def _add_generic_handlers(state, generic_handlers):
with state.lock:
@@ -690,6 +705,7 @@ def _request_call(state):
# TODO(https://github.com/grpc/grpc/issues/6597): delete this function.
def _stop_serving(state):
if not state.rpc_states and not state.due:
+ state.server.destroy()
for shutdown_event in state.shutdown_events:
shutdown_event.set()
state.stage = _ServerStage.STOPPED
@@ -703,49 +719,69 @@ def _on_call_completed(state):
state.active_rpc_count -= 1
-def _serve(state):
- while True:
- event = state.completion_queue.poll()
- if event.tag is _SHUTDOWN_TAG:
+def _process_event_and_continue(state, event):
+ should_continue = True
+ if event.tag is _SHUTDOWN_TAG:
+ with state.lock:
+ state.due.remove(_SHUTDOWN_TAG)
+ if _stop_serving(state):
+ should_continue = False
+ elif event.tag is _REQUEST_CALL_TAG:
+ with state.lock:
+ state.due.remove(_REQUEST_CALL_TAG)
+ concurrency_exceeded = (
+ state.maximum_concurrent_rpcs is not None and
+ state.active_rpc_count >= state.maximum_concurrent_rpcs)
+ rpc_state, rpc_future = _handle_call(
+ event, state.generic_handlers, state.interceptor_pipeline,
+ state.thread_pool, concurrency_exceeded)
+ if rpc_state is not None:
+ state.rpc_states.add(rpc_state)
+ if rpc_future is not None:
+ state.active_rpc_count += 1
+ rpc_future.add_done_callback(
+ lambda unused_future: _on_call_completed(state))
+ if state.stage is _ServerStage.STARTED:
+ _request_call(state)
+ elif _stop_serving(state):
+ should_continue = False
+ else:
+ rpc_state, callbacks = event.tag(event)
+ for callback in callbacks:
+ callable_util.call_logging_exceptions(callback,
+ 'Exception calling callback!')
+ if rpc_state is not None:
with state.lock:
- state.due.remove(_SHUTDOWN_TAG)
+ state.rpc_states.remove(rpc_state)
if _stop_serving(state):
- return
- elif event.tag is _REQUEST_CALL_TAG:
- with state.lock:
- state.due.remove(_REQUEST_CALL_TAG)
- concurrency_exceeded = (
- state.maximum_concurrent_rpcs is not None and
- state.active_rpc_count >= state.maximum_concurrent_rpcs)
- rpc_state, rpc_future = _handle_call(
- event, state.generic_handlers, state.interceptor_pipeline,
- state.thread_pool, concurrency_exceeded)
- if rpc_state is not None:
- state.rpc_states.add(rpc_state)
- if rpc_future is not None:
- state.active_rpc_count += 1
- rpc_future.add_done_callback(
- lambda unused_future: _on_call_completed(state))
- if state.stage is _ServerStage.STARTED:
- _request_call(state)
- elif _stop_serving(state):
- return
- else:
- rpc_state, callbacks = event.tag(event)
- for callback in callbacks:
- callable_util.call_logging_exceptions(
- callback, 'Exception calling callback!')
- if rpc_state is not None:
- with state.lock:
- state.rpc_states.remove(rpc_state)
- if _stop_serving(state):
- return
+ should_continue = False
+ return should_continue
+
+
+def _serve(state):
+ while True:
+ timeout = time.time() + _DEALLOCATED_SERVER_CHECK_PERIOD_S
+ event = state.completion_queue.poll(timeout)
+ if state.server_deallocated:
+ _begin_shutdown_once(state)
+ if event.completion_type != cygrpc.CompletionType.queue_timeout:
+ if not _process_event_and_continue(state, event):
+ return
# We want to force the deletion of the previous event
# ~before~ we poll again; if the event has a reference
# to a shutdown Call object, this can induce spinlock.
event = None
+def _begin_shutdown_once(state):
+ with state.lock:
+ if state.stage is _ServerStage.STARTED:
+ state.server.shutdown(state.completion_queue, _SHUTDOWN_TAG)
+ state.stage = _ServerStage.GRACE
+ state.shutdown_events = []
+ state.due.add(_SHUTDOWN_TAG)
+
+
def _stop(state, grace):
with state.lock:
if state.stage is _ServerStage.STOPPED:
@@ -753,11 +789,7 @@ def _stop(state, grace):
shutdown_event.set()
return shutdown_event
else:
- if state.stage is _ServerStage.STARTED:
- state.server.shutdown(state.completion_queue, _SHUTDOWN_TAG)
- state.stage = _ServerStage.GRACE
- state.shutdown_events = []
- state.due.add(_SHUTDOWN_TAG)
+ _begin_shutdown_once(state)
shutdown_event = threading.Event()
state.shutdown_events.append(shutdown_event)
if grace is None:
@@ -828,7 +860,9 @@ class _Server(grpc.Server):
return _stop(self._state, grace)
def __del__(self):
- _stop(self._state, None)
+ # We can not grab a lock in __del__(), so set a flag to signal the
+ # serving daemon thread (if it exists) to initiate shutdown.
+ self._state.server_deallocated = True
def create_server(thread_pool, generic_rpc_handlers, interceptors, options,
diff --git a/src/python/grpcio/grpc_core_dependencies.py b/src/python/grpcio/grpc_core_dependencies.py
index ce65c594fe..c6ca970bee 100644
--- a/src/python/grpcio/grpc_core_dependencies.py
+++ b/src/python/grpcio/grpc_core_dependencies.py
@@ -322,7 +322,6 @@ CORE_SOURCE_FILES = [
'src/core/ext/filters/client_channel/http_connect_handshaker.cc',
'src/core/ext/filters/client_channel/http_proxy.cc',
'src/core/ext/filters/client_channel/lb_policy.cc',
- 'src/core/ext/filters/client_channel/lb_policy_factory.cc',
'src/core/ext/filters/client_channel/lb_policy_registry.cc',
'src/core/ext/filters/client_channel/parse_address.cc',
'src/core/ext/filters/client_channel/proxy_mapper.cc',
@@ -331,6 +330,7 @@ CORE_SOURCE_FILES = [
'src/core/ext/filters/client_channel/resolver_registry.cc',
'src/core/ext/filters/client_channel/resolver_result_parsing.cc',
'src/core/ext/filters/client_channel/retry_throttle.cc',
+ 'src/core/ext/filters/client_channel/server_address.cc',
'src/core/ext/filters/client_channel/subchannel.cc',
'src/core/ext/filters/client_channel/subchannel_index.cc',
'src/core/ext/filters/deadline/deadline_filter.cc',
diff --git a/src/python/grpcio_channelz/grpc_channelz/v1/channelz.py b/src/python/grpcio_channelz/grpc_channelz/v1/channelz.py
index 573b9d0d5a..00eca311dc 100644
--- a/src/python/grpcio_channelz/grpc_channelz/v1/channelz.py
+++ b/src/python/grpcio_channelz/grpc_channelz/v1/channelz.py
@@ -66,7 +66,8 @@ class ChannelzServicer(_channelz_pb2_grpc.ChannelzServicer):
try:
return json_format.Parse(
cygrpc.channelz_get_server_sockets(request.server_id,
- request.start_socket_id),
+ request.start_socket_id,
+ request.max_results),
_channelz_pb2.GetServerSocketsResponse(),
)
except ValueError as e:
diff --git a/src/python/grpcio_status/.gitignore b/src/python/grpcio_status/.gitignore
new file mode 100644
index 0000000000..19d1523efd
--- /dev/null
+++ b/src/python/grpcio_status/.gitignore
@@ -0,0 +1,3 @@
+build/
+grpcio_status.egg-info/
+dist/
diff --git a/src/python/grpcio_status/MANIFEST.in b/src/python/grpcio_status/MANIFEST.in
new file mode 100644
index 0000000000..09b8ea721e
--- /dev/null
+++ b/src/python/grpcio_status/MANIFEST.in
@@ -0,0 +1,4 @@
+include grpc_version.py
+recursive-include grpc_status *.py
+global-exclude *.pyc
+include LICENSE
diff --git a/src/python/grpcio_status/README.rst b/src/python/grpcio_status/README.rst
new file mode 100644
index 0000000000..dc2f7b1dab
--- /dev/null
+++ b/src/python/grpcio_status/README.rst
@@ -0,0 +1,9 @@
+gRPC Python Status Proto
+===========================
+
+Reference package for GRPC Python status proto mapping.
+
+Dependencies
+------------
+
+Depends on the `grpcio` package, available from PyPI via `pip install grpcio`.
diff --git a/src/python/grpcio_status/grpc_status/BUILD.bazel b/src/python/grpcio_status/grpc_status/BUILD.bazel
new file mode 100644
index 0000000000..223a077c3f
--- /dev/null
+++ b/src/python/grpcio_status/grpc_status/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@grpc_python_dependencies//:requirements.bzl", "requirement")
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "grpc_status",
+ srcs = ["rpc_status.py",],
+ deps = [
+ "//src/python/grpcio/grpc:grpcio",
+ requirement('protobuf'),
+ requirement('googleapis-common-protos'),
+ ],
+ imports=["../",],
+)
diff --git a/src/python/grpcio_status/grpc_status/__init__.py b/src/python/grpcio_status/grpc_status/__init__.py
new file mode 100644
index 0000000000..38fdfc9c5c
--- /dev/null
+++ b/src/python/grpcio_status/grpc_status/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/src/python/grpcio_status/grpc_status/rpc_status.py b/src/python/grpcio_status/grpc_status/rpc_status.py
new file mode 100644
index 0000000000..e23a20968e
--- /dev/null
+++ b/src/python/grpcio_status/grpc_status/rpc_status.py
@@ -0,0 +1,92 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Reference implementation for status mapping in gRPC Python."""
+
+import collections
+
+import grpc
+
+# TODO(https://github.com/bazelbuild/bazel/issues/6844)
+# Due to Bazel issue, the namespace packages won't resolve correctly.
+# Adding this unused-import as a workaround to avoid module-not-found error
+# under Bazel builds.
+import google.protobuf # pylint: disable=unused-import
+from google.rpc import status_pb2
+
+_CODE_TO_GRPC_CODE_MAPPING = dict([(x.value[0], x) for x in grpc.StatusCode])
+
+_GRPC_DETAILS_METADATA_KEY = 'grpc-status-details-bin'
+
+
+class _Status(
+ collections.namedtuple(
+ '_Status', ('code', 'details', 'trailing_metadata')), grpc.Status):
+ pass
+
+
+def _code_to_grpc_status_code(code):
+ try:
+ return _CODE_TO_GRPC_CODE_MAPPING[code]
+ except KeyError:
+ raise ValueError('Invalid status code %s' % code)
+
+
+def from_call(call):
+ """Returns a google.rpc.status.Status message corresponding to a given grpc.Call.
+
+ This is an EXPERIMENTAL API.
+
+ Args:
+ call: A grpc.Call instance.
+
+ Returns:
+ A google.rpc.status.Status message representing the status of the RPC.
+
+ Raises:
+ ValueError: If the gRPC call's code or details are inconsistent with the
+ status code and message inside of the google.rpc.status.Status.
+ """
+ for key, value in call.trailing_metadata():
+ if key == _GRPC_DETAILS_METADATA_KEY:
+ rich_status = status_pb2.Status.FromString(value)
+ if call.code().value[0] != rich_status.code:
+ raise ValueError(
+ 'Code in Status proto (%s) doesn\'t match status code (%s)'
+ % (_code_to_grpc_status_code(rich_status.code),
+ call.code()))
+ if call.details() != rich_status.message:
+ raise ValueError(
+ 'Message in Status proto (%s) doesn\'t match status details (%s)'
+ % (rich_status.message, call.details()))
+ return rich_status
+ return None
+
+
+def to_status(status):
+ """Convert a google.rpc.status.Status message to grpc.Status.
+
+ This is an EXPERIMENTAL API.
+
+ Args:
+ status: a google.rpc.status.Status message representing the non-OK status
+ to terminate the RPC with and communicate it to the client.
+
+ Returns:
+ A grpc.Status instance representing the input google.rpc.status.Status message.
+ """
+ return _Status(
+ code=_code_to_grpc_status_code(status.code),
+ details=status.message,
+ trailing_metadata=((_GRPC_DETAILS_METADATA_KEY,
+ status.SerializeToString()),))
diff --git a/src/python/grpcio_status/grpc_version.py b/src/python/grpcio_status/grpc_version.py
new file mode 100644
index 0000000000..e009843b94
--- /dev/null
+++ b/src/python/grpcio_status/grpc_version.py
@@ -0,0 +1,17 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# AUTO-GENERATED FROM `$REPO_ROOT/templates/src/python/grpcio_status/grpc_version.py.template`!!!
+
+VERSION = '1.18.0.dev0'
diff --git a/src/python/grpcio_status/setup.py b/src/python/grpcio_status/setup.py
new file mode 100644
index 0000000000..983d3ea430
--- /dev/null
+++ b/src/python/grpcio_status/setup.py
@@ -0,0 +1,93 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Setup module for the GRPC Python package's status mapping."""
+
+import os
+
+import setuptools
+
+# Ensure we're in the proper directory whether or not we're being used by pip.
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+# Break import-style to ensure we can actually find our local modules.
+import grpc_version
+
+
+class _NoOpCommand(setuptools.Command):
+ """No-op command."""
+
+ description = ''
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ pass
+
+
+CLASSIFIERS = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'License :: OSI Approved :: Apache Software License',
+]
+
+PACKAGE_DIRECTORIES = {
+ '': '.',
+}
+
+INSTALL_REQUIRES = (
+ 'protobuf>=3.6.0',
+ 'grpcio>={version}'.format(version=grpc_version.VERSION),
+ 'googleapis-common-protos>=1.5.5',
+)
+
+try:
+ import status_commands as _status_commands
+ # we are in the build environment, otherwise the above import fails
+ COMMAND_CLASS = {
+ # Run preprocess from the repository *before* doing any packaging!
+ 'preprocess': _status_commands.Preprocess,
+ 'build_package_protos': _NoOpCommand,
+ }
+except ImportError:
+ COMMAND_CLASS = {
+ # wire up commands to no-op not to break the external dependencies
+ 'preprocess': _NoOpCommand,
+ 'build_package_protos': _NoOpCommand,
+ }
+
+setuptools.setup(
+ name='grpcio-status',
+ version=grpc_version.VERSION,
+ description='Status proto mapping for gRPC',
+ author='The gRPC Authors',
+ author_email='grpc-io@googlegroups.com',
+ url='https://grpc.io',
+ license='Apache License 2.0',
+ classifiers=CLASSIFIERS,
+ package_dir=PACKAGE_DIRECTORIES,
+ packages=setuptools.find_packages('.'),
+ install_requires=INSTALL_REQUIRES,
+ cmdclass=COMMAND_CLASS)
diff --git a/src/python/grpcio_status/status_commands.py b/src/python/grpcio_status/status_commands.py
new file mode 100644
index 0000000000..78cd497f62
--- /dev/null
+++ b/src/python/grpcio_status/status_commands.py
@@ -0,0 +1,39 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Provides distutils command classes for the GRPC Python setup process."""
+
+import os
+import shutil
+
+import setuptools
+
+ROOT_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
+LICENSE = os.path.join(ROOT_DIR, '../../../LICENSE')
+
+
+class Preprocess(setuptools.Command):
+ """Command to copy LICENSE from root directory."""
+
+ description = ''
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ if os.path.isfile(LICENSE):
+ shutil.copyfile(LICENSE, os.path.join(ROOT_DIR, 'LICENSE'))
diff --git a/src/python/grpcio_testing/grpc_testing/_server/_servicer_context.py b/src/python/grpcio_testing/grpc_testing/_server/_servicer_context.py
index 90eeb130d3..5b1dfeacdf 100644
--- a/src/python/grpcio_testing/grpc_testing/_server/_servicer_context.py
+++ b/src/python/grpcio_testing/grpc_testing/_server/_servicer_context.py
@@ -70,6 +70,9 @@ class ServicerContext(grpc.ServicerContext):
def abort(self, code, details):
raise NotImplementedError()
+ def abort_with_status(self, status):
+ raise NotImplementedError()
+
def set_code(self, code):
self._rpc.set_code(code)
diff --git a/src/python/grpcio_tests/commands.py b/src/python/grpcio_tests/commands.py
index 65e9a99950..496bcfbcbf 100644
--- a/src/python/grpcio_tests/commands.py
+++ b/src/python/grpcio_tests/commands.py
@@ -22,7 +22,6 @@ import re
import shutil
import subprocess
import sys
-import traceback
import setuptools
from setuptools.command import build_ext
@@ -133,6 +132,14 @@ class TestGevent(setuptools.Command):
# TODO(https://github.com/grpc/grpc/issues/15411) unpin gevent version
# This test will stuck while running higher version of gevent
'unit._auth_context_test.AuthContextTest.testSessionResumption',
+ # TODO(https://github.com/grpc/grpc/issues/15411) enable these tests
+ 'unit._exit_test.ExitTest.test_in_flight_unary_unary_call',
+ 'unit._exit_test.ExitTest.test_in_flight_unary_stream_call',
+ 'unit._exit_test.ExitTest.test_in_flight_stream_unary_call',
+ 'unit._exit_test.ExitTest.test_in_flight_stream_stream_call',
+ 'unit._exit_test.ExitTest.test_in_flight_partial_unary_stream_call',
+ 'unit._exit_test.ExitTest.test_in_flight_partial_stream_unary_call',
+ 'unit._exit_test.ExitTest.test_in_flight_partial_stream_stream_call',
# TODO(https://github.com/grpc/grpc/issues/17330) enable these three tests
'channelz._channelz_servicer_test.ChannelzServicerTest.test_many_subchannels',
'channelz._channelz_servicer_test.ChannelzServicerTest.test_many_subchannels_and_sockets',
diff --git a/src/python/grpcio_tests/setup.py b/src/python/grpcio_tests/setup.py
index f56425ac6d..800b865da6 100644
--- a/src/python/grpcio_tests/setup.py
+++ b/src/python/grpcio_tests/setup.py
@@ -37,12 +37,19 @@ PACKAGE_DIRECTORIES = {
}
INSTALL_REQUIRES = (
- 'coverage>=4.0', 'enum34>=1.0.4',
+ 'coverage>=4.0',
+ 'enum34>=1.0.4',
'grpcio>={version}'.format(version=grpc_version.VERSION),
- 'grpcio-channelz>={version}'.format(version=grpc_version.VERSION),
+ # TODO(https://github.com/pypa/warehouse/issues/5196)
+ # Re-enable it once we got the name back
+ # 'grpcio-channelz>={version}'.format(version=grpc_version.VERSION),
+ 'grpcio-status>={version}'.format(version=grpc_version.VERSION),
'grpcio-tools>={version}'.format(version=grpc_version.VERSION),
'grpcio-health-checking>={version}'.format(version=grpc_version.VERSION),
- 'oauth2client>=1.4.7', 'protobuf>=3.6.0', 'six>=1.10', 'google-auth>=1.0.0',
+ 'oauth2client>=1.4.7',
+ 'protobuf>=3.6.0',
+ 'six>=1.10',
+ 'google-auth>=1.0.0',
'requests>=2.14.2')
if not PY3:
diff --git a/src/python/grpcio_tests/tests/channelz/_channelz_servicer_test.py b/src/python/grpcio_tests/tests/channelz/_channelz_servicer_test.py
index 8ca5189522..c63ff5cd84 100644
--- a/src/python/grpcio_tests/tests/channelz/_channelz_servicer_test.py
+++ b/src/python/grpcio_tests/tests/channelz/_channelz_servicer_test.py
@@ -88,11 +88,10 @@ def _generate_channel_server_pairs(n):
def _close_channel_server_pairs(pairs):
for pair in pairs:
pair.server.stop(None)
- # TODO(ericgribkoff) This del should not be required
- del pair.server
pair.channel.close()
+@unittest.skip('https://github.com/pypa/warehouse/issues/5196')
class ChannelzServicerTest(unittest.TestCase):
def _send_successful_unary_unary(self, idx):
diff --git a/src/python/grpcio_tests/tests/status/BUILD.bazel b/src/python/grpcio_tests/tests/status/BUILD.bazel
new file mode 100644
index 0000000000..937e50498e
--- /dev/null
+++ b/src/python/grpcio_tests/tests/status/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@grpc_python_dependencies//:requirements.bzl", "requirement")
+
+package(default_visibility = ["//visibility:public"])
+
+py_test(
+ name = "grpc_status_test",
+ srcs = ["_grpc_status_test.py"],
+ main = "_grpc_status_test.py",
+ size = "small",
+ deps = [
+ "//src/python/grpcio/grpc:grpcio",
+ "//src/python/grpcio_status/grpc_status:grpc_status",
+ "//src/python/grpcio_tests/tests/unit:test_common",
+ "//src/python/grpcio_tests/tests/unit/framework/common:common",
+ requirement('protobuf'),
+ requirement('googleapis-common-protos'),
+ ],
+ imports = ["../../",],
+)
diff --git a/src/python/grpcio_tests/tests/status/__init__.py b/src/python/grpcio_tests/tests/status/__init__.py
new file mode 100644
index 0000000000..38fdfc9c5c
--- /dev/null
+++ b/src/python/grpcio_tests/tests/status/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/src/python/grpcio_tests/tests/status/_grpc_status_test.py b/src/python/grpcio_tests/tests/status/_grpc_status_test.py
new file mode 100644
index 0000000000..519c372a96
--- /dev/null
+++ b/src/python/grpcio_tests/tests/status/_grpc_status_test.py
@@ -0,0 +1,173 @@
+# Copyright 2018 The gRPC Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests of grpc_status."""
+
+import unittest
+
+import logging
+import traceback
+
+import grpc
+from grpc_status import rpc_status
+
+from tests.unit import test_common
+
+from google.protobuf import any_pb2
+from google.rpc import code_pb2, status_pb2, error_details_pb2
+
+_STATUS_OK = '/test/StatusOK'
+_STATUS_NOT_OK = '/test/StatusNotOk'
+_ERROR_DETAILS = '/test/ErrorDetails'
+_INCONSISTENT = '/test/Inconsistent'
+_INVALID_CODE = '/test/InvalidCode'
+
+_REQUEST = b'\x00\x00\x00'
+_RESPONSE = b'\x01\x01\x01'
+
+_GRPC_DETAILS_METADATA_KEY = 'grpc-status-details-bin'
+
+_STATUS_DETAILS = 'This is an error detail'
+_STATUS_DETAILS_ANOTHER = 'This is another error detail'
+
+
+def _ok_unary_unary(request, servicer_context):
+ return _RESPONSE
+
+
+def _not_ok_unary_unary(request, servicer_context):
+ servicer_context.abort(grpc.StatusCode.INTERNAL, _STATUS_DETAILS)
+
+
+def _error_details_unary_unary(request, servicer_context):
+ details = any_pb2.Any()
+ details.Pack(
+ error_details_pb2.DebugInfo(
+ stack_entries=traceback.format_stack(),
+ detail='Intentionally invoked'))
+ rich_status = status_pb2.Status(
+ code=code_pb2.INTERNAL,
+ message=_STATUS_DETAILS,
+ details=[details],
+ )
+ servicer_context.abort_with_status(rpc_status.to_status(rich_status))
+
+
+def _inconsistent_unary_unary(request, servicer_context):
+ rich_status = status_pb2.Status(
+ code=code_pb2.INTERNAL,
+ message=_STATUS_DETAILS,
+ )
+ servicer_context.set_code(grpc.StatusCode.NOT_FOUND)
+ servicer_context.set_details(_STATUS_DETAILS_ANOTHER)
+ # User put inconsistent status information in trailing metadata
+ servicer_context.set_trailing_metadata(((_GRPC_DETAILS_METADATA_KEY,
+ rich_status.SerializeToString()),))
+
+
+def _invalid_code_unary_unary(request, servicer_context):
+ rich_status = status_pb2.Status(
+ code=42,
+ message='Invalid code',
+ )
+ servicer_context.abort_with_status(rpc_status.to_status(rich_status))
+
+
+class _GenericHandler(grpc.GenericRpcHandler):
+
+ def service(self, handler_call_details):
+ if handler_call_details.method == _STATUS_OK:
+ return grpc.unary_unary_rpc_method_handler(_ok_unary_unary)
+ elif handler_call_details.method == _STATUS_NOT_OK:
+ return grpc.unary_unary_rpc_method_handler(_not_ok_unary_unary)
+ elif handler_call_details.method == _ERROR_DETAILS:
+ return grpc.unary_unary_rpc_method_handler(
+ _error_details_unary_unary)
+ elif handler_call_details.method == _INCONSISTENT:
+ return grpc.unary_unary_rpc_method_handler(
+ _inconsistent_unary_unary)
+ elif handler_call_details.method == _INVALID_CODE:
+ return grpc.unary_unary_rpc_method_handler(
+ _invalid_code_unary_unary)
+ else:
+ return None
+
+
+class StatusTest(unittest.TestCase):
+
+ def setUp(self):
+ self._server = test_common.test_server()
+ self._server.add_generic_rpc_handlers((_GenericHandler(),))
+ port = self._server.add_insecure_port('[::]:0')
+ self._server.start()
+
+ self._channel = grpc.insecure_channel('localhost:%d' % port)
+
+ def tearDown(self):
+ self._server.stop(None)
+ self._channel.close()
+
+ def test_status_ok(self):
+ _, call = self._channel.unary_unary(_STATUS_OK).with_call(_REQUEST)
+
+ # Succeed RPC doesn't have status
+ status = rpc_status.from_call(call)
+ self.assertIs(status, None)
+
+ def test_status_not_ok(self):
+ with self.assertRaises(grpc.RpcError) as exception_context:
+ self._channel.unary_unary(_STATUS_NOT_OK).with_call(_REQUEST)
+ rpc_error = exception_context.exception
+
+ self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL)
+ # Failed RPC doesn't automatically generate status
+ status = rpc_status.from_call(rpc_error)
+ self.assertIs(status, None)
+
+ def test_error_details(self):
+ with self.assertRaises(grpc.RpcError) as exception_context:
+ self._channel.unary_unary(_ERROR_DETAILS).with_call(_REQUEST)
+ rpc_error = exception_context.exception
+
+ status = rpc_status.from_call(rpc_error)
+ self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL)
+ self.assertEqual(status.code, code_pb2.Code.Value('INTERNAL'))
+
+ # Check if the underlying proto message is intact
+ self.assertEqual(status.details[0].Is(
+ error_details_pb2.DebugInfo.DESCRIPTOR), True)
+ info = error_details_pb2.DebugInfo()
+ status.details[0].Unpack(info)
+ self.assertIn('_error_details_unary_unary', info.stack_entries[-1])
+
+ def test_code_message_validation(self):
+ with self.assertRaises(grpc.RpcError) as exception_context:
+ self._channel.unary_unary(_INCONSISTENT).with_call(_REQUEST)
+ rpc_error = exception_context.exception
+ self.assertEqual(rpc_error.code(), grpc.StatusCode.NOT_FOUND)
+
+ # Code/Message validation failed
+ self.assertRaises(ValueError, rpc_status.from_call, rpc_error)
+
+ def test_invalid_code(self):
+ with self.assertRaises(grpc.RpcError) as exception_context:
+ self._channel.unary_unary(_INVALID_CODE).with_call(_REQUEST)
+ rpc_error = exception_context.exception
+ self.assertEqual(rpc_error.code(), grpc.StatusCode.UNKNOWN)
+ # Invalid status code exception raised during coversion
+ self.assertIn('Invalid status code', rpc_error.details())
+
+
+if __name__ == '__main__':
+ logging.basicConfig()
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio_tests/tests/tests.json b/src/python/grpcio_tests/tests/tests.json
index 9cffd3df19..f202a3f932 100644
--- a/src/python/grpcio_tests/tests/tests.json
+++ b/src/python/grpcio_tests/tests/tests.json
@@ -15,10 +15,12 @@
"protoc_plugin._split_definitions_test.SplitProtoSingleProtocExecutionProtocStyleTest",
"protoc_plugin.beta_python_plugin_test.PythonPluginTest",
"reflection._reflection_servicer_test.ReflectionServicerTest",
+ "status._grpc_status_test.StatusTest",
"testing._client_test.ClientTest",
"testing._server_test.FirstServiceServicerTest",
"testing._time_test.StrictFakeTimeTest",
"testing._time_test.StrictRealTimeTest",
+ "unit._abort_test.AbortTest",
"unit._api_test.AllTest",
"unit._api_test.ChannelConnectivityTest",
"unit._api_test.ChannelTest",
@@ -55,6 +57,7 @@
"unit._reconnect_test.ReconnectTest",
"unit._resource_exhausted_test.ResourceExhaustedTest",
"unit._rpc_test.RPCTest",
+ "unit._server_shutdown_test.ServerShutdown",
"unit._server_ssl_cert_config_test.ServerSSLCertConfigFetcherParamsChecks",
"unit._server_ssl_cert_config_test.ServerSSLCertReloadTestCertConfigReuse",
"unit._server_ssl_cert_config_test.ServerSSLCertReloadTestWithClientAuth",
diff --git a/src/python/grpcio_tests/tests/unit/BUILD.bazel b/src/python/grpcio_tests/tests/unit/BUILD.bazel
index de33b81e32..1b462ec67a 100644
--- a/src/python/grpcio_tests/tests/unit/BUILD.bazel
+++ b/src/python/grpcio_tests/tests/unit/BUILD.bazel
@@ -3,6 +3,7 @@ load("@grpc_python_dependencies//:requirements.bzl", "requirement")
package(default_visibility = ["//visibility:public"])
GRPCIO_TESTS_UNIT = [
+ "_abort_test.py",
"_api_test.py",
"_auth_context_test.py",
"_auth_test.py",
@@ -27,6 +28,7 @@ GRPCIO_TESTS_UNIT = [
# TODO(ghostwriternr): To be added later.
# "_server_ssl_cert_config_test.py",
"_server_test.py",
+ "_server_shutdown_test.py",
"_session_cache_test.py",
]
@@ -49,6 +51,11 @@ py_library(
)
py_library(
+ name = "_server_shutdown_scenarios",
+ srcs = ["_server_shutdown_scenarios.py"],
+)
+
+py_library(
name = "_thread_pool",
srcs = ["_thread_pool.py"],
)
@@ -69,6 +76,7 @@ py_library(
":resources",
":test_common",
":_exit_scenarios",
+ ":_server_shutdown_scenarios",
":_thread_pool",
":_from_grpc_import_star",
"//src/python/grpcio_tests/tests/unit/framework/common",
diff --git a/src/python/grpcio_tests/tests/unit/_abort_test.py b/src/python/grpcio_tests/tests/unit/_abort_test.py
new file mode 100644
index 0000000000..6438f6897a
--- /dev/null
+++ b/src/python/grpcio_tests/tests/unit/_abort_test.py
@@ -0,0 +1,124 @@
+# Copyright 2018 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests server context abort mechanism"""
+
+import unittest
+import collections
+import logging
+
+import grpc
+
+from tests.unit import test_common
+from tests.unit.framework.common import test_constants
+
+_ABORT = '/test/abort'
+_ABORT_WITH_STATUS = '/test/AbortWithStatus'
+_INVALID_CODE = '/test/InvalidCode'
+
+_REQUEST = b'\x00\x00\x00'
+_RESPONSE = b'\x00\x00\x00'
+
+_ABORT_DETAILS = 'Abandon ship!'
+_ABORT_METADATA = (('a-trailing-metadata', '42'),)
+
+
+class _Status(
+ collections.namedtuple(
+ '_Status', ('code', 'details', 'trailing_metadata')), grpc.Status):
+ pass
+
+
+def abort_unary_unary(request, servicer_context):
+ servicer_context.abort(
+ grpc.StatusCode.INTERNAL,
+ _ABORT_DETAILS,
+ )
+ raise Exception('This line should not be executed!')
+
+
+def abort_with_status_unary_unary(request, servicer_context):
+ servicer_context.abort_with_status(
+ _Status(
+ code=grpc.StatusCode.INTERNAL,
+ details=_ABORT_DETAILS,
+ trailing_metadata=_ABORT_METADATA,
+ ))
+ raise Exception('This line should not be executed!')
+
+
+def invalid_code_unary_unary(request, servicer_context):
+ servicer_context.abort(
+ 42,
+ _ABORT_DETAILS,
+ )
+
+
+class _GenericHandler(grpc.GenericRpcHandler):
+
+ def service(self, handler_call_details):
+ if handler_call_details.method == _ABORT:
+ return grpc.unary_unary_rpc_method_handler(abort_unary_unary)
+ elif handler_call_details.method == _ABORT_WITH_STATUS:
+ return grpc.unary_unary_rpc_method_handler(
+ abort_with_status_unary_unary)
+ elif handler_call_details.method == _INVALID_CODE:
+ return grpc.stream_stream_rpc_method_handler(
+ invalid_code_unary_unary)
+ else:
+ return None
+
+
+class AbortTest(unittest.TestCase):
+
+ def setUp(self):
+ self._server = test_common.test_server()
+ port = self._server.add_insecure_port('[::]:0')
+ self._server.add_generic_rpc_handlers((_GenericHandler(),))
+ self._server.start()
+
+ self._channel = grpc.insecure_channel('localhost:%d' % port)
+
+ def tearDown(self):
+ self._channel.close()
+ self._server.stop(0)
+
+ def test_abort(self):
+ with self.assertRaises(grpc.RpcError) as exception_context:
+ self._channel.unary_unary(_ABORT)(_REQUEST)
+ rpc_error = exception_context.exception
+
+ self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL)
+ self.assertEqual(rpc_error.details(), _ABORT_DETAILS)
+
+ def test_abort_with_status(self):
+ with self.assertRaises(grpc.RpcError) as exception_context:
+ self._channel.unary_unary(_ABORT_WITH_STATUS)(_REQUEST)
+ rpc_error = exception_context.exception
+
+ self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL)
+ self.assertEqual(rpc_error.details(), _ABORT_DETAILS)
+ self.assertEqual(rpc_error.trailing_metadata(), _ABORT_METADATA)
+
+ def test_invalid_code(self):
+ with self.assertRaises(grpc.RpcError) as exception_context:
+ self._channel.unary_unary(_INVALID_CODE)(_REQUEST)
+ rpc_error = exception_context.exception
+
+ self.assertEqual(rpc_error.code(), grpc.StatusCode.UNKNOWN)
+ self.assertEqual(rpc_error.details(), _ABORT_DETAILS)
+
+
+if __name__ == '__main__':
+ logging.basicConfig()
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio_tests/tests/unit/_api_test.py b/src/python/grpcio_tests/tests/unit/_api_test.py
index 38072861a4..427894bfe9 100644
--- a/src/python/grpcio_tests/tests/unit/_api_test.py
+++ b/src/python/grpcio_tests/tests/unit/_api_test.py
@@ -32,6 +32,7 @@ class AllTest(unittest.TestCase):
'Future',
'ChannelConnectivity',
'StatusCode',
+ 'Status',
'RpcError',
'RpcContext',
'Call',
diff --git a/src/python/grpcio_tests/tests/unit/_exit_test.py b/src/python/grpcio_tests/tests/unit/_exit_test.py
index 5226537579..b429ee089f 100644
--- a/src/python/grpcio_tests/tests/unit/_exit_test.py
+++ b/src/python/grpcio_tests/tests/unit/_exit_test.py
@@ -71,7 +71,6 @@ def wait(process):
process.wait()
-@unittest.skip('https://github.com/grpc/grpc/issues/7311')
class ExitTest(unittest.TestCase):
def test_unstarted_server(self):
@@ -130,6 +129,8 @@ class ExitTest(unittest.TestCase):
stderr=sys.stderr)
interrupt_and_wait(process)
+ @unittest.skipIf(os.name == 'nt',
+ 'os.kill does not have required permission on Windows')
def test_in_flight_unary_unary_call(self):
process = subprocess.Popen(
BASE_COMMAND + [_exit_scenarios.IN_FLIGHT_UNARY_UNARY_CALL],
@@ -138,6 +139,8 @@ class ExitTest(unittest.TestCase):
interrupt_and_wait(process)
@unittest.skipIf(six.PY2, 'https://github.com/grpc/grpc/issues/6999')
+ @unittest.skipIf(os.name == 'nt',
+ 'os.kill does not have required permission on Windows')
def test_in_flight_unary_stream_call(self):
process = subprocess.Popen(
BASE_COMMAND + [_exit_scenarios.IN_FLIGHT_UNARY_STREAM_CALL],
@@ -145,6 +148,8 @@ class ExitTest(unittest.TestCase):
stderr=sys.stderr)
interrupt_and_wait(process)
+ @unittest.skipIf(os.name == 'nt',
+ 'os.kill does not have required permission on Windows')
def test_in_flight_stream_unary_call(self):
process = subprocess.Popen(
BASE_COMMAND + [_exit_scenarios.IN_FLIGHT_STREAM_UNARY_CALL],
@@ -153,6 +158,8 @@ class ExitTest(unittest.TestCase):
interrupt_and_wait(process)
@unittest.skipIf(six.PY2, 'https://github.com/grpc/grpc/issues/6999')
+ @unittest.skipIf(os.name == 'nt',
+ 'os.kill does not have required permission on Windows')
def test_in_flight_stream_stream_call(self):
process = subprocess.Popen(
BASE_COMMAND + [_exit_scenarios.IN_FLIGHT_STREAM_STREAM_CALL],
@@ -161,6 +168,8 @@ class ExitTest(unittest.TestCase):
interrupt_and_wait(process)
@unittest.skipIf(six.PY2, 'https://github.com/grpc/grpc/issues/6999')
+ @unittest.skipIf(os.name == 'nt',
+ 'os.kill does not have required permission on Windows')
def test_in_flight_partial_unary_stream_call(self):
process = subprocess.Popen(
BASE_COMMAND +
@@ -169,6 +178,8 @@ class ExitTest(unittest.TestCase):
stderr=sys.stderr)
interrupt_and_wait(process)
+ @unittest.skipIf(os.name == 'nt',
+ 'os.kill does not have required permission on Windows')
def test_in_flight_partial_stream_unary_call(self):
process = subprocess.Popen(
BASE_COMMAND +
@@ -178,6 +189,8 @@ class ExitTest(unittest.TestCase):
interrupt_and_wait(process)
@unittest.skipIf(six.PY2, 'https://github.com/grpc/grpc/issues/6999')
+ @unittest.skipIf(os.name == 'nt',
+ 'os.kill does not have required permission on Windows')
def test_in_flight_partial_stream_stream_call(self):
process = subprocess.Popen(
BASE_COMMAND +
diff --git a/src/python/grpcio_tests/tests/unit/_server_shutdown_scenarios.py b/src/python/grpcio_tests/tests/unit/_server_shutdown_scenarios.py
new file mode 100644
index 0000000000..1d1fdba11e
--- /dev/null
+++ b/src/python/grpcio_tests/tests/unit/_server_shutdown_scenarios.py
@@ -0,0 +1,97 @@
+# Copyright 2018 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Defines a number of module-scope gRPC scenarios to test server shutdown."""
+
+import argparse
+import os
+import threading
+import time
+import logging
+
+import grpc
+from tests.unit import test_common
+
+from concurrent import futures
+from six.moves import queue
+
+WAIT_TIME = 1000
+
+REQUEST = b'request'
+RESPONSE = b'response'
+
+SERVER_RAISES_EXCEPTION = 'server_raises_exception'
+SERVER_DEALLOCATED = 'server_deallocated'
+SERVER_FORK_CAN_EXIT = 'server_fork_can_exit'
+
+FORK_EXIT = '/test/ForkExit'
+
+
+def fork_and_exit(request, servicer_context):
+ pid = os.fork()
+ if pid == 0:
+ os._exit(0)
+ return RESPONSE
+
+
+class GenericHandler(grpc.GenericRpcHandler):
+
+ def service(self, handler_call_details):
+ if handler_call_details.method == FORK_EXIT:
+ return grpc.unary_unary_rpc_method_handler(fork_and_exit)
+ else:
+ return None
+
+
+def run_server(port_queue):
+ server = test_common.test_server()
+ port = server.add_insecure_port('[::]:0')
+ port_queue.put(port)
+ server.add_generic_rpc_handlers((GenericHandler(),))
+ server.start()
+ # threading.Event.wait() does not exhibit the bug identified in
+ # https://github.com/grpc/grpc/issues/17093, sleep instead
+ time.sleep(WAIT_TIME)
+
+
+def run_test(args):
+ if args.scenario == SERVER_RAISES_EXCEPTION:
+ server = test_common.test_server()
+ server.start()
+ raise Exception()
+ elif args.scenario == SERVER_DEALLOCATED:
+ server = test_common.test_server()
+ server.start()
+ server.__del__()
+ while server._state.stage != grpc._server._ServerStage.STOPPED:
+ pass
+ elif args.scenario == SERVER_FORK_CAN_EXIT:
+ port_queue = queue.Queue()
+ thread = threading.Thread(target=run_server, args=(port_queue,))
+ thread.daemon = True
+ thread.start()
+ port = port_queue.get()
+ channel = grpc.insecure_channel('localhost:%d' % port)
+ multi_callable = channel.unary_unary(FORK_EXIT)
+ result, call = multi_callable.with_call(REQUEST, wait_for_ready=True)
+ os.wait()
+ else:
+ raise ValueError('unknown test scenario')
+
+
+if __name__ == '__main__':
+ logging.basicConfig()
+ parser = argparse.ArgumentParser()
+ parser.add_argument('scenario', type=str)
+ args = parser.parse_args()
+ run_test(args)
diff --git a/src/python/grpcio_tests/tests/unit/_server_shutdown_test.py b/src/python/grpcio_tests/tests/unit/_server_shutdown_test.py
new file mode 100644
index 0000000000..47446d65a5
--- /dev/null
+++ b/src/python/grpcio_tests/tests/unit/_server_shutdown_test.py
@@ -0,0 +1,90 @@
+# Copyright 2018 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests clean shutdown of server on various interpreter exit conditions.
+
+The tests in this module spawn a subprocess for each test case, the
+test is considered successful if it doesn't hang/timeout.
+"""
+
+import atexit
+import os
+import subprocess
+import sys
+import threading
+import unittest
+import logging
+
+from tests.unit import _server_shutdown_scenarios
+
+SCENARIO_FILE = os.path.abspath(
+ os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ '_server_shutdown_scenarios.py'))
+INTERPRETER = sys.executable
+BASE_COMMAND = [INTERPRETER, SCENARIO_FILE]
+
+processes = []
+process_lock = threading.Lock()
+
+
+# Make sure we attempt to clean up any
+# processes we may have left running
+def cleanup_processes():
+ with process_lock:
+ for process in processes:
+ try:
+ process.kill()
+ except Exception: # pylint: disable=broad-except
+ pass
+
+
+atexit.register(cleanup_processes)
+
+
+def wait(process):
+ with process_lock:
+ processes.append(process)
+ process.wait()
+
+
+class ServerShutdown(unittest.TestCase):
+
+ # Currently we shut down a server (if possible) after the Python server
+ # instance is garbage collected. This behavior may change in the future.
+ def test_deallocated_server_stops(self):
+ process = subprocess.Popen(
+ BASE_COMMAND + [_server_shutdown_scenarios.SERVER_DEALLOCATED],
+ stdout=sys.stdout,
+ stderr=sys.stderr)
+ wait(process)
+
+ def test_server_exception_exits(self):
+ process = subprocess.Popen(
+ BASE_COMMAND + [_server_shutdown_scenarios.SERVER_RAISES_EXCEPTION],
+ stdout=sys.stdout,
+ stderr=sys.stderr)
+ wait(process)
+
+ @unittest.skipIf(os.name == 'nt', 'fork not supported on windows')
+ def test_server_fork_can_exit(self):
+ process = subprocess.Popen(
+ BASE_COMMAND + [_server_shutdown_scenarios.SERVER_FORK_CAN_EXIT],
+ stdout=sys.stdout,
+ stderr=sys.stderr)
+ wait(process)
+
+
+if __name__ == '__main__':
+ logging.basicConfig()
+ unittest.main(verbosity=2)