Agent: Optional middleware to rate limit NOTIFYs

Currently, the Agent naively does an AXFR/backend call for every
NOTIFY that it receives. It should be able to get a NOTIFY for a zone,
and then ignore successive NOTIFYs for a time period, and then do the
zone transfer, so as to catch all of the updates that came in.

This middleware accomplishes that by implementing a small locking
dictionary that doesn't allow more than one NOTIFY to kick off AXFR
per zone, per process for a configurable time period.

The Agent gracefully hanldes the situation where it's sleeping on a NOTIFY,
and a DELETE zone come through. When the NOTIFY wakes up, and the zone is already gone,
it refuses the NOTIFY because the domain doesn't exist.

Change-Id: If5655f8da201202482fa8c44af9b1c8496bf3281
This commit is contained in:
Tim Simmons
2015-06-02 16:41:24 +00:00
parent 68780870b3
commit 12b8a4e96f
5 changed files with 149 additions and 0 deletions

View File

@@ -13,7 +13,11 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from dns import zone as dnszone
import dns.message
import dns.rdatatype
import dns.rcode
from designate import dnsutils
from designate.tests import TestCase
@@ -95,3 +99,68 @@ class TestUtils(TestCase):
self.assertEqual(len(SAMPLES), len(zone.recordsets))
self.assertEqual('example.com.', zone.name)
def test_zone_lock(self):
# Initialize a ZoneLock
lock = dnsutils.ZoneLock(0.1)
# Ensure there's no lock for different zones
for zone_name in ['foo.com.', 'bar.com.', 'example.com.']:
self.assertEqual(True, lock.acquire(zone_name))
# Ensure a lock for successive calls for the same zone
self.assertEqual(True, lock.acquire('example2.com.'))
self.assertEqual(False, lock.acquire('example2.com.'))
# Acquire, release, and reacquire
self.assertEqual(True, lock.acquire('example3.com.'))
lock.release('example3.com.')
self.assertEqual(True, lock.acquire('example3.com.'))
def test_limit_notify_middleware(self):
# Set the delay
self.config(notify_delay=.1,
group='service:agent')
# Initialize the middlware
placeholder_app = None
middleware = dnsutils.LimitNotifyMiddleware(placeholder_app)
# Prepare a NOTIFY
zone_name = 'example.com.'
notify = dns.message.make_query(zone_name, dns.rdatatype.SOA)
notify.flags = 0
notify.set_opcode(dns.opcode.NOTIFY)
notify.flags |= dns.flags.AA
# Send the NOTIFY through the middleware
# No problem, middleware should return None to pass it on
self.assertEqual(middleware.process_request(notify), None)
@mock.patch('designate.dnsutils.ZoneLock.acquire', return_value=False)
def test_limit_notify_middleware_no_acquire(self, acquire):
# Set the delay
self.config(notify_delay=.1,
group='service:agent')
# Initialize the middlware
placeholder_app = None
middleware = dnsutils.LimitNotifyMiddleware(placeholder_app)
# Prepare a NOTIFY
zone_name = 'example.com.'
notify = dns.message.make_query(zone_name, dns.rdatatype.SOA)
notify.flags = 0
notify.set_opcode(dns.opcode.NOTIFY)
notify.flags |= dns.flags.AA
# Make a response object to match the middleware's return
response = dns.message.make_response(notify)
# Provide an authoritative answer
response.flags |= dns.flags.AA
# Send the NOTIFY through the middleware
# Lock can't be acquired, a NOTIFY is already being worked on
# so just return what would have come back for a successful NOTIFY
# This needs to be a one item tuple for the serialization middleware
self.assertEqual(middleware.process_request(notify), (response,))