Skip to content
Closed
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
44 changes: 19 additions & 25 deletions src/main/java/com/redis/commands/ICommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,23 @@ public interface ICommand {
String execute(List<String> args, ChannelHandlerContext ctx);

/**
* Returns the command name (e.g., "SET", "GET", "DEL").
* Command names are case-insensitive at lookup time.
*/
* Provides the canonical name of the command.
*
* @return the command name (e.g., "SET", "GET", "DEL"); lookup of command names is case-insensitive
*/
String name();

/**
* Returns the canonical arguments for replication.
* <p>
* Override this method when the command needs to be rewritten for replication
* to ensure a consistent state across replicas. Common cases include:
* <ul>
* <li>Auto-generated values (e.g., XADD with * ID → actual ID)</li>
* <li>Relative time to absolute time (e.g., SET EX 60 → SET PXAT timestamp)</li>
* </ul>
* <p>
* The default implementation returns null, indicating the original args
* should be used for propagation.
* Provide the canonical argument list to use when propagating this command to replicas.
*
* @param originalArgs The original command arguments
* @param response The response from execute() - can be used to extract generated values
* @return Canonical arguments for replication, or null to use original args
* <p>Override to return a fixed, canonical form when replication must not depend on
* client-provided or runtime-generated values (for example: generated IDs, or relative
* time arguments converted to absolute timestamps).
*
* @param originalArgs the original command arguments (excluding command name)
* @param response the response produced by {@link #execute(List, io.netty.channel.ChannelHandlerContext)},
* which may contain generated values needed to form canonical args
* @return the canonical arguments to replicate, or `null` to indicate the original arguments should be used
*/
default List<String> getReplicationArgs(List<String> originalArgs, String response) {
return null; // Default: use original args
Expand All @@ -103,16 +99,14 @@ default boolean isWriteCommand() {
}

/**
* Returns the command name to use for replication.
* <p>
* Override when the command should be replicated as a different command.
* For example, EXPIRE should be replicated as PEXPIREAT to use absolute timestamps.
* <p>
* Default returns null, meaning use the original command name.
* Specifies an alternative command name to use when replicating this command.
*
* Override to substitute a different command name for replication (for example,
* replicate EXPIRE as PEXPIREAT to use absolute timestamps).
*
* @return The command name to use for replication, or null to use original
* @return the replication command name, or `null` to use the original command name
*/
default String getReplicationCommandName() {
return null;
}
}
}
12 changes: 11 additions & 1 deletion src/main/java/com/redis/commands/generic/DelCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,23 @@ public String execute(List<String> args, ChannelHandlerContext ctx) {
.toString();
}

/**
* Identifier for the DEL command implemented by this class.
*
* @return the command name "DEL"
*/
@Override
public String name() {
return "DEL";
}

