Skip to content

Commit 2d69b24

Browse files
committed
Merge pull request #239 from taigaio/bug/2083/user-story-history-repeats-files-and-has-no-order
Locking the element on take_snapshot to avoid racing condition issues
2 parents 985838c + 50fd9b6 commit 2d69b24

File tree

2 files changed

+54
-50
lines changed

2 files changed

+54
-50
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ django-ipware==0.1.0
3030
premailer==2.8.1
3131
django-transactional-cleanup==0.1.13
3232
lxml==3.4.1
33+
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
3334

3435
# Comment it if you are using python >= 3.4
3536
enum34==1.0

taiga/projects/history/services.py

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def create(request):
3737
from django.core.paginator import Paginator, InvalidPage
3838
from django.apps import apps
3939
from django.db import transaction as tx
40+
from django_pglocks import advisory_lock
4041

4142
from taiga.mdrender.service import render as mdrender
4243
from taiga.base.utils.db import get_typename_for_model_class
@@ -269,6 +270,7 @@ def get_modified_fields(obj:object, last_modifications):
269270

270271
return modified_fields
271272

273+
272274
@tx.atomic
273275
def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
274276
"""
@@ -280,56 +282,57 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
280282
"""
281283

282284
key = make_key_from_model_object(obj)
283-
typename = get_typename_for_model_class(obj.__class__)
284-
285-
new_fobj = freeze_model_instance(obj)
286-
old_fobj, need_real_snapshot = get_last_snapshot_for_key(key)
287-
288-
entry_model = apps.get_model("history", "HistoryEntry")
289-
user_id = None if user is None else user.id
290-
user_name = "" if user is None else user.get_full_name()
291-
292-
# Determine history type
293-
if delete:
294-
entry_type = HistoryType.delete
295-
elif new_fobj and not old_fobj:
296-
entry_type = HistoryType.create
297-
elif new_fobj and old_fobj:
298-
entry_type = HistoryType.change
299-
else:
300-
raise RuntimeError("Unexpected condition")
301-
302-
fdiff = make_diff(old_fobj, new_fobj)
303-
304-
# If diff and comment are empty, do
305-
# not create empty history entry
306-
if (not fdiff.diff and not comment
307-
and old_fobj is not None
308-
and entry_type != HistoryType.delete):
309-
310-
return None
311-
312-
fvals = make_diff_values(typename, fdiff)
313-
314-
if len(comment) > 0:
315-
is_hidden = False
316-
else:
317-
is_hidden = is_hidden_snapshot(fdiff)
318-
319-
kwargs = {
320-
"user": {"pk": user_id, "name": user_name},
321-
"key": key,
322-
"type": entry_type,
323-
"snapshot": fdiff.snapshot if need_real_snapshot else None,
324-
"diff": fdiff.diff,
325-
"values": fvals,
326-
"comment": comment,
327-
"comment_html": mdrender(obj.project, comment),
328-
"is_hidden": is_hidden,
329-
"is_snapshot": need_real_snapshot,
330-
}
331-
332-
return entry_model.objects.create(**kwargs)
285+
with advisory_lock(key) as acquired_key_lock:
286+
typename = get_typename_for_model_class(obj.__class__)
287+
288+
new_fobj = freeze_model_instance(obj)
289+
old_fobj, need_real_snapshot = get_last_snapshot_for_key(key)
290+
291+
entry_model = apps.get_model("history", "HistoryEntry")
292+
user_id = None if user is None else user.id
293+
user_name = "" if user is None else user.get_full_name()
294+
295+
# Determine history type
296+
if delete:
297+
entry_type = HistoryType.delete
298+
elif new_fobj and not old_fobj:
299+
entry_type = HistoryType.create
300+
elif new_fobj and old_fobj:
301+
entry_type = HistoryType.change
302+
else:
303+
raise RuntimeError("Unexpected condition")
304+
305+
fdiff = make_diff(old_fobj, new_fobj)
306+
307+
# If diff and comment are empty, do
308+
# not create empty history entry
309+
if (not fdiff.diff and not comment
310+
and old_fobj is not None
311+
and entry_type != HistoryType.delete):
312+
313+
return None
314+
315+
fvals = make_diff_values(typename, fdiff)
316+
317+
if len(comment) > 0:
318+
is_hidden = False
319+
else:
320+
is_hidden = is_hidden_snapshot(fdiff)
321+
322+
kwargs = {
323+
"user": {"pk": user_id, "name": user_name},
324+
"key": key,
325+
"type": entry_type,
326+
"snapshot": fdiff.snapshot if need_real_snapshot else None,
327+
"diff": fdiff.diff,
328+
"values": fvals,
329+
"comment": comment,
330+
"comment_html": mdrender(obj.project, comment),
331+
"is_hidden": is_hidden,
332+
"is_snapshot": need_real_snapshot,
333+
}
334+
335+
return entry_model.objects.create(**kwargs)
333336

334337

335338
# High level query api

0 commit comments

Comments
 (0)