From 67bf966a6a2e14cebb1593d67d565c4bcbc12dee Mon Sep 17 00:00:00 2001
From: Arthur Lu <learthurgo@gmail.com>
Date: Fri, 14 Feb 2025 22:35:54 +0000
Subject: [PATCH] add tiered cache

---
 tiered_cache.py         | 94 +++++++++++++++++++++++++++++++++++++++++
 tiered_cache/.gitignore |  2 +
 2 files changed, 96 insertions(+)
 create mode 100644 tiered_cache.py
 create mode 100644 tiered_cache/.gitignore

diff --git a/tiered_cache.py b/tiered_cache.py
new file mode 100644
index 0000000..3954b20
--- /dev/null
+++ b/tiered_cache.py
@@ -0,0 +1,94 @@
+from cache import BaselineCache
+from collections import OrderedDict
+import os
+
+class TieredCache(BaselineCache):
+    l2_limit = None
+    l2_map = None
+
+    def __init__(self, limit, l2_limit = 100):
+        super().__init__(limit)
+        self.l2_limit = l2_limit
+        self.l2_map = OrderedDict()
+
+    def get(self, key):
+        # first look in the l1 cache
+        s = super().get(key)
+        if s != None:
+            return s
+        else: # on a miss, check the l2 cache mapping
+            if key in self.l2_map: # if it is in l2 cache (disk), open the file and return the values
+                f = open(self.l2_map[key], "r")
+                v = f.read()
+                f.close()
+                return v
+            else: # otherwise its a cache miss and return None
+                return None
+    
+    def put(self, key, val):
+        evict = False
+        if len(self.cache) >= self.limit:
+            if len(self.l2_map) >= self.l2_limit:
+                self.l2_map.popitem(last = False)
+                evict = True
+
+            k,v = self.cache.popitem(last = False)
+            path = f"tiered_cache/{k}"
+            self.l2_map[k] = path
+            f = open(path, "w+")
+            f.write(v)
+            f.close()
+            
+        self.cache[key] = val
+
+        return evict
+
+    def invalidate(self, key: str) -> bool:
+        # basic delete invalidation, no (p)refetching
+        if key in self.cache:
+            del self.cache[key]
+            return True
+        elif key in self.l2_map:
+            os.remove(self.l2_map[key]) # this is so sketchy
+            del self.l2_map[key]
+            return True
+        else:
+            return False
+
+if __name__ == "__main__": # basic testing, should never be called when importing
+    cache = TieredCache(10)
+
+    for i in range(10):
+        assert cache.put(str(i), str(i+1)) == False
+    
+    assert len(cache) == 10
+    assert cache == OrderedDict({'0': '1', '1': '2', '2': '3', '3': '4', '4': '5', '5': '6', '6': '7', '7': '8', '8': '9', '9': '10'})
+
+    assert cache.get("5") == "6"
+    assert cache.get("8") == "9"
+    assert cache.get("0") == "1"
+
+    assert len(cache) == 10
+    assert cache == OrderedDict({'1': '2', '2': '3', '3': '4', '4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1'})
+
+    assert cache.get("a") == None
+    assert cache.get("b") == None
+    assert cache.get("c") == None
+
+    assert cache.put("a", "b") == False
+    assert cache.put("b", "c") == False
+    assert cache.put("c", "d") == False
+
+    assert len(cache) == 10
+    assert cache == OrderedDict({'4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1', 'a': 'b', 'b' : 'c', 'c': 'd'})
+
+    assert cache.get("c") == "d"
+    assert cache.get("b") == "c"
+    assert cache.get("a") == "b"
+
+    assert cache.get("1") == "2"
+    assert cache.get("2") == "3"
+    assert cache.get("3") == "4"
+
+    assert len(cache) == 10
+    assert cache == OrderedDict({'4': '5', '6': '7', '7': '8', '9': '10', '5': '6', '8': '9', '0': '1', 'c': 'd', 'b' : 'c', 'a': 'b'})
\ No newline at end of file
diff --git a/tiered_cache/.gitignore b/tiered_cache/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/tiered_cache/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file