Просмотр исходного кода

Fix race condition in available-prefix/ip APIs

Implement advisory lock to prevent duplicate records being inserted
when making simultaneous calls. Fixes #2519
Matt Olenik 6 лет назад
Родитель
Сommit
2e83ce76ed
4 измененных файлов с 26 добавлено и 0 удалено
  1. 4 0
      base_requirements.txt
  2. 10 0
      netbox/ipam/api/views.py
  3. 11 0
      netbox/utilities/constants.py
  4. 1 0
      requirements.txt

+ 4 - 0
base_requirements.txt

@@ -22,6 +22,10 @@ django-filter
 # https://github.com/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
 # https://github.com/korfuri/django-prometheus
 django-prometheus

+ 10 - 0
netbox/ipam/api/views.py

@@ -1,6 +1,7 @@
 from django.conf import settings
 from django.db.models import Count
 from django.shortcuts import get_object_or_404
+from django_pglocks import advisory_lock
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
@@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
 from ipam import filters
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.api import FieldChoicesViewSet, ModelViewSet
+from utilities.constants import ADVISORY_LOCK_KEYS
 from utilities.utils import get_subquery
 from . import serializers
 
@@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
     filterset_class = filters.PrefixFilterSet
 
     @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
     def available_prefixes(self, request, pk=None):
         """
         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)
         available_prefixes = prefix.get_available_prefixes()
@@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
             return Response(serializer.data)
 
     @action(detail=True, url_path='available-ips', methods=['get', 'post'])
+    @advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
     def available_ips(self, request, pk=None):
         """
         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,
         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)
 

+ 11 - 0
netbox/utilities/constants.py

@@ -27,3 +27,14 @@ COLOR_CHOICES = (
     ('111111', 'Black'),
     ('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,
+}

+ 1 - 0
requirements.txt

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