Search code examples
databasedjangolockingatomic

Django: lock particular rows in table


I have the following django method:

def setCurrentSong(request, player):    
  try:
     newCurrentSong = ActivePlaylistEntry.objects.get(
       song__player_lib_song_id=request.POST['lib_id'],
       song__player=player,   
       state=u'QE')
   except ObjectDoesNotExist:
     toReturn = HttpResponseNotFound()
     toReturn[MISSING_RESOURCE_HEADER] = 'song'    
     return toReturn
 
   try:
     currentSong = ActivePlaylistEntry.objects.get(song__player=player, state=u'PL')
     currentSong.state=u'FN'  
     currentSong.save()
   except ObjectDoesNotExist:  
     pass
   except MultipleObjectsReturned:     
     #This is bad. It means that
     #this function isn't getting executed atomically like we hoped it would be
     #I think we may actually need a mutex to protect this critial section :(
     ActivePlaylistEntry.objects.filter(song__player=player, state=u'PL').update(state=u'FN')

   newCurrentSong.state = u'PL'
   newCurrentSong.save()
   PlaylistEntryTimePlayed(playlist_entry=newCurrentSong).save()
   return HttpResponse("Song changed")

Essentially, I want it to be so that for a given player, there is only one ActivePlaylistEntry that has a 'PL' (playing) state at any given time. However, I have actually experienced cases where, as a result of quickly calling this method twice in a row, I get two songs for the same player with a state of 'PL'. This is bad as I have other application logic that relies on the fact that a player only has one playing song at any given time (plus semantically it doesn't make sense to be playing two different songs at the same time on the same player). Is there a way for me to do this update atomically? Just running the method as a transaction with the on_commit_success decorator doesn't seem to work. Is there like a way to lock the table for all songs belonging to a particular player? I was thinking of adding a lock column to my model (boolean field) and either just spinning on it or pausing the thread for a few milliseconds and checking again but these feel super hackish and dirty. I was also thinking about creating a stored procedure but that's not really database independent.


Solution

  • Locking queries were added in 1.4.

    with transaction.commit_manually():
      ActivePlayListEntry.objects.select_for_update().filter(...)
      aple = ActivePlayListEntry.objects.get(...)
      aple.state = ...
      transaction.commit()
    

    But you should consider refactoring so that a separate table with a ForeignKey is used to indicate the "active" song.