From 445f6bf3dc1b61477407c375133d8e975564566c Mon Sep 17 00:00:00 2001 From: kyleknap Date: Thu, 18 Sep 2014 13:35:30 -0700 Subject: Inserted break when iterating Route53 records. ``find_records()`` would iterate through the entire zone even though there were no more matching records since records are returned sorted. Added break statement so iteration stops after there is no more matching records to return. --- boto/route53/zone.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/boto/route53/zone.py b/boto/route53/zone.py index 167a0891..b21c8de4 100644 --- a/boto/route53/zone.py +++ b/boto/route53/zone.py @@ -233,7 +233,14 @@ class Zone(object): # name/type for get_all_rrsets sets the starting record; they # are not a filter - results = [r for r in returned if r.name == name and r.type == type] + results = [] + for r in returned: + if r.name == name and r.type == type: + results.append(r) + # Is at the end of the list of matched records. No need to continue + # since the records are sorted by name and type. + else: + break weight = None region = None -- cgit v1.2.1 From 1af7c058ac701d3cb3164c2b7f43055a2ec7e116 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Thu, 18 Sep 2014 16:08:06 -0700 Subject: Added backoff support for route53 throttling. Instead of erroring out on 400-Throttling responses from route53, perform exponential backoff retries on the request. --- boto/route53/connection.py | 10 ++++++++-- tests/unit/route53/test_connection.py | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/boto/route53/connection.py b/boto/route53/connection.py index 9f17781a..c13ab2e0 100644 --- a/boto/route53/connection.py +++ b/boto/route53/connection.py @@ -521,12 +521,18 @@ class Route53Connection(AWSAuthConnection): if response.status == 400: code = response.getheader('Code') - if code and 'PriorRequestNotComplete' in code: + if code: # This is a case where we need to ignore a 400 error, as # Route53 returns this. See # http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html + if 'PriorRequestNotComplete' in code: + error = 'PriorRequestNotComplete' + elif 'Throttling' in code: + error = 'Throttling' + else: + return status msg = "%s, retry attempt %s" % ( - 'PriorRequestNotComplete', + error, i ) next_sleep = min(random.random() * (2 ** i), diff --git a/tests/unit/route53/test_connection.py b/tests/unit/route53/test_connection.py index a748f307..3c696c7a 100644 --- a/tests/unit/route53/test_connection.py +++ b/tests/unit/route53/test_connection.py @@ -54,7 +54,7 @@ class TestRoute53Connection(AWSMockServiceTestCase): def test_typical_400(self): self.set_http_response(status_code=400, header=[ - ['Code', 'Throttling'], + ['Code', 'AccessDenied'], ]) with self.assertRaises(DNSServerError) as err: @@ -62,11 +62,22 @@ class TestRoute53Connection(AWSMockServiceTestCase): self.assertTrue('It failed.' in str(err.exception)) - @mock.patch('time.sleep') - def test_retryable_400(self, sleep_mock): + def test_retryable_400_prior_request_not_complete(self): + # Test ability to retry on ``PriorRequestNotComplete``. self.set_http_response(status_code=400, header=[ ['Code', 'PriorRequestNotComplete'], ]) + self.do_retry_handler() + + def test_retryable_400_throttling(self): + # Test ability to rety on ``Throttling``. + self.set_http_response(status_code=400, header=[ + ['Code', 'Throttling'], + ]) + self.do_retry_handler() + + @mock.patch('time.sleep') + def do_retry_handler(self, sleep_mock): def incr_retry_handler(func): def _wrapper(*args, **kwargs): -- cgit v1.2.1 From cea3e26e4c8a1c078251bc184d93c59530ab7044 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Mon, 22 Sep 2014 17:20:50 -0700 Subject: Add test for iteraton break of Route53 records --- tests/unit/route53/test_zone.py | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/unit/route53/test_zone.py diff --git a/tests/unit/route53/test_zone.py b/tests/unit/route53/test_zone.py new file mode 100644 index 00000000..12d1d254 --- /dev/null +++ b/tests/unit/route53/test_zone.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# Copyright (c) 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, dis- +# tribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the fol- +# lowing conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- +# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +from boto.route53.zone import Zone +from tests.compat import mock, unittest + + +class TestZone(unittest.TestCase): + def test_find_records(self): + mock_connection = mock.Mock() + zone = Zone(mock_connection, {}) + zone.id = None + rr_names = ['amazon.com', 'amazon.com', 'aws.amazon.com', + 'aws.amazon.com'] + mock_rrs = [] + # Create some mock resource records. + for rr_name in rr_names: + mock_rr = mock.Mock() + mock_rr.name = rr_name + mock_rr.type = 'A' + mock_rr.weight = None + mock_rr.region = None + mock_rrs.append(mock_rr) + + # Set the last resource record to ``None``. The ``find_records`` loop + # should never hit this. + mock_rrs[3] = None + + mock_connection.get_all_rrsets.return_value = mock_rrs + mock_connection._make_qualified.return_value = 'amazon.com' + + # Ensure that the ``None`` type object was not iterated over. + try: + result_rrs = zone.find_records('amazon.com', 'A', all=True) + except AttributeError as e: + self.fail("find_records() iterated too far into resource" + " record list.") + + # Determine that the resulting records are correct. + self.assertEqual(result_rrs, [mock_rrs[0], mock_rrs[1]]) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.1