1# Copyright 2021 The gRPC Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14import logging
15from typing import Tuple
16
17from absl import flags
18from absl.testing import absltest
19import grpc
20
21from framework import xds_url_map_testcase
22from framework.helpers import skips
23from framework.test_app import client_app
24
25# Type aliases
26HostRule = xds_url_map_testcase.HostRule
27PathMatcher = xds_url_map_testcase.PathMatcher
28GcpResourceManager = xds_url_map_testcase.GcpResourceManager
29DumpedXdsConfig = xds_url_map_testcase.DumpedXdsConfig
30RpcTypeUnaryCall = xds_url_map_testcase.RpcTypeUnaryCall
31XdsTestClient = client_app.XdsTestClient
32ExpectedResult = xds_url_map_testcase.ExpectedResult
33_Lang = skips.Lang
34
35logger = logging.getLogger(__name__)
36flags.adopt_module_key_flags(xds_url_map_testcase)
37
38# The first batch of RPCs don't count towards the result of test case. They are
39# meant to prove the communication between driver and client is fine.
40_NUM_RPCS = 10
41_LENGTH_OF_RPC_SENDING_SEC = 16
42# We are using sleep to synchronize test driver and the client... Even though
43# the client is sending at QPS rate, we can't assert that exactly QPS *
44# SLEEP_DURATION number of RPC is finished. The final completed RPC might be
45# slightly more or less.
46_NON_RANDOM_ERROR_TOLERANCE = 0.01
47_RPC_BEHAVIOR_HEADER_NAME = 'rpc-behavior'
48
49
50def _build_retry_route_rule(retryConditions, num_retries):
51    return {
52        'priority': 0,
53        'matchRules': [{
54            'fullPathMatch': '/grpc.testing.TestService/UnaryCall'
55        }],
56        'service': GcpResourceManager().default_backend_service(),
57        'routeAction': {
58            'retryPolicy': {
59                'retryConditions': retryConditions,
60                'numRetries': num_retries,
61            }
62        },
63    }
64
65
66def _is_supported(config: skips.TestConfig) -> bool:
67    # Per "Retry" in
68    # https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md
69    if config.client_lang in _Lang.CPP | _Lang.JAVA | _Lang.PYTHON:
70        return config.version_gte('v1.40.x')
71    elif config.client_lang == _Lang.GO:
72        return config.version_gte('v1.41.x')
73    elif config.client_lang == _Lang.NODE:
74        return config.version_gte('v1.8.x')
75    return True
76
77
78class TestRetryUpTo3AttemptsAndFail(xds_url_map_testcase.XdsUrlMapTestCase):
79
80    @staticmethod
81    def is_supported(config: skips.TestConfig) -> bool:
82        return _is_supported(config)
83
84    @staticmethod
85    def url_map_change(
86            host_rule: HostRule,
87            path_matcher: PathMatcher) -> Tuple[HostRule, PathMatcher]:
88        path_matcher["routeRules"] = [
89            _build_retry_route_rule(retryConditions=["unavailable"],
90                                    num_retries=3)
91        ]
92        return host_rule, path_matcher
93
94    def xds_config_validate(self, xds_config: DumpedXdsConfig):
95        self.assertNumEndpoints(xds_config, 1)
96        retry_config = xds_config.rds['virtualHosts'][0]['routes'][0]['route'][
97            'retryPolicy']
98        self.assertEqual(3, retry_config['numRetries'])
99        self.assertEqual('unavailable', retry_config['retryOn'])
100
101    def rpc_distribution_validate(self, test_client: XdsTestClient):
102        self.configure_and_send(test_client,
103                                rpc_types=(RpcTypeUnaryCall,),
104                                metadata=[
105                                    (RpcTypeUnaryCall,
106                                     _RPC_BEHAVIOR_HEADER_NAME,
107                                     'succeed-on-retry-attempt-4,error-code-14')
108                                ],
109                                num_rpcs=_NUM_RPCS)
110        self.assertRpcStatusCode(test_client,
111                                 expected=(ExpectedResult(
112                                     rpc_type=RpcTypeUnaryCall,
113                                     status_code=grpc.StatusCode.UNAVAILABLE,
114                                     ratio=1),),
115                                 length=_LENGTH_OF_RPC_SENDING_SEC,
116                                 tolerance=_NON_RANDOM_ERROR_TOLERANCE)
117
118
119class TestRetryUpTo4AttemptsAndSucceed(xds_url_map_testcase.XdsUrlMapTestCase):
120
121    @staticmethod
122    def is_supported(config: skips.TestConfig) -> bool:
123        return _is_supported(config)
124
125    @staticmethod
126    def url_map_change(
127            host_rule: HostRule,
128            path_matcher: PathMatcher) -> Tuple[HostRule, PathMatcher]:
129        path_matcher["routeRules"] = [
130            _build_retry_route_rule(retryConditions=["unavailable"],
131                                    num_retries=4)
132        ]
133        return host_rule, path_matcher
134
135    def xds_config_validate(self, xds_config: DumpedXdsConfig):
136        self.assertNumEndpoints(xds_config, 1)
137        retry_config = xds_config.rds['virtualHosts'][0]['routes'][0]['route'][
138            'retryPolicy']
139        self.assertEqual(4, retry_config['numRetries'])
140        self.assertEqual('unavailable', retry_config['retryOn'])
141
142    def rpc_distribution_validate(self, test_client: XdsTestClient):
143        self.configure_and_send(test_client,
144                                rpc_types=(RpcTypeUnaryCall,),
145                                metadata=[
146                                    (RpcTypeUnaryCall,
147                                     _RPC_BEHAVIOR_HEADER_NAME,
148                                     'succeed-on-retry-attempt-4,error-code-14')
149                                ],
150                                num_rpcs=_NUM_RPCS)
151        self.assertRpcStatusCode(test_client,
152                                 expected=(ExpectedResult(
153                                     rpc_type=RpcTypeUnaryCall,
154                                     status_code=grpc.StatusCode.OK,
155                                     ratio=1),),
156                                 length=_LENGTH_OF_RPC_SENDING_SEC,
157                                 tolerance=_NON_RANDOM_ERROR_TOLERANCE)
158
159
160if __name__ == '__main__':
161    absltest.main()
162