tkem/cachetools

Invalidating cached values in "cachemethod"

ShivKJ opened this issue · 10 comments

Hi,

Is there a way to update/remove cache value returned by cachemethod?

For example, in the below code,

import operator as op
from dataclasses import dataclass, field

from cachetools import TTLCache, cachedmethod


@dataclass
class A:
    cache: TTLCache = field(init=False)

    def __post_init__(self):
        self.cache = TTLCache(maxsize=32, ttl=2 * 3600)

    @cachedmethod(cache=op.attrgetter('cache'))
    def compute(self, i):
        print('value of i', i)
        return i * 2

Suppose, I want to remove cached value for a given i from cache, then it is not possible to do directly.

Meanwhile, I learned about hashkey function in keys module. But it is not exposed in cachetools __init__, so can not be imported (of course there are hacks around it). Using this, we can updated cached value.


So, I think there could be two solutions to it,

  1. As cachemethod wraps a method to enable caching feature in it, so apart from the caching feature, it can add functionality to invalidate. For example,
a = A()

print(a.compute(10))

a.compute.remove(10) # remove can have signature *args, **kwargs. Instead of word `remove`, a more better function name can be thought
  1. Exposing keys module in cachetools __init__.py.
from cachetools.keys import hashkey

a = A()

i = 10
print(a.compute(i))

del a.cache[hashkey(i)]

I think approach 1 would be more user friendly.

tkem commented

There is an example for removing individual cache items using hashkey in the @cached decorator docs: https://cachetools.readthedocs.io/en/stable/#cachetools.cached
Maybe I should state explicitly that this work similar for @cachedmethod.
However, I agree that it is quite cumbersome to use as it is. OTOH, my usual advice for anybody trying to do fancy stuff with the @cached or @cachedmethod decorators is: Don't. Use the decorators for the most simple use cases, for which they were designed. If you think you need to do something that requires more control of the underlying cache object, just access/query the cache object directly.

tkem commented

AFAICS this is also quite similar to #176.

I am in similar situation. Is there any way to delete a specific cache key from TTLCache. The key in TTLCache is stored as tuple.

TTLCache({(<class 'cachetools.keys._HashedTuple'>, 'token', 'd4e498d636e76dd3ea19fb727dc8f828'): TokenClass object at 0x000001F939670250>}

How do I reconstruct this cache key to pop its value?

token_function.cache.pop(token_function.cache_key(?), None)

When I tried to reconstruct the tuple to pass it as a key, it raises KeyError.

token_function.cache.pop(token_function.cache_key('token', 'd4e498d636e76dd3ea19fb727dc8f828'), None)

I'm not able to reconstruct the first element of this tuple <class 'cachetools.keys._HashedTuple'.

tkem commented

@infohash: Are you talking about the @cached or the @cachedmethod decorator?
@cachedmethod properties are still considered somewhat "experimental", and are therefore not properly documented yet.
For @cached, the arguments to the cache_key function are simply the same as for the wrapped function, i.e. not a tuple:

from cachetools import TTLCache, cached

cache = TTLCache(maxsize=1024, ttl=600)

@cached(cache=cache)
def token_function(token, id):
    return (token, id)

print(token_function('token', 'd4e498d636e76dd3ea19fb727dc8f828'))
print(cache)
token_function.cache.pop(token_function.cache_key('token', 'd4e498d636e76dd3ea19fb727dc8f828'), None)
print(cache)

I am using @cachedmethod because the method that I am applying cache on is an instance method of a class and I initialize TTLCache inside the init method.

from cachetools import TTLCache, cachedmethod


class TokenClass:
    def __init__(self):
        self.cache = TTLCache(maxsize=128, ttl=300)

    @cachedmethod(cache=lambda self: self.cache)
    def token_function(self, token):
        ...

It works fine for storing the cache, the problem happens when I try to remove a specific key from it. I have changed my design so I don't need to pop a key from TTLCache anymore. But it's something you can check because it almost works.

tkem commented

So, the token argument is actually a tuple?
Bear in mind that cache in @cachedmethod is actually a method itself (i.e. needs passing self), and the cache_key gets passed self as its first argument:

from cachetools import TTLCache, cachedmethod

class TokenClass:
    def __init__(self):
        self.cache = TTLCache(maxsize=128, ttl=300)

    @cachedmethod(cache=lambda self: self.cache)
    def token_function(self, token):
        return token

tc = TokenClass()
print(tc.token_function(('token', 'd4e498d636e76dd3ea19fb727dc8f828')))
print(tc.token_function.cache(tc))
tc.token_function.cache(tc).pop(tc.token_function.cache_key(tc, ('token', 'd4e498d636e76dd3ea19fb727dc8f828')), None)
print(tc.token_function.cache(tc))

It's cumbersome, and I'm not too happy with it myself, so it will probably remain an experimental, undocumented feature until someone comes up with something better (or somebody provides sensible documentation for this)...

@tkem I have solved it finally. So if the method is called with the keyword argument, the cache_key must also take the keyword argument.

tc.token_function.cache(tc).pop(tc.token_function.cache_key(tc, token=d4e498d636e76dd3ea19fb727dc8f828'), None)
tkem commented

@infohash: Glad you solved it. Yes, documentation should (and eventually, will) be added for this.