Skip to content

Commit

Permalink
Merge pull request grpc#9776 from apolcyn/add_http2_flow_control_inte…
Browse files Browse the repository at this point in the history
…rop_tests

add http2 testing interop server uses small data frames and padding
  • Loading branch information
apolcyn committed Mar 23, 2017
2 parents 209c41a + 50fdc8a commit 5c8a47e
Show file tree
Hide file tree
Showing 14 changed files with 307 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,79 @@ Server Procedure:
1. Sets MAX_CONCURRENT_STREAMS to one after the connection is made.

*The assertion that the MAX_CONCURRENT_STREAMS limit is upheld occurs in the http2 library we used.*

### data_frame_padding

This test verifies that the client can correctly receive padded http2 data
frames. It also stresses the client's flow control (there is a high chance
that the sender will deadlock if the client's flow control logic doesn't
correctly account for padding).

Client Procedure:
(Note this is the same procedure as in the "large_unary" gRPC interop tests.
Clients should use their "large_unary" gRPC interop test implementations.)
Procedure:
1. Client calls UnaryCall with:

```
{
response_size: 314159
payload:{
body: 271828 bytes of zeros
}
}
```

Client asserts:
* call was successful
* response payload body is 314159 bytes in size
* clients are free to assert that the response payload body contents are zero
and comparing the entire response message against a golden response

Server Procedure:
1. Reply to the client's request with a `SimpleResponse`, with a payload
body length of `SimpleRequest.response_size`. But send it across specific
http2 data frames as follows:
* Each http2 data frame contains a 5 byte payload and 255 bytes of padding.

* Note the 5 byte payload and 255 byte padding are partly arbitrary,
and other numbers are also ok. With 255 bytes of padding for each 5 bytes of
payload containing actual gRPC message, the 300KB response size will
multiply into around 15 megabytes of flow control debt, which should stress
flow control accounting.

### no_df_padding_sanity_test

This test verifies that the client can correctly receive a series of small
data frames. Note that this test is intentionally a slight variation of
"data_frame_padding", with the only difference being that this test doesn't use data
frame padding when the response is sent. This test is primarily meant to
prove correctness of the http2 server implementation and highlight failures
of the "data_frame_padding" test.

Client Procedure:
(Note this is the same procedure as in the "large_unary" gRPC interop tests.
Clients should use their "large_unary" gRPC interop test implementations.)
Procedure:
1. Client calls UnaryCall with:

```
{
response_size: 314159
payload:{
body: 271828 bytes of zeros
}
}
```

Client asserts:
* call was successful
* response payload body is 314159 bytes in size
* clients are free to assert that the response payload body contents are zero
and comparing the entire response message against a golden response

Server Procedure:
1. Reply to the client's request with a `SimpleResponse`, with a payload
body length of `SimpleRequest.response_size`. But send it across series of
http2 data frames that contain 5 bytes of "payload" and zero bytes of
"padding" (the padding flags on the data frames should not be set).
35 changes: 22 additions & 13 deletions test/http2_test/http2_base_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

_READ_CHUNK_SIZE = 16384
_GRPC_HEADER_SIZE = 5
_MIN_SETTINGS_MAX_FRAME_SIZE = 16384

class H2ProtocolBaseServer(twisted.internet.protocol.Protocol):
def __init__(self):
Expand Down Expand Up @@ -121,38 +122,46 @@ def on_request_received_default(self, event):
)
self.transport.write(self._conn.data_to_send())

def on_window_update_default(self, event):
# send pending data, if any
self.default_send(event.stream_id)
def on_window_update_default(self, _, pad_length=None, read_chunk_size=_READ_CHUNK_SIZE):
# try to resume sending on all active streams (update might be for connection)
for stream_id in self._send_remaining:
self.default_send(stream_id, pad_length=pad_length, read_chunk_size=read_chunk_size)

def send_reset_stream(self):
self._conn.reset_stream(self._stream_id)
self.transport.write(self._conn.data_to_send())

def setup_send(self, data_to_send, stream_id):
def setup_send(self, data_to_send, stream_id, pad_length=None, read_chunk_size=_READ_CHUNK_SIZE):
logging.info('Setting up data to send for stream_id: %d' % stream_id)
self._send_remaining[stream_id] = len(data_to_send)
self._send_offset = 0
self._data_to_send = data_to_send
self.default_send(stream_id)
self.default_send(stream_id, pad_length=pad_length, read_chunk_size=read_chunk_size)

def default_send(self, stream_id):
def default_send(self, stream_id, pad_length=None, read_chunk_size=_READ_CHUNK_SIZE):
if not self._send_remaining.has_key(stream_id):
# not setup to send data yet
return

while self._send_remaining[stream_id] > 0:
lfcw = self._conn.local_flow_control_window(stream_id)
if lfcw == 0:
padding_bytes = pad_length + 1 if pad_length is not None else 0
if lfcw - padding_bytes <= 0:
logging.info('Stream %d. lfcw: %d. padding bytes: %d. not enough quota yet' % (stream_id, lfcw, padding_bytes))
break
chunk_size = min(lfcw, _READ_CHUNK_SIZE)
chunk_size = min(lfcw - padding_bytes, read_chunk_size)
bytes_to_send = min(chunk_size, self._send_remaining[stream_id])
logging.info('flow_control_window = %d. sending [%d:%d] stream_id %d' %
(lfcw, self._send_offset, self._send_offset + bytes_to_send,
stream_id))
logging.info('flow_control_window = %d. sending [%d:%d] stream_id %d. includes %d total padding bytes' %
(lfcw, self._send_offset, self._send_offset + bytes_to_send + padding_bytes,
stream_id, padding_bytes))
# The receiver might allow sending frames larger than the http2 minimum
# max frame size (16384), but this test should never send more than 16384
# for simplicity (which is always legal).
if bytes_to_send + padding_bytes > _MIN_SETTINGS_MAX_FRAME_SIZE:
raise ValueError("overload: sending %d" % (bytes_to_send + padding_bytes))
data = self._data_to_send[self._send_offset : self._send_offset + bytes_to_send]
try:
self._conn.send_data(stream_id, data, False)
self._conn.send_data(stream_id, data, end_stream=False, pad_length=pad_length)
except h2.exceptions.ProtocolError:
logging.info('Stream %d is closed' % stream_id)
break
Expand Down Expand Up @@ -200,5 +209,5 @@ def parse_received_data(self, stream_id):
req_proto_str = recv_buffer[5:5+grpc_msg_size]
sr = messages_pb2.SimpleRequest()
sr.ParseFromString(req_proto_str)
logging.info('Parsed request for stream %d: response_size=%s' % (stream_id, sr.response_size))
logging.info('Parsed simple request for stream %d' % stream_id)
return sr
10 changes: 9 additions & 1 deletion test/http2_test/http2_test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import test_rst_after_data
import test_rst_after_header
import test_rst_during_data
import test_data_frame_padding

_TEST_CASE_MAPPING = {
'rst_after_header': test_rst_after_header.TestcaseRstStreamAfterHeader,
Expand All @@ -52,6 +53,10 @@
'goaway': test_goaway.TestcaseGoaway,
'ping': test_ping.TestcasePing,
'max_streams': test_max_streams.TestcaseSettingsMaxStreams,

# Positive tests below:
'data_frame_padding': test_data_frame_padding.TestDataFramePadding,
'no_df_padding_sanity_test': test_data_frame_padding.TestDataFramePadding,
}

_exit_code = 0
Expand All @@ -73,6 +78,8 @@ def buildProtocol(self, addr):

if self._testcase == 'goaway':
return t(self._num_streams).get_base_server()
elif self._testcase == 'no_df_padding_sanity_test':
return t(use_padding=False).get_base_server()
else:
return t().get_base_server()

Expand All @@ -81,7 +88,8 @@ def parse_arguments():
parser.add_argument('--base_port', type=int, default=8080,
help='base port to run the servers (default: 8080). One test server is '
'started on each incrementing port, beginning with base_port, in the '
'following order: goaway,max_streams,ping,rst_after_data,rst_after_header,'
'following order: data_frame_padding,goaway,max_streams,'
'no_df_padding_sanity_test,ping,rst_after_data,rst_after_header,'
'rst_during_data'
)
return parser.parse_args()
Expand Down
94 changes: 94 additions & 0 deletions test/http2_test/test_data_frame_padding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright 2016, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import http2_base_server
import logging
import messages_pb2

# Set the number of padding bytes per data frame to be very large
# relative to the number of data bytes for each data frame sent.
_LARGE_PADDING_LENGTH = 255
_SMALL_READ_CHUNK_SIZE = 5

class TestDataFramePadding(object):
"""
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.
"""
def __init__(self, use_padding=True):
self._base_server = http2_base_server.H2ProtocolBaseServer()
self._base_server._handlers['DataReceived'] = self.on_data_received
self._base_server._handlers['WindowUpdated'] = self.on_window_update
self._base_server._handlers['RequestReceived'] = self.on_request_received

# _total_updates maps stream ids to total flow control updates received
self._total_updates = {}
# zero window updates so far for connection window (stream id '0')
self._total_updates[0] = 0
self._read_chunk_size = _SMALL_READ_CHUNK_SIZE

if use_padding:
self._pad_length = _LARGE_PADDING_LENGTH
else:
self._pad_length = None

def get_base_server(self):
return self._base_server

def on_data_received(self, event):
logging.info('on data received. Stream id: %d. Data length: %d' % (event.stream_id, len(event.data)))
self._base_server.on_data_received_default(event)
if len(event.data) == 0:
return
sr = self._base_server.parse_received_data(event.stream_id)
stream_bytes = ''
# Check if full grpc msg has been read into the recv buffer yet
if sr:
response_data = self._base_server.default_response_data(sr.response_size)
logging.info('Stream id: %d. total resp size: %d' % (event.stream_id, len(response_data)))
# Begin sending the response. Add ``self._pad_length`` padding to each
# data frame and split the whole message into data frames each carrying
# only self._read_chunk_size of data.
# The purpose is to have the majority of the data frame response bytes
# be padding bytes, since ``self._pad_length`` >> ``self._read_chunk_size``.
self._base_server.setup_send(response_data , event.stream_id, pad_length=self._pad_length, read_chunk_size=self._read_chunk_size)

def on_request_received(self, event):
self._base_server.on_request_received_default(event)
logging.info('on request received. Stream id: %s.' % event.stream_id)
self._total_updates[event.stream_id] = 0

# Log debug info and try to resume sending on all currently active streams.
def on_window_update(self, event):
logging.info('on window update. Stream id: %s. Delta: %s' % (event.stream_id, event.delta))
self._total_updates[event.stream_id] += event.delta
total = self._total_updates[event.stream_id]
logging.info('... - total updates for stream %d : %d' % (event.stream_id, total))
self._base_server.on_window_update_default(event, pad_length=self._pad_length, read_chunk_size=self._read_chunk_size)
2 changes: 1 addition & 1 deletion tools/doxygen/Doxyfile.c++
Original file line number Diff line number Diff line change
Expand Up @@ -780,11 +780,11 @@ doc/fail_fast.md \
doc/g_stands_for.md \
doc/health-checking.md \
doc/http-grpc-status-mapping.md \
doc/http2-interop-test-descriptions.md \
doc/internationalization.md \
doc/interop-test-descriptions.md \
doc/load-balancing.md \
doc/naming.md \
doc/negative-http2-interop-test-descriptions.md \
doc/server-reflection.md \
doc/server_reflection_tutorial.md \
doc/server_side_auth.md \
Expand Down
2 changes: 1 addition & 1 deletion tools/doxygen/Doxyfile.c++.internal
Original file line number Diff line number Diff line change
Expand Up @@ -780,11 +780,11 @@ doc/fail_fast.md \
doc/g_stands_for.md \
doc/health-checking.md \
doc/http-grpc-status-mapping.md \
doc/http2-interop-test-descriptions.md \
doc/internationalization.md \
doc/interop-test-descriptions.md \
doc/load-balancing.md \
doc/naming.md \
doc/negative-http2-interop-test-descriptions.md \
doc/server-reflection.md \
doc/server_reflection_tutorial.md \
doc/server_side_auth.md \
Expand Down
2 changes: 1 addition & 1 deletion tools/doxygen/Doxyfile.core
Original file line number Diff line number Diff line change
Expand Up @@ -780,11 +780,11 @@ doc/fail_fast.md \
doc/g_stands_for.md \
doc/health-checking.md \
doc/http-grpc-status-mapping.md \
doc/http2-interop-test-descriptions.md \
doc/internationalization.md \
doc/interop-test-descriptions.md \
doc/load-balancing.md \
doc/naming.md \
doc/negative-http2-interop-test-descriptions.md \
doc/server-reflection.md \
doc/server_reflection_tutorial.md \
doc/server_side_auth.md \
Expand Down
2 changes: 1 addition & 1 deletion tools/doxygen/Doxyfile.core.internal
Original file line number Diff line number Diff line change
Expand Up @@ -780,11 +780,11 @@ doc/fail_fast.md \
doc/g_stands_for.md \
doc/health-checking.md \
doc/http-grpc-status-mapping.md \
doc/http2-interop-test-descriptions.md \
doc/internationalization.md \
doc/interop-test-descriptions.md \
doc/load-balancing.md \
doc/naming.md \
doc/negative-http2-interop-test-descriptions.md \
doc/server-reflection.md \
doc/server_reflection_tutorial.md \
doc/server_side_auth.md \
Expand Down
2 changes: 1 addition & 1 deletion tools/internal_ci/linux/grpc_interop_badserver_java.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ cd $(dirname $0)/../../..

git submodule update --init

tools/run_tests/run_interop_tests.py -l java --use_docker --http2_badserver_interop $@
tools/run_tests/run_interop_tests.py -l java --use_docker --http2_server_interop $@

2 changes: 1 addition & 1 deletion tools/internal_ci/linux/grpc_interop_badserver_python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ cd $(dirname $0)/../../..

git submodule update --init

tools/run_tests/run_interop_tests.py -l python --use_docker --http2_badserver_interop $@
tools/run_tests/run_interop_tests.py -l python --use_docker --http2_server_interop $@

2 changes: 1 addition & 1 deletion tools/jenkins/run_interop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ export LANG=en_US.UTF-8
# Enter the gRPC repo root
cd $(dirname $0)/../..

tools/run_tests/run_interop_tests.py -l all -s all --cloud_to_prod --cloud_to_prod_auth --use_docker --http2_interop --http2_badserver_interop -t -j 12 $@ || true
tools/run_tests/run_interop_tests.py -l all -s all --cloud_to_prod --cloud_to_prod_auth --use_docker --http2_interop --http2_server_interop -t -j 12 $@ || true
10 changes: 5 additions & 5 deletions tools/run_tests/interop/interop_html_report.template
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,19 @@
% endfor
% endif

% if http2_badserver_cases:
<h2>HTTP/2 Bad Server Tests</h2>
% if http2_server_cases:
<h2>HTTP/2 Server Tests</h2>
## Each column header is the client language.
<table style="width:100%" border="1">
<tr bgcolor="#00BFFF">
<th>Client languages &#9658;<br/>Test Cases &#9660;</th>
% for client_lang in client_langs_http2_badserver_cases:
% for client_lang in client_langs:
<th>${client_lang}</th>
% endfor
</tr>
% for test_case in http2_badserver_cases:
% for test_case in http2_server_cases:
<tr><td><b>${test_case}</b></td>
% for client_lang in client_langs_http2_badserver_cases:
% for client_lang in client_langs:
<%
shortname = 'cloud_to_cloud:%s:http2_server:%s' % (client_lang,
test_case)
Expand Down
Loading

0 comments on commit 5c8a47e

Please sign in to comment.