Add check on return value from start_client_batch#8995
Conversation
nathanielmanistaatgoogle
left a comment
There was a problem hiding this comment.
Last time I saw a draft of this there were tests associated with it (right?); where'd they go?
src/python/grpcio/grpc/_channel.py
Outdated
| condition.wait(timeout=remaining) | ||
|
|
||
| def _check_call_error(call_error, metadata=None): | ||
| if call_error != cygrpc.CallError.ok: |
There was a problem hiding this comment.
Reduce the nesting by revising the control flow to be
if <one condition>:
raise <one expression>
elif <another condition>:
raise <another expression>
.
src/python/grpcio/grpc/_channel.py
Outdated
| else: | ||
| condition.wait(timeout=remaining) | ||
|
|
||
| def _check_call_error(call_error, metadata=None): |
There was a problem hiding this comment.
Keyword arguments generally are not appropriate for private methods - make metadata positional?
src/python/grpcio/grpc/_channel.py
Outdated
| event_handler) | ||
| if call_error != cygrpc.CallError.ok: | ||
| return _Rendezvous( | ||
| _call_error_RPCstate(call_error, metadata), None, None, deadline) |
There was a problem hiding this comment.
Normally I'm the guy who's totally against all mutation, but... I think this code is easier to reason about if there remains exactly one _RPCState per RPC. I think you should change _call_error_RPCstate to mutate the _RPCState passed to it rather than create a new instance.
|
Addressed comments on _channel.py and included my testing file in the commit. The testing file is pretty hacked up at this point. I can make it cleaner, and move it to the unit test folder if it deserves to be there. |
|
This change is significant enough that it must have its own test coverage. Please clean the test and put it where it's supposed to be. |
c65ae8b to
92a1a7e
Compare
|
Added a unit test for this new behavior |
nathanielmanistaatgoogle
left a comment
There was a problem hiding this comment.
Consider running pylint over your changed files - it'll probably spam you with complaints about the two-space indentation (can't wait!) but everything starting with E, everything starting with W, and most things starting with C should be actionable.
src/python/grpcio/grpc/_channel.py
Outdated
| condition.wait(timeout=remaining) | ||
|
|
||
| def _check_call_error(call_error, metadata): | ||
| if call_error == cygrpc.CallError.ok: |
There was a problem hiding this comment.
Why this rather than
if <one error condition>:
raise <one error>
elif <another error condition>:
raise <another error>
?
src/python/grpcio/grpc/_channel.py
Outdated
| elif call_error == cygrpc.CallError.invalid_metadata: | ||
| raise ValueError("metadata was invalid: %s" % metadata) | ||
| else: | ||
| raise ValueError('Internal gRPC CallError %d. Please report to ' |
There was a problem hiding this comment.
Avoid implicit string concatenation whenever you can.
If our issue tracker URL is going to appear twice in program source, define a constant for it?
src/python/grpcio/grpc/_channel.py
Outdated
| if call_error == cygrpc.CallError.ok: | ||
| return | ||
| elif call_error == cygrpc.CallError.invalid_metadata: | ||
| raise ValueError("metadata was invalid: %s" % metadata) |
There was a problem hiding this comment.
Maintain consistent use of single-quote as the string delimiter throughout this file.
| metadata = (('InVaLiD', 'UnaryRequestBlockingUnaryResponse'),) | ||
| expected_error = ValueError("metadata was invalid: %s" % metadata) | ||
| multi_callable = _unary_unary_multi_callable(self._channel) | ||
| with self.assertRaises(ValueError) as cm: |
There was a problem hiding this comment.
Avoid abbreviations for the same reason that single-letter identifiers are forbidden by the style guide. This should probably be named exception_context.
| def testUnaryRequestBlockingUnaryResponse(self): | ||
| request = b'\x07\x08' | ||
| metadata = (('InVaLiD', 'UnaryRequestBlockingUnaryResponse'),) | ||
| expected_error = ValueError("metadata was invalid: %s" % metadata) |
There was a problem hiding this comment.
You don't really use this ValueError instance except for the value of its message attribute. Why create it?
| expected_error_details = "metadata was invalid: %s" % metadata | ||
| multi_callable = _unary_unary_multi_callable(self._channel) | ||
| with self.assertRaises(Exception) as cm: | ||
| response_future = multi_callable.future(request, metadata=metadata) |
There was a problem hiding this comment.
Pull this out of the with so that the test has the effect of asserting that execution of this line does not raise an exception.
| metadata = (('InVaLiD', 'UnaryRequestFutureUnaryResponse'),) | ||
| expected_error_details = "metadata was invalid: %s" % metadata | ||
| multi_callable = _unary_unary_multi_callable(self._channel) | ||
| with self.assertRaises(Exception) as cm: |
There was a problem hiding this comment.
No more specific exception class? Exception is very broad.
| with self.assertRaises(Exception) as cm: | ||
| response_future = multi_callable.future(request, metadata=metadata) | ||
| response = response_future.result() | ||
| self.assertEqual(cm.exception.details(), expected_error_details) |
There was a problem hiding this comment.
Please also assert that response_future.code()'s return value is as expected (and maybe response_future.details()?
| multi_callable = _unary_unary_multi_callable(self._channel) | ||
| with self.assertRaises(ValueError) as cm: | ||
| response = multi_callable(request, metadata=metadata) | ||
| self.assertEqual(cm.exception.message, expected_error.message) |
There was a problem hiding this comment.
Please maintain the gRPC-Python-codebase-wide convention of self.assertEqual(<expected>, <actual>) over self.assertEqual(<actual>, <expected>).
| request = b'\x07\x08' | ||
| metadata = (('InVaLiD', 'UnaryRequestBlockingUnaryResponse'),) | ||
| expected_error = ValueError("metadata was invalid: %s" % metadata) | ||
| multi_callable = _unary_unary_multi_callable(self._channel) |
There was a problem hiding this comment.
Kind of an arbitrary matter of taste but consider moving the instantiation of the MultiCallable objects to setUp rather than having one-per-test-method:
self._unary_unary = <...>
self._unary_stream = <...>
<...>
.
|
Does the new test need to be recorded in |
92a1a7e to
962592d
Compare
|
Addressed all comments. Added the test to tests.json |
src/python/grpcio/grpc/_channel.py
Outdated
| else: | ||
| condition.wait(timeout=remaining) | ||
|
|
||
| _INTERNAL_CALLERROR_MESSAGE = ( |
There was a problem hiding this comment.
Underscore between CALL and ERROR (corresponding to and due to the way cygrpc.CallError is capitalized).
src/python/grpcio/grpc/_channel.py
Outdated
| else: | ||
| condition.wait(timeout=remaining) | ||
|
|
||
| _INTERNAL_CALLERROR_MESSAGE = ( |
There was a problem hiding this comment.
Since this contains a % to be replaced it is a message format rather than a message. _INTERNAL_CALL_ERROR_MESSAGE_FORMAT?
src/python/grpcio/grpc/_channel.py
Outdated
| condition.wait(timeout=remaining) | ||
|
|
||
| _INTERNAL_CALLERROR_MESSAGE = ( | ||
| 'Internal gRPC CallError %d. ' + |
There was a problem hiding this comment.
And just plain old ordinary "call error" here, since what semantically matters in this user-visible (or developer-visible) string is not the specific CallError class name.
| request = b'\x07\x08' | ||
| metadata = (('InVaLiD', 'UnaryRequestBlockingUnaryResponse'),) | ||
| expected_error_details = "metadata was invalid: %s" % metadata | ||
| with self.assertRaises(ValueError) as exception_ctx: |
There was a problem hiding this comment.
Avoid abbreviations as well as initialisms and acronyms - this should be exception_context.
| metadata = (('InVaLiD', 'UnaryRequestBlockingUnaryResponse'),) | ||
| expected_error_details = "metadata was invalid: %s" % metadata | ||
| with self.assertRaises(ValueError) as exception_ctx: | ||
| response = self._unary_unary(request, metadata=metadata) |
There was a problem hiding this comment.
Did you try running pylint? Did it alert you to this local field being assigned but never accessed?
I'd say drop the field, but if you think the test is more clear with it included call it unused_response.
| self.assertEqual(expected_error_details, exception_ctx.exception.message) | ||
|
|
||
| def testStreamRequestFutureUnaryResponse(self): | ||
| requests = tuple(b'\x07\x08' for _ in range(test_constants.STREAM_LENGTH)) |
There was a problem hiding this comment.
Why not just
request_iterator = (b'\x07\x08' for _ in range(test_constants.STREAM_LENGTH))rather than these two lines? You should only need to create the sequence value if you need to iterate over its elements more than once.
962592d to
12c3493
Compare
|
Addressed all comments. Now pylint only complains about the indentation and missing docstring, but the all the other unit tests don't have docstrings, so I assumed that was ok? |
nathanielmanistaatgoogle
left a comment
There was a problem hiding this comment.
Regarding pylint: yes.
Sorry for missing these two small but important comments in the last round.
| expected_error_details = "metadata was invalid: %s" % metadata | ||
| response_iterator = self._unary_stream(request, metadata=metadata) | ||
| with self.assertRaises(grpc.RpcError) as exception_context: | ||
| tuple(response_iterator) |
There was a problem hiding this comment.
By changing this from tuple to next you again narrow the execution from which you are asserting that an exception must rise (from "an exception arises from consuming the entire stream of responses (including the end-of-stream indicator)" to "an exception arises from consuming the very first response in the response stream").
| expected_error_details = "metadata was invalid: %s" % metadata | ||
| response_iterator = self._stream_stream(request_iterator, metadata=metadata) | ||
| with self.assertRaises(grpc.RpcError) as exception_context: | ||
| tuple(response_iterator) |
There was a problem hiding this comment.
Here too.
12c3493 to
61b8c89
Compare
|
Done |
nathanielmanistaatgoogle
left a comment
There was a problem hiding this comment.
Thank you!
The return value of client_start_batch was previously unchecked, which could cause a client to hang if it attempted to send a malformed header (see #8814).
There is now a check on the return of client_start_batch, and if there is a failure, then either an exception is raised, or an error rendezvous is returned depending on if the RPC was synchronous or asynchronous