Forums

Will the server and WSGI let me use thread local memory?

I am looking for a way to store the current request in thread memory so I can easily use it to seed fields when records are added (using the model.clean function).

The clean function has easy access to the data I want to pre-populate but no access to the request object or the logged in user (is there?).

So, I found this old code to create a middleware class that stores the request object in the chunk of RAM on the server that is handling the request:

1
2
3
4
5
6
#! Python
from threading import local
_thread_locals = local()
class CTS7_request_middlware(object):
    def process_request(self, request):
        _thread_locals.request = request

Then later on, you can get the request back using request = getattr(_thread_locals, "request", None)

I have been playing with this on the system, and it works fine for one user. But when I log in with 2 or 3 users at the same time, I can see the users occasionally over-write each other. Meaning the same physical chunk of server RAM is handling multiple sessions. My session IDs are unique.

So, I'm guessing there has to be some server or wsgi settings that tell it to use one thread per active session?

Thanks in advance!

Hi @rcooke -- are you trying to maintain state between requests within the same session? Or is all of this happening in the context of one request?

It's no surprise that the same chunk of RAM spans multiple sessions. What you're doing is asking for thread-local storage, which is just an area of memory which is unique to that thread. In a web hosting environment, all threads handle all requests - you can't assume that different sessions will be on different threads and you also can't assume that requests within the same session will be on the same thread as previous requests for that session either. You can't even assume it's going to be in the same process, or even handled by the same physical machine, in general.

This is a consequence of the fact that HTTP is a stateless protocol - each request can be handled entirely independently of the others. Of course, it's often useful to link together multiple requests to prevent users having to re-authenticate every time, and that's why session management was invented. This is the reason why session management always uses some persistent storage, whether that's files on disk or a database like MySQL. This is because that storage is shared between all the threads, processes and servers that are responsible for serving the web application. RAM, in general, is not guaranteed to be shared in all cases, and hence isn't safe to use.

So it sounds to me as if you should be storing this information within the session itself, assuming you want to keep it unique to a single session. The fact that the session data isn't available to the method you want to call is the problem you should be attempting to solve, not attempting to work around the limitation some other way.

You don't mention it in your question, but I'm assuming you're using Django and you're using this Model.clean() method to implement your functionality - if I've understood correctly you have some data that you want to save in the session as opposed to the model itself in this method? I think in that case, it doesn't look Django's set up to help you - judging from this SO question which seems somewhat related, you may have to do some fiddling to get what you want.

Is there any way you can move what you're trying to do outside the model instead?

@giles,

This is happening within the context of a single request.

@Cartoo You are exactly right. I'm trying to pass the request object, which includes the session object, to a chunk of Django 1.5 code that is tied to the model (model.clean). When running the Admin app, it does not have access to the request object.

So I tried stashing it in RAM only to get a crash course in how "shared" RAM is!

I have several choices I'm trying to figure out the one with the least amount of work. I would not be working in Automation if I weren't lazy.....

My goal is to auto-fill the company division the current session belongs to. This allows us to use the same business software for multiple divisions. Django 1.4 and 1.5 Admin has a lovely built-in easy method to filter records before they are displayed (ModelAdmin.queryset), the example given in the docs shows how to limit records to the user that "owns" them. I just changed that to "company division" (the list is stored in Site database), and presto! All data displayed anyplace inside the admin is restricted and the operation is totally transparent to the user.

What I need to do now is have an automatic method to populate each record's "site" field with the "site" value inside the current session.

  1. I can copy the admin view and make changes to it. Since it is passed the request, I can access the session object to get the seed value. This would be the "best practice" when I (eventually) get approval to replace the admin with our own task-based system. But I suspect its a lot of work to try it inside of the admin framework, It may have a domino effect resulting in the editing of a lot more related files. The html templates, for example.
  2. Which brings me to copying just the templates and changing them, since I think they are sent the session data as part of the request object. I have not learned how to work with templates, so I cannot judge how much effort this represents.
  3. Find some other mechanism within the admin framework. Find a place where I have access to both the data instance and the session data. Or a way I can pass one or the other to some code where I can make use of it.

Its the 3rd possibility I'm trolling for here. Since I don't have time to do choice 1 - at least the full custom version. And I dunno about #2.

There is another Django 1.4/1.5 feature I might be able to use: ModelAdmin.get_form. This is called to "get" the add, change or delete form. And it is passed the request object. I just have to learn if it also has access to the data instance. Or maybe I can pass "default" values as a keyword argument? I have a vague memory reading something about that. Have to dig back into the Admin docs and/or read the source code....

As always, any input, thoughts, or suggestions welcome!

I posted this on the Django forum too:

https://groups.google.com/forum/#!topic/django-users/Z5xTNCtVuVE

I got some good hints. And I posted this update to the Google group:

I stumbled upon this in the Django 1.5 Admin docs:

  • https://docs.djangoproject.com/en/1.5/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model

and this:

  • https://docs.djangoproject.com/en/1.5/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_formset

Changing my "ThreadLocal" middle ware to store the "company site" in the session information, I am testing this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#! Python
def save_model(self, request, obj, form, change):
    if change:
        if obj.site.id != request.session['site'].id:
            logger.debug("Contacts.Company.save_model: replacing site (%s) with (%s) " % (repr(obj.site), repr(request.session['site'])) )
    else:
        logger.debug("Contacts.Company.save_model: setting site (%s)" % (repr(request.session['site'])) )
    obj.site = request.session['site']
    obj.user = request.user
def save_formset(self, request, form, formset, change):
  instances = formset.save(commit=False)
  for instance in instances:
    if change:
        if instance.site.id != request.session['site'].id:
            logger.debug("Contacts.Company.save_model: replacing site (%s) with (%s) " % (repr(instance.site), repr(request.session['site'])) )
    else:
        logger.debug("Contacts.Company.save_model: setting site (%s)" % (repr(request.session['site'])) )
    instance.site = request.session['site']
    instance.user = request.user
    instance.save()
    formset.save_m2m()

This is added to your admin.py as part of your ModelAdmin class.

In case anybody else wants to do something like this:

After I switched to the above code to allow me to set the '''site''' field, I then switched to a login signal to capture which company division the logging in user belongs too. This code is at the end of my models.py file I have for my "MyUser" extension to the built-in User class. My extension is just the one field: "site",

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#! python
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver
@receiver(user_logged_in)
def listener_login(sender, user, request, **kwargs):
    '''Called on user login, gets signal, request, and user'''
    site_old = getattr(request.session, "site", None)
    if site_old:
        if site_old.id != user.MyUser.site.id:
           logger.debug("listener_login: REPLACING existing user site: " + repr(site_old.name) + " with site: " + repr(request.user.MyUser.site.name) )
    if request.user.is_authenticated():
        logger.debug( "listener_login: : storing request from user " + request.user.get_full_name() )
        try:
            request.session['site_name'] = request.user.myuser.site.name
            request.session['site'] = request.user.myuser.site
        except:
            logger.debug( "listener_login: Error trying to add data to session or change company name!" )
    else:
        logger.debug( "listener_login: skipping storage for non-user!" )
        request.session['site_id'] = None

Not that I'm too paranoid, but the first thing I check is if there is already a "site" value as part of the current session - this should not happen. And I log an error message if it ever does.

Then I log the fact this function is adding info to the sessions object. Then I have some error catchers in case that fails, for some reason, again logging message so I know something "bad" happened. And one final ELSE to catch if the user object is not a valid user - which should also never happen since this is the "login" signal.