Opened 16 years ago
Closed 16 years ago
#10470 closed (fixed)
Core handlers are not threadsafe
Reported by: | tdterry | Owned by: | tdterry |
---|---|---|---|
Component: | Core (Other) | Version: | 1.0 |
Severity: | Keywords: | wsgi, basehandler, mod_python, threadsafe, threading | |
Cc: | Triage Stage: | Accepted | |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
I have found an incomplete-initialization bug the WSGIHandler
when running under Apache mpm_worker with mod_wsgi. Code review shows this is a potential problem with all of the handlers.
On the first request the WSGIHandler.__call__(...)
, we set up the middleware. There is a lock wrapping the load of the middleware, but the logic still allows incomplete initialization on other threads. Below is the block of code in question from django/core/handlers/wsgi.py (line 226):
if self._request_middleware is None: self.initLock.acquire() # Check that middleware is still uninitialised. if self._request_middleware is None: self.load_middleware() self.initLock.release()
Example:
- Initial start or restart of the Apache instance
- Thread T1 - a request comes in.
self._request_middleware
isNone
- Thread T1 - acquires the
self.initLock
, callsself.load_middleware()
- Thread T2 - a request comes in (T1 is part of the way through
self.load_middleware()
) - Thread T2 -
self._request_middleware
is notNone
, but it's not completely loaded either, continues with the request - Thread T1 - completes
self.load_middleware()
, release the lock
The attached patch changes BaseHandler.load_middleware()
to do an atomic set of all four middleware lists. self._request_middleware
is set last to signal the completion of middleware loading.
Attachments (3)
Change History (24)
by , 16 years ago
Attachment: | thread-safe-handlers.diff added |
---|
comment:1 by , 16 years ago
Also, testing requires a multi-threaded setup and a good load test. These are not available in the framework, so I have not included a specific test for this patch.
comment:2 by , 16 years ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
comment:3 by , 16 years ago
May I suggest a minor improvement to the patch: Instead of assigning to the _request_middleware last, just use tuple assignment to assign them all together, i.e.
self._request_middleware, self._view_middleware, self._response_middleware, self._exception_middleware = \ _request_middleware, _view_middleware, _response_middleware, _exception_middleware
This way you don't need to make assumptions about the users.
comment:4 by , 16 years ago
Keywords: | threading added |
---|---|
Triage Stage: | Unreviewed → Accepted |
shai, tuple assignment is not atomic, so the suggestion does not help.
The patch looks OK, although, strictly speaking, temporary list is only required for _request_middleware
as only that is checked in client code (i.e. only that serves as initialization guard, not the other lists).
It would make things a bit less opaque if a _middleware_not_loaded
boolean property was added to BaseHandler
(that would do the very same return self._request_middleware is None
) and used throughout client code. It may make sense to avoid this for optimization though, method call would add quite a few bytecode instructions.
by , 16 years ago
Attachment: | thread-safe-handlers_minimal-patch.diff added |
---|
Minimal changes that assure thread-safety
comment:5 by , 16 years ago
Triage Stage: | Accepted → Unreviewed |
---|
On second thought, shouldn't initLock
guard against this? tdterry, were you hit by this or did you just think that this a possible scenario?
AFAIK:
- Initial start or restart of the Apache instance
- Thread T1 - a request comes in.
self._request_middleware
isNone
- Thread T1 - acquires the
self.initLock
, callsself.load_middleware()
- Thread T2 - a request comes in (T1 is part of the way through
self.load_middleware()
) - Thread T2 - waits for the
initLock
shared by all instances - Thread T1 - completes
self.load_middleware()
, releases the lock - Thread T2 - lock released, all middleware properly loaded
Looks good to me, pushing back to unreviewed.
follow-up: 16 comment:7 by , 16 years ago
mrts, I will take your word on tuple assignment as I can't find any reference in a quick search.
However, your threading analysis is wrong: In your line 5, T2 doesn't wait for the initLock. This is because without the (original) patch, self._request_middleware is not None (django/core/handlers/wsgi.py line 226).
comment:8 by , 16 years ago
Right so it gets past the first check, but then when it goes to acquite the lock it has to wait until the other thread is done, and then it fails the second test.
comment:9 by , 16 years ago
The point is that it doesn't wait for the lock, doesn't do the second check. It thinks that the middleware is already initialized, and just happily goes on to use it.
comment:10 by , 16 years ago
That's not consistant with the documented behavior of the lock: http://docs.python.org/library/threading.html#threading.Lock.acquire
comment:11 by , 16 years ago
The code does not attempt to acquire the lock. It only does that when self._request_middleware is None. In the described scenario, it isn't. The whole block is skipped.
comment:12 by , 16 years ago
(FWIW: the Python disassembler agrees with mrts; tuple assignment is not atomic. Sorry about that one).
comment:13 by , 16 years ago
Ok look at the code: http://code.djangoproject.com/browser/django/trunk/django/core/handlers/wsgi.py#L221
1) Is request middleware none if not go on
2) if it is acquire the lock -- block until we can
3) check if request middleware is none if not go on
4) load the middlware
5) release the lock
comment:14 by , 16 years ago
Ok I think we've been arguing past each other, the issue isn't anything that occurs in that block of code, the issue is that once that block of code begins exceuting for real in the first thread if a second thread comes in it won't block on that, while it should.
comment:15 by , 16 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:16 by , 16 years ago
Replying to shai:
However, your threading analysis is wrong: In your line 5, T2 doesn't wait for the initLock. This is because without the (original) patch, self._request_middleware is not None (django/core/handlers/wsgi.py line 226).
Indeed, my mistake, should have been more careful.
comment:17 by , 16 years ago
Resolution: | → fixed |
---|---|
Status: | assigned → closed |
comment:18 by , 16 years ago
Resolution: | fixed |
---|---|
Status: | closed → reopened |
Malcolm, it shows that it is quite early in your timezone :) -- there's an accidental omission in r10036, please see the attached patch.
comment:19 by , 16 years ago
Resolution: | → fixed |
---|---|
Status: | reopened → closed |
comment:20 by , 16 years ago
Resolution: | fixed |
---|---|
Status: | closed → reopened |
comment:21 by , 16 years ago
Resolution: | → fixed |
---|---|
Status: | reopened → closed |
Patch against r10030 to solve multi-thread issues with the core handlers