Files
cellxgene/server/common/immutable_kvcache.py

72 lines
2.2 KiB
Python

import threading
from collections.abc import MutableMapping
class ImmutableKVCache(MutableMapping):
"""
Guarantees that the factory will be called for each key once, and
only once.
"""
def __init__(self, factory):
self.factory = factory # user-provided factory function
self.lock = threading.Lock() # guards factory_calls
self.factory_calls = {} # per-key factory condition variables
self.cache = {} # result cache, indexed by key
super().__init__()
def __getitem__(self, key):
if key in self.cache:
return self.cache[key]
# we need to call factory. First grab the main lock and the per-key CV.
factory_calls = None
creation_thr = False
with self.lock:
if key in self.cache:
return self.cache[key]
if key not in self.factory_calls:
creation_thr = True
self.factory_calls[key] = {"cv": threading.Condition(), "is_done": False, "error": None}
factory_calls = self.factory_calls[key]
# with the CV, create the value (or wait for it to be created)
cv = factory_calls["cv"]
with cv:
if creation_thr:
try:
self.cache[key] = self.factory(key)
except Exception as e:
factory_calls["error"] = e
factory_calls["is_done"] = True
cv.notify_all()
else:
""" wait for the value to be available """
while not factory_calls["is_done"]:
cv.wait()
with self.lock:
if key in self.factory_calls:
del self.factory_calls[key]
return self.cache[key]
def __iter__(self):
""" weak iter, don't call factory """
return self.cache.__iter__()
def __len__(self):
return self.cache.__len__()
def __contains__(self, key):
""" weak contain - don't call factory """
return self.cache.__contains__(key)
def __delitem__(self, key):
del self.cache[key]
def __setitem__(self, key, value):
""" unsupported """
raise NotImplementedError