|
|
|
@ -56,17 +56,28 @@ _TEST_CASES = [ |
|
|
|
|
'secondary_locality_gets_requests_on_primary_failure', |
|
|
|
|
'traffic_splitting', |
|
|
|
|
] |
|
|
|
|
# Valid test cases, but not in all. So the tests can only run manually, and |
|
|
|
|
# aren't enabled automatically for all languages. |
|
|
|
|
# |
|
|
|
|
# TODO: Move them into _TEST_CASES when support is ready in all languages. |
|
|
|
|
_ADDITIONAL_TEST_CASES = ['path_matching', 'header_matching'] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_test_cases(arg): |
|
|
|
|
if arg == 'all': |
|
|
|
|
return _TEST_CASES |
|
|
|
|
if arg == '': |
|
|
|
|
return [] |
|
|
|
|
test_cases = arg.split(',') |
|
|
|
|
if all([test_case in _TEST_CASES for test_case in test_cases]): |
|
|
|
|
return test_cases |
|
|
|
|
raise Exception('Failed to parse test cases %s' % arg) |
|
|
|
|
arg_split = arg.split(',') |
|
|
|
|
test_cases = set() |
|
|
|
|
all_test_cases = _TEST_CASES + _ADDITIONAL_TEST_CASES |
|
|
|
|
for arg in arg_split: |
|
|
|
|
if arg == "all": |
|
|
|
|
test_cases = test_cases.union(_TEST_CASES) |
|
|
|
|
else: |
|
|
|
|
test_cases = test_cases.union([arg]) |
|
|
|
|
if not all([test_case in all_test_cases for test_case in test_cases]): |
|
|
|
|
raise Exception('Failed to parse test cases %s' % arg) |
|
|
|
|
# Perserve order. |
|
|
|
|
return [x for x in all_test_cases if x in test_cases] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_port_range(port_arg): |
|
|
|
@ -89,8 +100,10 @@ argp.add_argument( |
|
|
|
|
'--test_case', |
|
|
|
|
default='ping_pong', |
|
|
|
|
type=parse_test_cases, |
|
|
|
|
help='Comma-separated list of test cases to run, or \'all\' to run every ' |
|
|
|
|
'test. Available tests: %s' % ' '.join(_TEST_CASES)) |
|
|
|
|
help='Comma-separated list of test cases to run. Available tests: %s, ' |
|
|
|
|
'(or \'all\' to run every test). ' |
|
|
|
|
'Alternative tests not included in \'all\': %s' % |
|
|
|
|
(','.join(_TEST_CASES), ','.join(_ADDITIONAL_TEST_CASES))) |
|
|
|
|
argp.add_argument( |
|
|
|
|
'--bootstrap_file', |
|
|
|
|
default='', |
|
|
|
@ -237,6 +250,12 @@ _BOOTSTRAP_TEMPLATE = """ |
|
|
|
|
_TESTS_TO_FAIL_ON_RPC_FAILURE = [ |
|
|
|
|
'new_instance_group_receives_traffic', 'ping_pong', 'round_robin' |
|
|
|
|
] |
|
|
|
|
# Tests that run UnaryCall and EmptyCall. |
|
|
|
|
_TESTS_TO_RUN_MULTIPLE_RPCS = ['path_matching', 'header_matching'] |
|
|
|
|
# Tests that make UnaryCall with test metadata. |
|
|
|
|
_TESTS_TO_SEND_METADATA = ['header_matching'] |
|
|
|
|
_TEST_METADATA_KEY = 'xds_md' |
|
|
|
|
_TEST_METADATA_VALUE = 'exact_match' |
|
|
|
|
_PATH_MATCHER_NAME = 'path-matcher' |
|
|
|
|
_BASE_TEMPLATE_NAME = 'test-template' |
|
|
|
|
_BASE_INSTANCE_GROUP_NAME = 'test-ig' |
|
|
|
@ -348,6 +367,29 @@ def compare_distributions(actual_distribution, expected_distribution, |
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compare_expected_instances(stats, expected_instances): |
|
|
|
|
"""Compare if stats have expected instances for each type of RPC. |
|
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
stats: LoadBalancerStatsResponse reported by interop client. |
|
|
|
|
expected_instances: a dict with key as the RPC type (string), value as |
|
|
|
|
the expected backend instances (list of strings). |
|
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
Returns true if the instances are expected. False if not. |
|
|
|
|
""" |
|
|
|
|
for rpc_type, expected_peers in expected_instances.items(): |
|
|
|
|
rpcs_by_peer_for_type = stats.rpcs_by_method[rpc_type] |
|
|
|
|
rpcs_by_peer = rpcs_by_peer_for_type.rpcs_by_peer if rpcs_by_peer_for_type else None |
|
|
|
|
logger.debug('rpc: %s, by_peer: %s', rpc_type, rpcs_by_peer) |
|
|
|
|
peers = list(rpcs_by_peer.keys()) |
|
|
|
|
if set(peers) != set(expected_peers): |
|
|
|
|
logger.info('unexpected peers for %s, got %s, want %s', rpc_type, |
|
|
|
|
peers, expected_peers) |
|
|
|
|
return False |
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_backends_restart(gcp, backend_service, instance_group): |
|
|
|
|
logger.info('Running test_backends_restart') |
|
|
|
|
instance_names = get_instance_names(gcp, instance_group) |
|
|
|
@ -629,19 +671,20 @@ def test_secondary_locality_gets_requests_on_primary_failure( |
|
|
|
|
patch_backend_instances(gcp, backend_service, [primary_instance_group]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_traffic_splitting(gcp, original_backend_service, instance_group, |
|
|
|
|
alternate_backend_service, same_zone_instance_group): |
|
|
|
|
# This test start with all traffic going to original_backend_service. Then |
|
|
|
|
# it updates URL-map to set default action to traffic splitting between |
|
|
|
|
# original and alternate. It waits for all backends in both services to |
|
|
|
|
# receive traffic, then verifies that weights are expected. |
|
|
|
|
logger.info('Running test_traffic_splitting') |
|
|
|
|
def prepare_services_for_urlmap_tests(gcp, original_backend_service, |
|
|
|
|
instance_group, alternate_backend_service, |
|
|
|
|
same_zone_instance_group): |
|
|
|
|
''' |
|
|
|
|
This function prepares the services to be ready for tests that modifies |
|
|
|
|
urlmaps. |
|
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
Returns original and alternate backend names as lists of strings. |
|
|
|
|
''' |
|
|
|
|
# The config validation for proxyless doesn't allow setting |
|
|
|
|
# default_route_action. To test traffic splitting, we need to set the |
|
|
|
|
# route action to weighted clusters. Disable validate |
|
|
|
|
# validate_for_proxyless for this test. This can be removed when |
|
|
|
|
# validation accepts default_route_action. |
|
|
|
|
# default_route_action or route_rules. Disable validate |
|
|
|
|
# validate_for_proxyless for this test. This can be removed when validation |
|
|
|
|
# accepts default_route_action. |
|
|
|
|
logger.info('disabling validate_for_proxyless in target proxy') |
|
|
|
|
set_validate_for_proxyless(gcp, False) |
|
|
|
|
|
|
|
|
@ -665,6 +708,20 @@ def test_traffic_splitting(gcp, original_backend_service, instance_group, |
|
|
|
|
logger.info('waiting for traffic to all go to original backends') |
|
|
|
|
wait_until_all_rpcs_go_to_given_backends(original_backend_instances, |
|
|
|
|
_WAIT_FOR_STATS_SEC) |
|
|
|
|
return original_backend_instances, alternate_backend_instances |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_traffic_splitting(gcp, original_backend_service, instance_group, |
|
|
|
|
alternate_backend_service, same_zone_instance_group): |
|
|
|
|
# This test start with all traffic going to original_backend_service. Then |
|
|
|
|
# it updates URL-map to set default action to traffic splitting between |
|
|
|
|
# original and alternate. It waits for all backends in both services to |
|
|
|
|
# receive traffic, then verifies that weights are expected. |
|
|
|
|
logger.info('Running test_traffic_splitting') |
|
|
|
|
|
|
|
|
|
original_backend_instances, alternate_backend_instances = prepare_services_for_urlmap_tests( |
|
|
|
|
gcp, original_backend_service, instance_group, |
|
|
|
|
alternate_backend_service, same_zone_instance_group) |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
# Patch urlmap, change route action to traffic splitting between |
|
|
|
@ -728,6 +785,157 @@ def test_traffic_splitting(gcp, original_backend_service, instance_group, |
|
|
|
|
set_validate_for_proxyless(gcp, True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_path_matching(gcp, original_backend_service, instance_group, |
|
|
|
|
alternate_backend_service, same_zone_instance_group): |
|
|
|
|
# This test start with all traffic (UnaryCall and EmptyCall) going to |
|
|
|
|
# original_backend_service. |
|
|
|
|
# |
|
|
|
|
# Then it updates URL-map to add routes, to make UnaryCall and EmptyCall to |
|
|
|
|
# go different backends. It waits for all backends in both services to |
|
|
|
|
# receive traffic, then verifies that traffic goes to the expected |
|
|
|
|
# backends. |
|
|
|
|
logger.info('Running test_path_matching') |
|
|
|
|
|
|
|
|
|
original_backend_instances, alternate_backend_instances = prepare_services_for_urlmap_tests( |
|
|
|
|
gcp, original_backend_service, instance_group, |
|
|
|
|
alternate_backend_service, same_zone_instance_group) |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
# A list of tuples (route_rules, expected_instances). |
|
|
|
|
test_cases = [ |
|
|
|
|
( |
|
|
|
|
[{ |
|
|
|
|
'priority': 0, |
|
|
|
|
# FullPath EmptyCall -> alternate_backend_service. |
|
|
|
|
'matchRules': [{ |
|
|
|
|
'fullPathMatch': '/grpc.testing.TestService/EmptyCall' |
|
|
|
|
}], |
|
|
|
|
'service': alternate_backend_service.url |
|
|
|
|
}], |
|
|
|
|
{ |
|
|
|
|
"EmptyCall": alternate_backend_instances, |
|
|
|
|
"UnaryCall": original_backend_instances |
|
|
|
|
}), |
|
|
|
|
( |
|
|
|
|
[{ |
|
|
|
|
'priority': 0, |
|
|
|
|
# Prefix UnaryCall -> alternate_backend_service. |
|
|
|
|
'matchRules': [{ |
|
|
|
|
'prefixMatch': '/grpc.testing.TestService/Unary' |
|
|
|
|
}], |
|
|
|
|
'service': alternate_backend_service.url |
|
|
|
|
}], |
|
|
|
|
{ |
|
|
|
|
"UnaryCall": alternate_backend_instances, |
|
|
|
|
"EmptyCall": original_backend_instances |
|
|
|
|
}) |
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
for (route_rules, expected_instances) in test_cases: |
|
|
|
|
logger.info('patching url map with %s -> alternative', |
|
|
|
|
route_rules[0]['matchRules']) |
|
|
|
|
patch_url_map_backend_service(gcp, |
|
|
|
|
original_backend_service, |
|
|
|
|
route_rules=route_rules) |
|
|
|
|
|
|
|
|
|
# Wait for traffic to go to both services. |
|
|
|
|
logger.info( |
|
|
|
|
'waiting for traffic to go to all backends (including alternate)' |
|
|
|
|
) |
|
|
|
|
wait_until_all_rpcs_go_to_given_backends( |
|
|
|
|
original_backend_instances + alternate_backend_instances, |
|
|
|
|
_WAIT_FOR_STATS_SEC) |
|
|
|
|
|
|
|
|
|
retry_count = 10 |
|
|
|
|
# Each attempt takes about 10 seconds, 10 retries is equivalent to 100 |
|
|
|
|
# seconds timeout. |
|
|
|
|
for i in range(retry_count): |
|
|
|
|
stats = get_client_stats(_NUM_TEST_RPCS, _WAIT_FOR_STATS_SEC) |
|
|
|
|
if not stats.rpcs_by_method: |
|
|
|
|
raise ValueError( |
|
|
|
|
'stats.rpcs_by_method is None, the interop client stats service does not support this test case' |
|
|
|
|
) |
|
|
|
|
logger.info('attempt %d', i) |
|
|
|
|
if compare_expected_instances(stats, expected_instances): |
|
|
|
|
logger.info("success") |
|
|
|
|
break |
|
|
|
|
finally: |
|
|
|
|
patch_url_map_backend_service(gcp, original_backend_service) |
|
|
|
|
patch_backend_instances(gcp, alternate_backend_service, []) |
|
|
|
|
set_validate_for_proxyless(gcp, True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_matching(gcp, original_backend_service, instance_group, |
|
|
|
|
alternate_backend_service, same_zone_instance_group): |
|
|
|
|
# This test start with all traffic (UnaryCall and EmptyCall) going to |
|
|
|
|
# original_backend_service. |
|
|
|
|
# |
|
|
|
|
# Then it updates URL-map to add routes, to make RPCs with test headers to |
|
|
|
|
# go to different backends. It waits for all backends in both services to |
|
|
|
|
# receive traffic, then verifies that traffic goes to the expected |
|
|
|
|
# backends. |
|
|
|
|
logger.info('Running test_header_matching') |
|
|
|
|
|
|
|
|
|
original_backend_instances, alternate_backend_instances = prepare_services_for_urlmap_tests( |
|
|
|
|
gcp, original_backend_service, instance_group, |
|
|
|
|
alternate_backend_service, same_zone_instance_group) |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
# A list of tuples (route_rules, expected_instances). |
|
|
|
|
test_cases = [( |
|
|
|
|
[{ |
|
|
|
|
'priority': 0, |
|
|
|
|
# Header ExactMatch -> alternate_backend_service. |
|
|
|
|
# EmptyCall is sent with the metadata. |
|
|
|
|
'matchRules': [{ |
|
|
|
|
'prefixMatch': |
|
|
|
|
'/', |
|
|
|
|
'headerMatches': [{ |
|
|
|
|
'headerName': _TEST_METADATA_KEY, |
|
|
|
|
'exactMatch': _TEST_METADATA_VALUE |
|
|
|
|
}] |
|
|
|
|
}], |
|
|
|
|
'service': alternate_backend_service.url |
|
|
|
|
}], |
|
|
|
|
{ |
|
|
|
|
"EmptyCall": alternate_backend_instances, |
|
|
|
|
"UnaryCall": original_backend_instances |
|
|
|
|
})] |
|
|
|
|
|
|
|
|
|
for (route_rules, expected_instances) in test_cases: |
|
|
|
|
logger.info('patching url map with %s -> alternative', |
|
|
|
|
route_rules[0]['matchRules']) |
|
|
|
|
patch_url_map_backend_service(gcp, |
|
|
|
|
original_backend_service, |
|
|
|
|
route_rules=route_rules) |
|
|
|
|
|
|
|
|
|
# Wait for traffic to go to both services. |
|
|
|
|
logger.info( |
|
|
|
|
'waiting for traffic to go to all backends (including alternate)' |
|
|
|
|
) |
|
|
|
|
wait_until_all_rpcs_go_to_given_backends( |
|
|
|
|
original_backend_instances + alternate_backend_instances, |
|
|
|
|
_WAIT_FOR_STATS_SEC) |
|
|
|
|
|
|
|
|
|
retry_count = 10 |
|
|
|
|
# Each attempt takes about 10 seconds, 10 retries is equivalent to 100 |
|
|
|
|
# seconds timeout. |
|
|
|
|
for i in range(retry_count): |
|
|
|
|
stats = get_client_stats(_NUM_TEST_RPCS, _WAIT_FOR_STATS_SEC) |
|
|
|
|
if not stats.rpcs_by_method: |
|
|
|
|
raise ValueError( |
|
|
|
|
'stats.rpcs_by_method is None, the interop client stats service does not support this test case' |
|
|
|
|
) |
|
|
|
|
logger.info('attempt %d', i) |
|
|
|
|
if compare_expected_instances(stats, expected_instances): |
|
|
|
|
logger.info("success") |
|
|
|
|
break |
|
|
|
|
finally: |
|
|
|
|
patch_url_map_backend_service(gcp, original_backend_service) |
|
|
|
|
patch_backend_instances(gcp, alternate_backend_service, []) |
|
|
|
|
set_validate_for_proxyless(gcp, True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_serving_status(instances, service_port, serving): |
|
|
|
|
for instance in instances: |
|
|
|
|
with grpc.insecure_channel('%s:%d' % |
|
|
|
@ -1208,7 +1416,8 @@ def resize_instance_group(gcp, |
|
|
|
|
|
|
|
|
|
def patch_url_map_backend_service(gcp, |
|
|
|
|
backend_service=None, |
|
|
|
|
services_with_weights=None): |
|
|
|
|
services_with_weights=None, |
|
|
|
|
route_rules=None): |
|
|
|
|
'''change url_map's backend service |
|
|
|
|
|
|
|
|
|
Only one of backend_service and service_with_weights can be not None. |
|
|
|
@ -1230,6 +1439,7 @@ def patch_url_map_backend_service(gcp, |
|
|
|
|
'name': _PATH_MATCHER_NAME, |
|
|
|
|
'defaultService': default_service, |
|
|
|
|
'defaultRouteAction': default_route_action, |
|
|
|
|
'routeRules': route_rules, |
|
|
|
|
}] |
|
|
|
|
} |
|
|
|
|
logger.debug('Sending GCP request with body=%s', config) |
|
|
|
@ -1504,22 +1714,41 @@ try: |
|
|
|
|
test_log_filename = os.path.join(log_dir, _SPONGE_LOG_NAME) |
|
|
|
|
test_log_file = open(test_log_filename, 'w+') |
|
|
|
|
client_process = None |
|
|
|
|
|
|
|
|
|
if test_case in _TESTS_TO_RUN_MULTIPLE_RPCS: |
|
|
|
|
rpcs_to_send = '--rpc="UnaryCall,EmptyCall"' |
|
|
|
|
else: |
|
|
|
|
rpcs_to_send = '--rpc="UnaryCall"' |
|
|
|
|
|
|
|
|
|
if test_case in _TESTS_TO_SEND_METADATA: |
|
|
|
|
metadata_to_send = '--metadata="EmptyCall:{key}:{value}"'.format( |
|
|
|
|
key=_TEST_METADATA_KEY, value=_TEST_METADATA_VALUE) |
|
|
|
|
else: |
|
|
|
|
metadata_to_send = '--metadata=""' |
|
|
|
|
|
|
|
|
|
if test_case in _TESTS_TO_FAIL_ON_RPC_FAILURE: |
|
|
|
|
wait_for_config_propagation( |
|
|
|
|
gcp, instance_group, |
|
|
|
|
args.client_cmd.format(server_uri=server_uri, |
|
|
|
|
stats_port=args.stats_port, |
|
|
|
|
qps=args.qps, |
|
|
|
|
fail_on_failed_rpc=False), |
|
|
|
|
fail_on_failed_rpc=False, |
|
|
|
|
rpcs_to_send=rpcs_to_send, |
|
|
|
|
metadata_to_send=metadata_to_send), |
|
|
|
|
client_env) |
|
|
|
|
fail_on_failed_rpc = '--fail_on_failed_rpc=true' |
|
|
|
|
else: |
|
|
|
|
fail_on_failed_rpc = '--fail_on_failed_rpc=false' |
|
|
|
|
client_cmd = shlex.split( |
|
|
|
|
args.client_cmd.format(server_uri=server_uri, |
|
|
|
|
stats_port=args.stats_port, |
|
|
|
|
qps=args.qps, |
|
|
|
|
fail_on_failed_rpc=fail_on_failed_rpc)) |
|
|
|
|
|
|
|
|
|
client_cmd_formatted = args.client_cmd.format( |
|
|
|
|
server_uri=server_uri, |
|
|
|
|
stats_port=args.stats_port, |
|
|
|
|
qps=args.qps, |
|
|
|
|
fail_on_failed_rpc=fail_on_failed_rpc, |
|
|
|
|
rpcs_to_send=rpcs_to_send, |
|
|
|
|
metadata_to_send=metadata_to_send) |
|
|
|
|
logger.debug('running client: %s', client_cmd_formatted) |
|
|
|
|
client_cmd = shlex.split(client_cmd_formatted) |
|
|
|
|
try: |
|
|
|
|
client_process = subprocess.Popen(client_cmd, |
|
|
|
|
env=client_env, |
|
|
|
@ -1559,6 +1788,14 @@ try: |
|
|
|
|
test_traffic_splitting(gcp, backend_service, instance_group, |
|
|
|
|
alternate_backend_service, |
|
|
|
|
same_zone_instance_group) |
|
|
|
|
elif test_case == 'path_matching': |
|
|
|
|
test_path_matching(gcp, backend_service, instance_group, |
|
|
|
|
alternate_backend_service, |
|
|
|
|
same_zone_instance_group) |
|
|
|
|
elif test_case == 'header_matching': |
|
|
|
|
test_header_matching(gcp, backend_service, instance_group, |
|
|
|
|
alternate_backend_service, |
|
|
|
|
same_zone_instance_group) |
|
|
|
|
else: |
|
|
|
|
logger.error('Unknown test case: %s', test_case) |
|
|
|
|
sys.exit(1) |
|
|
|
|