Thursday, October 5

Practice caution when using cache (lru_cache in python)

I have a simple search code in python:

def SearchNode(name):
   for node in Nodes:
         if node.name == name:
              return node
   return None

Pretty simple. Nodes is global variable in this module. Because this function is called frequently, I added cache from functools for it:
@lru_cache(maxsize=5000)
def SearchNode(name):
   for node in Nodes:
         if node.name == name:
              return node
   return None
Sounds too trivial to create a unit test for it, so I apply it directly to my project. Two days later, my program is giving out different kinds of error, and it took me several hours to identify the source of error: the @lru_cache line. The call to this function always returns "None" for me.

 I actually added some code to iterate the legit names and do "SearchNode(name)", and the return values were always "None"!

After another hour of tracing and thinking, I finally understood the problem:

When building Nodes, I call this function for a new name, to make sure the name does not exist in the Nodes, before adding into the structure. So for each new name, the answer is always None, and the @lru_cache remembers this answer. So when I actually need the node, the @lru_cache still gives me the "None".


After locating the source of error, I can think of several ways to solve it (while still having the cache mechanism): Disable the cache first, only enable it after the Nodes is built; Reset cache after the the Nodes is built; Let the cache remember the non-None result. @lru_cache does not have the capability to do any of them, so I have make my own cache, using the third way:
SearchNode_cache = {}
def SearchNode(name):
   if node in SearchNode_cache:         return SearchNode_cache[node]
   for node in Nodes:
         if node.name == name:
              SearchNode_cache[node] = node              return node
   return None

You can easily add 2 lines to set a size limit for the cache, and remove the oldest item in it, to implement Least Recent Used mechanism.


Lesson learnt:
1, Any one simple line, no matter how innocent it looks, can jeopardize your project in a big way.
2, If the value of the Nodes can be modified, my current way is not correct.
3, So don't just copy my code. My application scenario might not be the same as yours.
4, Practice caution when using cache.

Labels: ,