Skip to content

Add check on return value from start_client_batch#8995

Merged
ncteisen merged 1 commit intogrpc:v1.0.xfrom
ncteisen:python_call_batch
Dec 15, 2016
Merged

Add check on return value from start_client_batch#8995
ncteisen merged 1 commit intogrpc:v1.0.xfrom
ncteisen:python_call_batch

Conversation

@ncteisen
Copy link
Copy Markdown
Contributor

@ncteisen ncteisen commented Dec 7, 2016

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

Copy link
Copy Markdown
Contributor

@nathanielmanistaatgoogle nathanielmanistaatgoogle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last time I saw a draft of this there were tests associated with it (right?); where'd they go?

condition.wait(timeout=remaining)

def _check_call_error(call_error, metadata=None):
if call_error != cygrpc.CallError.ok:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduce the nesting by revising the control flow to be

if <one condition>:
  raise <one expression>
elif <another condition>:
  raise <another expression>

.

else:
condition.wait(timeout=remaining)

def _check_call_error(call_error, metadata=None):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword arguments generally are not appropriate for private methods - make metadata positional?

event_handler)
if call_error != cygrpc.CallError.ok:
return _Rendezvous(
_call_error_RPCstate(call_error, metadata), None, None, deadline)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ncteisen
Copy link
Copy Markdown
Contributor Author

ncteisen commented Dec 8, 2016

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.

@nathanielmanistaatgoogle
Copy link
Copy Markdown
Contributor

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.

@ncteisen
Copy link
Copy Markdown
Contributor Author

Added a unit test for this new behavior

Copy link
Copy Markdown
Contributor

@nathanielmanistaatgoogle nathanielmanistaatgoogle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

condition.wait(timeout=remaining)

def _check_call_error(call_error, metadata):
if call_error == cygrpc.CallError.ok:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this rather than

if <one error condition>:
  raise <one error>
elif <another error condition>:
  raise <another error>

?

elif call_error == cygrpc.CallError.invalid_metadata:
raise ValueError("metadata was invalid: %s" % metadata)
else:
raise ValueError('Internal gRPC CallError %d. Please report to '
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

if call_error == cygrpc.CallError.ok:
return
elif call_error == cygrpc.CallError.invalid_metadata:
raise ValueError("metadata was invalid: %s" % metadata)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = <...>
<...>

.

@nathanielmanistaatgoogle
Copy link
Copy Markdown
Contributor

Does the new test need to be recorded in tests.json?

@ncteisen
Copy link
Copy Markdown
Contributor Author

Addressed all comments. Added the test to tests.json

else:
condition.wait(timeout=remaining)

_INTERNAL_CALLERROR_MESSAGE = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underscore between CALL and ERROR (corresponding to and due to the way cygrpc.CallError is capitalized).

else:
condition.wait(timeout=remaining)

_INTERNAL_CALLERROR_MESSAGE = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this contains a % to be replaced it is a message format rather than a message. _INTERNAL_CALL_ERROR_MESSAGE_FORMAT?

condition.wait(timeout=remaining)

_INTERNAL_CALLERROR_MESSAGE = (
'Internal gRPC CallError %d. ' +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ncteisen
Copy link
Copy Markdown
Contributor Author

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?

Copy link
Copy Markdown
Contributor

@nathanielmanistaatgoogle nathanielmanistaatgoogle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too.

@ncteisen
Copy link
Copy Markdown
Contributor Author

Done

Copy link
Copy Markdown
Contributor

@nathanielmanistaatgoogle nathanielmanistaatgoogle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@ncteisen ncteisen merged commit 3fa20cf into grpc:v1.0.x Dec 15, 2016
@ncteisen ncteisen deleted the python_call_batch branch December 15, 2016 19:20
@lock lock bot locked as resolved and limited conversation to collaborators Jan 25, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants