mirror of https://github.com/grpc/grpc.git
Fix Python xDS Interop Client Time Slicing (#29423)
* Initial unit testbench * Tentative fix * Reduce scope of fix. Clean up test * Formatting * Protect writing to RPC status store * Don't start client until server is running * Typo * Remove redundant log * More formatting * Review commentspull/29448/head
parent
667c81d9dc
commit
be1b4a6d3a
3 changed files with 209 additions and 11 deletions
@ -0,0 +1,182 @@ |
|||||||
|
# Copyright 2022 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. |
||||||
|
|
||||||
|
import collections |
||||||
|
import contextlib |
||||||
|
import logging |
||||||
|
import os |
||||||
|
import subprocess |
||||||
|
import sys |
||||||
|
import tempfile |
||||||
|
import time |
||||||
|
from typing import Iterable, List, Mapping, Set, Tuple |
||||||
|
import unittest |
||||||
|
|
||||||
|
import grpc.experimental |
||||||
|
import xds_interop_client |
||||||
|
import xds_interop_server |
||||||
|
|
||||||
|
from src.proto.grpc.testing import empty_pb2 |
||||||
|
from src.proto.grpc.testing import messages_pb2 |
||||||
|
from src.proto.grpc.testing import test_pb2 |
||||||
|
from src.proto.grpc.testing import test_pb2_grpc |
||||||
|
import src.python.grpcio_tests.tests.unit.framework.common as framework_common |
||||||
|
|
||||||
|
_CLIENT_PATH = os.path.abspath(os.path.realpath(xds_interop_client.__file__)) |
||||||
|
_SERVER_PATH = os.path.abspath(os.path.realpath(xds_interop_server.__file__)) |
||||||
|
|
||||||
|
_METHODS = ( |
||||||
|
(messages_pb2.ClientConfigureRequest.UNARY_CALL, "UNARY_CALL"), |
||||||
|
(messages_pb2.ClientConfigureRequest.EMPTY_CALL, "EMPTY_CALL"), |
||||||
|
) |
||||||
|
|
||||||
|
_QPS = 100 |
||||||
|
_NUM_CHANNELS = 20 |
||||||
|
|
||||||
|
_TEST_ITERATIONS = 10 |
||||||
|
_ITERATION_DURATION_SECONDS = 1 |
||||||
|
_SUBPROCESS_TIMEOUT_SECONDS = 2 |
||||||
|
|
||||||
|
|
||||||
|
def _set_union(a: Iterable, b: Iterable) -> Set: |
||||||
|
c = set(a) |
||||||
|
c.update(b) |
||||||
|
return c |
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager |
||||||
|
def _start_python_with_args( |
||||||
|
file: str, args: List[str] |
||||||
|
) -> Tuple[subprocess.Popen, tempfile.TemporaryFile, tempfile.TemporaryFile]: |
||||||
|
with tempfile.TemporaryFile(mode='r') as stdout: |
||||||
|
with tempfile.TemporaryFile(mode='r') as stderr: |
||||||
|
proc = subprocess.Popen((sys.executable, file) + tuple(args), |
||||||
|
stdout=stdout, |
||||||
|
stderr=stderr) |
||||||
|
yield proc, stdout, stderr |
||||||
|
|
||||||
|
|
||||||
|
def _dump_stream(process_name: str, stream_name: str, |
||||||
|
stream: tempfile.TemporaryFile): |
||||||
|
sys.stderr.write(f"{process_name} {stream_name}:\n") |
||||||
|
stream.seek(0) |
||||||
|
sys.stderr.write(stream.read()) |
||||||
|
|
||||||
|
|
||||||
|
def _dump_streams(process_name: str, stdout: tempfile.TemporaryFile, |
||||||
|
stderr: tempfile.TemporaryFile): |
||||||
|
_dump_stream(process_name, "stdout", stdout) |
||||||
|
_dump_stream(process_name, "stderr", stderr) |
||||||
|
sys.stderr.write(f"End {process_name} output.\n") |
||||||
|
|
||||||
|
|
||||||
|
def _index_accumulated_stats( |
||||||
|
response: messages_pb2.LoadBalancerAccumulatedStatsResponse |
||||||
|
) -> Mapping[str, Mapping[int, int]]: |
||||||
|
indexed = collections.defaultdict(lambda: collections.defaultdict(int)) |
||||||
|
for _, method_str in _METHODS: |
||||||
|
for status in response.stats_per_method[method_str].result.keys(): |
||||||
|
indexed[method_str][status] = response.stats_per_method[ |
||||||
|
method_str].result[status] |
||||||
|
return indexed |
||||||
|
|
||||||
|
|
||||||
|
def _subtract_indexed_stats(a: Mapping[str, Mapping[int, int]], |
||||||
|
b: Mapping[str, Mapping[int, int]]): |
||||||
|
c = collections.defaultdict(lambda: collections.defaultdict(int)) |
||||||
|
all_methods = _set_union(a.keys(), b.keys()) |
||||||
|
for method in all_methods: |
||||||
|
all_statuses = _set_union(a[method].keys(), b[method].keys()) |
||||||
|
for status in all_statuses: |
||||||
|
c[method][status] = a[method][status] - b[method][status] |
||||||
|
return c |
||||||
|
|
||||||
|
|
||||||
|
def _collect_stats(stats_port: int, |
||||||
|
duration: int) -> Mapping[str, Mapping[int, int]]: |
||||||
|
settings = { |
||||||
|
"target": f"localhost:{stats_port}", |
||||||
|
"insecure": True, |
||||||
|
} |
||||||
|
response = test_pb2_grpc.LoadBalancerStatsService.GetClientAccumulatedStats( |
||||||
|
messages_pb2.LoadBalancerAccumulatedStatsRequest(), **settings) |
||||||
|
before = _index_accumulated_stats(response) |
||||||
|
time.sleep(duration) |
||||||
|
response = test_pb2_grpc.LoadBalancerStatsService.GetClientAccumulatedStats( |
||||||
|
messages_pb2.LoadBalancerAccumulatedStatsRequest(), **settings) |
||||||
|
after = _index_accumulated_stats(response) |
||||||
|
return _subtract_indexed_stats(after, before) |
||||||
|
|
||||||
|
|
||||||
|
class XdsInteropClientTest(unittest.TestCase): |
||||||
|
|
||||||
|
def _assert_client_consistent(self, server_port: int, stats_port: int, |
||||||
|
qps: int, num_channels: int): |
||||||
|
settings = { |
||||||
|
"target": f"localhost:{stats_port}", |
||||||
|
"insecure": True, |
||||||
|
} |
||||||
|
for i in range(_TEST_ITERATIONS): |
||||||
|
target_method, target_method_str = _METHODS[i % len(_METHODS)] |
||||||
|
test_pb2_grpc.XdsUpdateClientConfigureService.Configure( |
||||||
|
messages_pb2.ClientConfigureRequest(types=[target_method]), |
||||||
|
**settings) |
||||||
|
delta = _collect_stats(stats_port, _ITERATION_DURATION_SECONDS) |
||||||
|
logging.info("Delta: %s", delta) |
||||||
|
for _, method_str in _METHODS: |
||||||
|
for status in delta[method_str]: |
||||||
|
if status == 0 and method_str == target_method_str: |
||||||
|
self.assertGreater(delta[method_str][status], 0, delta) |
||||||
|
else: |
||||||
|
self.assertEqual(delta[method_str][status], 0, delta) |
||||||
|
|
||||||
|
def test_configure_consistency(self): |
||||||
|
_, server_port, socket = framework_common.get_socket() |
||||||
|
|
||||||
|
with _start_python_with_args( |
||||||
|
_SERVER_PATH, |
||||||
|
[f"--port={server_port}", f"--maintenance_port={server_port}" |
||||||
|
]) as (server, server_stdout, server_stderr): |
||||||
|
# Send RPC to server to make sure it's running. |
||||||
|
logging.info("Sending RPC to server.") |
||||||
|
test_pb2_grpc.TestService.EmptyCall(empty_pb2.Empty(), |
||||||
|
f"localhost:{server_port}", |
||||||
|
insecure=True, |
||||||
|
wait_for_ready=True) |
||||||
|
logging.info("Server successfully started.") |
||||||
|
socket.close() |
||||||
|
_, stats_port, stats_socket = framework_common.get_socket() |
||||||
|
with _start_python_with_args(_CLIENT_PATH, [ |
||||||
|
f"--server=localhost:{server_port}", |
||||||
|
f"--stats_port={stats_port}", f"--qps={_QPS}", |
||||||
|
f"--num_channels={_NUM_CHANNELS}" |
||||||
|
]) as (client, client_stdout, client_stderr): |
||||||
|
stats_socket.close() |
||||||
|
try: |
||||||
|
self._assert_client_consistent(server_port, stats_port, |
||||||
|
_QPS, _NUM_CHANNELS) |
||||||
|
except: |
||||||
|
_dump_streams("server", server_stdout, server_stderr) |
||||||
|
_dump_streams("client", client_stdout, client_stderr) |
||||||
|
raise |
||||||
|
finally: |
||||||
|
server.kill() |
||||||
|
client.kill() |
||||||
|
server.wait(timeout=_SUBPROCESS_TIMEOUT_SECONDS) |
||||||
|
client.wait(timeout=_SUBPROCESS_TIMEOUT_SECONDS) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
logging.basicConfig() |
||||||
|
unittest.main(verbosity=2) |
Loading…
Reference in new issue