Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/caches #96

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions aiomisc/cache/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

34 changes: 34 additions & 0 deletions aiomisc/cache/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from abc import ABC, abstractmethod
from typing import Any, Hashable, Dict


class CachePolicy(ABC):
__slots__ = "max_size", "cache"

def __init__(self, cache: Dict[Hashable, Any], max_size: int = 0):
self.max_size = max_size
self.cache = cache
self._on_init()

def _on_init(self):
pass

@abstractmethod
def get(self, key: Hashable):
raise NotImplementedError

@abstractmethod
def remove(self, key: Hashable):
raise NotImplementedError

@abstractmethod
def set(self, key: Hashable, value: Any):
raise NotImplementedError

@abstractmethod
def __contains__(self, key: Hashable):
raise NotImplementedError

@abstractmethod
def __len__(self):
raise NotImplementedError
239 changes: 239 additions & 0 deletions aiomisc/cache/dllist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
from multiprocessing import RLock
from typing import Optional, Any, Hashable, Type


class Node:
__slots__ = 'prev', 'next', 'value', 'parent'

prev: Optional["Node"]
next: Optional["Node"]
parent: "DLList"

def __init__(self, parent: "DLList", prev: "Node" = None,
next: "Node" = None):
self.parent = parent
self.prev = prev
self.next = next
self.value = None

def __repr__(self) -> str:
return f"<{self.__class__.__name__} " \
f"{id(self)}: next={id(self.next)} " \
f"prev={id(self.prev)}>"

def remove(self):
if self.next is None:
return

with self.parent.lock:
self.prev.next = self.next
self.prev = None
self.next = None

def append_left(self, node: "Node") -> "Node":
"""
Appends node before the parent node

before:

... <-> [self] <-> ...

after:

... <-> [node] <-> [self] <-> ...

"""
with self.parent.lock:
self.parent.nodes.add(node)
node.next = self

if self.prev is not None:
node.prev = self.prev

self.prev = node
return node

def append_right(self, node: "Node") -> "Node":
"""
Appends node after parent node

before:

... <-> [self] <-> ...

after:

... <-> [self] <-> [node] <-> ...

"""

with self.parent.lock:
self.parent.nodes.add(node)
node.prev = self

if self.next is not None:
node.next = self.next

self.next = node
return node

def swap(self, other: "Node"):
"""
Swaps two Nodes and change neighbor links

Example: doubly linked list looks like:

[x] <-> [self] <-> [z] <-> [other] <-> [y]
node x node self node z node other node y
p n p n p n p n p n
-------------------------------------------------------
- self x z self other z y other -

After swap should looks like:

[x] <-> [other] <-> [z] <-> [self] <-> [y]
node x node other node z node self node y
p n p n p n p n p n
--------------------------------------------------------
- other x z other b z y other -

That's means we should make 8 changes

# 4 for "a" and "b"
a.prev, a.next, b.prev, b.next = b.prev, b.next, a.prev, a.next

# 4 for neighbors
x.next, z.prev, z.next, y.prev = b, b, a, a

After general case is should be:

a.prev.next, a.next.prev, b.prev.next, b.next.prev = b, b, a, a
a.prev, a.next, b.prev, b.next = b.prev, b.next, a.prev, a.next

"""
with self.parent.lock:
# store original links
self_prev, self_next, other_prev, other_next = (
self.prev, self.next, other.prev, other.next
)

if self_prev is not None:
self_prev.next = other
self.prev = other_prev

if self_next is not None:
self_next.prev = other
self.next = other_next

if other_next is not None:
other_next.prev = self
other.next = other_next

if other_prev is not None:
other_prev.next = self
other.prev = self_prev

first_set = False
last_set = False

if not last_set and self is self.parent.last:
self.parent.last = other
last_set = True

if not first_set and self is self.parent.first:
self.parent.first = other
first_set = True

if not last_set and other is self.parent.last:
self.parent.last = self

if not first_set and other is self.parent.first:
self.parent.first = self


class Item:
__slots__ = 'node', 'key', 'value'

node: Node
key: Hashable
value: Any

def __init__(self, node: Node, key: Hashable, value: Any):
self.node = node
self.key = key
self.value = value


