Question

I'm currently developing a game, in which user can get experience points. My (custom) user model looks something like this:

class TriariadUser(AbstractBaseUser, PermissionsMixin):
    pseudonym = models.CharField(max_length=40)
    level = models.PositiveSmallIntegerField(default=1)
    experience_points = models.PositiveIntegerField(default=0)

    def add_ep_points(self, points):
        self.experience_points += points

        if self.experience_points >= get_next_level_ep(self.level):
            level += 1

        self.save()

Now I have various signal listeners that can add experience points to a user. The problem is: If multiple XP gains occur during one request, the last XP gain overwrites all others.

Clearly this is a race condition, so I tried to modify my function to be static and use select_for_update:

@staticmethod
def add_ep_points(user_id, points):
    user = TriariadUser.objects.select_for_update.get(pk=user_id)

    user.experience_points += points

    ...

This works as intended, however the user object in the template is not updated. That is, that a new request must be made that the user sees what happened. Using the django debug toolbar I can see, that the request, which loads the user is made at the beginning. After that all relevant updates are made. But the user is not reloaded afterwards, so the old state is displayed.

I can think of various workarounds like a reload by using JavaScript, but there must be some other solutions for this (at least I hope so).

Is there a way to lock a object in django? Is there a way to tell that an object needs to be reloaded? Is there a better way to accomplish this (maybe using some kind of Middleware?).

Was it helpful?

Solution

To avoid that here's what I would do. First create a UserExpRecord model with a relation to the user and a +/- amount for how much xp you're adding or removing. Then these signals can add a new UserExpRecord for giving a user xp. Have the UserExpRecord emit a save signal that notifies the user model it needs to recollect (SUM) all the xp records related for a user and save that value to the user.

This gives you the immediate benefit of having a record of when and how much xp was added for a user. The secondary benefit is you can avoid any sort of race condition because you aren't trying to lock a table row and increment the value.

That said, depending on your backend there may be an atomic thread-safe “upsert” or “increment” function that will allow you to within a transaction safely increment a value while blocking all other writes. This will allow the writes to stack correctly. I believe the first solution (separate xp records) will come with a smaller headache (cache invalidation) than this or your current solution (unknown race conditions with missing / dropped xp updates).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top