/**
* Indicates whether this command performs a write operation on the database.
*
* @return `true` if the command modifies the database, `false` otherwise.
*/
@Override
public boolean isWriteCommand() {
return true;
}
}
}
47 changes: 26 additions & 21 deletions src/main/java/com/redis/commands/generic/ExpireCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,15 @@ public class ExpireCommand implements ICommand {
private static final ThreadLocal<Long> lastComputedExpiry = new ThreadLocal<>();

/**
* Set a seconds-based expiration timestamp for the specified key.
*
* Expects {@code args} to contain the key at index 0 and the expiry in seconds at index 1.
* Returns the command error reply {@code ERR_WRONG_ARGS} if fewer than two arguments are provided,
* or {@code ERR_VALUE} if the expiry value is not a valid integer.
*
* @param args the command arguments: {@code [key, seconds]}
* @param ctx the Netty channel context (not used by this implementation)
* @return {@code ":1\r\n"} if the expiry was set, {@code ":0\r\n"} otherwise
*/
* Set an expiration for the given key using a relative seconds value.
*
* Expects {@code args} to contain the key at index 0 and the expiry in seconds at index 1.
* On successful expiry set, records the computed absolute expiry timestamp (milliseconds since epoch)
* in a thread-local for replication rewriting.
*
* @param args the command arguments: {@code [key, seconds]}
* @return {@code ":1\r\n"} if the expiry was set, {@code ":0\r\n"} otherwise
*/
@Override
public String execute(List<String> args, ChannelHandlerContext ctx) {
lastComputedExpiry.remove();
Expand Down Expand Up @@ -76,21 +75,26 @@ public String name() {
return "EXPIRE";
}

/**
* Indicates this command performs a write operation that modifies the dataset.
*
* @return `true` if the command modifies the data (is a write), `false` otherwise.
*/
@Override
public boolean isWriteCommand() {
return true;
}

/**
* Returns canonical arguments for replication.
* <p>
* Converts EXPIRE (relative seconds) to PEXPIREAT (absolute milliseconds)
* to ensure all replicas set the same absolute expiry time.
*
* @param originalArgs The original arguments [key, seconds]
* @param response The response from execute()
* @return PEXPIREAT arguments [key, timestamp_ms], or null if failed
*/
* Produce replication arguments by converting EXPIRE's relative seconds into
* PEXPIREAT's absolute expiration timestamp in milliseconds.
*
* <p>Only rewrites when the original command succeeded; otherwise returns null.
*
* @param originalArgs the original EXPIRE arguments as [key, seconds]
* @param response the raw response returned by execute()
* @return the replication arguments as [key, timestamp_ms], or null if the command did not succeed or the computed expiry is unavailable
*/
@Override
public List<String> getReplicationArgs(List<String> originalArgs, String response) {
// Only rewrite if successful
Expand All @@ -114,8 +118,9 @@ public List<String> getReplicationArgs(List<String> originalArgs, String respons
}

/**
* Returns the rewritten command name for replication.
* EXPIRE becomes PEXPIREAT for absolute time handling.
* Provide the replication command name to use when rewriting EXPIRE for replication.
*
* @return the replication command name "PEXPIREAT", which represents expiry as an absolute Unix-time-millisecond timestamp
*/
public String getReplicationCommandName() {
return "PEXPIREAT";
Expand Down
19 changes: 18 additions & 1 deletion src/main/java/com/redis/commands/generic/PExpireAtCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ public class PExpireAtCommand implements ICommand {
private static final String ERR_WRONG_ARGS = "-ERR wrong number of arguments for 'PEXPIREAT' command\r\n";
private static final String ERR_VALUE = "-ERR value is not an integer or out of range\r\n";

/**
* Sets the absolute expiration time (milliseconds since epoch) for the given key and returns a Redis protocol response.
*
* @param args command arguments where args.get(0) is the key and args.get(1) is the expiry timestamp in milliseconds
* @param ctx the Netty channel handler context
* @return ":1\r\n" if the expiry was set, ":0\r\n" if it was not set, ERR_WRONG_ARGS if fewer than two arguments were provided, or ERR_VALUE if the timestamp is not a valid integer
*/
@Override
public String execute(List<String> args, ChannelHandlerContext ctx) {
if (args.size() < 2) {
Expand All @@ -48,13 +55,23 @@ public String execute(List<String> args, ChannelHandlerContext ctx) {
return success ? ":1\r\n" : ":0\r\n";
}

/**
* The Redis command name handled by this command implementation.
*
* @return the command name "PEXPIREAT"
*/
@Override
public String name() {
return "PEXPIREAT";
}

/**
* Indicates that this command modifies the database state.
*
* @return {@code true} if the command performs a write operation, {@code false} otherwise.
*/
@Override
public boolean isWriteCommand() {
return true;
}
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/redis/commands/list/BLPopCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ public String name() {
return "BLPOP";
}

/**
* Indicates that BLPOP modifies the dataset.
*
* @return `true` if the command modifies the dataset, `false` otherwise
*/
@Override
public boolean isWriteCommand() {
return true;
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/redis/commands/list/LPopCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ public String name() {
return "LPOP";
}

/**
* Indicates whether this command modifies the database state.
*
* @return `true` if the command modifies the database state, `false` otherwise.
*/
@Override
public boolean isWriteCommand() {
return true;
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/com/redis/commands/list/LPushCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,23 @@ public String execute(List<String> args, ChannelHandlerContext ctx) {
return ":" + resultSize.get() + "\r\n";
}

/**
* The registered command name for this implementation.
*
* @return the command name ("LPUSH")
*/
@Override
public String name() {
return "LPUSH";
}

/**
* Indicates that this command performs a write operation on the data store.
*
* @return `true` if the command modifies the data store, `false` otherwise
*/
@Override
public boolean isWriteCommand() {
return true;
}
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/redis/commands/list/RPushCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public String name() {
return "RPUSH";
}

/**
* Indicates that this command mutates the database state.
*
* @return `true` if the command performs a write operation, `false` otherwise.
*/
@Override
public boolean isWriteCommand() {
return true;
Expand Down
58 changes: 55 additions & 3 deletions src/main/java/com/redis/commands/replication/InfoCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ public class InfoCommand implements ICommand {
STATIC_SERVER_HEADER = sb.toString();
}

/**
* Assembles the requested Redis INFO sections and returns them in Redis Bulk String format.
*
* If no section is specified (or the first argument is "all"), the response includes the
* server, replication, clients, memory, and stats sections. Valid section names are:
* "server", "replication", "clients", "memory", and "stats".
*
* @param args optional arguments where the first element selects which INFO section to include;
* when omitted or "all", all sections are included
* @return a Redis Bulk String containing the requested INFO content (format: $&lt;length&gt;\r\n&lt;content&gt;\r\n)
*/
@Override
public String execute(List<String> args, ChannelHandlerContext ctx) {
String section = args.isEmpty() ? "all" : args.get(0).toLowerCase();
Expand Down Expand Up @@ -58,6 +69,14 @@ public String execute(List<String> args, ChannelHandlerContext ctx) {
return "$" + content.length() + "\r\n" + content + "\r\n";
}

/**
* Appends the "Server" INFO section to the provided StringBuilder.
*
* The section includes the precomputed server header, `tcp_port`, `uptime_in_seconds`,
* and `uptime_in_days`, and is terminated with an empty line.
*
* @param sb the StringBuilder to append the server information to; this builder is modified
*/
private void appendServerInfo(StringBuilder sb) {
long uptimeMs = System.currentTimeMillis() - SERVER_START_TIME;
sb.append(STATIC_SERVER_HEADER);
Expand All @@ -67,14 +86,33 @@ private void appendServerInfo(StringBuilder sb) {
sb.append("\r\n");
}

/**
* Appends the replication section content to the given StringBuilder.
*
* Appends the replication manager's INFO output followed by a CRLF ("\r\n").
*/
private void appendReplicationInfo(StringBuilder sb) {
sb.append(ReplicationManager.getInstance().getInfoReplication()).append("\r\n");
}

/**
* Appends the "Clients" INFO section to the provided StringBuilder.
*
* @param sb the StringBuilder to append the Clients section to; receives `connected_clients` and `blocked_clients` lines
*/
private void appendClientsInfo(StringBuilder sb) {
sb.append("# Clients\r\nconnected_clients:1\r\nblocked_clients:0\r\n\r\n");
}

/**
* Appends the "Memory" INFO section to the provided StringBuilder.
*
* The section includes `used_memory`, a human-readable `used_memory_human`,
* `total_system_memory`, and a human-readable `total_system_memory_human`,
* followed by a blank line.
*
* @param sb the StringBuilder to append the Memory section to
*/
private void appendMemoryInfo(StringBuilder sb) {
Runtime rt = Runtime.getRuntime();
long usedMemory = rt.totalMemory() - rt.freeMemory();
Expand All @@ -88,14 +126,23 @@ private void appendMemoryInfo(StringBuilder sb) {
sb.append("\r\n");
}

/**
* Append the Redis INFO "Stats" section to the provided StringBuilder.
*
* @param sb the StringBuilder to append the Stats section to
*/
private void appendStatsInfo(StringBuilder sb) {
sb.append("# Stats\r\ntotal_connections_received:1\r\ntotal_commands_processed:0\r\n\r\n");
}

/**
* Optimized byte formatter:
* 1. Appends directly to existing StringBuilder to avoid temporary String objects.
* 2. Uses basic math instead of expensive String.format().
* Append a human-readable representation of a byte count to the given StringBuilder using unit suffixes.
*
* Formats values as bytes ("B") or with one decimal place using "K", "M", or "G" as appropriate and appends
* the result directly to the provided StringBuilder.
*
* @param sb the StringBuilder to append the formatted value to
* @param bytes the byte count to format
*/
private void formatBytes(StringBuilder sb, long bytes) {
if (bytes < 1024) {
Expand All @@ -109,6 +156,11 @@ private void formatBytes(StringBuilder sb, long bytes) {
}
}

/**
* Provide the command name for this ICommand implementation.
*
* @return the command name "INFO"
*/
@Override
public String name() {
return "INFO";
Expand Down
Loading
Loading