Skip to content

Commit

Permalink
Use HashTreeSet to manage expiration. refactor command processor.
Browse files Browse the repository at this point in the history
  • Loading branch information
minkenlai committed May 6, 2016
1 parent 1ace6ce commit b86757a
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 84 deletions.
85 changes: 15 additions & 70 deletions src/com/kenlai/MKLRedis/CachingStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;

Expand All @@ -14,82 +13,20 @@
public class CachingStore {
private boolean verbose = Boolean.getBoolean("verbose");
private boolean debug = Boolean.getBoolean("debug");
private int initialSize = Integer.getInteger("initialSize", 16);

private static final String OK = "OK";

private final static Pattern validatorPattern =
Pattern.compile("\\A[ a-zA-Z0-9-_]*\\z");
private final static Pattern integerPattern =
Pattern.compile("\\A[+-]?[0-9]+\\z");

private HashMap<String, Object> map;

private List<String> expirables = new LinkedList<>();

public CachingStore() {
map = new HashMap<String, Object>(initialSize);
}
private HashTreeSet expirables = new HashTreeSet();

public CachingStore(int size) {
map = new HashMap<String, Object>(size);
}

/**
* Parses the full command string and forwards to appropriate method.
*
* @return result value of the command
*/
public String process(String fullCmd) {
if (!validatorPattern.matcher(fullCmd).matches()) {
verbosePrintln("invalid input characters detected");
return "ERROR invalid input characters detected";
}
String[] tokens = fullCmd.split(" ");
try {
Command cmd = Command.valueOf(tokens[0]);
switch (cmd) {
case SET:
Long expiration = null;
if (tokens.length == 5 && tokens[3].equals("EX")) {
expiration = Long.getLong(tokens[4]);
} else if (tokens.length != 3) {
verbosePrintln("incorrect parameters for SET");
return "ERROR bad SET parameters";
}
return set(tokens[1], tokens[2], expiration);
case GET:
verifyLength(tokens, 2);
return get(tokens[1]);
case INCR:
verifyLength(tokens, 2);
return incr(tokens[1]).toString();
case DEL:
verifyLength(tokens, 2);
return Integer.toString(del(tokens[1]));
case DBSIZE:
verifyLength(tokens, 1);
return Integer.toString(dbsize());
default:
verbosePrintln("Command " + tokens[0]
+ " is not yet implemented");
}
} catch (IllegalArgumentException e) {
verbosePrintln("bad command: " + tokens[0]);
return "ERROR bad command";
} catch (IndexOutOfBoundsException e) {
verbosePrintln("incorrect number of parameters");
return "ERROR number of parameters";
}
return null;
}

private void verifyLength(String[] tokens, int length) {
if (tokens.length != length) {
throw new IllegalArgumentException("incorrect number of parameters");
}
}

/**
* Note: this has side-effect of garbage collecting expired entries, O(n).
*
Expand All @@ -108,11 +45,16 @@ public int dbsize() {
* but it incurs O(log(n)) for insertion.
*/
private void garbageCollect() {
Iterator<String> iterator = expirables.iterator();
long currentTime = System.currentTimeMillis();
Iterator<ScoredMember> iterator = expirables.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
ExpirableValue ev = (ExpirableValue) map.get(key);
if (ev.isExpired()) {
ScoredMember m = iterator.next();
if (m.score < currentTime) {
String key = m.getMember();
if (debug) {
ExpirableValue ev = (ExpirableValue) map.get(key);
assert ev.isExpired();
}
iterator.remove();
map.remove(key);
}
Expand All @@ -128,6 +70,9 @@ private void garbageCollect() {
*/
public String get(String key) {
Object value = getUnexpired(key);
if (value instanceof HashTreeSet) {
throw new IllegalArgumentException("incorrect value type");
}
return value != null ? value.toString() : null;
}

Expand Down Expand Up @@ -158,7 +103,7 @@ public int del(String key) {
}
// clean from expirables lists
if (value instanceof ExpirableValue) {
expirables.remove(key);
expirables.removeByMember(key);
}
return 1;
}
Expand All @@ -183,7 +128,7 @@ public String set(String key, String value, Long timeToLive) {
}
long expiresAt = System.currentTimeMillis() + timeToLive * 1000;
val = new ExpirableValue(val, expiresAt);
expirables.add(key);
expirables.add(new ScoredMember(expiresAt, key));
}
map.put(key, val);
return OK;
Expand Down
97 changes: 97 additions & 0 deletions src/com/kenlai/MKLRedis/CommandProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.kenlai.MKLRedis;

import java.util.regex.Pattern;

public class CommandProcessor {
private boolean verbose = Boolean.getBoolean("verbose");
private int initialSize = Integer.getInteger("initialSize", 16);

private final static Pattern validatorPattern =
Pattern.compile("\\A[ a-zA-Z0-9-_]*\\z");

private CachingStore store = new CachingStore(initialSize);

/**
* Parses the full command string and forwards to appropriate method.
*
* @return result value of the command
*/
public String process(String fullCmd) {
if (!validatorPattern.matcher(fullCmd).matches()) {
verbosePrintln("invalid input characters detected");
return "ERROR invalid input characters detected";
}
String[] tokens = fullCmd.split(" ");
try {
Command cmd = Command.valueOf(tokens[0]);
switch (cmd) {
case SET:
Long expiration = null;
if (tokens.length == 5 && tokens[3].equals("EX")) {
expiration = Long.getLong(tokens[4]);
} else if (tokens.length != 3) {
verbosePrintln("incorrect parameters for SET");
return "ERROR bad SET parameters";
}
return store.set(tokens[1], tokens[2], expiration);
case GET:
verifyLength(tokens, 2);
return store.get(tokens[1]);
case INCR:
verifyLength(tokens, 2);
return store.incr(tokens[1]).toString();
case DEL:
verifyLength(tokens, 2);
return Integer.toString(store.del(tokens[1]));
case DBSIZE:
verifyLength(tokens, 1);
return Integer.toString(store.dbsize());
case ZADD:
verifyLength(tokens, 4);
return store.zadd(tokens[1], Long.parseLong(tokens[2]), tokens[3]);
case ZCARD:
verifyLength(tokens, 2);
return Integer.toString(store.zcard(tokens[1]));
case ZRANK:
verifyLength(tokens, 3);
return Integer.toString(store.zrank(tokens[1], tokens[2]));
case ZRANGE:
verifyLength(tokens, 4);
return listToString(store.zrange(tokens[1],
Integer.parseInt(tokens[2]),
Integer.parseInt(tokens[3])));
default:
verbosePrintln("Command " + tokens[0]
+ " is not yet implemented");
}
} catch (IllegalArgumentException e) {
verbosePrintln("bad command: " + tokens[0]);
return "ERROR bad command";
} catch (IndexOutOfBoundsException e) {
verbosePrintln("incorrect number of parameters");
return "ERROR number of parameters";
}
return null;
}

private void verifyLength(String[] tokens, int length) {
if (tokens.length != length) {
throw new IllegalArgumentException("incorrect number of parameters");
}
}

private String listToString(Iterable<String> list) {
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
sb.append("\n");
}
return sb.toString();
}

private void verbosePrintln(String msg) {
if (verbose) {
System.out.println(msg);
}
}
}
7 changes: 7 additions & 0 deletions src/com/kenlai/MKLRedis/HashTreeSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ public boolean add(ScoredMember e) {
return super.add(e) && existingMember != null;
}

public boolean removeByMember(String value) {
ScoredMember remove = hashMap.remove(value);
if (remove != null) {
return super.remove(remove);
}
return false;
}
}
18 changes: 4 additions & 14 deletions test/com/kenlai/MKLRedis/CachingStoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.List;
Expand All @@ -12,27 +11,18 @@