class DLList:
__slots__ = 'first', 'last', 'lock', 'nodes'

NODE_CLASS: Type[Node] = Node
ITEM_CLASS: Type[Item] = Item

first: Optional[NODE_CLASS]
last: Optional[NODE_CLASS]

def __init__(self):
self.lock = RLock()
self.first = None
self.last = None
self.nodes = set()

def __len__(self):
return len(self.nodes)

def __contains__(self, item: NODE_CLASS):
return item in self.nodes

def __iter__(self):
with self.lock:
first = self.first
while first is not None:
yield first
first = first.next

def _create_node(self, *args, **kwargs):
node = self.NODE_CLASS(self, *args, **kwargs)
self.nodes.add(node)
return node

def remove(self, node: NODE_CLASS):
if node not in self.nodes:
raise ValueError(f"Node {node!r} is not part of {self!r}")

with self.lock:
self.nodes.remove(node)
if node.prev is not None:
node.prev.next = node.next
if node.next is not None:
node.next.prev = node.prev

if self.first is node:
self.first = node.next

if self.last is node:
self.last = node.prev

def create_left(self) -> NODE_CLASS:
with self.lock:
if self.first is None:
self.first = self._create_node()
self.last = self.first
return self.first

node = self._create_node(next=self.first)
self.first.prev = node
self.first = node
return node

def create_right(self) -> NODE_CLASS:
with self.lock:
if self.first is None:
self.last = self._create_node()
self.first = self.last
return self.first

node = self._create_node(prev=self.last)
self.last.next = node
self.last = node
return node
110 changes: 110 additions & 0 deletions aiomisc/cache/lfu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from threading import RLock
from typing import Any, Hashable, Optional, Dict, Set
from aiomisc.cache.base import CachePolicy
from llist import dllist, dllistnode

from aiomisc.cache.dllist import Item


class LFUCachePolicy(CachePolicy):
"""
LFU cache implementation

>>> cache = {}
>>> lfu = LFUCachePolicy(cache, 3)
>>> lfu.set("foo", "bar")
>>> assert "foo" in lfu
>>> lfu.get('foo')
'bar'

>>> lfu.remove('foo')
>>> assert "foo" not in lfu
>>> lfu.get("foo")
Traceback (most recent call last):
...
KeyError: 'foo'
>>> lfu.remove("foo")

>>> lfu.set("bar", "foo")
>>> lfu.set("spam", "egg")
>>> lfu.set("foo", "bar")
>>> lfu.get("foo")
'bar'
>>> lfu.get("spam")
'egg'
>>> assert len(lfu) == 3
>>> lfu.set("egg", "spam")
>>> assert len(lfu) == 3
"""

__slots__ = "usages", "lock"

def _on_init(self):
self.usages: dllist = dllist()
self.lock: RLock = RLock()

def _update_usage(self, item: Item):
with self.lock:
pass

def _item_remove(self, item: Item):
with self.lock:
if item in item.node.value:
item.node.value.remove(item)

item.node = None

def _on_overflow(self):
with self.lock:
while self._is_overflow():
if not self.usages.value:
if self.usages.next is not None:
self.usages.next.prev = None
self.usages = self.usages.next
else:
self.usages = Node(prev=None, next=None)
continue

item = self.usages.value.pop()
self.cache.pop(item.key, None)
self._item_remove(item)

def _is_overflow(self) -> bool:
return len(self.cache) > self.max_size

def __contains__(self, key: Hashable) -> Any:
if key in self.cache:
self._update_usage(self.cache[key])
return True
return False

def __len__(self):
return len(self.cache)

def get(self, key: Hashable):
item: Item = self.cache[key]
self._update_usage(item)
return item.value

def remove(self, key: Hashable):
with self.lock:
item: Optional[Item] = self.cache.pop(key, None)
if item is None:
return

self._item_remove(item)

def set(self, key: Hashable, value: Any):
with self.lock:
node: Optional[Node] = self.usages

if node is None:
node = Node()
self.usages = node

item = Item(node=node, key=key, value=value)
node.value.add(item)
self.cache[key] = item

if self._is_overflow():
self._on_overflow()