Fixing deadlock and condition race errrors

remotes/origin/issue/4795/notification_even_they_are_disabled
Alejandro Alonso 2016-02-12 20:10:25 +01:00
parent 3b9975351c
commit 6553ad34e4
9 changed files with 62 additions and 14 deletions

View File

@ -54,7 +54,7 @@ from .settings import api_settings
from .utils import get_object_or_404
from .. import exceptions as exc
from ..decorators import model_pk_lock
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
"""
@ -161,12 +161,15 @@ class UpdateModelMixin:
"""
@tx.atomic
@model_pk_lock
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none()
self.check_permissions(request, 'update', self.object)
if self.object is None:
raise Http404
serializer = self.get_serializer(self.object, data=request.DATA,
files=request.FILES, partial=partial)
@ -227,6 +230,7 @@ class DestroyModelMixin:
Destroy a model instance.
"""
@tx.atomic
@model_pk_lock
def destroy(self, request, *args, **kwargs):
obj = self.get_object_or_none()
self.check_permissions(request, 'destroy', obj)

View File

@ -15,8 +15,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Rest Framework 2.4 backport some decorators.
from django_pglocks import advisory_lock
def detail_route(methods=['get'], **kwargs):
"""
@ -42,3 +41,21 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs
return func
return decorator
def model_pk_lock(func):
"""
This decorator is designed to be used in ModelViewsets methods to lock them based
on the model and the id of the selected object.
"""
def decorator(self, *args, **kwargs):
from taiga.base.utils.db import get_typename_for_model_class
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
pk = self.kwargs.get(self.pk_url_kwarg, None)
tn = get_typename_for_model_class(self.get_queryset().model)
key = "{0}:{1}".format(tn, pk)
with advisory_lock(key) as acquired_key_lock:
return func(self, *args, **kwargs)
return decorator

View File

@ -19,6 +19,8 @@ from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.shortcuts import _get_queryset
from django_pglocks import advisory_lock
from . import functions
import re
@ -116,7 +118,6 @@ def update_in_bulk(instances, list_of_new_values, callback=None, precall=None):
callback(instance)
@transaction.atomic
def update_in_bulk_with_ids(ids, list_of_new_values, model):
"""Update a table using a list of ids.
@ -125,8 +126,11 @@ def update_in_bulk_with_ids(ids, list_of_new_values, model):
to the instance in the same index position as the dict.
:param model: Model of the ids.
"""
tn = get_typename_for_model_class(model)
for id, new_values in zip(ids, list_of_new_values):
model.objects.filter(id=id).update(**new_values)
key = "{0}:{1}".format(tn, id)
with advisory_lock(key) as acquired_key_lock:
model.objects.filter(id=id).update(**new_values)
def to_tsquery(term):

View File

@ -55,5 +55,6 @@ def on_delete_any_model(sender, instance, **kwargs):
return
sesionid = mw.get_current_session_id()
events.emit_event_for_model(instance, sessionid=sesionid, type="delete")
emit_event = lambda: events.emit_event_for_model(instance, sessionid=sesionid, type="delete")
connection.on_commit(emit_event)

View File

@ -23,6 +23,17 @@ from django.db.models import signals
def connect_userstories_signals():
from taiga.projects import signals as generic_handlers
from . import signals as handlers
# When deleting user stories we must disable task signals while delating and
# enabling them in the end
signals.pre_delete.connect(handlers.disable_task_signals,
sender=apps.get_model("userstories", "UserStory"),
dispatch_uid='disable_task_signals')
signals.post_delete.connect(handlers.enable_tasks_signals,
sender=apps.get_model("userstories", "UserStory"),
dispatch_uid='enable_tasks_signals')
# Cached prev object version
signals.pre_save.connect(handlers.cached_prev_us,
sender=apps.get_model("userstories", "UserStory"),

View File

@ -18,6 +18,17 @@
from contextlib import suppress
from django.core.exceptions import ObjectDoesNotExist
from taiga.projects.history.services import take_snapshot
from taiga.projects.tasks.apps import connect_all_tasks_signals, disconnect_all_tasks_signals
# Enable tasks signals
def enable_tasks_signals(sender, instance, **kwargs):
connect_all_tasks_signals()
# Disable tasks signals
def disable_task_signals(sender, instance, **kwargs):
disconnect_all_tasks_signals()
####################################
# Signals for cached prev US

View File

@ -61,7 +61,7 @@ def test_storage_update(client, data):
storage_data["key"] = "test"
storage_data = json.dumps(storage_data)
results = helper_test_http_method(client, 'put', url, storage_data, users)
assert results == [401, 200, 201]
assert results == [401, 200, 404]
def test_storage_delete(client, data):
@ -118,4 +118,4 @@ def test_storage_patch(client, data):
patch_data = json.dumps({"value": {"test": "test-value"}})
results = helper_test_http_method(client, 'patch', url, patch_data, users)
assert results == [401, 200, 201]
assert results == [401, 200, 404]

View File

@ -144,10 +144,7 @@ def test_update_entries(client):
assert response.status_code == 404
response = client.json.put(reverse("user-storage-detail", args=[form["key"]]),
json.dumps(form))
assert response.status_code == 201
response = client.json.get(reverse("user-storage-detail", args=[form["key"]]))
assert response.status_code == 200
assert response.data["value"] == form["value"]
assert response.status_code == 404
def test_delete_storage_entry(client):

View File

@ -14,6 +14,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
from unittest import mock
@ -23,6 +24,8 @@ import re
from taiga.base.utils.urls import get_absolute_url, is_absolute_url, build_url
from taiga.base.utils.db import save_in_bulk, update_in_bulk, update_in_bulk_with_ids, to_tsquery
pytestmark = pytest.mark.django_db
def test_is_absolute_url():
assert is_absolute_url("http://domain/path")