diff --git a/src/main/java/com/redis/commands/CommandRegistry.java b/src/main/java/com/redis/commands/CommandRegistry.java
index 5b1c329..b2b8eb1 100644
--- a/src/main/java/com/redis/commands/CommandRegistry.java
+++ b/src/main/java/com/redis/commands/CommandRegistry.java
@@ -7,48 +7,78 @@
import java.util.concurrent.ConcurrentHashMap;
/**
- * Registry for all available Redis commands.
- * Supports dynamic command registration and lookup.
- * This is a singleton initialized with built-in commands via ServiceLoader.
- *
- * Optimizations:
- * - Case-insensitive lookups with single toUpperCase() call
- * - ConcurrentHashMap with capacity hints for faster lookup
- * - Dynamic discovery via ServiceLoader for easy extensibility
+ * The Central Command Registry.
+ *
+ * Role: Acts as the directory for all executable commands. It maps a string (e.g., "SET")
+ * to the actual Java object capable of executing that logic.
+ *
+ * Design Pattern: Singleton
+ * We use a Singleton because the list of commands is static for the application's lifetime.
+ * Re-scanning for commands for every client connection would be incredibly slow.
+ *
+ * Design Pattern: Strategy / Command
+ * This registry enables the "Command Pattern". The network layer doesn't need to know how "SET" works;
+ * it just retrieves the command object and calls .execute().
*/
public class CommandRegistry {
- private static CommandRegistry INSTANCE;
+ // The single instance of this class (Volatile is implied by the memory model of synchronized,
+ // but usually good practice to mark volatile in double-checked locking to prevent instruction reordering).
+ private static volatile CommandRegistry INSTANCE;
+
+ // The actual storage.
+ // Key = Command Name (UPPERCASE), Value = Command Object.
+ // We use ConcurrentHashMap because the registry might be read by multiple Netty threads simultaneously.
+ // While writes happen mostly at startup, safe reads are critical.
private final Map registry = new ConcurrentHashMap<>(32);
/**
- * Initialize the singleton registry and register all commands found via ServiceLoader.
+ * Private constructor to enforce Singleton usage.
+ *
+ * Mechanism: ServiceLoader (SPI)
+ * Instead of hardcoding "new SetCommand()", we ask Java to look at the classpath.
+ * Java looks for a file: META-INF/services/com.redis.commands.ICommand
+ * It reads the class names listed there and instantiates them.
*/
private CommandRegistry() {
+ // Step 1: Initialize the loader for the ICommand interface
ServiceLoader loader = ServiceLoader.load(ICommand.class);
+
+ // Step 2: Iterate through found implementations.
+ // The ServiceLoader lazily instantiates the classes as we iterate.
for (ICommand cmd : loader) {
register(cmd);
}
+
+ // Logging is helpful to verify that your META-INF file is set up correctly.
System.out.println("[Redis] Registered " + registry.size() + " commands");
}
/**
- * Get the singleton instance of CommandRegistry.
+ * The Holder Class.
+ *
+ * 1. Lazy: This class is NOT loaded when CommandRegistry is loaded.
+ * It is only loaded when getInstance() is called for the first time.
+ * 2. Thread-Safe: The JVM guarantees that static field initialization
+ * (INSTANCE = new ...) happens atomically. No synchronized keyword needed.
+ */
+ private static class RegistryHolder {
+ private static final CommandRegistry INSTANCE = new CommandRegistry();
+ }
+
+ /**
+ * Get the singleton instance.
+ * Triggers the loading of RegistryHolder and the creation of INSTANCE.
*/
public static CommandRegistry getInstance() {
- if (INSTANCE == null) {
- synchronized (CommandRegistry.class) {
- if (INSTANCE == null) {
- INSTANCE = new CommandRegistry();
- }
- }
- }
- return INSTANCE;
+ return RegistryHolder.INSTANCE;
}
/**
- * Register a command in the registry.
- * Command names are stored in uppercase for case-insensitive lookup.
+ * Registers a command into the map.
+ *
+ * Normalization: We store all keys in UPPERCASE. This ensures that
+ * "set", "Set", and "SET" all resolve to the same entry.
*/
public void register(ICommand cmd) {
String cmdName = cmd.name().toUpperCase();
@@ -56,34 +86,37 @@ public void register(ICommand cmd) {
}
/**
- * Look up a command by name (case-insensitive).
- * Returns the command instance or null if not found.
- * Optimization: Single toUpperCase() call, direct HashMap lookup
+ * Retrieves the command object for a given name.
+ *
+ * Performance: This is a "Hot Path" method called for every single request.
+ * It must be O(1) and very fast.
+ *
+ * @param name The command name (e.g., "set")
+ * @return The ICommand instance, or null if not found.
*/
public ICommand get(String name) {
if (name == null || name.isEmpty()) {
return null;
}
+ // Convert input to uppercase to match the storage key format.
return registry.get(name.toUpperCase());
}
/**
- * Check if a command is registered.
+ * Utility to check command existence without retrieving it.
*/
public boolean exists(String name) {
return get(name) != null;
}
/**
- * Get all registered command names (unmodifiable).
+ * Returns a Read-Only view of all available commands.
+ * Useful for the "COMMAND" command in Redis which lists capabilities.
*/
public Set getRegisteredCommands() {
return Collections.unmodifiableSet(registry.keySet());
}
- /**
- * Get the number of registered commands.
- */
public int size() {
return registry.size();
}
diff --git a/src/main/java/com/redis/commands/stream/XAddCommand.java b/src/main/java/com/redis/commands/stream/XAddCommand.java
index 6dfbb36..e656cc7 100644
--- a/src/main/java/com/redis/commands/stream/XAddCommand.java
+++ b/src/main/java/com/redis/commands/stream/XAddCommand.java
@@ -13,11 +13,20 @@
import java.util.concurrent.atomic.AtomicReference;
/**
- * XADD key ID field value [field value ...]
- * Appends the specified stream entry to the stream at the specified key.
- * If the key does not exist, as a side effect the stream is created.
+ * Implementation of the XADD command.
+ *
+ * Syntax: XADD key ID field value [field value ...]
+ *
+ * Role: Appends a new entry to a stream. This is the "Writer" command for streams.
+ *
+ * Concurrency Strategy:
+ * This command relies heavily on {@link RedisDatabase#compute} to ensure atomic ID generation.
+ * Since Stream IDs must be strictly monotonic (always increasing), we must lock the stream
+ * while we calculate the next ID to prevent two clients from generating the same ID simultaneously.
*/
public class XAddCommand implements ICommand {
+
+ // Standard Redis Error Messages
private static final String ERR_WRONG_ARGS = "-ERR wrong number of arguments for 'XADD' command\r\n";
private static final String ERR_WRONG_TYPE = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
private static final String ERR_ID_TOO_SMALL = "-ERR The ID specified in XADD is equal or smaller than the target stream top item\r\n";
@@ -25,6 +34,9 @@ public class XAddCommand implements ICommand {
@Override
public String execute(List args, ChannelHandlerContext ctx) {
+ // Minimum args: key, ID, field, value (4 args -> size 4).
+ // Note: The List args usually includes key at 0.
+ // Format: XADD ...
if (args.size() < 3) {
return ERR_WRONG_ARGS;
}
@@ -32,81 +44,117 @@ public String execute(List args, ChannelHandlerContext ctx) {
String key = args.get(0);
String idArg = args.get(1);
+ // Validation: Fields and Values must come in pairs
int fieldStart = 2;
- // Basic argument validation for field-value pairs
if ((args.size() - fieldStart) % 2 != 0) {
- return ERR_WRONG_ARGS;
+ return ERR_WRONG_ARGS;
}
+ // Parse fields into a LinkedHashMap to preserve insertion order (Redis convention)
Map fields = new LinkedHashMap<>();
for (int i = fieldStart; i < args.size(); i += 2) {
fields.put(args.get(i), args.get(i + 1));
}
RedisDatabase db = RedisDatabase.getInstance();
+
+ // We use AtomicReferences to extract results/errors from inside the lambda
AtomicReference error = new AtomicReference<>(null);
AtomicReference addedId = new AtomicReference<>(null);
+ // CRITICAL SECTION: Atomic Read-Modify-Write
+ // We lock the key to ensure no one else inserts while we determine the next ID.
db.compute(key, existing -> {
ConcurrentSkipListMap> streamMap;
+
+ // 1. Initialization / Type Check
if (existing == null) {
+ // New Key: Create a new SkipList (Ordered Thread-Safe Map)
streamMap = new ConcurrentSkipListMap<>();
} else if (existing.getType() != RedisValue.Type.STREAM) {
+ // Wrong Type: Cannot append stream data to a String/List
error.set(ERR_WRONG_TYPE);
return existing;
} else {
+ // Existing Key: Cast the raw data
@SuppressWarnings("unchecked")
var data = (Map>) existing.getData();
+ // We know it's a SkipList because we created it that way in RedisValue factory
streamMap = (ConcurrentSkipListMap>) data;
}
+ // 2. Determine Context (What is the last ID?)
+ // If stream is empty, assume "0-0" is the predecessor
StreamId lastId = streamMap.isEmpty() ? new StreamId(0, 0) : streamMap.lastKey();
StreamId newId;
try {
+ // 3. ID Generation Logic
if (idArg.equals("*")) {
+ // AUTO-GENERATE: "*"
long now = System.currentTimeMillis();
if (now > lastId.time()) {
+ // Standard case: New millisecond, reset sequence to 0
newId = new StreamId(now, 0);
} else {
+ // Collision or Clock Skew: Time is the same (or older) than the last entry.
+ // We must increment the sequence number of the LAST entry's time.
+ // This handles high-throughput bursts within the same ms.
newId = new StreamId(lastId.time(), lastId.sequence() + 1);
}
} else if (idArg.endsWith("-*")) {
+ // PARTIAL ID: "123456-*"
long ms = Long.parseLong(idArg.substring(0, idArg.length() - 2));
+
if (ms < lastId.time()) {
error.set(ERR_ID_TOO_SMALL);
return existing;
}
+ // If time is the same, increment sequence. If time is new, seq is 0.
long seq = (ms == lastId.time()) ? lastId.sequence() + 1 : 0;
newId = new StreamId(ms, seq);
} else {
+ // EXPLICIT ID: "123456-0"
newId = StreamId.parse(idArg);
}
+ // 4. Validation Rules
+ // Rule A: ID must be > 0-0
if (newId.time() == 0 && newId.sequence() == 0) {
error.set(ERR_ID_ZERO);
return existing;
}
+ // Rule B: ID must be strictly greater than the last ID
+ // Note: If stream is empty, lastId is 0-0, so any valid ID passes.
if (!streamMap.isEmpty() && !newId.isGreaterThan(lastId)) {
error.set(ERR_ID_TOO_SMALL);
return existing;
}
+ // 5. Execution: Insert into map
streamMap.put(newId, fields);
addedId.set(newId);
+
+ // Return wrapped value to update DB (or keep existing reference)
return RedisValue.stream(streamMap);
+
} catch (Exception e) {
+ // Handle parsing errors (e.g., malformed ID string)
error.set("-ERR " + e.getMessage() + "\r\n");
return existing;
}
});
+ // 6. Response Handling
+ // If the lambda set an error, return it.
if (error.get() != null) {
return error.get();
}
+ // Otherwise return the ID we just generated/inserted.
String idStr = addedId.get().toString();
+ // Redis Bulk String format: $\r\n\r\n
return "$" + idStr.length() + "\r\n" + idStr + "\r\n";
}
@@ -114,4 +162,4 @@ public String execute(List args, ChannelHandlerContext ctx) {
public String name() {
return "XADD";
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/redis/commands/stream/XRangeCommand.java b/src/main/java/com/redis/commands/stream/XRangeCommand.java
index 9dada69..779d430 100644
--- a/src/main/java/com/redis/commands/stream/XRangeCommand.java
+++ b/src/main/java/com/redis/commands/stream/XRangeCommand.java
@@ -11,15 +11,22 @@
import java.util.NavigableMap;
/**
- * XRANGE key start end [COUNT count]
- * Returns the stream entries matching a given range of IDs.
+ * Implementation of the XRANGE command.
+ *
+ * Architecture: Asynchronous Polling
+ * This command demonstrates how to handle "Blocking" operations in a Non-Blocking framework (Netty).
+ * We cannot block the thread (Thread.sleep) because it would freeze the server.
+ * Instead, if no data is found, we schedule a background task to check again later (Polling).
*/
public class XReadCommand implements ICommand {
private static final String ERR_WRONG_ARGS = "-ERR wrong number of arguments for 'XREAD' command\r\n";
private static final String ERR_WRONG_TYPE = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n";
- private static final String RESP_NIL_ARRAY = "*-1\r\n";
- private static final long POLL_INTERVAL_MS = 50;
+ private static final String RESP_NIL_ARRAY = "*-1\r\n"; // Standard Redis "Null Array" response
+ private static final long POLL_INTERVAL_MS = 50; // How often to check for new data during BLOCK
@Override
public String execute(List args, ChannelHandlerContext ctx) {
if (args.size() < 3) return ERR_WRONG_ARGS;
- int count = Integer.MAX_VALUE;
- long blockMs = -1;
- int streamsIdx = -1;
+ // --- Step 1: Parse Options (COUNT, BLOCK) ---
+ int count = Integer.MAX_VALUE; // Default: Return all available
+ long blockMs = -1; // Default: Non-blocking
+ int streamsIdx = -1; // Index where "STREAMS" keyword appears
try {
for (int i = 0; i < args.size(); i++) {
@@ -44,21 +50,26 @@ public String execute(List args, ChannelHandlerContext ctx) {
blockMs = Long.parseLong(args.get(++i));
} else if (arg.equals("STREAMS")) {
streamsIdx = i + 1;
- break;
+ break; // Parsing done, everything after this is Keys/IDs
}
}
} catch (NumberFormatException e) {
return "-ERR value is not an integer or out of range\r\n";
}
+ // Validation: Must have STREAMS keyword and arguments following it
if (streamsIdx == -1 || streamsIdx >= args.size()) return ERR_WRONG_ARGS;
+ // The remaining arguments must be even (Key1, Key2 ... ID1, ID2)
int remainingArgs = args.size() - streamsIdx;
if (remainingArgs <= 0 || remainingArgs % 2 != 0) return ERR_WRONG_ARGS;
int numStreams = remainingArgs / 2;
List keys = new ArrayList<>(numStreams);
List ids = new ArrayList<>(numStreams);
+
+ // Split arguments into Keys and IDs lists
+ // Example: STREAMS s1 s2 0-0 0-0 -> Keys:[s1, s2], IDs:[0-0, 0-0]
for (int i = 0; i < numStreams; i++) {
keys.add(args.get(streamsIdx + i));
ids.add(args.get(streamsIdx + numStreams + i));
@@ -66,44 +77,58 @@ public String execute(List args, ChannelHandlerContext ctx) {
RedisDatabase db = RedisDatabase.getInstance();
- // Immediate read
+ // --- Step 2: Attempt Immediate Read ---
+ // If data is already there, return immediately. No need to block.
String result = tryRead(db, keys, ids, count);
if (result != null) return result;
- if (blockMs < 0) return "*-1\r\n"; // Non-blocking and no data: Redis returns nil for XREAD?
- // Wait, standard Redis XREAD (without BLOCK) returns (nil) if no data.
+ // --- Step 3: Handle Blocking ---
+
+ // Case A: No BLOCK option requested.
+ // Standard Redis behavior: If no data found in non-blocking mode, return Nil.
+ if (blockMs < 0) return "*-1\r\n";
- // Blocking read
+ // Case B: BLOCK requested.
+ // Calculate when we should stop waiting. (0 means block forever).
long deadline = blockMs == 0 ? Long.MAX_VALUE : System.currentTimeMillis() + blockMs;
if (ctx.executor() != null) {
+ // ASYNC PATH (Correct for Netty):
+ // We return null to tell the main handler "Don't send a response yet".
+ // We schedule a background task to keep checking.
schedulePolling(ctx, keys, ids, count, deadline, db);
return null;
} else {
+ // SYNC PATH (Fallback / Testing):
+ // Should be avoided in production as it blocks the thread.
return pollSynchronously(keys, ids, count, deadline, db);
}
}
+ /**
+ * core logic to check the database for matching entries.
+ * Returns a RESP-formatted string if data is found, or null if nothing is found.
+ */
private String tryRead(RedisDatabase db, List keys, List idArgs, int count) {
List streamResponses = new ArrayList<>();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String idArg = idArgs.get(i);
-
+
RedisValue value = db.getValue(key);
- if (value == null) continue;
- if (value.getType() != RedisValue.Type.STREAM) {
- // In Redis, if one key is not a stream, it might error or skip.
- // Usually it errors.
- continue;
- }
+ if (value == null) continue; // Stream doesn't exist
+ if (value.getType() != RedisValue.Type.STREAM) continue; // Skip wrong types
+ // Access the underlying SkipList
@SuppressWarnings("unchecked")
NavigableMap> streamMap = (NavigableMap>) value.getData();
-
+
+ // Resolve the ID to start reading from
StreamId lastId;
if (idArg.equals("$")) {
+ // Special ID "$": Means "Only new messages".
+ // So we start strictly AFTER the current last key.
lastId = streamMap.isEmpty() ? new StreamId(0, 0) : streamMap.lastKey();
} else {
try {
@@ -113,25 +138,37 @@ private String tryRead(RedisDatabase db, List keys, List idArgs,
}
}
+ // CRITICAL: tailMap(lastId, false)
+ // 'false' means EXCLUSIVE (strictly greater than lastId).
+ // This gives us O(log N) access to the new entries.
NavigableMap> tail = streamMap.tailMap(lastId, false);
if (tail.isEmpty()) continue;
+ // --- Build RESP Response for this Stream ---
StringBuilder streamSb = new StringBuilder();
+
+ // 1. Array Header for this stream (Name + Entries)
streamSb.append("*2\r\n");
- // Stream name
+
+ // 2. Stream Name
streamSb.append("$").append(key.length()).append("\r\n").append(key).append("\r\n");
-
- // Entries
+
+ // 3. Array of Entries
int entriesToReturn = Math.min(count, tail.size());
streamSb.append("*").append(entriesToReturn).append("\r\n");
-
+
int j = 0;
for (Map.Entry> entry : tail.entrySet()) {
if (j++ >= count) break;
+
+ // Entry Structure: [ID, [Field, Value, ...]]
streamSb.append("*2\r\n");
+
+ // ID
String sid = entry.getKey().toString();
streamSb.append("$").append(sid.length()).append("\r\n").append(sid).append("\r\n");
-
+
+ // Field-Value Array
Map fields = entry.getValue();
streamSb.append("*").append(fields.size() * 2).append("\r\n");
for (Map.Entry f : fields.entrySet()) {
@@ -146,29 +183,41 @@ private String tryRead(RedisDatabase db, List keys, List idArgs,
if (streamResponses.isEmpty()) return null;
+ // Wrap all stream responses in one outer array
StringBuilder sb = new StringBuilder();
sb.append("*").append(streamResponses.size()).append("\r\n");
for (String s : streamResponses) sb.append(s);
return sb.toString();
}
+ /**
+ * Async Polling Logic.
+ * Schedules itself to run repeatedly on the EventLoop until data is found or timeout occurs.
+ */
private void schedulePolling(ChannelHandlerContext ctx, List keys, List ids, int count, long deadline, RedisDatabase db) {
ctx.executor().schedule(() -> {
+ // 1. Check Timeout
if (System.currentTimeMillis() >= deadline) {
writeResponse(ctx, RESP_NIL_ARRAY);
return;
}
+ // 2. Check Data
String result = tryRead(db, keys, ids, count);
if (result != null) {
writeResponse(ctx, result);
return;
}
+ // 3. Reschedule (Recursive step, but async so no stack overflow)
schedulePolling(ctx, keys, ids, count, deadline, db);
}, POLL_INTERVAL_MS, TimeUnit.MILLISECONDS);
}
+ /**
+ * Fallback for contexts without an executor (e.g. unit tests).
+ * WARNING: Blocks the calling thread.
+ */
private String pollSynchronously(List keys, List ids, int count, long deadline, RedisDatabase db) {
while (System.currentTimeMillis() < deadline) {
String result = tryRead(db, keys, ids, count);
@@ -187,4 +236,4 @@ private void writeResponse(ChannelHandlerContext ctx, String response) {
public String name() {
return "XREAD";
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/redis/config/RedisConfig.java b/src/main/java/com/redis/config/RedisConfig.java
index e47ad01..d6de2c9 100644
--- a/src/main/java/com/redis/config/RedisConfig.java
+++ b/src/main/java/com/redis/config/RedisConfig.java
@@ -6,11 +6,11 @@
/**
* Central configuration for the Redis server.
- * Loads defaults and can be overridden by application.properties or environment variables.
+ * Loads default and can be overridden by application.properties or environment variables.
*/
public class RedisConfig {
private static RedisConfig instance;
- private Properties properties = new Properties();
+ private final Properties properties = new Properties();
// Default configuration values
private static final int DEFAULT_PORT = 6379;
diff --git a/src/main/java/com/redis/server/RedisCommandHandler.java b/src/main/java/com/redis/server/RedisCommandHandler.java
index 8b87241..193f903 100644
--- a/src/main/java/com/redis/server/RedisCommandHandler.java
+++ b/src/main/java/com/redis/server/RedisCommandHandler.java
@@ -12,62 +12,95 @@
import java.util.List;
/**
- * Netty channel handler for processing incoming Redis commands.
- * Parses RESP protocol using ByteToMessageDecoder for fragmentation support.
- *
- * IMPORTANT: This handler is instantiated per-channel (see NettyRedisServer.initChannel()),
- * ensuring thread-safety for the argsBuffer field. Each channel has its own handler instance.
- *
- * Optimizations:
- * - Reuses ArrayList for argument parsing
- * - Fast integer parsing without object allocation
- * - Robust handling of pipelined commands
+ * Netty channel handler responsible for the "Framing" phase of the Redis protocol.
+ *
+ * Role in Pipeline:
+ * This handler sits between the raw network bytes and the command execution logic.
+ * Its job is to turn a stream of fragmented TCP bytes into discrete, actionable Redis commands.
+ *
+ * Thread Safety & Lifecycle:
+ * IMPORTANT: This class is stateful (it holds `argsBuffer`). It must be instantiated
+ * per-channel (one instance per connected client). It is NOT @ChannelHandler.Sharable.
+ *
+ * Optimizations:
+ * 1. Zero-Allocation Integer Parsing: Avoids creating String objects just to read lengths.
+ * 2. Object Pooling: Reuses a single ArrayList {@code argsBuffer} for the lifetime of the connection
+ * to reduce Garbage Collection pressure during high-throughput bursts.
*/
public class RedisCommandHandler extends ByteToMessageDecoder {
- // Preallocate list to avoid allocations for small commands
+
+ // Initial capacity for the arguments list. Most Redis commands have fewer than 16 arguments.
private static final int INITIAL_ARGS_CAPACITY = 16;
+
+ /**
+ * A reusable buffer for command arguments.
+ * We clear and refill this list for every command instead of allocating a new ArrayList each time.
+ * This significantly reduces "Object Churn" in the JVM.
+ */
private final List argsBuffer = new ArrayList<>(INITIAL_ARGS_CAPACITY);
/**
- * Parses and processes as many complete RESP array commands as are available in the input buffer.
- *
- *
Reads RESP arrays from {@code in}, reusing the handler's argument buffer to collect the command
- * name and arguments. If a full command is not yet available the reader index is reset and decoding
- * stops so more data can arrive. For each complete command, resolves the command implementation,
- * executes it with a view of the argument list, and writes the RESP response to the channel. Unknown
- * commands result in the Redis error reply "-ERR unknown command 'name'". This method supports
- * pipelined commands by looping until the input buffer has no more complete commands.
+ * Sentinel value indicating that the buffer does not yet contain a complete integer ending in CRLF.
+ */
+ private static final int INCOMPLETE = Integer.MIN_VALUE;
+
+ /**
+ * The main entry point called by Netty whenever new data arrives from the network.
+ * * @param ctx Context to interact with the channel pipeline (e.g., writing responses).
*
- * @param ctx the Netty channel handler context used to write responses
- * @param in the inbound byte buffer containing RESP data; reader index is advanced for consumed data
- * @param out the list to which decoded messages would normally be added (not used; responses are written directly)
+ * @param in The input ByteBuf containing raw bytes received from the OS.
+ * @param out (Unused) We write responses directly to ctx, rather than passing objects up the pipeline.
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List