| 1 | from __future__ import with_statement
|
|---|
| 2 | from django.core.handlers.wsgi import WSGIHandler as DjangoWSGIHandler
|
|---|
| 3 |
|
|---|
| 4 | from threading import Lock
|
|---|
| 5 |
|
|---|
| 6 | class WSGIHandler(DjangoWSGIHandler):
|
|---|
| 7 | """
|
|---|
| 8 | This provides a threadsafe drop-in replacement of django's WSGIHandler.
|
|---|
| 9 |
|
|---|
| 10 | Initialisation of django via a multithreaded wsgi handler is not safe.
|
|---|
| 11 | It is vulnerable to a A-B B-A deadlock.
|
|---|
| 12 |
|
|---|
| 13 | When two threads bootstrap django via different urls you have a change to hit
|
|---|
| 14 | the following deadlock.
|
|---|
| 15 |
|
|---|
| 16 | thread 1 thread 2
|
|---|
| 17 | view A view B
|
|---|
| 18 | import file foo import lock foo import file bar import lock bar
|
|---|
| 19 | bootstrap django lock AppCache.write_lock
|
|---|
| 20 | import file bar import lock bar <-- blocks
|
|---|
| 21 | bootstrap django lock AppCache.write_lock <----- deadlock
|
|---|
| 22 |
|
|---|
| 23 | workaround for an AB BA deadlock: wrap it in a lock C.
|
|---|
| 24 |
|
|---|
| 25 | lock C lock C
|
|---|
| 26 | lock A lock B
|
|---|
| 27 | lock B lock A
|
|---|
| 28 | release B release A
|
|---|
| 29 | release A release A
|
|---|
| 30 | release C release C
|
|---|
| 31 |
|
|---|
| 32 |
|
|---|
| 33 | Thats exactly what this class does, but... only for the first few calls.
|
|---|
| 34 | After that we remove the lock C. as the AppCache.write_lock is only held when django is booted.
|
|---|
| 35 |
|
|---|
| 36 | If we would not remove the lock C after the first few calls, that would make the whole app single threaded again.
|
|---|
| 37 |
|
|---|
| 38 | Usage:
|
|---|
| 39 | in your wsgi file replace the following lines
|
|---|
| 40 | import django.core.handlers.wsgi.WSGIHandler
|
|---|
| 41 | application = django.core.handlers.wsgi.WSGIHandler
|
|---|
| 42 | by
|
|---|
| 43 | import threadsafe_wsgi
|
|---|
| 44 | application = threadsafe_wsgi.WSGIHandler
|
|---|
| 45 |
|
|---|
| 46 |
|
|---|
| 47 | FAQ:
|
|---|
| 48 | Q: why would you want threading in the first place ?
|
|---|
| 49 | A: to reduce memory. Big apps can consume hundeds of megabytes each. adding processes is then much more expensive than threads.
|
|---|
| 50 | that memory is better spend caching, when threads are almost free.
|
|---|
| 51 |
|
|---|
| 52 | Q: this deadlock, it looks far-fetched, is this real ?
|
|---|
| 53 | A: yes we had this problem on production machines.
|
|---|
| 54 | """
|
|---|
| 55 | __initLock = Lock() # lock C
|
|---|
| 56 | __initialized = 0
|
|---|
| 57 |
|
|---|
| 58 | def __call__(self, environ, start_response):
|
|---|
| 59 | # the first calls (4) we squeeze everybody through lock C
|
|---|
| 60 | # this basically serializes all threads
|
|---|
| 61 | MIN_INIT_CALLS = 4
|
|---|
| 62 | if self.__initialized < MIN_INIT_CALLS:
|
|---|
| 63 | with self.__initLock:
|
|---|
| 64 | ret = DjangoWSGIHandler.__call__(self, environ, start_response)
|
|---|
| 65 | self.__initialized += 1
|
|---|
| 66 | return ret
|
|---|
| 67 | else:
|
|---|
| 68 | # we are safely bootrapped, skip lock C
|
|---|
| 69 | # now we are running multi-threaded again
|
|---|
| 70 | return DjangoWSGIHandler.__call__(self, environ, start_response)
|
|---|