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

InMemoryCache #2

Open
wants to merge 9 commits into
base: main
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions src/com/lld/inmemorycache/MainApplication.java
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)
Copy link
Owner

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.

{
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");

}
}
5 changes: 5 additions & 0 deletions src/com/lld/inmemorycache/README.md
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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More could be added like support generic key value pairs, concurrency.

77 changes: 77 additions & 0 deletions src/com/lld/inmemorycache/model/DoublyLinkedList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.lld.inmemorycache.model;

public class DoublyLinkedList {
Copy link
Owner

Choose a reason for hiding this comment

The 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;
}
}
14 changes: 14 additions & 0 deletions src/com/lld/inmemorycache/model/Node.java
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;
}
}
14 changes: 14 additions & 0 deletions src/com/lld/inmemorycache/service/AbstractCache.java
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);

}
9 changes: 9 additions & 0 deletions src/com/lld/inmemorycache/service/EvictionPolicy.java
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();
}
8 changes: 8 additions & 0 deletions src/com/lld/inmemorycache/service/Storage.java
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();
}
64 changes: 64 additions & 0 deletions src/com/lld/inmemorycache/service/impl/Cache.java
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;
}
}
54 changes: 54 additions & 0 deletions src/com/lld/inmemorycache/service/impl/InMemoryStorage.java
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;
}
}