0

So I am successfully storing a complex object (non-model) in my session in development. I've tried every session engine and cache type and they are all working in development (Pycharm). However, when I move the code to production, while no error are thrown, the session losses the object.

Here is the method I use to set the session object:

def instantiate_command_object(request):
    try:
        ssc = request.session['specimen_search_criteria']
        logger.debug('found ssc session variable')
    except KeyError:
        logger.debug('failed to find ssc session variable')
        ssc = SpecimenSearchCommand()

    return ssc

Then in a method that runs asynchronously via an ajax call I start making changes to the object in the session:

def ajax_add_collection_to_search(request):
    ssc = instantiate_command_object(request)

    collection_id = request.GET.get('collection')

    collection = Collection.objects.get(pk=collection_id)

    if collection and collection not in ssc.collections:
        ssc.collections.append(collection)
        # save change to session
        request.session['specimen_search_criteria'] = ssc

    # refresh search results
    ssc.search()

    return render(request, '_search.html')

All this works as far as it goes. However, if I then refresh the browser, the session is lost. Here is a snippet from the template:

{% with criteria=request.session.specimen_search_criteria %}
    <div class="search-criteria" id="search-criteria">
        <div class="row">
                Sesssion:
                 {{ request.session }}<br/>
                Search:
                 {{ request.session.specimen_search_criteria }}<br/>
                Created:
                 {{ request.session.specimen_search_criteria.key }}<br/>
                Collections:
                 {{ request.session.specimen_search_criteria.collections }}<br/>

Again, in development I can refresh all day and the same object will be returned. In production, it will either create a new object or occasionally will return a previously created copy.

A few relevant items:

The production server is running Apache httpd with mod_wsgi. I've tried memcached, databasecache, etc. the behavior remains the same. Always works in development, never in production.

I've tried it with

SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'

and without. I can see the session info in the database and when I unpickle it it just seems to be pointing to a location in memory for the complex object.

I'm guessing this might have something to do with running in a multi-user environment, but again, I'm not using locmem and I've tried all of the caching approaches to no effect.

To be clear, the session itself seems to be fine, I can store a string or other simple item in it and it will stick. It's the complex object within the session that seems to be getting lost.

Edit: I might also point out that if I refresh the browser immediately following the return of the search criteria it will actually return successfully. Anything more than about a second and it will disappear.

Edit (adding code of SpecimenSearchCommand):

class SpecimenSearchCommand:
    def __init__(self):
        pass

    created = datetime.datetime.now()
    key = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(6))
    jurisdictions = []
    taxa = []
    strata = []
    collections = []
    chrons = []
    has_images = False

    query = None  # The active SQL query, not the actual result records
    page_size = 50
    current_page = 1
    sort_order = 'number'
    results = []  # Page of results from paginator

    def is_empty(self):
        if len(self.jurisdictions) == 0 and len(self.taxa) == 0 and len(self.strata) == 0 and \
                        len(self.collections) == 0 and len(self.chrons) == 0 and self.has_images is False:
            return True
        else:
            return False

    def get_results(self):
        paginator = Paginator(self.query, self.page_size)
        try:
            self.results = paginator.page(self.current_page)
        except PageNotAnInteger:
            self.results = paginator.page(1)
        except TypeError:
            return []
        except EmptyPage:
            self.results = paginator.page(paginator.num_pages)
        return self.results

    def get_results_json(self):

        points = []

        for s in self.results:
            if s.locality.latitude and s.locality.longitude:
                points.append({"type": "Feature",
                               "geometry": {"type": "Point",
                                            "coordinates": [s.locality.longitude, s.locality.latitude]},
                               "properties": {"specimen_id": s.id,
                                              "sci_name": s.taxon.scientific_name(),
                                              "cat_num": s.specimen_number(),
                                              "jurisdiction": s.locality.jurisdiction.full_name()}
                               })

        return json.dumps({"type": "FeatureCollection", "features": points})

    def search(self):
        if self.is_empty():
            self.query = None
            return

        query = Specimen.objects.filter().distinct().order_by(self.sort_order)
        if len(self.taxa) > 0:
            query = query.filter(taxon__in=get_hierarchical_search_elements(self.taxa))
        if len(self.jurisdictions) > 0:
            query = query.filter(locality__jurisdiction__in=get_hierarchical_search_elements(self.jurisdictions))
        if len(self.strata) > 0:
            query = query.filter(stratum__in=get_hierarchical_search_elements(self.strata))
        if len(self.chrons) > 0:
            query = query.filter(chron__in=get_hierarchical_search_elements(self.chrons))
        if len(self.collections) > 0:
            query = query.filter(collection__in=get_hierarchical_search_elements(self.collections))
        if self.has_images:
            query = query.filter(images__isnull=False)

        self.query = query
        return


def get_hierarchical_search_elements(elements):
    search_elements = []
    for element in elements:
        search_elements = set().union(search_elements, element.get_descendants(True))
    return search_elements
Tomislav Urban
  • 95
  • 1
  • 10
  • Can you show the code for SpecimenSearchCommand? I would say though that serializing model instances is rarely a good idea; rather, you should serialize the IDs and re-query them from the db as necessary. – Daniel Roseman Oct 27 '16 at 19:26
  • Yeah, so that's the thing, this is not a model instance. It is a object used to retain user's search criteria. So it has no ID and is not intended to be persisted. – Tomislav Urban Oct 27 '16 at 19:30
  • Yes but Collections *are* instances, and you're serializing them as part of the SSC. – Daniel Roseman Oct 27 '16 at 19:31
  • True, I could save the IDs of the instance elements to the SSC instead of the objects themselves, I'll give that a try. – Tomislav Urban Oct 27 '16 at 19:36
  • So, I do like that approach and I'm going to switch to it, but unfortunately it had no effect on the underlying problem. Still losing session object. – Tomislav Urban Oct 27 '16 at 20:10
  • 2
    OK. You have some serious problems how you've set up the attributes of the SSC class though; they are all class-level, not instance-level, which is almost certainly the source of your problem. See [this question](http://stackoverflow.com/questions/1680528/how-do-i-avoid-having-class-data-shared-among-instances) for example. – Daniel Roseman Oct 27 '16 at 20:16
  • Yes! That was it. Thank you so very much. – Tomislav Urban Oct 27 '16 at 20:41

1 Answers1

1

OK, so as Daniel pointed out, the attributes of the SSC class were class-level instead of instance level. The correct version looks like this now:

    self.created = datetime.datetime.now()
    self.key = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(6))
    self.jurisdictions = []
    self.taxa = []
    self.strata = []
    self.collections = []
    self.chrons = []
    self.has_images = False

    self.query = None  # The active SQL query, not the actual result records
    self.page_size = 50
    self.current_page = 1
    self.sort_order = 'number'
    self.results = []  # Page of results from paginator
Tomislav Urban
  • 95
  • 1
  • 10