Domanda

I have a Django app working in multiprocessing+multithreading mode under uWSGI on two dedicated servers. Load balancing is done via nGinx. There is also Postgres and Redis server.

Users cause race condition on critical data processing by clicking button twice accidentally and by spamming web server to gain more points. I'm thinking about using some kind of per user lock at the beginning of request processing to prevent both data corruption and server overloading. The problem is that the request processing is distributed between machines.

What is the most robust and efficient solution in such case? And what if I'd have a single uWSGI server but still with multiple processes?

È stato utile?

Soluzione

My current choice is to use PostgreSQL Advisory Locks. And it seems it works fine.

Below is a middleware class to be placed above all another middlewares that may work with database. It may be a good idea to use different postgres cluster configured for large amount of connections and different connection pooler in session mode.

from django.contrib.auth import SESSION_KEY as USER_ID_SESSION_KEY

class SessionLock(object):
    """
    Prevents users from making simultaneous requests.
    """

    def __init__(self):
        if not getattr(settings, 'SESSION_LOCK', True):
            raise MiddlewareNotUsed

        from django.db import connections, DEFAULT_DB_ALIAS
        from django.contrib.sessions.middleware import SessionMiddleware
        self.connections = connections
        self.db_alias = getattr(settings, 'SESSION_LOCK_DB', DEFAULT_DB_ALIAS)

        # Get session initialisation function from session middleware.
        self.load_session = SessionMiddleware.process_request.__func__

    def process_request(self, request):
        # Load the session to retrieve user id from it.
        self.load_session(None, request)

        # Generate a lock id.
        user_id = request.session.get(USER_ID_SESSION_KEY)
        if user_id is not None:
            request.session_lock_id = ('user_lock_%d' % user_id).__hash__()
        else:
            # If user is anonymous then use meta info for identification.
            request.session_lock_id = (
                request.META.get('HTTP_HOST', '') + ':' +
                request.META.get('HTTP_USER_AGENT', '')
            ).__hash__()

        # Acquire the lock.
        cursor = self.connections[self.db_alias].cursor()
        cursor.execute('SELECT pg_advisory_lock(%d)' % request.session_lock_id)

    def process_response(self, request, response):
        self._release_lock(request)
        return response

    def process_exception(self, request, exception):
        self._release_lock(request)

    def _release_lock(self, request):
        if hasattr(request, 'session_lock_id'):
            cursor = self.connections[self.db_alias].cursor()
            cursor.execute('SELECT pg_advisory_unlock(%d)' %
                           request.session_lock_id)
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top