Caching
One of the batteries included in Frappe Framework is inbuilt caching using Redis. Redis is fast, simple to use, in-memory key-value storage. Redis with Frappe Framework can be used to speed up repeated long-running computations or avoid database queries for data that doesn't change often.
Redis is spawned as a separate service by the bench and each Frappe web/background worker can connect to it using frappe.cache
.
Note: On older versions of Frappe, you may need to use
frappe.cache()
instead offrappe.cache
to access Redis connection.
Redis Data Types and Usage
Redis supports many data types, but here we will only cover a few most used datatypes:
- Strings are the most popular and most used datatype in Redis. They are stored as key-value pairs.
In [1]: frappe.cache.set_value("key", "value")
In [2]: frappe.cache.get_value("key")
Out[2]: 'value'
- Hashes are used to represent complicated objects, fields of which can be updated separately without sending the entire object. You can imagine Hashes as dictionaries stored on Redis.
# Get fields separately
In [1]: frappe.cache.hset("user|admin", "name", "Admin")
In [2]: frappe.cache.hset("user|admin", "email", "admin@example.com")
# Get single field value
In [3]: frappe.cache.hget("user|admin", "name")
Out[3]: 'Admin'
# Or retrieve all fields at once.
In [4]: frappe.cache.hgetall("user|admin")
Out[4]: {'name': 'Admin', 'email': 'admin@example.com'}
Cached Documents
Frappe has an inbuilt function for getting documents from the cache instead of the database.
Getting a document from the cache is usually faster, so you should use them when the document doesn't change that often. A common usage for this is getting user configured settings.
system_settings = frappe.get_cached_doc("System Settings")
Cached documents are automatically cleared using "best-effort" cache invalidation.
Whenever Frappe's ORM encounters a change using doc.save
or frappe.db.set_value
, it clears the related document's cache. However, this isn't possible if a raw query to the database is issued.
Note: Manual cache invalidation can be done using frappe.clear_document_cache(doctype, name)
.
Implementing Custom Caching
When you're dealing with a long expensive computation, the outcome of which is deterministic for the same inputs then it might make sense to cache the output.
Let's attempt to implement a custom cache in this toy function which is slow.
def slow_add(a, b):
import time; time.sleep(1) # Fake workload
return a + b
The most important part of implementing custom caching is generating a unique key. In this example the outcome of the cached value is dependent on two input variables, hence they should be part of the key.
def slow_add(a, b):
key = f"slow_addition|{a}+{b}" # unique key representing this computation
# If this key exists in cache, then return value
if cached_value := frappe.cache.get_value(key):
return cached_value
import time; time.sleep(1) # Fake workload
result = a + b
# Set the computed value in cache so next time we dont have to do the work
frappe.cache.set_value(key, result)
return result
Cache Invalidation
Two strategies are recommended for avoiding stale cache issues:
- Setting short TTL while setting cached values.
# This cached value will automatically expire in one hour
frappe.cache.set_value(key, result, expires_in_sec=60*60)
- Manually clearing the cache when cached values are modified.
frappe.cache.delete_value(key) # `frappe.cache.hdel` if using hashes.
@redis_cache
decorator
Frappe provides a decorator to automatically cache the results of a function call.
You can use it to quickly implement caching on top of any existing function which might be slow.
In [1]: def slow_function(a, b):
...: import time; time.sleep(1) # fake expensive computation
...: return a + b
...:
In [2]: # This takes 1 second to execute every time.
...: %time slow_function(40, 2)
...:
Wall time: 1 s
Out[2]: 42
In [3]: %time slow_function(40, 2)
Wall time: 1 s
Out[3]: 42
In [4]: from frappe.utils.caching import redis_cache
...:
In [5]: @redis_cache
...: def slow_function(a, b):
...: import time; time.sleep(1) # fake expensive computation
...: return a + b
...:
In [6]: # Now first call takes 1 second, but all subsequent calls return instantly.
...: %time slow_function(40, 2)
...:
Wall time: 1 s
Out[6]: 42
In [7]: %time slow_function(40, 2)
...:
Wall time: 897 µs
Out[7]: 42
Cache Invalidation
There are two ways to invalidate cached values from @redis_cache
.
- Setting appropriate expiry period (TTL in seconds) so cache invalidates automatically after some time. Example:
@redis_cache(ttl=60)
will cause cached value to expire after 60 seconds. - Manual clearing of cache. This is done by calling function's
clear_cache
method.
@redis_cache
def slow_function(...):
...
def invalidate_cache():
slow_function.clear_cache()
Frappe's Redis Setup
Bench sets up Redis by default. You will find Redis config in {bench}/config/
directory.
Bench also configures Procfile
and supervisor configuration file to launch Redis server when the bench is started.
redis_cache: redis-server config/redis_cache.conf
A sample config looks like this:
dbfilename redis_cache.rdb
dir /home/user/benches/develop/config/pids
pidfile /home/user/benches/develop/config/pids/redis_cache.pid
bind 127.0.0.1
port 13000
maxmemory 737mb
maxmemory-policy allkeys-lru
appendonly no
save ""
You can modify this maxmemory
in this config to increase the maximum memory allocated for caching. We do not recommend modifying anything else.
Refer to the official config documentation to understand more: https://redis.io/docs/management/config-file/
Implementation details
Multi-tenancy
frappe.cache
internally prefixes keys by some site context. Hence calling frappe.cache.set_value("key")
from two different sites on the same bench will create two separate entries for each site.
To see implementation details of this see frappe.cache.make_key
function.
Complex Types
Frappe uses pickle
module to serialize complex objects like documents in bytes. Hence when using frappe.cache
you don't have to worry about serializing/de-serializing values.
Read more about pickling here: https://docs.python.org/3/library/pickle.html
Client Side caching
Frappe implements client-side cache on top of Redis cache inside frappe.local.cache
to avoid repeated calls to Redis.
Any repeated calls to Redis within the same request/job return data from the client cache instead of calling Redis again.
RedisWrapper
All Frappe related changes are made by wrapping the default Redis client and extending the methods. You can find this code in frappe.utils.redis_wrapper
the module.