Use whenever a user saw another user's or tenant's data, or a per-user response goes through a shared cache, Redis or a CDN, or a cache key is designed or changed. Checks the key holds every value the response depends on (tenant, user, scope, locale), writes the fixed key, Vary header and tests.
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The common folk description of this bug is that self is not part of
the cache key, so all instances share one entry. That is wrong, and the
real mechanism matters because it changes the fix.
What the Python documentation actually says:
functools.cached_property "stores results at the instance level"
and functools.lru_cache stores them "at the class level"
(Python FAQ, how do I cache method calls?).self instance argument is included in
the cache"
(functools docs).lru_cache "creates a reference to the instance unless special
efforts are made to pass in weak references", and "instances are kept
alive until they age out of the cache or until the cache is cleared"
(Python FAQ).So the entry is keyed on the whole argument tuple, self included, in
one cache that lives on the class. Whether two instances collide
therefore depends entirely on how self hashes:
id()"
(Python glossary, hashable).
Identity-based hashing means each instance gets its own entries. The
defect here is retention, not collision: a class-level cache pins every
request-scoped object it has seen until the entry ages out of maxsize.__eq__
and __hash__ opts into value-based hashing. If the fields that
participate in equality omit the tenant, then a repository object for
tenant A and one for tenant B compare equal, hash equal, and share
every cached entry. That is a genuine cross-tenant collision, and it is
invisible in the key expression because the collision happens inside
__hash__.Fixes, in order of preference:
functools.cached_property when the value is per-instance and
takes no arguments. It stores at the instance level and does not
create a reference to the instance, so the result is released with the
instance
(Python FAQ).