Skip to content

Commit 0db5785

Browse files
matthiasdienerinducer
authored andcommitted
WriteOncePersistentDict: use functools.lru_cache
1 parent 2c0c9e7 commit 0db5785

2 files changed

Lines changed: 23 additions & 142 deletions

File tree

pytools/persistent_dict.py

Lines changed: 15 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
THE SOFTWARE.
3030
"""
3131

32-
import collections.abc as abc
3332
import errno
3433
import hashlib
3534
import logging
@@ -443,125 +442,6 @@ def update_for_frozendict(self, key_hash: Hash, key: Mapping) -> None:
443442
# }}}
444443

445444

446-
# {{{ lru cache
447-
448-
class _LinkedList:
449-
"""The list operates on nodes of the form [value, leftptr, rightpr]. To create a
450-
node of this form you can use `LinkedList.new_node().`
451-
452-
Supports inserting at the left and deleting from an arbitrary location.
453-
"""
454-
def __init__(self):
455-
self.count = 0
456-
self.head = None
457-
self.end = None
458-
459-
@staticmethod
460-
def new_node(element):
461-
return [element, None, None]
462-
463-
def __len__(self):
464-
return self.count
465-
466-
def appendleft_node(self, node):
467-
self.count += 1
468-
469-
if self.head is None:
470-
self.head = self.end = node
471-
return
472-
473-
self.head[1] = node
474-
node[2] = self.head
475-
476-
self.head = node
477-
478-
def pop_node(self):
479-
end = self.end
480-
self.remove_node(end)
481-
return end
482-
483-
def remove_node(self, node):
484-
self.count -= 1
485-
486-
if self.head is self.end:
487-
assert node is self.head
488-
self.head = self.end = None
489-
return
490-
491-
left = node[1]
492-
right = node[2]
493-
494-
if left is None:
495-
self.head = right
496-
else:
497-
left[2] = right
498-
499-
if right is None:
500-
self.end = left
501-
else:
502-
right[1] = left
503-
504-
node[1] = node[2] = None
505-
506-
507-
class _LRUCache(abc.MutableMapping):
508-
"""A mapping that keeps at most *maxsize* items with an LRU replacement policy.
509-
"""
510-
def __init__(self, maxsize):
511-
self.lru_order = _LinkedList()
512-
self.maxsize = maxsize
513-
self.cache = {}
514-
515-
def __delitem__(self, item):
516-
node = self.cache[item]
517-
self.lru_order.remove_node(node)
518-
del self.cache[item]
519-
520-
def __getitem__(self, item):
521-
node = self.cache[item]
522-
self.lru_order.remove_node(node)
523-
self.lru_order.appendleft_node(node)
524-
# A linked list node contains a tuple of the form (item, value).
525-
return node[0][1]
526-
527-
def __contains__(self, item):
528-
return item in self.cache
529-
530-
def __iter__(self):
531-
return iter(self.cache)
532-
533-
def __len__(self) -> int:
534-
return len(self.cache)
535-
536-
def clear(self):
537-
self.cache.clear()
538-
self.lru_order = _LinkedList()
539-
540-
def __setitem__(self, item, value):
541-
if self.maxsize < 1:
542-
return
543-
544-
try:
545-
node = self.cache[item]
546-
self.lru_order.remove_node(node)
547-
except KeyError:
548-
if len(self.lru_order) >= self.maxsize:
549-
# Make room for new elements.
550-
end_node = self.lru_order.pop_node()
551-
del self.cache[end_node[0][0]]
552-
553-
node = self.lru_order.new_node((item, value))
554-
self.cache[item] = node
555-
556-
self.lru_order.appendleft_node(node)
557-
558-
assert len(self.cache) == len(self.lru_order), \
559-
(len(self.cache), len(self.lru_order))
560-
assert len(self.lru_order) <= self.maxsize
561-
562-
# }}}
563-
564-
565445
# {{{ top-level
566446

567447
class NoSuchEntryError(KeyError):
@@ -720,7 +600,7 @@ class WriteOncePersistentDict(_PersistentDictBase):
720600
"""A concurrent disk-backed dictionary that disallows overwriting/deletion.
721601
722602
Compared with :class:`PersistentDict`, this class has faster
723-
retrieval times.
603+
retrieval times because it uses an LRU cache to cache entries in memory.
724604
725605
.. automethod:: __init__
726606
.. automethod:: __getitem__
@@ -742,14 +622,15 @@ def __init__(self, identifier, key_builder=None, container_dir=None,
742622
"""
743623
_PersistentDictBase.__init__(self, identifier, key_builder, container_dir)
744624
self._in_mem_cache_size = in_mem_cache_size
745-
self.clear_in_mem_cache()
625+
from functools import lru_cache
626+
self._fetch = lru_cache(maxsize=in_mem_cache_size)(self._fetch)
746627

747628
def clear_in_mem_cache(self) -> None:
748629
"""
749630
.. versionadded:: 2023.1.1
750631
"""
751632

752-
self._cache = _LRUCache(self._in_mem_cache_size)
633+
self._fetch.cache_clear()
753634

754635
def _spin_until_removed(self, lock_file, stacklevel):
755636
from os.path import exists
@@ -807,19 +688,14 @@ def store(self, key, value, _skip_if_present=False, _stacklevel=0):
807688
def fetch(self, key, _stacklevel=0):
808689
hexdigest_key = self.key_builder(key)
809690

810-
# {{{ in memory cache
691+
(stored_key, stored_value) = self._fetch(hexdigest_key, 1 + _stacklevel)
811692

812-
try:
813-
stored_key, stored_value = self._cache[hexdigest_key]
814-
except KeyError:
815-
pass
816-
else:
817-
logger.debug("%s: in mem cache hit [key=%s]",
818-
self.identifier, hexdigest_key)
819-
self._collision_check(key, stored_key, 1 + _stacklevel)
820-
return stored_value
693+
self._collision_check(key, stored_key, 1 + _stacklevel)
821694

822-
# }}}
695+
return stored_value
696+
697+
def _fetch(self, hexdigest_key, _stacklevel=0): # pylint:disable=method-hidden
698+
# This is separate from fetch() to allow for LRU caching
823699

824700
# {{{ check path exists and is unlocked
825701

@@ -829,7 +705,7 @@ def fetch(self, key, _stacklevel=0):
829705
if not isdir(item_dir):
830706
logger.debug("%s: disk cache miss [key=%s]",
831707
self.identifier, hexdigest_key)
832-
raise NoSuchEntryError(key)
708+
raise NoSuchEntryError(hexdigest_key)
833709

834710
lock_file = self._lock_file(hexdigest_key)
835711
self._spin_until_removed(lock_file, 1 + _stacklevel)
@@ -852,9 +728,7 @@ def fetch(self, key, _stacklevel=0):
852728
f"Remove the directory '{item_dir}' if necessary. "
853729
f"(caught: {type(e).__name__}: {e})",
854730
stacklevel=1 + _stacklevel)
855-
raise NoSuchEntryInvalidKeyError(key)
856-
857-
self._collision_check(key, read_key, 1 + _stacklevel)
731+
raise NoSuchEntryInvalidKeyError(hexdigest_key)
858732

859733
# }}}
860734

@@ -871,16 +745,15 @@ def fetch(self, key, _stacklevel=0):
871745
f"Remove the directory '{item_dir}' if necessary."
872746
f"(caught: {type(e).__name__}: {e})",
873747
stacklevel=1 + _stacklevel)
874-
raise NoSuchEntryInvalidContentsError(key)
748+
raise NoSuchEntryInvalidContentsError(hexdigest_key)
875749

876750
# }}}
877751

878-
self._cache[hexdigest_key] = (key, read_contents)
879-
return read_contents
752+
return (read_key, read_contents)
880753

881754
def clear(self):
882755
_PersistentDictBase.clear(self)
883-
self._cache.clear()
756+
self._fetch.cache_clear()
884757

885758

886759
class PersistentDict(_PersistentDictBase):

pytools/test/test_persistent_dict.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,14 @@ def test_write_once_persistent_dict_lru_policy():
309309
pdict.fetch(4)
310310
assert pdict.fetch(1) is not val1
311311

312+
# test clear_in_mem_cache
313+
val1 = pdict.fetch(1)
314+
pdict.clear_in_mem_cache()
315+
assert pdict.fetch(1) is not val1
316+
317+
val1 = pdict.fetch(1)
318+
assert pdict.fetch(1) is val1
319+
312320
finally:
313321
shutil.rmtree(tmpdir)
314322

0 commit comments

Comments
 (0)