Skip to content

HTTP/2 test server#8900

Merged
makdharma merged 8 commits intogrpc:masterfrom
makdharma:http2_test
Dec 15, 2016
Merged

HTTP/2 test server#8900
makdharma merged 8 commits intogrpc:masterfrom
makdharma:http2_test

Conversation

@makdharma
Copy link
Copy Markdown
Contributor

HTTP2 protocol test cases.

@makdharma makdharma changed the title experimental HTTP/2 test server Dec 5, 2016
@makdharma
Copy link
Copy Markdown
Contributor Author

@ncteisen @ericgribkoff

test this please

Copy link
Copy Markdown
Contributor

@ncteisen ncteisen left a comment

Choose a reason for hiding this comment

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

Looked over all file and made some comments. Approving because nothing depends on our new tests yet.

In the future I still think it makes sense to change the server design to never close, and somehow determine what test to run per request (maybe by using the metadata of the req). That way the client never needs to ensure the server is running the test case of the same type. That seems cleaned than a scrip to start them both up

if self._conn.data_to_send:
self.transport.write(self._conn.data_to_send())
for event in events:
if isinstance(event, RequestReceived) and self._handlers.has_key('RequestReceived'):
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.

For this section maybe we should change the logic to be if is instance of EventType but _handlers does not have the key, throw an exception, or print some error.

Because if we receive and event that we haven't set up a handler for, wouldn't it be better to fail than to do nothing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is not an error case. There will be events that we don't need to process, example RemoteSettingsChanged.

break
self._send_remaining[stream_id] -= bytes_to_send
self._send_offset += bytes_to_send
if self._send_remaining[stream_id] == 0:
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.

This could be pulled out of the while loop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not really. default_send gets called every time window_update is received. pulling it out of while loop will cause it to send trailer even when no data was sent.


def parse_received_data(self, stream_id):
recv_buffer = self._recv_buffer[stream_id]
""" returns a grpc framed string of bytes containing response proto of the size
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.

Put docstring line directly after function definition

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

#logging.error('not enough data to decode req proto. size = %d, needed %s'%(len(recv_buffer), 5+grpc_msg_size))
return None
req_proto_str = recv_buffer[5:5+grpc_msg_size]
sr = messages_pb2.SimpleRequest()
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.

Looks like all this can parse for now is a SimpleRequest. Probably makes sense in the future to accepts all supported TestService types

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes.


def on_send_done(self, stream_id):
self._base_server.on_send_done_default(stream_id)
time.sleep(1)
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.

Is this needed to avoid race conditions? Maybe add a comment rather than have the naked sleep

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not really needed.. leftover from hack. removed.

class TestcaseSettingsMaxStreams(object):
"""
This test sets MAX_CONCURRENT_STREAMS to 1 and asserts that at any point
only 1 stream is active.
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.

Mentioned that all the assertions happen at the twisted layer. Otherwise its confusing when people look through the file and don't see any assertions.

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.

We should also specify somewhere that the client should first make a single RPC and block on the response before initiating this test. The C++ client can/will otherwise send 10 concurrent streams and complete before it receives the MAX_CONCURRENT_STREAMS setting from the server.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added a TODO for now.

if self._send_remaining[stream_id] == 0:
self._handlers['SendDone'](stream_id)

def default_ping(self):
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.

Add logging? It would make the ping test clearer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

self._handlers['PingAcknowledged'](event)
self.transport.write(self._conn.data_to_send())

def on_ping_acknowledged_default(self, event):
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.

Add logging?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Copy Markdown
Contributor

@ericgribkoff ericgribkoff left a comment

Choose a reason for hiding this comment

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

Mostly minor style comments, plus a few questions about clarifying test specifications.

@@ -0,0 +1,173 @@
import struct
import messages_pb2
import logging
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.

Ordering: should be in lexicographical order.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

from twisted.internet import reactor
from h2.connection import H2Connection
from h2.events import RequestReceived, DataReceived, WindowUpdated, RemoteSettingsChanged, PingAcknowledged
from h2.exceptions import ProtocolError
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.

Ordering: h2 should come before twisted. The Python style guide also recommends avoiding importing individual classes instead of modules.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

from h2.exceptions import ProtocolError

READ_CHUNK_SIZE = 16384
GRPC_HEADER_SIZE = 5
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.

These look like internal constants, and should start with an underscore, _READ_CHUNK_SIZE, _GRPC_HEADER_SIZE.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

self.transport.write(self._conn.data_to_send())

def on_connection_lost(self, reason):
logging.info('Disconnected %s'%reason)
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.

space before and after the % sign: logging.info('Disconnected %s' % reason)

I think this should also be done in the other format strings below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

def dataReceived(self, data):
try:
events = self._conn.receive_data(data)
except ProtocolError:
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I could only make it generate ProtocolError in my testing.


class H2Factory(Factory):
def __init__(self, testcase):
logging.info('In H2Factory')
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.

Remove?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed log message. This is useful for debugging when a new connection is made.

