Skip to content

Commit a521225

Browse files
p3rf Teamcopybara-github
p3rf Team
authored andcommitted
Remove <curl_time_total: from response even when response is unexpected
PiperOrigin-RevId: 570481465
1 parent 3b0ffec commit a521225

File tree

3 files changed

+115
-83
lines changed

3 files changed

+115
-83
lines changed
Lines changed: 89 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Scripts to poll http endpoint."""
22

3-
import logging
43
import re
54
import subprocess
65
import sys
@@ -13,16 +12,20 @@
1312

1413
_ENDPOINT = flags.DEFINE_string('endpoint', None, 'Endpoint to use.')
1514
_MAX_RETRIES = flags.DEFINE_integer('max_retries', 0, 'Number of retries.')
16-
_RETRY_INTERVAL = flags.DEFINE_float('retry_interval', 0.5,
17-
'Gap in seconds between retries.')
15+
_RETRY_INTERVAL = flags.DEFINE_float(
16+
'retry_interval', 0.5, 'Gap in seconds between retries.'
17+
)
1818
_TIMEOUT = flags.DEFINE_integer('timeout', 600, 'Polling deadline in seconds.')
19-
_EXPECTED_RESPONSE = flags.DEFINE_string('expected_response', '',
20-
'Expected response.')
19+
_EXPECTED_RESPONSE = flags.DEFINE_string(
20+
'expected_response', '', 'Expected response.'
21+
)
2122
_EXPECTED_RESPONSE_CODE = flags.DEFINE_string(
22-
'expected_response_code', r'2\d\d',
23+
'expected_response_code',
24+
r'2\d\d',
2325
'Expected http response code. Can be either a regexp or '
2426
'a integer in string format. '
25-
'By default, any 2xx response code is valid.')
27+
'By default, any 2xx response code is valid.',
28+
)
2629
_HEADERS = flags.DEFINE_list('headers', [], 'Headers applying to requests.')
2730
_HTTP_RESPCODE_REGEXP = r'HTTPS?/[\d\.]*\s+(\d+)'
2831
_CURL_METRICS = '<curl_time_total:%{time_total}>'
@@ -33,8 +36,12 @@
3336
FAILURE_CURL = 'curl_failure'
3437
FAILURE_INVALID_RESPONSE = 'invalid_response'
3538
FAILURE_INVALID_RESPONSE_CODE = 'invalid_response_code'
36-
POLL_STATUS = (POLL_SUCCEED, FAILURE_CURL,
37-
FAILURE_INVALID_RESPONSE, FAILURE_INVALID_RESPONSE_CODE)
39+
POLL_STATUS = (
40+
POLL_SUCCEED,
41+
FAILURE_CURL,
42+
FAILURE_INVALID_RESPONSE,
43+
FAILURE_INVALID_RESPONSE_CODE,
44+
)
3845

3946

4047
def _generate_file_names():
@@ -53,77 +60,89 @@ def _popen(curl_cmd, stdout, stderr, shell):
5360
return subprocess.Popen(curl_cmd, stdout=stdout, stderr=stderr, shell=shell)
5461

5562

56-
def call_curl(endpoint, headers=None, expected_response_code=None,
57-
expected_response=None):
63+
def call_curl(
64+
endpoint, headers=None, expected_response_code=None, expected_response=None
65+
):
5866
"""Curl endpoint once and return response.
5967
6068
Args:
6169
endpoint: string. HTTP(S) endpoint to curl.
6270
headers: list. A list of strings, each representing a header.
63-
expected_response_code: string. Expected http response code. Can be either
64-
a string or a regex.
65-
expected_response: string. Expected http response. Can be either a string
66-
or a regex.
71+
expected_response_code: string. Expected http response code. Can be either a
72+
string or a regex.
73+
expected_response: string. Expected http response. Can be either a string or
74+
a regex.
6775
6876
Returns:
69-
A tuple of float, string, integer and string:
77+
A tuple of float, string, string, and string:
7078
Float indicates latency in second of the polling attempt.
71-
String indicates whether the HTTP endpoint is reachable and respond
79+
First string indicates whether the HTTP endpoint is reachable and respond
7280
with expected responses or the reason of failure;
7381
List of current values are:
7482
[succeed, curl_failure, invalid_response_code, invalid_response]
7583
76-
Integer indicates the actual HTTP response code;
77-
String indicates the actual HTTP response.
84+
Second string is the actual HTTP response.
85+
Third string is additional logged info.
7886
"""
7987
poll_stats = POLL_SUCCEED
8088
outid, errid = _generate_file_names()
8189
header = ''
90+
logs = ''
8291
if headers:
8392
header = '-H %s ' % ' '.join(headers)
8493

8594
curl_cmd = 'curl -w "{metrics}" -v {header}"{endpoint}"'.format(
86-
metrics=_CURL_METRICS,
87-
header=header,
88-
endpoint=endpoint)
95+
metrics=_CURL_METRICS, header=header, endpoint=endpoint
96+
)
8997
with open(outid, 'w+') as stdout:
9098
with open(errid, 'w+') as stderr:
91-
p = _popen(
92-
curl_cmd, stdout=stdout, stderr=stderr, shell=True)
99+
p = _popen(curl_cmd, stdout=stdout, stderr=stderr, shell=True)
93100
retcode = p.wait()
94101
http_resp_code = None
95102
with open(errid, 'r') as stderr:
96103
http_stderr = stderr.read()
97104
for line in http_stderr.split('\n'):
98105
match = re.search(_HTTP_RESPCODE_REGEXP, line)
99-
if match:
106+
if expected_response_code and match:
100107
http_resp_code = match.group(1)
101108
if re.search(expected_response_code, http_resp_code) is None:
102109
poll_stats = FAILURE_INVALID_RESPONSE_CODE
103110
if retcode:
104111
poll_stats = FAILURE_CURL
105112
http_resp_code = '-1'
106-
logging.info(
107-
'Error: received non-zero return code running %s, stderr: %s',
108-
curl_cmd, http_stderr)
113+
logs += (
114+
f'Error: received non-zero return code running {curl_cmd}, '
115+
f'stderr: {http_stderr};'
116+
)
109117
latency = 0
110118
with open(outid, 'r') as stdout:
111119
http_stdout = stdout.read()
112120
if expected_response and (
113-
re.search(expected_response, http_stdout) is None):
121+
re.search(expected_response, http_stdout) is None
122+
):
114123
poll_stats = FAILURE_INVALID_RESPONSE
115-
else:
116-
match = re.search(_CURL_METRICS_REGEXP, http_stdout)
117-
if match:
118-
latency = float(match.group(1))
119-
# Remove <curl_time_total:##> from the response.
120-
http_stdout = http_stdout.split(match.group(0))[0]
121-
122-
return latency or -1, poll_stats, int(http_resp_code), http_stdout
123-
124-
125-
def poll(endpoint, headers=None, max_retries=0, retry_interval=0.5,
126-
timeout=600, expected_response_code=None, expected_response=None):
124+
match = re.search(_CURL_METRICS_REGEXP, http_stdout)
125+
if match:
126+
latency = float(match.group(1))
127+
# Remove <curl_time_total:##> from the response.
128+
http_stdout = http_stdout.split(match.group(0))[0]
129+
130+
logs += (
131+
f'call_curl values - latency: {latency}, pass/fail: {poll_stats}, '
132+
f'response code: {http_resp_code}, stdout: {http_stdout};'
133+
)
134+
return latency or -1, poll_stats, http_stdout, logs
135+
136+
137+
def poll(
138+
endpoint,
139+
headers=None,
140+
max_retries=0,
141+
retry_interval=0.5,
142+
timeout=600,
143+
expected_response_code=None,
144+
expected_response=None,
145+
):
127146
"""Poll HTTP endpoint until expected response code is received or time out.
128147
129148
Args:
@@ -132,54 +151,62 @@ def poll(endpoint, headers=None, max_retries=0, retry_interval=0.5,
132151
max_retries: int. Indicates how many retires before give up.
133152
retry_interval: float. Indicates interval in sec between retries.
134153
timeout: int. Deadline before giving up.
135-
expected_response_code: string. Expected http response code. Can be either
136-
a string or a regex.
137-
expected_response: string. Expected http response. Can be either a string
138-
or a regex.
154+
expected_response_code: string. Expected http response code. Can be either a
155+
string or a regex.
156+
expected_response: string. Expected http response. Can be either a string or
157+
a regex.
139158
140159
Returns:
141-
A tuple of float, float, string, string:
142-
First float indicates total time spent in seconds polling before
143-
giving up or succeed.
144-
Second float indicates latency for successful polls. If polling failed,
160+
A tuple of float, string, string, and string:
161+
Float indicates latency for successful polls. If polling failed,
145162
latency is default to -1.
146-
String indicates if HTTP(S) endpoint is reachable and responds with
163+
First string indicates if HTTP(S) endpoint is reachable and responds with
147164
expected response and code, or reasons of failure.
148-
String represents actual response.
165+
Second string represents actual response.
166+
Third string has additional debugging info worth logging.
149167
"""
150168
start = _get_time()
151169
end = start
152170
latency = 0
153171
attempt = 0
154172
poll_stats = None
173+
resp = ''
174+
logs = ''
155175
while end - start < timeout and attempt <= max_retries:
156-
attempt_latency, poll_stats, _, resp = call_curl(endpoint, headers,
157-
expected_response_code,
158-
expected_response)
176+
attempt_latency, poll_stats, resp, logs = call_curl(
177+
endpoint, headers, expected_response_code, expected_response
178+
)
159179
if poll_stats != POLL_SUCCEED:
160180
time.sleep(retry_interval)
161181
end = _get_time()
162182
attempt += 1
163183
else:
164184
latency = attempt_latency
165185
break
166-
return _get_time() - start - latency, latency or -1, poll_stats, resp
186+
logs += f'Time before request started: {_get_time() - start - latency};'
187+
return latency or -1, poll_stats, resp, logs
167188

168189

169190
def main():
170191
if not _ENDPOINT.value:
171-
logging.info('Invalid arguments, no endpoint provided.')
192+
print(
193+
-1, 'INVALID_ARGUMENTS', '', 'Invalid arguments, no endpoint provided.'
194+
)
172195
return 1
173196
print(
174-
poll(_ENDPOINT.value, _HEADERS.value, _MAX_RETRIES.value,
175-
_RETRY_INTERVAL.value, _TIMEOUT.value, _EXPECTED_RESPONSE_CODE.value,
176-
_EXPECTED_RESPONSE.value))
197+
poll(
198+
_ENDPOINT.value,
199+
_HEADERS.value,
200+
_MAX_RETRIES.value,
201+
_RETRY_INTERVAL.value,
202+
_TIMEOUT.value,
203+
_EXPECTED_RESPONSE_CODE.value,
204+
_EXPECTED_RESPONSE.value,
205+
)
206+
)
177207
return 0
178208

179209

180210
if __name__ == '__main__':
181211
FLAGS(sys.argv)
182-
logging.basicConfig(
183-
level=logging.INFO,
184-
format='%(asctime)-15s %(message)s')
185212
sys.exit(main())

perfkitbenchmarker/linux_packages/http_poller.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def AptInstall(vm):
3535
_Install(vm)
3636

3737

38-
_SUCCEED_RESP = 'succeed'
38+
_SUCCESS = 'succeed'
3939
_FAILURE_INVALID_RESPONSE = 'invalid_response'
4040

4141

@@ -105,15 +105,15 @@ def Run(self, vm, endpoint,
105105
endpoint, headers, retries, retry_interval, timeout,
106106
expected_response_code, expected_response)
107107
out, _ = vm.RemoteCommand(cmd)
108-
_, latency, resp, ret = ast.literal_eval(out)
109-
if resp == _FAILURE_INVALID_RESPONSE:
108+
latency, ret, resp, _ = ast.literal_eval(out)
109+
if ret == _FAILURE_INVALID_RESPONSE:
110110
logging.debug(
111111
'Failure polling endpoint %s. Received unexpected response %s != '
112112
'expected %s.',
113-
endpoint, ret, expected_response)
114-
elif float(latency) == -1 or resp != _SUCCEED_RESP:
113+
endpoint, resp, expected_response)
114+
elif float(latency) == -1 or ret != _SUCCESS:
115115
logging.debug(
116116
'Unexpected response or unable to reach endpoint %s, resp: %s',
117117
endpoint, resp)
118118
return PollingResponse(
119-
success=resp == _SUCCEED_RESP, response=ret, latency=latency)
119+
success=resp == _SUCCESS, response=resp, latency=latency)

tests/poll_http_endpoint_test.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,44 +43,49 @@ def setUp(self):
4343
'400 request fails',
4444
'< HTTP/1.1 400 Bad Request',
4545
'invalid_response_code',
46-
400,
4746
),
48-
('200 request passes', '< HTTP/1.1 200 OK'
49-
, 'succeed', 200),
47+
('200 request passes', '< HTTP/1.1 200 OK', 'succeed'),
5048
)
5149
def test_call_curl_response_types(
52-
self, response_stderr, expected_response_type, expected_response_code
50+
self, response_stderr, expected_response_type
5351
):
5452
self.mock_subprocess.side_effect = fake_subprocess_popen_output(
5553
'hello world<curl_time_total:0.5>', response_stderr
5654
)
57-
latency, response_type, response_code, response = (
58-
poll_http_endpoint.call_curl(
59-
'http://site.com', expected_response_code=r'2\d\d'
60-
)
55+
latency, response_type, response, _ = poll_http_endpoint.call_curl(
56+
'http://site.com', expected_response_code=r'2\d\d'
6157
)
6258
self.assertEqual(response_type, expected_response_type)
63-
self.assertEqual(response_code, expected_response_code)
6459
self.assertAlmostEqual(latency, 0.5)
6560
self.assertEqual(response, 'hello world')
6661

62+
def test_call_curl_unexpected_no_response(self):
63+
self.mock_subprocess.side_effect = fake_subprocess_popen_output(
64+
'<curl_time_total:0.5>', '< HTTP/1.1 400 Bad Request'
65+
)
66+
latency, _, response, _ = poll_http_endpoint.call_curl(
67+
'http://site.com', expected_response='foo'
68+
)
69+
self.assertAlmostEqual(latency, 0.5)
70+
self.assertEqual(response, '')
71+
6772
@parameterized.named_parameters(
6873
('response match', 'succeed', 'hello world'),
6974
('response not match', 'invalid_response', 'hi folks'),
7075
)
7176
def test_call_curl_matching_expected_response(
72-
self, response_type, expected_response
77+
self, expected_response_type, expected_response
7378
):
7479
self.mock_subprocess.side_effect = fake_subprocess_popen_output(
7580
'hello world<curl_time_total:0.5>', '< HTTP/1.1 200 OK'
7681
)
77-
_, response_type, response_code, _ = poll_http_endpoint.call_curl(
82+
_, response_type, response, _ = poll_http_endpoint.call_curl(
7883
'http://site.com',
7984
expected_response_code=r'2\d\d',
8085
expected_response=expected_response,
8186
)
82-
self.assertEqual(response_type, response_type)
83-
self.assertEqual(response_code, 200)
87+
self.assertEqual(expected_response_type, response_type)
88+
self.assertEqual(response, 'hello world')
8489

8590
@parameterized.named_parameters(
8691
(
@@ -108,7 +113,7 @@ def test_poll(
108113
self.mock_subprocess.side_effect = fake_subprocess_popen_output(
109114
'hello world<curl_time_total:0.5>', response_stderr
110115
)
111-
_, latency, response_type, response = poll_http_endpoint.poll(
116+
latency, response_type, response, _ = poll_http_endpoint.poll(
112117
'http://site.com',
113118
expected_response_code=r'2\d\d',
114119
max_retries=2,
@@ -123,7 +128,7 @@ def test_poll_fails_with_timeout(self):
123128
mock.patch.object(
124129
poll_http_endpoint,
125130
'call_curl',
126-
return_value=(1, 'invalid_response_code', '', 'hello'),
131+
return_value=('invalid_response_code', '', 'hello', ''),
127132
)
128133
)
129134
self.enter_context(

0 commit comments

Comments
 (0)