Always try to parse responses from GET after async polling (#382)

Previously, if async polling from a PUT response in a certain format was received, that was used as the final
response from which to parse dynamic objects, otherwise the GET request was used.   This did not always work -
in some cases, there were properties missing from the response or an empty body was returned.

After this change, the first response on which the response parser will be invoked will be the one from the GET
request.  If the GET failed or parsing the response of the GET, the response from the PUT will still be tried as before.

Testing: ad-hoc testing using original repro (in progress).
This commit is contained in:
marina-p 2021-10-20 14:53:52 -07:00 коммит произвёл GitHub
Родитель af3b6ea272
Коммит 9e91e8810b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 76 добавлений и 46 удалений

Просмотреть файл

@ -117,9 +117,9 @@ class CheckerBase:
async_wait = Settings().get_max_async_resource_creation_time(request.request_id)
if check_async:
response_to_parse, _, _ = async_request_utilities.try_async_poll(
responses_to_parse, _, _ = async_request_utilities.try_async_poll(
rendered_data, response, async_wait)
request_utilities.call_response_parser(parser, response_to_parse)
request_utilities.call_response_parser(parser, None, responses=responses_to_parse)
seq.append_data_to_sent_list(rendered_data, parser, response, producer_timing_delay=0, max_async_wait_time=async_wait)
return response, response_to_parse

Просмотреть файл

@ -1166,9 +1166,9 @@ class PayloadBodyChecker(CheckerBase):
# send out the request and parse the response
response = self._send_request(parser, rendered_data)
async_wait = Settings().get_max_async_resource_creation_time(request.request_id)
response_to_parse, _, _ = async_request_utilities.try_async_poll(
responses_to_parse, _, _ = async_request_utilities.try_async_poll(
rendered_data, response, async_wait)
request_utilities.call_response_parser(parser, response_to_parse)
request_utilities.call_response_parser(parser, None, responses=responses_to_parse)
self._set_refresh_req(request, response)

Просмотреть файл

@ -141,9 +141,13 @@ def try_async_poll(request_data, response, max_async_wait_time):
@type max_async_wait_time: Int
@return: A tuple containing:
- The response for parsing, which will either be the @response
passed to this function or the response from the GET request
if it was an async request.
- The list of responses for parsing, which will either contain the @response
passed to this function, or in the case of an async request, the
response from the end of the async operation (if finished),
and the response from the GET request.
It is possible that the response of the async request when finished does not
contain all of the expected fields in the schema of the async response, so as a
workaround the GET response is also returned to try to obtain this additional information.
- A boolean that is True if there was an error when creating the
resource
- A Boolean that tells the caller whether or not async polling was
@ -154,7 +158,7 @@ def try_async_poll(request_data, response, max_async_wait_time):
from utils.logger import raw_network_logging as RAW_LOGGING
from utils.logger import print_async_results as LOG_RESULTS
response_to_parse = response
responses_to_parse = []
resource_error = False
async_waited = False
if Settings().wait_for_async_resource_creation and max_async_wait_time > 0\
@ -178,11 +182,22 @@ def try_async_poll(request_data, response, max_async_wait_time):
if poll_response.status_code in ['200' ,'201']:
LOG_RESULTS(request_data,
f"Resource creation succeeded after {time_str} seconds.")
# Break and return the polling response to be parsed
response_to_parse = poll_response
responses_to_parse.append(poll_response)
# Also, attempt to execute a GET request corresponding to this resource and
# return the response. This is used in case all of the expected properties are
# not present in the async response.
RAW_LOGGING("Attempting to get resources from GET request...")
get_response = try_parse_GET_request(request_data)
if get_response:
# Use response from the GET request for parsing. If the GET request failed,
# the caller will know to try and use the original PUT response for parsing
responses_to_parse.insert(0, get_response)
# Break and return the responses to be parsed
break
else:
# The data is not in the polling response and must be fetched separately.
# Try to execute a corresponding GET request and obtain
# information from the response
# This comes from Azure-Async responses that do not contain a Location: field
# Check for the status of the resource
response_body = json.loads(poll_response.json_body)
@ -192,13 +207,14 @@ def try_async_poll(request_data, response, max_async_wait_time):
f"Resource creation succeeded after {time_str} seconds.")
get_response = try_parse_GET_request(request_data)
if get_response:
response_to_parse = get_response
break
responses_to_parse.append(get_response)
elif done == "failed":
LOG_RESULTS(request_data,
f"The server reported that the resource creation Failed after {time_str} seconds.")
resource_error = True
break
# Break and return the responses to be parsed
break
except json.JSONDecodeError as err:
LOG_RESULTS(request_data, "Failed to parse body of async response, retrying.")
# This may have been due to a connection failure. Retry until max_async_wait_time.
@ -228,6 +244,8 @@ def try_async_poll(request_data, response, max_async_wait_time):
if get_response:
# Use response from the GET request for parsing. If the GET request failed,
# the caller will know to try and use the original PUT response for parsing
response_to_parse = get_response
responses_to_parse.append(get_response)
return response_to_parse, resource_error, async_waited
if len(responses_to_parse) == 0:
responses_to_parse.append(response)
return responses_to_parse, resource_error, async_waited

Просмотреть файл

@ -244,7 +244,7 @@ def send_request_data(rendered_data):
return response
def call_response_parser(parser, response, request=None):
def call_response_parser(parser, response, request=None, responses=None):
""" Calls a specified parser on a response
@param parser: The parser function to calls
@ -253,6 +253,9 @@ def call_response_parser(parser, response, request=None):
@type response: HttpResponse
@param request: The request whose parser is being called
@type request: Request (None ok)
@param responses: A list of responses
This parameter is used if the response is not specified
@type responses: List[HttpResponse]
@return False if there was a parser exception
@rtype Boolean
@ -260,27 +263,36 @@ def call_response_parser(parser, response, request=None):
"""
from utils.logger import write_to_main
# parse response and set dependent variables (for garbage collector)
try:
if parser:
# For backwards compatibility, check if the parser accepts named arguments.
# If not, this is an older grammar that only supports a json body as the argument
import inspect
args, varargs, varkw, defaults = inspect.getargspec(parser)
if varkw=='kwargs':
parser(response.json_body, headers=response.headers_dict)
else:
parser(response.json_body)
# Check request's producers to verify dynamic objects were set
if request:
for producer in request.produces:
if dependencies.get_variable(producer) == 'None':
err_str = f'Failed to parse {producer}; it is now set to None.'
write_to_main(err_str)
_RAW_LOGGING(err_str)
except (ResponseParsingException, AttributeError) as error:
_RAW_LOGGING(str(error))
return False
return True
if responses is None:
responses = []
responses.append(response)
for response in responses:
try:
if parser:
# For backwards compatibility, check if the parser accepts named arguments.
# If not, this is an older grammar that only supports a json body as the argument
import inspect
args, varargs, varkw, defaults = inspect.getargspec(parser)
if varkw=='kwargs':
parser(response.json_body, headers=response.headers_dict)
else:
parser(response.json_body)
# Print a diagnostic message if some dynamic objects were not set.
# The parser only fails if all of the objects were not set.
if request:
for producer in request.produces:
if dependencies.get_variable(producer) == 'None':
err_str = f'Failed to parse {producer}; it is now set to None.'
write_to_main(err_str)
_RAW_LOGGING(err_str)
return True
except (ResponseParsingException, AttributeError) as error:
_RAW_LOGGING(str(error))
return False
def get_hostname_from_line(line):
""" Gets the hostname from a request definition's Host: line

Просмотреть файл

@ -380,12 +380,12 @@ class Sequence(object):
prev_producer_timing_delay = Settings().get_producer_timing_delay(prev_request.request_id)
prev_response = request_utilities.send_request_data(prev_rendered_data)
prev_response_to_parse, resource_error, async_waited = async_request_utilities.try_async_poll(
prev_responses_to_parse, resource_error, async_waited = async_request_utilities.try_async_poll(
prev_rendered_data, prev_response, prev_req_async_wait)
prev_parser_threw_exception = False
# Response may not exist if there was an error sending the request or a timeout
if prev_parser and prev_response_to_parse:
prev_parser_threw_exception = not request_utilities.call_response_parser(prev_parser, prev_response_to_parse, prev_request)
if prev_parser and prev_responses_to_parse:
prev_parser_threw_exception = not request_utilities.call_response_parser(prev_parser, None, request=prev_request, responses=prev_responses_to_parse)
prev_status_code = prev_response.status_code
# If the async logic waited for the resource, this wait already included the required
@ -487,12 +487,12 @@ class Sequence(object):
req_async_wait = Settings().get_max_async_resource_creation_time(request.request_id)
response = request_utilities.send_request_data(rendered_data)
response_to_parse, resource_error, _ = async_request_utilities.try_async_poll(
responses_to_parse, resource_error, _ = async_request_utilities.try_async_poll(
rendered_data, response, req_async_wait)
parser_exception_occurred = False
# Response may not exist if there was an error sending the request or a timeout
if parser and response_to_parse:
parser_exception_occurred = not request_utilities.call_response_parser(parser, response_to_parse, request)
if parser and responses_to_parse:
parser_exception_occurred = not request_utilities.call_response_parser(parser, None, request=request, responses=responses_to_parse)
status_code = response.status_code
if not status_code:
return RenderedSequence(failure_info=FailureInformation.MISSING_STATUS_CODE)
@ -652,10 +652,10 @@ class Sequence(object):
""" Gets the token, sends the requst, performs async wait, parses response, returns status code """
rendered_data = self.get_request_data_with_token(request_data.rendered_data)
response = request_utilities.send_request_data(rendered_data)
response_to_parse, _, _ = async_request_utilities.try_async_poll(
responses_to_parse, _, _ = async_request_utilities.try_async_poll(
rendered_data, response, request_data.max_async_wait_time)
if request_data.parser:
request_utilities.call_response_parser(request_data.parser, response_to_parse)
request_utilities.call_response_parser(request_data.parser, None, responses=responses_to_parse)
return response.status_code
# Send all but the last request in the sequence