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