-
Notifications
You must be signed in to change notification settings - Fork 93
InMemoryCache #2
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
06858bf
6a6cf82
00c2522
668ad46
d6d27f3
c543f7b
e6ba8a1
78abadd
4a8286f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.lld.inmemorycache; | ||
|
||
import com.lld.inmemorycache.model.IEvictionPolicy; | ||
import com.lld.inmemorycache.model.impl.Cache; | ||
import com.lld.inmemorycache.model.impl.InMemoryStorage; | ||
import com.lld.inmemorycache.model.impl.LRUEvictionPolicy; | ||
|
||
import java.util.Objects; | ||
|
||
public class MainApplication { | ||
|
||
public static void main(String[] args) | ||
{ | ||
InMemoryStorage inMemoryStorage = new InMemoryStorage(); | ||
IEvictionPolicy 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"); | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More could be added like support generic key value pairs, concurrency. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.lld.inmemorycache.model; | ||
|
||
public abstract class AbstractCache { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of having both the key and value as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if |
||
public IStorage storage; | ||
public IEvictionPolicy evictionPolicy; | ||
public int capacity; | ||
public abstract boolean put(String key, String value); | ||
public abstract String get(String key); | ||
public abstract boolean remove(String key); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package com.lld.inmemorycache.model; | ||
|
||
public class DoublyLinkedList { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why to implement our own double linked list implementation? It's better to use existing language provided data structures unless there is some customization required. Implementing our own is always more error prone. We can use our own Node structure with key and value with Java linked list. |
||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.lld.inmemorycache.model; | ||
|
||
public interface IEvictionPolicy { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although the naming convention for interfaces starting with Please follow the convention to make the repo consistent.
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be more like a verb - |
||
public String getKeyToEvict(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.lld.inmemorycache.model; | ||
|
||
public interface IStorage { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
public boolean put(String key, String value); | ||
public String get(String key); | ||
public boolean remove(String key); | ||
public int count(); | ||
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package com.lld.inmemorycache.model.impl; | ||
|
||
import com.lld.inmemorycache.model.AbstractCache; | ||
import com.lld.inmemorycache.model.IEvictionPolicy; | ||
import com.lld.inmemorycache.model.IStorage; | ||
|
||
public class Cache extends AbstractCache { | ||
|
||
public Cache(IStorage storage, IEvictionPolicy evictionPolicy, int capacity) | ||
{ | ||
this.storage = storage; | ||
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.evictionPolicy = evictionPolicy; | ||
this.capacity = capacity; | ||
} | ||
@Override | ||
public boolean put(String key, String value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The operations across storage and eviction layer are not synchronized. A lock at the cache level has to guard these operations together. |
||
if(key == null || value == null) | ||
return false; | ||
if(storage.count() >= 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) { | ||
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return storage.get(key); | ||
} | ||
|
||
@Override | ||
public boolean remove(String key) { | ||
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return storage.remove(key); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package com.lld.inmemorycache.model.impl; | ||
|
||
import com.lld.inmemorycache.model.IStorage; | ||
|
||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.locks.ReentrantLock; | ||
|
||
public class InMemoryStorage implements IStorage { | ||
private ConcurrentHashMap<String, String> _storage; | ||
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since storage is used by the cache along with eviction policy, taking individual locks in the storage or eviction side alone won't work. |
||
try { | ||
// access _storage. do not allow an exception here. | ||
_storage.put(key, value); | ||
} | ||
catch (Exception ex) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What kind of exception does it throw here? |
||
{ | ||
return false; | ||
} | ||
finally { | ||
lock.unlock(); | ||
} | ||
return true; | ||
} | ||
|
||
@Override | ||
public String get(String key) { | ||
if(_storage.containsKey(key)) | ||
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return _storage.get(key); | ||
} | ||
return null; | ||
} | ||
|
||
@Override | ||
public boolean remove(String key) { | ||
lock.lock(); | ||
try { | ||
// access _storage. do not allow an exception here. | ||
_storage.remove(key); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What exception is thrown here? |
||
} | ||
catch (Exception ex) | ||
{ | ||
return false; | ||
} | ||
finally { | ||
lock.unlock(); | ||
} | ||
return true; | ||
} | ||
|
||
@Override | ||
public int count() | ||
{ | ||
return _storage.size(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package com.lld.inmemorycache.model.impl; | ||
|
||
import com.lld.inmemorycache.model.DoublyLinkedList; | ||
import com.lld.inmemorycache.model.IEvictionPolicy; | ||
import com.lld.inmemorycache.model.Node; | ||
|
||
import java.util.HashMap; | ||
import java.util.LinkedHashMap; | ||
import java.util.concurrent.locks.ReentrantLock; | ||
|
||
public class LRUEvictionPolicy implements IEvictionPolicy { | ||
devanshu0987 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should add more eviction policies like |
||
private DoublyLinkedList keys; | ||
private HashMap<String, Node> mapper; | ||
|
||
private ReentrantLock lock; | ||
|
||
public LRUEvictionPolicy() { | ||
keys = new DoublyLinkedList(); | ||
mapper = new LinkedHashMap<>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like managing a hash map here just to find out if the eviction policy has a key or not is an overkill. Already storage layer is maintaining another hash map. If we maintain a map here, it just doubles the space with not much gain. Probably we can simply remove / get the key from the double linked list when a key is accessed or removed from the cache and return either the node or |
||
lock = new ReentrantLock(true); | ||
} | ||
|
||
@Override | ||
public void keyAccessed(String key) { | ||
lock.lock(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned, taking individual locks at the storage or eviction layer won't work. |
||
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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest writing multiple tests simulating multiple scenarios.