Issue #20 resolved

Allow expiration_time to be a callable

David Beitey
created an issue

My current use-case is to cache a set of values for a relative amount of time -- in one case, until the end of the current day, in another, until the end of the given week, and finally, until a certain date/time.

Currently, dogpile.cache accepts an expiration_time as an integer, which represents a fixed number seconds from now (eg in 24 hours time), and not a relative or otherwise dynamic value. In order to obtain a relative expiration_time for use, you could use get_or_create directly and calculate the necessary value when run, but for the cache_on_arguments decorator, this isn't possible given its nature as a decorator.

So, my suggestion is to allow expiration_time to be specified as callable that returns a integer and call this whenever expiration_time is used (eg in the CacheRegion.get and get_or_create functions). Thus, the expiration time is dynamic and since a function, the resulting relative time could be based upon anything (not just relative times as I mention above).

For example:

class CacheRegion(object):
    ...
    def get_or_create(self, key, creator, expiration_time=None):
        if hasattr(expiration_time, '__call__'):
            expiration_time = expiration_time()
        ...

def seconds_til_tomorrow():
    #or something else like a database call or whatever
    tomorrow = date.today() + timedelta(days=1)
    til_tomorrow = datetime.combine(tomorrow, time(0)) - datetime.now()
    return math.ceil(til_tomorrow.total_seconds())


region = make_region().configure('memory', expiration_time=seconds_til_tomorrow)
config.rest_of_day.get_or_create('1', lambda: datetime.date.today()) #Caches value of today's date til tomorrow

Comments (5)

  1. Mike Bayer repo owner

    fair enough, though do you think the callable is really only needed by cache_on_arguments()? at least that way we could avoid invoking callable(invalidation_time) for every cache get.

  2. David Beitey reporter

    If you set a callable expiration_time, and it is resolved in cache_on_arguments, then the value is relative to when the decorator was applied/executed (eg application startup), rather than relative to when the caching happens. So, this means that the callable would need to be run within the decorated function itself -- to run whenever this decorated function is called so the expiration time can be dynamic (relative). My thinking is that this could be just before the call to get_or_create (line 577 of region.py).

    It does present potential for performance issues, but it would be on the user not to do something overly complicated in the callable. However, the expires callable could itself be cached by the user, too.

    Any other suggestions otherwise?

  3. Mike Bayer repo owner

    If you set a callable expiration_time, and it is resolved in cache_on_arguments, then the value is relative to when the decorator was applied/executed (eg application startup), rather than relative to when the caching happens.

    I'm not suggesting that the function be called immediately within the cache_on_arguments decorator, just that we'd determine at app startup if the argument is a callable or not:

    # inside the decorator, at app startup time
    is_callable = callable(argument)
    
    # ...
    
    # inside the decorator when we're about to call get_or_create
    if is_callable:
        timeout = argument()
    else:
       timeout = argument
    

    My thinking is that this could be just before the call to get_or_create (line 577 of region.py).

    yes, this is where we'd actually call the callable.

    It does present potential for performance issues, but it would be on the user not to do something overly complicated in the callable.

    well only if a callable is presented. I'm just trying to avoid latency for the vast majority of use cases that only send a scalar value here.

  4. Log in to comment