-
Notifications
You must be signed in to change notification settings - Fork 73
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
InMemoryCache #2
base: main
Are you sure you want to change the base?
Changes from all 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.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"); | ||
|
||
} | ||
} |
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,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,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,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); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, String> 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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Node> 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; | ||
} | ||
} |
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.