symposion_app/registrasion/controllers/batch.py

120 lines
3.4 KiB
Python
Raw Normal View History

2016-04-30 11:42:02 +00:00
import contextlib
import functools
2016-05-01 01:12:35 +00:00
from django.contrib.auth.models import User
2016-04-30 11:42:02 +00:00
class BatchController(object):
''' Batches are sets of operations where certain queries for users may be
repeated, but are also unlikely change within the boundaries of the batch.
Batches are keyed per-user. You can mark the edge of the batch with the
``batch`` context manager. If you nest calls to ``batch``, only the
outermost call will have the effect of ending the batch.
Batches store results for functions wrapped with ``memoise``. These results
for the user are flushed at the end of the batch.
If a return for a memoised function has a callable attribute called
``end_batch``, that attribute will be called at the end of the batch.
'''
_user_caches = {}
2016-05-01 00:36:52 +00:00
_NESTING_KEY = "nesting_count"
2016-04-30 11:42:02 +00:00
@classmethod
@contextlib.contextmanager
def batch(cls, user):
''' Marks the entry point for a batch for the given user. '''
2016-05-01 00:36:52 +00:00
cls._enter_batch_context(user)
try:
yield
finally:
# Make sure we clean up in case of errors.
cls._exit_batch_context(user)
@classmethod
def _enter_batch_context(cls, user):
if user not in cls._user_caches:
cls._user_caches[user] = cls._new_cache()
cache = cls._user_caches[user]
cache[cls._NESTING_KEY] += 1
@classmethod
def _exit_batch_context(cls, user):
cache = cls._user_caches[user]
cache[cls._NESTING_KEY] -= 1
if cache[cls._NESTING_KEY] == 0:
cls._call_end_batch_methods(user)
del cls._user_caches[user]
2016-05-01 01:25:48 +00:00
@classmethod
def _call_end_batch_methods(cls, user):
cache = cls._user_caches[user]
ended = set()
while True:
keys = set(cache.keys())
if ended == keys:
break
keys_to_end = keys - ended
for key in keys_to_end:
2016-05-01 01:25:48 +00:00
item = cache[key]
if hasattr(item, 'end_batch') and callable(item.end_batch):
item.end_batch()
ended = ended | keys_to_end
2016-04-30 11:42:02 +00:00
@classmethod
def memoise(cls, func):
''' Decorator that stores the result of the stored function in the
2016-05-01 01:12:35 +00:00
user's results cache until the batch completes. Keyword arguments are
not yet supported.
2016-04-30 11:42:02 +00:00
Arguments:
2016-05-01 01:12:35 +00:00
func (callable(*a)): The function whose results we want
to store. The positional arguments, ``a``, are used as cache
keys.
2016-04-30 11:42:02 +00:00
Returns:
2016-05-01 01:12:35 +00:00
callable(*a): The memosing version of ``func``.
2016-04-30 11:42:02 +00:00
'''
@functools.wraps(func)
2016-05-01 01:12:35 +00:00
def f(*a):
for arg in a:
if isinstance(arg, User):
user = arg
break
else:
raise ValueError("One position argument must be a User")
2016-04-30 11:42:02 +00:00
2016-05-01 01:12:35 +00:00
func_key = (func, tuple(a))
2016-04-30 11:42:02 +00:00
cache = cls.get_cache(user)
2016-05-01 01:12:35 +00:00
if func_key not in cache:
cache[func_key] = func(*a)
return cache[func_key]
2016-04-30 11:42:02 +00:00
return f
@classmethod
def get_cache(cls, user):
if user not in cls._user_caches:
2016-05-01 00:36:52 +00:00
# Return blank cache here, we'll just discard :)
return cls._new_cache()
2016-04-30 11:42:02 +00:00
return cls._user_caches[user]
2016-05-01 00:36:52 +00:00
@classmethod
def _new_cache(cls):
''' Returns a new cache dictionary. '''
cache = {}
cache[cls._NESTING_KEY] = 0
return cache