I'm in a bit of muddle about how and where my concurrency editing functions should be implemented, so that a Mutex concurrency editing cannot be performed. My code:
models.py
class Order(models.Model):
edit_version = models.IntegerField(default=0, editable=True) # For concurrency editing
### Added for concurrency with 2 or more users wanting to edit the same form ###
locked = models.BooleanField(default = False)
def lock_edit(self):
self.locked = True
print ("locked_1: {0}".format(self.locked)) #Test purposes only
super().save() # what's this doing exctly??
def save_edit(self):
self.locked = False
print ("locked_2: {0}".format(self.locked)) #Test purposes only
super().save()
view.py
@permission_required('myapp.edit_order', fn=objectgetter(Order, 'id'))
def edit_order(request,id = None):
"""
"""
order = Order.objects.get(id=id)
print ("order: {0}".format(order))
print ("EDIT_VERSION: {0}".format(order.edit_version))
if settings.USE_LOCKS:
print("order.locked: {0}".format(order.locked))
order.lock_edit()
#order.locked = False # only to force the else clause for testing
if order.locked:
print ("Editing this form is prohibited because another user has already locked it.")
messages.info(request, 'Editing this form is prohibited because another user has already locked it.') # TODO: Pop-up and redirect necessary
return HttpResponseRedirect('/sanorder')
#raise ConcurrencyEditUpdateError #Define this somewhere
else:
print ("Order lock is False")
order.lock_edit()
print("order.locked_new: {0}".format(order.locked))
updated = Order.objects.filter(edit_version=order.edit_version).update(edit_version=order.edit_version+1)
print ("UPDATED: {0}".format(updated))
print ("EDIT_VERSION_NEW: {0}".format(order.edit_version))
#return Order.objects.filter(edit_version=order.edit_version).update(edit_version=order.edit_version+1)
return updated > 0
### Here are further functions in the form executed ###
if updated > 0: # For concurrency editing
order.save_edit()
return render(request, 'myapp/order_edit.html',
{'order':order,
'STATUS_CHOICES_ALREADY_REVIEWED': dSTATUS_CHOICES_ALREADY_REVIEWED,
'bolError': bolError,
'formQuorum': formQuorum,
'formCustomer': formCustomer,
'formInfo': formInfo,
})
The intention is, a user can access and edit a specific form, but only if no one else is editing it. Otherwise the user sees a pop-up message and is redirected to the main page. When the user is editing, a lock is triggered and released on submitting the form. In this case, this pertains to the lines:
if updated > 0: # For concurrency editing
order.save_edit()
However this is not working. Where am I going wrong? The intention is what should be a relative simple Mutex implementation. I'm trying to follow the example given here
The main problem I can see in your code is that aside from saving - you don't seem to be releasing the lock anywhere (also, I think that your indentation is broken in the original post, but that's not relevant, as I could guess the intention).
In order to properly implement the lock, IMO you need to have a note at least on the following:
The general idea of how the lock will work in that case will be:
So, a pseudo-implementation would look like the following:
The model:
class Order(models.Model):
LOCK_EXPIRE = datetime.timedelta(minutes=3)
locked_by = models.ForeignKey(User, null=True, blank=True)
lock_expires = models.DateTimeField(null=True, blank=True)
def lock(self, user):
self.locked_by = user
self.lock_expires = datetime.datetime.now() + self.LOCK_EXPIRE
self.save()
def unlock(self):
self.locked_by = None
self.lock_expires = None
self.save()
def can_edit(self, user):
return self.locked_by == user or self.lock_expires < datetime.datetime.now()
The view:
def edit_order(request, order_id = None):
with transaction.atomic():
# get_object_or_404 is just to avoid redundant '404' handling
# .select_for_update() should put a database lock on that particular
# row, preventing a race condition from happening
order = get_object_or_404(Order.objects.select_for_update(), id=order_id)
if not order.can_edit(request.user):
raise Http403
order.lock(request.user)
# Handle form logic
order.unlock()
order.save()
For further improvement, you could create a simple locking endpoint and put some JavaScript code on your website that would continuously (e.g. every minute) lock the order edition, which should keep the order locked until the person that locked it closes his tab. Alternatively (probably better than the above) would be to warn the user that his lock is about to expire and if he wants to extend it - that's up to you.