Fix race condition in available-prefix/ip APIs

Implement advisory lock to prevent duplicate records being inserted
when making simultaneous calls. Fixes #2519
This commit is contained in:
Matt Olenik 2019-12-13 11:52:59 -08:00
parent 2ab382eec5
commit 2e83ce76ed
4 changed files with 26 additions and 0 deletions

View File

@ -22,6 +22,10 @@ django-filter
# https://github.com/django-mptt/django-mptt # https://github.com/django-mptt/django-mptt
django-mptt django-mptt
# Context managers for PostgreSQL advisory locks
# https://github.com/Xof/django-pglocks
django-pglocks
# Prometheus metrics library for Django # Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus # https://github.com/korfuri/django-prometheus
django-prometheus django-prometheus

View File

@ -1,6 +1,7 @@
from django.conf import settings from django.conf import settings
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_pglocks import advisory_lock
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
from ipam import filters from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, ModelViewSet from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.constants import ADVISORY_LOCK_KEYS
from utilities.utils import get_subquery from utilities.utils import get_subquery
from . import serializers from . import serializers
@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
filterset_class = filters.PrefixFilterSet filterset_class = filters.PrefixFilterSet
@action(detail=True, url_path='available-prefixes', methods=['get', 'post']) @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
def available_prefixes(self, request, pk=None): def available_prefixes(self, request, pk=None):
""" """
A convenience method for returning available child prefixes within a parent. A convenience method for returning available child prefixes within a parent.
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
invoked in parallel, which results in a race condition where multiple insertions can occur.
""" """
prefix = get_object_or_404(Prefix, pk=pk) prefix = get_object_or_404(Prefix, pk=pk)
available_prefixes = prefix.get_available_prefixes() available_prefixes = prefix.get_available_prefixes()
@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, url_path='available-ips', methods=['get', 'post']) @action(detail=True, url_path='available-ips', methods=['get', 'post'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def available_ips(self, request, pk=None): def available_ips(self, request, pk=None):
""" """
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
however results will not be paginated. however results will not be paginated.
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
invoked in parallel, which results in a race condition where multiple insertions can occur.
""" """
prefix = get_object_or_404(Prefix, pk=pk) prefix = get_object_or_404(Prefix, pk=pk)

View File

@ -27,3 +27,14 @@ COLOR_CHOICES = (
('111111', 'Black'), ('111111', 'Black'),
('ffffff', 'White'), ('ffffff', 'White'),
) )
# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
# the advisory_lock contextmanager. When a lock is acquired,
# one of these keys will be used to identify said lock.
#
# When adding a new key, pick something arbitrary and unique so
# that it is easily searchable in query logs.
ADVISORY_LOCK_KEYS = {
'available-prefixes': 100100,
'available-ips': 100200,
}

View File

@ -4,6 +4,7 @@ django-cors-headers==3.2.1
django-debug-toolbar==2.1 django-debug-toolbar==2.1
django-filter==2.2.0 django-filter==2.2.0
django-mptt==0.9.1 django-mptt==0.9.1
django-pglocks==1.0.4
django-prometheus==1.1.0 django-prometheus==1.1.0
django-rq==2.2.0 django-rq==2.2.0
django-tables2==2.2.1 django-tables2==2.2.1