Skip to content

Commit 53655f5

Browse files
yampmltuhuynh27
andcommitted
more STRING commands implementation (#129)
* getex draft * more string cmds * add tests & doc add remove expire key Co-authored-by: Huynh, Tu | RASIA <[email protected]>
1 parent 7484c21 commit 53655f5

File tree

10 files changed

+371
-4
lines changed

10 files changed

+371
-4
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package dev.keva.core.command.impl.string;
2+
3+
import dev.keva.core.command.annotation.CommandImpl;
4+
import dev.keva.core.command.annotation.Execute;
5+
import dev.keva.core.command.annotation.ParamLength;
6+
import dev.keva.core.exception.CommandException;
7+
import dev.keva.ioc.annotation.Autowired;
8+
import dev.keva.ioc.annotation.Component;
9+
import dev.keva.protocol.resp.reply.BulkReply;
10+
import dev.keva.protocol.resp.reply.Reply;
11+
import dev.keva.store.KevaDatabase;
12+
13+
import java.nio.charset.StandardCharsets;
14+
import java.sql.Timestamp;
15+
16+
@Component
17+
@CommandImpl("getex")
18+
@ParamLength(type = ParamLength.Type.AT_LEAST, value = 2)
19+
public class GetEx {
20+
private final KevaDatabase database;
21+
22+
private final String EX = "EX";
23+
private final String PX = "PX";
24+
private final String EXAT = "EXAT";
25+
private final String PXAT = "PXAT";
26+
private final String PERSIST = "PERSIST";
27+
28+
private final int EX_FLG = 1 << 2;
29+
private final int PX_FLG = 1 << 3;
30+
private final int EXAT_FLG = 1 << 6;
31+
private final int PXAT_FLG = 1 << 7;
32+
private final int PERSIST_FLG = 1 << 8;
33+
34+
@Autowired
35+
public GetEx(KevaDatabase database) {
36+
this.database = database;
37+
}
38+
39+
@Execute
40+
public BulkReply execute(byte[][] params) {
41+
int flgs = 0;
42+
String nextArg = null;
43+
long expireTime = 0;
44+
45+
for (int i = 1; i < params.length; i++) {
46+
String opt = new String(params[i], StandardCharsets.UTF_8);
47+
nextArg = i == params.length - 1 ? null : new String(params[i + 1], StandardCharsets.UTF_8);
48+
if (EX.equalsIgnoreCase(opt) && nextArg != null && !isHavingOneOfTheseFlgs(flgs, EX_FLG, PX_FLG, EXAT_FLG, PXAT_FLG, PERSIST_FLG)) {
49+
flgs |= EX_FLG;
50+
i++;
51+
} else if (PX.equalsIgnoreCase(opt) && nextArg != null && !isHavingOneOfTheseFlgs(flgs, EX_FLG, PX_FLG, EXAT_FLG, PXAT_FLG, PERSIST_FLG)) {
52+
flgs |= PX_FLG;
53+
i++;
54+
} else if (EXAT.equalsIgnoreCase(opt) && nextArg != null && !isHavingOneOfTheseFlgs(flgs, EX_FLG, PX_FLG, EXAT_FLG, PXAT_FLG, PERSIST_FLG)) {
55+
flgs |= EXAT_FLG;
56+
i++;
57+
} else if (PXAT.equalsIgnoreCase(opt) && nextArg != null && !isHavingOneOfTheseFlgs(flgs, EX_FLG, PX_FLG, EXAT_FLG, PXAT_FLG, PERSIST_FLG)) {
58+
flgs |= PXAT_FLG;
59+
i++;
60+
} else if (PERSIST.equalsIgnoreCase(opt) && nextArg == null && !isHavingOneOfTheseFlgs(flgs, EX_FLG, PX_FLG, EXAT_FLG, PXAT_FLG, PERSIST_FLG)) {
61+
flgs |= PERSIST_FLG;
62+
} else {
63+
throw new CommandException("Syntax error");
64+
}
65+
}
66+
byte[] got = database.get(params[0]);
67+
if (got == null) {
68+
return BulkReply.NIL_REPLY;
69+
}
70+
71+
if (isHavingOneOfTheseFlgs(flgs, PERSIST_FLG)) {
72+
database.removeExpire(params[0]);
73+
} else {
74+
try {
75+
expireTime = Long.parseLong(nextArg);
76+
} catch (NumberFormatException e) {
77+
throw new CommandException("value is not an integer or out of range");
78+
}
79+
if (expireTime < 1) {
80+
throw new CommandException("invalid expire time in 'getex' command");
81+
}
82+
83+
long expireAt = expireTime;
84+
if (isHavingOneOfTheseFlgs(flgs, EX_FLG, EXAT_FLG)) {
85+
expireAt *= 1000;
86+
}
87+
if (isHavingOneOfTheseFlgs(flgs, EX_FLG, PX_FLG)) {
88+
expireAt += System.currentTimeMillis();
89+
}
90+
91+
if (expireAt < System.currentTimeMillis()) {
92+
database.remove(params[0]);
93+
} else {
94+
database.expireAt(params[0], expireAt);
95+
}
96+
}
97+
return new BulkReply(got);
98+
}
99+
100+
private boolean isHavingOneOfTheseFlgs(int flgs, int... flgsToCompare) {
101+
for (int i = 0; i < flgsToCompare.length; i++) {
102+
if ((flgs & flgsToCompare[i]) != 0) {
103+
return true;
104+
}
105+
}
106+
return false;
107+
}
108+
109+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package dev.keva.core.command.impl.string;
2+
3+
import dev.keva.core.command.annotation.CommandImpl;
4+
import dev.keva.core.command.annotation.Execute;
5+
import dev.keva.core.command.annotation.ParamLength;
6+
import dev.keva.core.exception.CommandException;
7+
import dev.keva.ioc.annotation.Autowired;
8+
import dev.keva.ioc.annotation.Component;
9+
import dev.keva.protocol.resp.reply.BulkReply;
10+
import dev.keva.protocol.resp.reply.IntegerReply;
11+
import dev.keva.protocol.resp.reply.StatusReply;
12+
import dev.keva.store.KevaDatabase;
13+
14+
import java.util.Arrays;
15+
16+
import static dev.keva.core.command.annotation.ParamLength.Type.AT_LEAST;
17+
18+
@Component
19+
@CommandImpl("msetnx")
20+
@ParamLength(type = AT_LEAST, value = 2)
21+
public class MSetNX {
22+
private final KevaDatabase database;
23+
24+
@Autowired
25+
public MSetNX(KevaDatabase database) {
26+
this.database = database;
27+
}
28+
29+
@Execute
30+
public IntegerReply execute(byte[]... params) {
31+
if (params.length % 2 != 0) {
32+
throw new CommandException("Wrong number of arguments for MSET");
33+
}
34+
35+
byte[][] keys = new byte[params.length / 2][];
36+
for (int i = 0; i < params.length; i += 2) {
37+
keys[i / 2] = params[i];
38+
}
39+
byte[][] gets = database.mget(keys);
40+
41+
if (Arrays.stream(gets).anyMatch(get -> get != null)) {
42+
return new IntegerReply(0);
43+
}
44+
45+
database.mset(params);
46+
return new IntegerReply(1);
47+
}
48+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dev.keva.core.command.impl.string;
2+
3+
import dev.keva.core.command.annotation.CommandImpl;
4+
import dev.keva.core.command.annotation.Execute;
5+
import dev.keva.core.command.annotation.ParamLength;
6+
import dev.keva.core.exception.CommandException;
7+
import dev.keva.ioc.annotation.Autowired;
8+
import dev.keva.ioc.annotation.Component;
9+
import dev.keva.protocol.resp.reply.StatusReply;
10+
import dev.keva.store.KevaDatabase;
11+
12+
import java.nio.charset.StandardCharsets;
13+
14+
@Component
15+
@CommandImpl("psetex")
16+
@ParamLength(type = ParamLength.Type.EXACT, value = 3)
17+
public class PSetEX {
18+
private final KevaDatabase database;
19+
20+
@Autowired
21+
public PSetEX(KevaDatabase database) {
22+
this.database = database;
23+
}
24+
25+
@Execute
26+
public StatusReply execute(byte[][] params) {
27+
String key = new String(params[0], StandardCharsets.UTF_8);
28+
long milliseconds;
29+
try {
30+
milliseconds = Long.parseLong(new String(params[1]));
31+
} catch (NumberFormatException e) {
32+
throw new CommandException("value is not an integer or out of range");
33+
}
34+
if (milliseconds < 1) {
35+
throw new CommandException("invalid expire time in 'psetex' command");
36+
}
37+
database.put(params[0], params[2]);
38+
database.expireAt(params[0], System.currentTimeMillis() + milliseconds);
39+
return StatusReply.OK;
40+
}
41+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dev.keva.core.command.impl.string;
2+
3+
import dev.keva.core.command.annotation.CommandImpl;
4+
import dev.keva.core.command.annotation.Execute;
5+
import dev.keva.core.command.annotation.ParamLength;
6+
import dev.keva.core.exception.CommandException;
7+
import dev.keva.ioc.annotation.Autowired;
8+
import dev.keva.ioc.annotation.Component;
9+
import dev.keva.protocol.resp.reply.BulkReply;
10+
import dev.keva.protocol.resp.reply.Reply;
11+
import dev.keva.protocol.resp.reply.StatusReply;
12+
import dev.keva.store.KevaDatabase;
13+
14+
import java.nio.charset.StandardCharsets;
15+
16+
@Component
17+
@CommandImpl("setex")
18+
@ParamLength(type = ParamLength.Type.EXACT, value = 3)
19+
public class SetEX {
20+
private final KevaDatabase database;
21+
22+
@Autowired
23+
public SetEX(KevaDatabase database) {
24+
this.database = database;
25+
}
26+
27+
@Execute
28+
public StatusReply execute(byte[][] params) {
29+
String key = new String(params[0], StandardCharsets.UTF_8);
30+
long seconds;
31+
try {
32+
seconds = Long.parseLong(new String(params[1]));
33+
} catch (NumberFormatException e) {
34+
throw new CommandException("value is not an integer or out of range");
35+
}
36+
if (seconds < 1) {
37+
throw new CommandException("invalid expire time in 'setex' command");
38+
}
39+
database.put(params[0], params[2]);
40+
database.expireAt(params[0], System.currentTimeMillis() + seconds * 1000);
41+
return StatusReply.OK;
42+
}
43+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dev.keva.core.command.impl.string;
2+
3+
import dev.keva.core.command.annotation.CommandImpl;
4+
import dev.keva.core.command.annotation.Execute;
5+
import dev.keva.core.command.annotation.ParamLength;
6+
import dev.keva.core.exception.CommandException;
7+
import dev.keva.ioc.annotation.Autowired;
8+
import dev.keva.ioc.annotation.Component;
9+
import dev.keva.protocol.resp.reply.IntegerReply;
10+
import dev.keva.store.KevaDatabase;
11+
12+
import java.util.Arrays;
13+
14+
import static dev.keva.core.command.annotation.ParamLength.Type.AT_LEAST;
15+
import static dev.keva.core.command.annotation.ParamLength.Type.EXACT;
16+
17+
@Component
18+
@CommandImpl("setnx")
19+
@ParamLength(type = EXACT, value = 2)
20+
public class SetNX {
21+
private final KevaDatabase database;
22+
23+
@Autowired
24+
public SetNX(KevaDatabase database) {
25+
this.database = database;
26+
}
27+
28+
@Execute
29+
public IntegerReply execute(byte[]... params) {
30+
byte[] get = database.get(params[0]);
31+
if(get != null) {
32+
return new IntegerReply(0);
33+
}
34+
database.put(params[0], params[1]);
35+
return new IntegerReply(1);
36+
}
37+
}

core/src/test/java/dev/keva/core/server/AbstractServerTest.java

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
package dev.keva.core.server;
22

3+
import dev.keva.core.command.impl.string.GetEx;
34
import lombok.val;
45
import org.junit.jupiter.api.Test;
56
import org.junit.jupiter.api.Timeout;
67
import redis.clients.jedis.Jedis;
78
import redis.clients.jedis.JedisPubSub;
89
import redis.clients.jedis.exceptions.JedisDataException;
910

10-
import java.util.Arrays;
11-
import java.util.Collections;
12-
import java.util.HashMap;
13-
import java.util.Map;
11+
import java.util.*;
1412
import java.util.concurrent.CompletableFuture;
1513
import java.util.concurrent.ExecutionException;
1614

15+
import redis.clients.jedis.params.GetExParams;
1716
import redis.clients.jedis.params.StrAlgoLCSParams;
1817
import redis.clients.jedis.params.ZAddParams;
1918
import redis.clients.jedis.resps.LCSMatchResult;
@@ -1137,4 +1136,74 @@ void substr() {
11371136
fail(e);
11381137
}
11391138
}
1139+
1140+
@Test
1141+
void getex() {
1142+
try {
1143+
jedis.set("mykey", "Hello");
1144+
// String getex = jedis.getEx("mykey", GetExParams.getExParams()); // How to call getex without param??
1145+
// assertEquals("Hello", getex);
1146+
// long ttl = jedis.ttl("mykey");
1147+
// assertEquals(-1, ttl); // ttl unimplemented
1148+
// String getex = jedis.getEx("mykey", GetExParams.getExParams().ex(60));
1149+
// long ttl = jedis.ttl("mykey");
1150+
// assert(ttl <= 60);
1151+
} catch (Exception e) {
1152+
fail(e);
1153+
}
1154+
}
1155+
1156+
@Test
1157+
void msetnx() {
1158+
try {
1159+
long val1 = jedis.msetnx("key1", "Hello", "key2", "there");
1160+
assertEquals(1, val1);
1161+
long val2 = jedis.msetnx("key2", "new", "key3", "world");
1162+
assertEquals(0, val2);
1163+
List<String> values = jedis.mget("key1", "key2", "key3");
1164+
assertEquals("Hello", values.get(0));
1165+
assertEquals("there", values.get(1));
1166+
assertEquals(null, values.get(2));
1167+
} catch (Exception e) {
1168+
fail(e);
1169+
}
1170+
}
1171+
1172+
@Test
1173+
void psetnx() {
1174+
try {
1175+
String val1 = jedis.psetex("mykey", 1000, "Hello");
1176+
assertEquals("OK", val1);
1177+
// long val2 = jedis.pttl("mykey"); // pttl unimplemented
1178+
// assert(val2 <= 1000);
1179+
} catch (Exception e) {
1180+
fail(e);
1181+
}
1182+
}
1183+
1184+
@Test
1185+
void setex() {
1186+
try {
1187+
String val1 = jedis.setex("mykey", 10, "Hello");
1188+
assertEquals("OK", val1);
1189+
// long val2 = jedis.pttl("mykey"); // pttl unimplemented
1190+
// assert(val2 <= 1000);
1191+
} catch (Exception e) {
1192+
fail(e);
1193+
}
1194+
}
1195+
1196+
@Test
1197+
void setnx() {
1198+
try {
1199+
long val1 = jedis.setnx("mykey", "Hello");
1200+
assertEquals(1, val1);
1201+
long val2 = jedis.setnx("mykey", "World");
1202+
assertEquals(0, val2);
1203+
String val3 = jedis.get("mykey");
1204+
assertEquals("Hello", val3);
1205+
} catch (Exception e) {
1206+
fail(e);
1207+
}
1208+
}
11401209
}

docs/src/guide/overview/commands.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ This is a list of commands that are available in the current latest version of K
5757
- MSET
5858
- INCRBYFLOAT
5959
- SUBSTR
60+
- STRALGO LCS
61+
- GETEX
62+
- MSETNX
63+
- PSETEX
64+
- SETEX
65+
- SETNX
6066

6167
</details>
6268

0 commit comments

Comments
 (0)