from contextlib import contextmanager import idmap.models import idmap.tls import threading import idmap _toggle = threading.local() def is_active(): return getattr(_toggle, 'is_active', False) def deactivate_cache(): ''' The Idmap cache is always being populated, even if it's not "active" We just ignore its contents. It doesn't look like idmap has a way to turn it off entirely. ''' _toggle.is_active = False # 2017-08-07: flush both in activate and deactivate, just to be sure # i was getting some unexpected behavior in tests, when a test without # IDmap ran after a test with IDmap idmap.flush() def activate_cache(): idmap.flush() _toggle.is_active = True @contextmanager def use_cache(): activate_cache() try: yield finally: deactivate_cache() class IdMapModel(idmap.models.IdMapModel): class Meta: abstract = True # needs to be here for Participant and Session # 2018-11-24: but it seems Participant and Session don't inherit # from this; that's why we set use_strong_refs in the metaclass use_strong_refs = True # The ``get_cached_instance`` method is the canonical access point for # idmap to retrieve objects for a particular model from the cache. If it # returns None, then it's meant as a cache miss and the object is retrieved # from the database. # We intercept this so that we can disable the idmap cache. That is # required as we only want it to be active on experiment views. idmap has # it's problems when used together with django-channels as channels is # re-using threads between requests. That will result in a shared idmap # cache between requests, which again results in unpredictable data # returned from the cache as it might contain stale data. # The solution is to only use the cache when processing a view and clear # the cache before activating the use. See the activate_cache() for the # implementation. @classmethod def get_cached_instance(cls, *args, **kwargs): if is_active(): return super().get_cached_instance(*args, **kwargs) CLASSES_TO_SAVE = {'Session', 'Participant', 'Subsession', 'Group', 'Player'} def _get_save_objects_model_instances(): """ Get all model instances that should be saved. This implementation uses the idmap cache to determine which instances have been loaded. """ # 2017-08-08: adding this because a test was failing unexpectedly for me # it seems reasonable that if we haven't activated the cache, we should # disregard it entirely. if not is_active(): return [] cache = getattr(idmap.tls._tls, 'idmap_cache', {}) instances = [] for model_class, model_cache in cache.items(): # Collect instances if it's a subclass of one of the monitored # models. is_monitored = model_class.__name__ in CLASSES_TO_SAVE if is_monitored: cached_instances = list(model_cache.values()) instances.extend(cached_instances) return instances def save_objects(): for instance in _get_save_objects_model_instances(): # if there are no changes on the instance, SaveTheChange will detect # that very quickly and skip saving. I tested and it made no difference # if I test whether _changed_fields is empty. instance.save()