public class CachingStoreTest {
private static final String OK = "OK";
private static final String ERROR = "ERROR";

@Test
public void testProcess() {
CachingStore store = new CachingStore();
assertEquals(OK, store.process("SET foo bar"));
assertTrue(store.process("BAD").startsWith(ERROR));
assertTrue(store.process("What+is&this?").startsWith(ERROR));
}

@Test
public void testCachingStore() throws Exception {
CachingStore store = new CachingStore();
CachingStore store = new CachingStore(16);

// CASE: SET, GET, EX, DBSIZE
assertEquals(OK, store.set("A", "A", 1L));
assertEquals(OK, store.set("B", "B", null));
assertEquals("A", store.get("A"));
assertEquals("B", store.get("B"));
assertEquals(2, store.dbsize());
Thread.sleep(1000L);
Thread.sleep(2000L);
assertNull(store.get("A"));
assertEquals("B", store.get("B"));
assertEquals(1, store.dbsize());
Expand All @@ -49,7 +39,7 @@ public void testCachingStore() throws Exception {
assertEquals(OK, store.set("B", "3", 1L));
assertEquals("4", store.incr("B").toString());
assertEquals(1, store.dbsize());
Thread.sleep(1000L);
Thread.sleep(2000L);
assertNull(store.get("B"));
assertEquals(0, store.dbsize());

Expand Down Expand Up @@ -92,7 +82,7 @@ public void testGarbageCollection() throws Exception {

@Test
public void testSortedSet() {
CachingStore store = new CachingStore();
CachingStore store = new CachingStore(16);
// CASE: not-exist / empty
assertEquals(0, store.zcard("Z"));
List<String> list = store.zrange("Z", 0, 0);
Expand Down
42 changes: 42 additions & 0 deletions test/com/kenlai/MKLRedis/CommandProcessorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.kenlai.MKLRedis;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class CommandProcessorTest {
private static final String OK = "OK";
private static final String ERROR = "ERROR";

private CommandProcessor cp = new CommandProcessor();

@Test
public void testProcess() {
assertEquals(OK, cp.process("SET foo bar"));
assertTrue(cp.process("BAD").startsWith(ERROR));
assertTrue(cp.process("What+is&this?").startsWith(ERROR));

assertEquals("1", cp.process("INCR numerical"));
assertEquals("2", cp.process("DBSIZE"));

assertEquals("bar", cp.process("GET foo"));
assertEquals("1", cp.process("DEL foo"));
assertEquals("0", cp.process("DEL foo"));
assertEquals("1", cp.process("DBSIZE"));

assertEquals("OK", cp.process("ZADD set 10 ten"));
assertEquals("2", cp.process("DBSIZE"));
assertEquals("1", cp.process("ZCARD set"));
assertEquals("OK", cp.process("ZADD set 10 tenB"));
assertEquals("1", cp.process("ZRANK set tenB"));
assertEquals("tenB\n", cp.process("ZRANGE set -1 -1"));

assertTrue(cp.process("GET set").startsWith(ERROR));
assertTrue(cp.process("INCR set").startsWith(ERROR));

assertEquals("1", cp.process("DEL set"));
assertEquals("1", cp.process("DBSIZE"));
}

}

0 comments on commit b86757a

Please sign in to comment.