response_len = len(response_data)
truncated_response_data = response_data[0:response_len/2]
self._base_server.setup_send(truncated_response_data, event.stream_id)
# send reset stream
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.

Remove or move comment? on_send_done() sends the reset stream, so not clear what this means here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

"""
In response to an incoming request, this test sends headers, followed by
data, followed by a reset stream frame. Client asserts that the RPC failed.
Client needs to deliver the complete message to the application layer.
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.

Are we sure that all client languages can do this? For instance,Java can only deliver the completed message to the application layer using the async API. I think PHP, at least, might still be missing async calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Let's propose changes after we make inventory of what is possible for each language.

class TestcaseSettingsMaxStreams(object):
"""
This test sets MAX_CONCURRENT_STREAMS to 1 and asserts that at any point
only 1 stream is active.
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.

We should also specify somewhere that the client should first make a single RPC and block on the response before initiating this test. The C++ client can/will otherwise send 10 concurrent streams and complete before it receives the MAX_CONCURRENT_STREAMS setting from the server.

This test does the following:
Process incoming request normally, i.e. send headers, data and trailers.
Then send a GOAWAY frame with the stream id of the processed request.
It assert that the next request is made on a different TCP connection.
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.

s/assert/asserts

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.

More importantly, where is the assertion done? If this is relying on H2/Twisted internals to catch any errors, maybe add that to comments?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is actually not an assertion but the comment on line 27 explains the logic.

def on_connection_lost(self, reason):
logging.info('Disconnect received. Count %d' % self._iteration)
# _iteration == 2 => Two different connections have been used.
if self._iteration == 2:
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.

After looking at the base server implementation, I think I understand what this accomplishes: if _iteration != 2, the server won't acknowledge the connection lost, and consequently the server won't be shutdown (which happens from _base_server.on_connection_lost()). This would then be regarded as a failed test.

Would it be better to "fail fast" here and throw an exception if _iteration != 2? Otherwise, it seems like the test runner would have to wait until the test timeouts to see that the server is still running and the test failed, if I understand how the test driver will execute.


if __name__ == "__main__":
logging.basicConfig(format = "%(levelname) -10s %(asctime)s %(module)s:%(lineno)s | %(message)s", level=logging.INFO)
parser = argparse.ArgumentParser()
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.

I think this would be easier to use with a help message that lists the test cases. The following snippet adds help messages, and switches to --flag=value style arguments to be consistent with the existing interop test binaries. I also assigned a default value to --port - this makes things easier for me when running locally for testing. Feel free to just take the help messages if you prefer positional arguments.

if __name__ == '__main__':
  logging.basicConfig(
    format='%(levelname) -10s %(asctime)s %(module)s:%(lineno)s | %(message)s', 
    level=logging.INFO)
  parser = argparse.ArgumentParser()
  parser.add_argument('--test', choices=_TEST_CASE_MAPPING.keys(), 
    help='test case to run')
  parser.add_argument('--port', type=int, default=25678, 
    help='port to run the server (default: 25678)')
  args = parser.parse_args()
  if args.test not in _TEST_CASE_MAPPING.keys():
    logging.error('unknown test: %s' % args.test)
  else:
    endpoint = twisted.internet.endpoints.TCP4ServerEndpoint(
      twisted.internet.reactor, args.port, backlog=128)
    endpoint.listen(H2Factory(args.test))
    twisted.internet.reactor.run()

I also added some line wrapping. I think we probably want to stay <= 80 columns, which I didn't check for when I was reviewing this yesterday.

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.

Maybe sorting the _TEST_CASE_MAPPING keys is better to keep a consistent ordering in the help message:

parser.add_argument('--test', choices=sorted(_TEST_CASE_MAPPING.keys()), 
    help='test case to run')

@makdharma
Copy link
Copy Markdown
Contributor Author

Merging since python issues are unrelated to this PR and the code in this PR is not currently used anywhere.

@makdharma makdharma merged commit 2249e92 into grpc:master Dec 15, 2016
@y-zeng
Copy link
Copy Markdown
Contributor

y-zeng commented Dec 15, 2016

@ncteisen
Copy link
Copy Markdown
Contributor

Fixing PR is #9131

@dgquintas
Copy link
Copy Markdown
Contributor

This PR has cause sanity to start failing: https://grpc-testing.appspot.com/job/gRPC_master_linux/lastCompletedBuild/testReport/(root)/sanity_linux_dbg/tools_distrib_check_copyright_py/

Also, please check the tests before merging and follow the guidelines regarding mentioning the failures in the PR description. eg: #9089 (comment)

ericgribkoff added a commit to grpc/grpc-java that referenced this pull request Dec 20, 2016
These tests are for the new HTTP/2 test server (grpc/grpc#8900). They are designed to test client behavior when the server sends unexpected responses during rpcs.
@makdharma makdharma deleted the http2_test branch June 9, 2017 23:39
@lock lock bot locked as resolved and limited conversation to collaborators Jan 23, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants