From 4bb22a5301e59113dac4e1c08cb5ff8ec924fba4 Mon Sep 17 00:00:00 2001 From: Prijen Khokhani <88327452+prijendev@users.noreply.github.com> Date: Wed, 15 Jun 2022 18:05:16 +0530 Subject: [PATCH] Tdl 19235 handle uncaught exceptions (#61) * Added backoff for 5xx, 429 and ReadTimeout errors. * Resolved pylint error. * Updated comments in the unittest cases. * Updated error handling. --- tap_google_ads/streams.py | 13 +++- tests/unittests/test_backoff.py | 113 ++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/test_backoff.py diff --git a/tap_google_ads/streams.py b/tap_google_ads/streams.py index 783691b..811782d 100644 --- a/tap_google_ads/streams.py +++ b/tap_google_ads/streams.py @@ -7,6 +7,8 @@ from singer import utils from google.protobuf.json_format import MessageToJson from google.ads.googleads.errors import GoogleAdsException +from google.api_core.exceptions import ServerError, TooManyRequests +from requests.exceptions import ReadTimeout import backoff from . import report_definitions @@ -117,6 +119,13 @@ def generate_hash(record, metadata): def should_give_up(ex): + + # ServerError is the parent class of InternalServerError, MethodNotImplemented, BadGateway, + # ServiceUnavailable, GatewayTimeout, DataLoss and Unknown classes. + # Return False for all above errors and ReadTimeout error. + if isinstance(ex, (ServerError, TooManyRequests, ReadTimeout)): + return False + if isinstance(ex, AttributeError): if str(ex) == "'NoneType' object has no attribute 'Call'": LOGGER.info('Retrying request due to AttributeError') @@ -141,12 +150,14 @@ def on_giveup_func(err): @backoff.on_exception(backoff.expo, (GoogleAdsException, + ServerError, TooManyRequests, + ReadTimeout, AttributeError), max_tries=5, jitter=None, giveup=should_give_up, on_giveup=on_giveup_func, - logger=None) + ) def make_request(gas, query, customer_id): response = gas.search(query=query, customer_id=customer_id) return response diff --git a/tests/unittests/test_backoff.py b/tests/unittests/test_backoff.py new file mode 100644 index 0000000..eef14fe --- /dev/null +++ b/tests/unittests/test_backoff.py @@ -0,0 +1,113 @@ +import unittest +from unittest.mock import Mock, patch +from tap_google_ads.streams import make_request +from google.api_core.exceptions import InternalServerError, BadGateway, MethodNotImplemented, ServiceUnavailable, GatewayTimeout, TooManyRequests +from requests.exceptions import ReadTimeout + +@patch('time.sleep') +class TestBackoff(unittest.TestCase): + + def test_500_internal_server_error(self, mock_sleep): + """ + Check whether the tap backoffs properly for 5 times in case of InternalServerError. + """ + mocked_google_ads_client = Mock() + mocked_google_ads_client.search.side_effect = InternalServerError("Internal error encountered") + + try: + make_request(mocked_google_ads_client, "", "") + except InternalServerError: + pass + + # Verify that tap backoff for 5 times + self.assertEquals(mocked_google_ads_client.search.call_count, 5) + + def test_501_not_implemented_error(self, mock_sleep): + """ + Check whether the tap backoffs properly for 5 times in case of MethodNotImplemented error. + """ + mocked_google_ads_client = Mock() + mocked_google_ads_client.search.side_effect = MethodNotImplemented("Not Implemented") + + try: + make_request(mocked_google_ads_client, "", "") + except MethodNotImplemented: + pass + + # Verify that tap backoff for 5 times + self.assertEquals(mocked_google_ads_client.search.call_count, 5) + + def test_502_bad_gaetway_error(self, mock_sleep): + """ + Check whether the tap backoffs properly for 5 times in case of BadGateway error. + """ + mocked_google_ads_client = Mock() + mocked_google_ads_client.search.side_effect = BadGateway("Bad Gateway") + + try: + make_request(mocked_google_ads_client, "", "") + except BadGateway: + pass + + # Verify that tap backoff for 5 times + self.assertEquals(mocked_google_ads_client.search.call_count, 5) + + def test_503_service_unavailable_error(self, mock_sleep): + """ + Check whether the tap backoffs properly for 5 times in case of ServiceUnavailable error. + """ + mocked_google_ads_client = Mock() + mocked_google_ads_client.search.side_effect = ServiceUnavailable("Service Unavailable") + + try: + make_request(mocked_google_ads_client, "", "") + except ServiceUnavailable: + pass + + # Verify that tap backoff for 5 times + self.assertEquals(mocked_google_ads_client.search.call_count, 5) + + def test_504_gateway_timeout_error(self, mock_sleep): + """ + Check whether the tap backoffs properly for 5 times in case of GatewayTimeout error. + """ + mocked_google_ads_client = Mock() + mocked_google_ads_client.search.side_effect = GatewayTimeout("GatewayTimeout") + + try: + make_request(mocked_google_ads_client, "", "") + except GatewayTimeout: + pass + + # Verify that tap backoff for 5 times + self.assertEquals(mocked_google_ads_client.search.call_count, 5) + + def test_429_too_may_request_error(self, mock_sleep): + """ + Check whether the tap backoffs properly for 5 times in case of TooManyRequests error. + """ + mocked_google_ads_client = Mock() + mocked_google_ads_client.search.side_effect = TooManyRequests("Resource has been exhausted") + + try: + make_request(mocked_google_ads_client, "", "") + except TooManyRequests: + pass + + # Verify that tap backoff for 5 times + self.assertEquals(mocked_google_ads_client.search.call_count, 5) + + def test_read_timeout_error(self, mock_sleep): + """ + Check whether the tap backoffs properly for 5 times in case of ReadTimeout error. + """ + mocked_google_ads_client = Mock() + mocked_google_ads_client.search.side_effect = ReadTimeout("HTTPSConnectionPool(host='tap-tester-api.sandbox.stitchdata.com', port=443)") + + try: + make_request(mocked_google_ads_client, "", "") + except ReadTimeout: + pass + + # Verify that tap backoff for 5 times + self.assertEquals(mocked_google_ads_client.search.call_count, 5)