One of the batteries included in DontManage Framework is inbuilt caching using Redis. Redis is fast, simple to use, in-memory key-value storage. Redis with DontManage 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 DontManage web/background worker can connect to it using dontmanage.cache.

Note: On older versions of DontManage, you may need to use dontmanage.cache() instead of dontmanage.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:

  1. Strings are the most popular and most used datatype in Redis. They are stored as key-value pairs.

    In [1]: dontmanage.cache.set_value("key", "value")
    
    In [2]: dontmanage.cache.get_value("key")
    Out[2]: 'value'
    
  2. 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]: dontmanage.cache.hset("user|admin", "name", "Admin")
    In [2]: dontmanage.cache.hset("user|admin", "email", "admin@example.com")
    
    # Get single field value
    In [3]: dontmanage.cache.hget("user|admin", "name")
    Out[3]: 'Admin'
    
    # Or retrieve all fields at once.
    In [4]: dontmanage.cache.hgetall("user|admin")
    Out[4]: {'name': 'Admin', 'email': 'admin@example.com'}
    

Cached Documents

DontManage 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 = dontmanage.get_cached_doc("System Settings")

Cached documents are automatically cleared using "best-effort" cache invalidation.

Whenever DontManage's ORM encounters a change using doc.save or dontmanage.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 dontmanage.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 := dontmanage.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
    dontmanage.cache.set_value(key, result)

    return result

Cache Invalidation

Two strategies are recommended for avoiding stale cache issues:

  1. Setting short TTL while setting cached values.

    # This cached value will automatically expire in one hour
    dontmanage.cache.set_value(key, result, expires_in_sec=60*60)
    
  2. Manually clearing the cache when cached values are modified.

    dontmanage.cache.delete_value(key) # `dontmanage.cache.hdel` if using hashes.
    

@redis_cache decorator

DontManage 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 dontmanage.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.

  1. 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.

  2. 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()

DontManage'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

dontmanage.cache internally prefixes keys by some site context. Hence calling dontmanage.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 dontmanage.cache.make_key function.

Complex Types

DontManage uses pickle module to serialize complex objects like documents in bytes. Hence when using dontmanage.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

DontManage implements client-side cache on top of Redis cache inside dontmanage.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 DontManage related changes are made by wrapping the default Redis client and extending the methods. You can find this code in dontmanage.utils.redis_wrapper the module.