diff --git a/.idea/misc.xml b/.idea/misc.xml index cf9abe6..b95853c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/README.md b/README.md index 62d4abb..8e48a7f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Various Low Level Object-Oriented System Design problems are discussed in this s 10. Chat application 11. Notification system 12. Leetcode / Hackerrank like online judge +13. InMemoryCache with LRU eviction policy. # Planned (In no particular order) diff --git a/src/com/lld/inmemorycache/MainApplication.java b/src/com/lld/inmemorycache/MainApplication.java new file mode 100644 index 0000000..f702b41 --- /dev/null +++ b/src/com/lld/inmemorycache/MainApplication.java @@ -0,0 +1,29 @@ +package com.lld.inmemorycache; + +import com.lld.inmemorycache.service.EvictionPolicy; +import com.lld.inmemorycache.service.impl.Cache; +import com.lld.inmemorycache.service.impl.InMemoryStorage; +import com.lld.inmemorycache.service.impl.policy.LRUEvictionPolicy; + +import java.util.Objects; + +public class MainApplication { + + public static void main(String[] args) + { + InMemoryStorage inMemoryStorage = new InMemoryStorage(); + EvictionPolicy lruEvictionPolicy = new LRUEvictionPolicy(); + Cache cache = new Cache(inMemoryStorage, lruEvictionPolicy, 2); + + // Test 1 + cache.put("c", "1"); + cache.put("a", "1"); + cache.put("a", "2"); + assert Objects.equals(cache.get("a"), "2"); + + cache.put("b", "3"); + cache.put("b", "4"); + assert Objects.equals(cache.get("b"), "4"); + + } +} diff --git a/src/com/lld/inmemorycache/README.md b/src/com/lld/inmemorycache/README.md new file mode 100644 index 0000000..f706d18 --- /dev/null +++ b/src/com/lld/inmemorycache/README.md @@ -0,0 +1,5 @@ +Design a Cache with LRU cache eviction policy. + +1. Cache should support get(), put methods. +2. For simplicity, all keys and values are string. +3. Make it extendable to support multiple other eviction policies in the future. \ No newline at end of file diff --git a/src/com/lld/inmemorycache/model/DoublyLinkedList.java b/src/com/lld/inmemorycache/model/DoublyLinkedList.java new file mode 100644 index 0000000..2bcd901 --- /dev/null +++ b/src/com/lld/inmemorycache/model/DoublyLinkedList.java @@ -0,0 +1,77 @@ +package com.lld.inmemorycache.model; + +public class DoublyLinkedList { + private Node head; + private Node tail; + private int count; + public DoublyLinkedList() + { + head = null; + tail = null; + count = 0; + } + + public Node last() + { + return tail; + } + + public Node addFront(String data) + { + Node temp = new Node(data, head, null); + if(head != null) + { + head.prev = temp; + } + // We have a new head. + head = temp; + + if(tail == null) + { + tail = temp; + } + count++; + return head; + } + + public void delete(Node item) + { + if(item == null) + return; + if(head == null) + return; + // deleting the top. + if(item == head) + { + // update the head. + head = head.next; + if(head != null) + head.prev = null; + else { + // if head is null, then tail is null as well. + tail = null; + } + } + else if(item == tail) + { + // go back. + tail = tail.prev; + tail.next = null; + } + else { + // some mid node we need to delete. + Node next = item.next; + Node prev = item.prev; + prev.next = next; + next.prev = prev; + } + count--; + item.next = null; + item.prev = null; + } + + public int count() + { + return count; + } +} diff --git a/src/com/lld/inmemorycache/model/Node.java b/src/com/lld/inmemorycache/model/Node.java new file mode 100644 index 0000000..36c7a0c --- /dev/null +++ b/src/com/lld/inmemorycache/model/Node.java @@ -0,0 +1,14 @@ +package com.lld.inmemorycache.model; + +public class Node { + public String data; + public Node next; + public Node prev; + + public Node(String data, Node next, Node prev) + { + this.data = data; + this.next = next; + this.prev = prev; + } +} diff --git a/src/com/lld/inmemorycache/service/AbstractCache.java b/src/com/lld/inmemorycache/service/AbstractCache.java new file mode 100644 index 0000000..9905766 --- /dev/null +++ b/src/com/lld/inmemorycache/service/AbstractCache.java @@ -0,0 +1,14 @@ +package com.lld.inmemorycache.service; + +import com.lld.inmemorycache.service.EvictionPolicy; +import com.lld.inmemorycache.service.Storage; + +public abstract class AbstractCache { + public Storage storage; + public EvictionPolicy evictionPolicy; + public int capacity; + public abstract boolean put(String key, String value); + public abstract String get(String key); + public abstract boolean remove(String key); + +} diff --git a/src/com/lld/inmemorycache/service/EvictionPolicy.java b/src/com/lld/inmemorycache/service/EvictionPolicy.java new file mode 100644 index 0000000..4bd2d37 --- /dev/null +++ b/src/com/lld/inmemorycache/service/EvictionPolicy.java @@ -0,0 +1,9 @@ +package com.lld.inmemorycache.service; + +public interface EvictionPolicy { + // Update statistics for the key which was accessed. + public void keyAccessed(String key); + // Update statistics for the key which was evicted. + public void keyEvicted(String key); + public String getKeyToEvict(); +} diff --git a/src/com/lld/inmemorycache/service/Storage.java b/src/com/lld/inmemorycache/service/Storage.java new file mode 100644 index 0000000..68cfa86 --- /dev/null +++ b/src/com/lld/inmemorycache/service/Storage.java @@ -0,0 +1,8 @@ +package com.lld.inmemorycache.service; + +public interface Storage { + public boolean put(String key, String value); + public String get(String key); + public boolean remove(String key); + public int size(); +} diff --git a/src/com/lld/inmemorycache/service/impl/Cache.java b/src/com/lld/inmemorycache/service/impl/Cache.java new file mode 100644 index 0000000..f28c169 --- /dev/null +++ b/src/com/lld/inmemorycache/service/impl/Cache.java @@ -0,0 +1,64 @@ +package com.lld.inmemorycache.service.impl; + +import com.lld.inmemorycache.service.AbstractCache; +import com.lld.inmemorycache.service.EvictionPolicy; +import com.lld.inmemorycache.service.Storage; + +public class Cache extends AbstractCache { + + public Cache(Storage storage, EvictionPolicy evictionPolicy, int capacity) { + this.storage = storage; + this.evictionPolicy = evictionPolicy; + this.capacity = capacity; + } + + @Override + public boolean put(String key, String value) { + if (key == null || value == null) + return false; + if (storage.size() >= capacity) { + // we need to evict keys because the capacity is full. + String keyToEvict = evictionPolicy.getKeyToEvict(); + boolean status = storage.remove(keyToEvict); + if (status) { + // eviction complete. + evictionPolicy.keyEvicted(keyToEvict); + } else { + // eviction failed. + // Multiple options here: + // 1. throw exception: not a good option perf wise to throw exceptions. + // 2. ignore the add and expect user to retry + // 3. execute random eviction policy. + // Implementing Option 2. + return false; + } + } + + // space present + storage.put(key, value); + evictionPolicy.keyAccessed(key); + return true; + } + + // Returns null if Key is not found. + @Override + public String get(String key) { + String value = storage.get(key); + // no point updating statistics for a key that is not present. + // in future maybe we can get some information out of it but for now, skipping it. + if (value != null) { + evictionPolicy.keyAccessed(key); + } + return value; + } + + @Override + public boolean remove(String key) { + + boolean status = storage.remove(key); + if (status) { + evictionPolicy.keyEvicted(key); + } + return status; + } +} diff --git a/src/com/lld/inmemorycache/service/impl/InMemoryStorage.java b/src/com/lld/inmemorycache/service/impl/InMemoryStorage.java new file mode 100644 index 0000000..b648400 --- /dev/null +++ b/src/com/lld/inmemorycache/service/impl/InMemoryStorage.java @@ -0,0 +1,54 @@ +package com.lld.inmemorycache.service.impl; + +import com.lld.inmemorycache.service.Storage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +public class InMemoryStorage implements Storage { + private ConcurrentHashMap storage; + private static ReentrantLock lock; + + public InMemoryStorage() { + storage = new ConcurrentHashMap<>(); + // fairness: first come, first served. + lock = new ReentrantLock(true); + } + + @Override + public boolean put(String key, String value) { + lock.lock(); + try { + // access _storage. do not allow an exception here. + storage.put(key, value); + } catch (Exception ex) { + return false; + } finally { + lock.unlock(); + } + return true; + } + + @Override + public String get(String key) { + return storage.get(key); + } + + @Override + public boolean remove(String key) { + lock.lock(); + try { + // access _storage. do not allow an exception here. + storage.remove(key); + } catch (Exception ex) { + return false; + } finally { + lock.unlock(); + } + return true; + } + + @Override + public int size() { + return storage.size(); + } +} diff --git a/src/com/lld/inmemorycache/service/impl/policy/LRUEvictionPolicy.java b/src/com/lld/inmemorycache/service/impl/policy/LRUEvictionPolicy.java new file mode 100644 index 0000000..35d7126 --- /dev/null +++ b/src/com/lld/inmemorycache/service/impl/policy/LRUEvictionPolicy.java @@ -0,0 +1,70 @@ +package com.lld.inmemorycache.service.impl.policy; + +import com.lld.inmemorycache.model.DoublyLinkedList; +import com.lld.inmemorycache.service.EvictionPolicy; +import com.lld.inmemorycache.model.Node; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.concurrent.locks.ReentrantLock; + +public class LRUEvictionPolicy implements EvictionPolicy { + private DoublyLinkedList keys; + private HashMap mapper; + + private ReentrantLock lock; + + public LRUEvictionPolicy() { + keys = new DoublyLinkedList(); + mapper = new LinkedHashMap<>(); + lock = new ReentrantLock(true); + } + + @Override + public void keyAccessed(String key) { + lock.lock(); + try { + // key is already present. + if (mapper.containsKey(key)) { + // access the node and move it to the front. + Node keyNode = mapper.get(key); + // delete the node. + keys.delete(keyNode); + // add to front. + keys.addFront(key); + } else { + // first time encountering this key. + Node front = keys.addFront(key); + mapper.put(key, front); + } + + } catch (Exception ex) { + // do something here. + } finally { + lock.unlock(); + } + } + + @Override + public void keyEvicted(String key) { + lock.lock(); + try { + if (mapper.containsKey(key)) { + Node keyNode = mapper.get(key); + keys.delete(keyNode); + mapper.remove(key); + } + } catch (Exception ex) { + // do something here. + + } finally { + lock.unlock(); + } + } + + @Override + public String getKeyToEvict() { + if (keys.count() > 0) return keys.last().data; + return null; + } +}