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)