Skip to content

Refactored + Added New Commands - #41

Merged
unikdahal merged 17 commits into
mainfrom
feature/LPushCommand
Jan 26, 2026
Merged

Refactored + Added New Commands#41
unikdahal merged 17 commits into
mainfrom
feature/LPushCommand

Conversation

@unikdahal

@unikdahal unikdahal commented Jan 25, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added PING, ECHO, BLPOP, LPOP, LLEN, EXPIRE and TTL commands; improved pipelining and fragmented-request handling.
  • Bug Fixes

    • More reliable key expiry behavior and safer shutdown of expiry background processing.
  • Documentation

    • README redesigned with badges, streamlined sections, concise examples, and architecture overview.
  • Tests

    • Large suite of unit and integration tests added plus a test orchestration script.
  • Chores

    • Maven build/config updates and refined .gitignore for Java/Maven projects.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 25, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds multiple Redis-like commands (PING, ECHO, LLEN, LPOP, BLPOP, EXPIRE, TTL), refactors request decoding to support pipelining/fragmentation, extends expiry/DB singleton APIs, centralizes Maven config, updates docs and gitignore, and adds extensive unit/integration tests plus test orchestration scripts.

Changes

Cohort / File(s) Summary
New Command Implementations
src/main/java/com/redis/commands/BLPopCommand.java, src/main/java/com/redis/commands/EchoCommand.java, src/main/java/com/redis/commands/ExpireCommand.java, src/main/java/com/redis/commands/LLenCommand.java, src/main/java/com/redis/commands/LPopCommand.java, src/main/java/com/redis/commands/PingCommand.java, src/main/java/com/redis/commands/TtlCommand.java
Added seven ICommand handlers exposing name()/execute(...): BLPOP (blocking polling + async polling path), ECHO, EXPIRE, LLEN, LPOP (count support), PING, TTL.
Command Registry & Arg Access Fixes
src/main/java/com/redis/commands/CommandRegistry.java, src/main/java/com/redis/commands/{GetCommand.java,LPushCommand.java,LRangeCommand.java,RPushCommand.java}
Registered new commands and unified first-argument access replacing List.getFirst() with List.get(0).
Server Handler Decoding Refactor
src/main/java/com/redis/server/RedisCommandHandler.java
Replaced ChannelInboundHandlerAdapter with ByteToMessageDecoder; added per-channel argsBuffer, INCOMPLETE handling, decode loop supporting fragmented frames and pipelined multiple commands.
Storage & Expiry Enhancements
src/main/java/com/redis/storage/RedisDatabase.java, src/main/java/com/redis/storage/ExpiryManager.java
Added RedisDatabase.getInstance(), getExpiryTime(), setExpiryTime(); put/remove clear expiry. ExpiryManager: clearExpiry(key) and improved shutdown (interrupt/join).
Build & VCS config
pom.xml, .gitignore
Centralized dependencyManagement/pluginManagement, explicit Netty modules, plugin config changes; .gitignore refocused to Java/Maven/IDE artifacts.
Documentation & Test Orchestration
README.md, run_all_tests.sh, src/main/test/integration_tests.sh (deleted: src/main/test/test.sh)
README rewritten; added run_all_tests.sh to build/start server and run integration tests; new integration_tests.sh replaces legacy test.sh.
Unit & Integration Tests
src/test/java/com/redis/commands/*, src/test/java/com/redis/server/PipeliningTest.java, src/test/java/com/redis/storage/ExpiryBugTest.java
New and expanded JUnit tests covering BLPOP, LPOP, LLEN, PING, ECHO, EXPIRE/TTL, pipelining/fragmentation and expiry edge-cases; many integration-style shell tests added.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as RedisCommandHandler
    participant DB as RedisDatabase
    participant Expiry as ExpiryManager

    Client->>Handler: Send BLPOP key1 key2 <timeout>
    Handler->>DB: compute/pop attempt on key1
    alt element available
        DB-->>Handler: element + key
        Handler-->>Client: RESP array [key, element]
    else no element
        Handler->>Handler: schedule/poll loop (event-loop or sync test fallback)
        loop until timeout or element found
            Handler->>DB: compute/pop attempt on keys...
            DB-->>Handler: null or element
        end
        alt element found later
            Handler-->>Client: RESP array [key, element]
        else timeout
            Handler-->>Client: RESP nil
        end
    end
    note right of Expiry: ExpiryManager may asynchronously remove expired keys referenced by DB
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A hop, a pop, a blocking wait,
Commands arrive, pipelines create,
TTLs tick down, expired crumbs fall,
Tests march in lines — I celebrate all,
Carrot-powered server, springing tall!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Refactored + Added New Commands' is vague and generic, using non-descriptive terms that don't convey meaningful information about the specific changes made. Provide a more specific title that describes the primary change, such as 'Add BLPOP, LLEN, LPOP, PING, ECHO, EXPIRE, and TTL commands with pipelining support' or 'Restructure pom.xml with dependency management and add Redis command implementations'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jan 25, 2026

Copy link
Copy Markdown
Contributor

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #42

Docstrings generation was requested by @unikdahal.

* #41 (comment)

The following files were modified:

* `run_all_tests.sh`
* `src/main/java/com/redis/commands/BLPopCommand.java`
* `src/main/java/com/redis/commands/CommandRegistry.java`
* `src/main/java/com/redis/commands/EchoCommand.java`
* `src/main/java/com/redis/commands/ExpireCommand.java`
* `src/main/java/com/redis/commands/LLenCommand.java`
* `src/main/java/com/redis/commands/LPopCommand.java`
* `src/main/java/com/redis/commands/PingCommand.java`
* `src/main/java/com/redis/commands/TtlCommand.java`
* `src/main/java/com/redis/server/RedisCommandHandler.java`
* `src/main/java/com/redis/storage/ExpiryManager.java`
* `src/main/java/com/redis/storage/RedisDatabase.java`
* `src/main/test/integration_tests.sh`
📝 Add docstrings to `feature/LPushCommand`

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 20

🤖 Fix all issues with AI agents
In @.gitignore:
- Around line 9-10: Add the missing ignore for the build/ directory to
.gitignore and remove the currently committed build artifacts from the repo
index so they stop being tracked; specifically, add the pattern "build/" to
.gitignore, run a cached removal of the tracked build files (e.g., remove
build/classes/ and related build outputs from the index), and commit both the
updated .gitignore and the removal so future builds are untracked. Ensure you
verify the repo no longer lists build/ files as changes after committing.

In `@build/classes/application.properties`:
- Around line 1-19: Remove the committed build artifact application.properties
from version control (the compiled file in your build output) by running git rm
--cached on that file and commit the removal, and update your .gitignore to
include the build/ directory (add a line "build/") so future build outputs are
ignored; ensure the canonical source application.properties in
src/main/resources remains tracked.

In `@build/classes/META-INF/INDEX.LIST`:
- Around line 1-8: Remove the tracked build output and the stale jar index
referencing netty-all-4.1.94.Final.jar by deleting the build/classes/ directory
(which contains META-INF/INDEX.LIST) from the repo and committing that deletion;
then update .gitignore to include build/classes/ (and any other build output
patterns) so generated artifacts aren’t re-added, and verify pom.xml's modular
Netty dependencies (not netty-all) remain as the source of truth.

In `@build/classes/META-INF/io.netty.versions.properties`:
- Around line 1-9: The checked-in Netty metadata file
io.netty.versions.properties under build/classes/ is a generated build artifact
and should be removed from the repo; delete io.netty.versions.properties and any
other files under build/classes/, add the build/ directory (or at minimum
build/classes/) to .gitignore so generated files are not tracked, and commit a
removal commit that updates the repository state accordingly.

In `@build/classes/META-INF/MANIFEST.MF`:
- Around line 1-27: The committed MANIFEST.MF contains stale Netty artifact
metadata (e.g., Implementation-Version: 4.1.94.Final, Build-Jdk: 1.8.0_252,
Bundle-Name: Netty/All-in-One) and should be removed: delete this MANIFEST.MF
and the entire build/ directory from version control, add an entry to ignore
build/ in .gitignore (or similar VCS ignore) to prevent re-committing, and
commit the removal with a brief message like “remove stale Netty build
artifacts.”

In `@build/classes/META-INF/maven/io.netty/netty-all/pom.properties`:
- Around line 1-4: Remove the committed Maven build artifact (pom.properties)
from source control and prevent future build outputs from being tracked: add a
rule to .gitignore to exclude the build/ directory, remove the tracked file from
the repository index (e.g., git rm --cached for the artifact), and commit the
change so the file is no longer present in the repo while remaining in local
build outputs.

In `@build/classes/META-INF/maven/io.netty/netty-all/pom.xml`:
- Around line 20-22: A checked-in build artifact contains a vulnerable Netty
dependency (pom.xml with groupId "io.netty", artifactId "netty-all" and version
"4.1.94.Final") that must be removed and the root build updated: delete the
committed build artifact file, then update the Netty dependency in the root
pom.xml to at least version "4.1.130.Final" (replace any occurrences of
"io.netty:netty-all:4.1.94.Final" or similar entries) so all listed CVEs are
patched.

In `@pom.xml`:
- Around line 170-179: The exec-maven-plugin configuration is passing
--enable-preview as a program argument (inside <arguments>) to
com.redis.server.NettyRedisServer instead of as a JVM option; move the flag into
the plugin's <configuration><jvmArgs> section (e.g., add
<jvmArgs><jvmArg>--enable-preview</jvmArg></jvmArgs>) so the JVM runs with
preview enabled while keeping <arguments> for program args and preserving the
existing <mainClass> value.

In `@README.md`:
- Around line 3-5: The README contains a Build Status badge whose link target is
a placeholder "#" — locate the markdown badge line for the Build Status (the
line with "[![Build
Status](https://img.shields.io/badge/Build-Maven-green.svg)](#)") and either
replace the "#" with the real CI build URL (e.g., your GitHub Actions or other
CI badge link) or remove the entire badge line until a valid CI URL is
available; ensure the README ends up with no placeholder links.

In `@run_all_tests.sh`:
- Around line 7-9: The script currently hard-codes
PROJECT_DIR="/home/unik/Coding/backend/redis-java" and then cds to
"$PROJECT_DIR", which breaks portability; change PROJECT_DIR to be derived from
the script location or overrideable via an environment variable (e.g., set
PROJECT_DIR=${PROJECT_DIR:-<derived path using script dir>} ), then keep the
existing cd "$PROJECT_DIR" so callers can either export PROJECT_DIR or rely on
the script-located path; update references to PROJECT_DIR in the script
accordingly.

In `@src/main/java/com/redis/commands/BLPopCommand.java`:
- Around line 87-92: The BLPopCommand currently treats a timeout of 0 as an
immediate no-result (see the timeout handling branch in BLPopCommand), which
deviates from Redis semantics where timeout 0 blocks indefinitely; either
implement true indefinite blocking by replacing the immediate-break behavior
with an asynchronous wait (e.g., register a listener/CompletableFuture or use
the server's list-notify mechanism to resume the command when an element is
pushed) in the BLPopCommand execution flow, or explicitly document the
limitation by updating BLPopCommand's javadoc and the public compatibility
notes/README to state that timeout==0 returns nil instead of blocking
indefinitely; locate the timeout check in BLPopCommand and either (A) wire into
the server's async notification/wait facility and complete the blocking future
on push, or (B) add a clear comment and public documentation change describing
the non-standard behavior.
- Around line 64-93: The BLPopCommand currently uses Thread.sleep() inside its
blocking loop which will block Netty's event loop; replace the blocking loop in
BLPopCommand (the code using Thread.sleep and POLL_INTERVAL_MS) with an
asynchronous scheduled retry using the ChannelHandlerContext executor: extract
the check logic that iterates keys and calls tryPopFromKey and formatResult into
a helper (e.g., scheduleCheck) and use ctx.executor().schedule(...,
POLL_INTERVAL_MS, TimeUnit.MILLISECONDS) to re-schedule the checks until
deadline or a result is found, writing RESP_NIL or the formatted result via
ctx.writeAndFlush; remove any Thread.sleep/InterruptedException handling so the
event loop is never blocked.
- Around line 131-135: The BLPopCommand currently treats wrongType.get() as a
signal to skip keys and return null; instead, when any key is a non-list you
must immediately return a WRONGTYPE error to the client. Update the logic in
BLPopCommand where wrongType.get() is checked so that it does not return null or
continue scanning, but triggers the existing error response path (e.g., throw or
call the server's WRONGTYPE error helper / sendWrongTypeError) as soon as
wrongType.get() is true, ensuring BLPOP replies immediately with a WRONGTYPE
error for the first non-list key encountered.

In `@src/main/java/com/redis/commands/EchoCommand.java`:
- Around line 10-14: The RESP bulk string length is computed using Java char
count which breaks for multibyte UTF-8; in EchoCommand replace the use of
msg.length() with the actual byte length (e.g.
msg.getBytes(StandardCharsets.UTF_8).length) when building the
"$<len>\r\n<msg>\r\n" response, and add the necessary import for
StandardCharsets if missing.

In `@src/main/java/com/redis/commands/PingCommand.java`:
- Around line 11-18: The execute method in PingCommand allows more than one
argument and incorrectly returns RESP_PONG; add explicit argument count
validation in execute: if args != null and args.size() > 1 return the Redis
error string for wrong argument count (e.g. "-ERR wrong number of arguments for
'ping' command\r\n"); keep the existing branch for exactly one argument (echo as
bulk string) and the fallback return of RESP_PONG for zero arguments. Ensure you
reference the execute method and RESP_PONG constant when making this change.

In `@src/main/java/com/redis/commands/TtlCommand.java`:
- Around line 18-20: The execute method in TtlCommand currently only checks for
empty args and ignores extra arguments; update the arity validation in
com.redis.commands.TtlCommand.execute to require exactly one argument
(args.size() != 1) and return ERR_WRONG_ARGS when the count is not one, so extra
arguments are rejected per Redis spec; keep the rest of the method logic and
ChannelHandlerContext usage unchanged.

In `@src/main/java/com/redis/server/RedisCommandHandler.java`:
- Around line 63-105: parseRespArray currently swallows protocol errors by
returning false, which makes the decoder re-read the same bad byte and stall;
change it so only incomplete data (readInteger == INCOMPLETE or
buf.readableBytes checks) returns false, but any malformed frame conditions
(e.g., missing '*' marker check after buf.readByte(), unexpected marker != '$',
negative lengths that are not the defined RESP nil semantics, or inconsistent
readableBytes < strLen+2) throw a protocol exception (e.g.,
CorruptedFrameException or a custom ProtocolException) instead of returning
false; also remove or narrow the broad try/catch so exceptions propagate to
exceptionCaught for proper channel close. Ensure references: parseRespArray,
readInteger, INCOMPLETE, and exceptionCaught when making the change.

In `@src/test/java/com/redis/commands/ExpireTtlCommandTest.java`:
- Around line 38-47: Replace the fixed Thread.sleep(3000) in
ExpireTtlCommandTest with a polling loop that repeatedly calls
ttl.execute(List.of(key), ctx) until it returns ":-2\r\n" or a short timeout
elapses (e.g., 5 seconds); implement the loop using System.currentTimeMillis()
to check elapsed time and Thread.sleep with a small interval (e.g., 50–100ms)
between polls, then assert that the final ttl.execute result equals ":-2\r\n" to
fail the test if the timeout is reached.

In `@src/test/java/com/redis/commands/LLenCommandTest.java`:
- Around line 77-96: The test testConcurrentSafePreserveTTLWithLPush uses fixed
Thread.sleep calls causing flakiness; replace them with bounded polling loops
that repeatedly check db.getValue("ttl-list") with short sleeps (e.g., 50–100ms)
and a hard timeout instead of the fixed Thread.sleep durations. Specifically,
after calling LPushCommand.execute(...) assert the key still exists by looping
until either db.getValue("ttl-list") != null or a 1.5s timeout elapses; then for
the post-expiry check loop until db.getValue("ttl-list") == null or a 3s timeout
elapses. Keep references to db.put(..., 2000), LPushCommand, and
db.getValue("ttl-list") so the replacement logic is applied in the same test
method.

In `@src/test/java/com/redis/server/PipeliningTest.java`:
- Around line 25-29: The test reads outbound ByteBufs (response1 and response2)
from channel.readOutbound() but never releases them; update the test to release
each ByteBuf after asserting (use response1.release()/response2.release() or
ReferenceCountUtil.release(...)) and/or call channel.finishAndReleaseAll() or
channel.close() in the test teardown to ensure EmbeddedChannel cleans up
resources; locate the usages of response1, response2, and channel.readOutbound()
in PipeliningTest and add the releases/final cleanup accordingly.
🧹 Nitpick comments (11)
build/MANIFEST.MF (1)

1-3: Build artifact should not be tracked in version control.

The build/ directory appears to be a build output directory. This MANIFEST.MF file will be auto-generated by the Maven Shade plugin (configured in pom.xml with ManifestResourceTransformer). Tracking build artifacts in version control can cause merge conflicts and inconsistencies.

Consider adding build/ to your .gitignore and removing these files from the repository.

README.md (1)

22-47: Add blank lines around tables for proper Markdown rendering.

Tables should be surrounded by blank lines per Markdown best practices. This ensures consistent rendering across different Markdown parsers.

Suggested fix (example for first table)
 ### 🔑 Connection & Utility
+
 | Command | Usage | Description |
 |:---|:---|:---|
 | `PING` | `PING [message]` | Returns `PONG` or the provided message. |
 | `ECHO` | `ECHO message` | Returns the provided message. |
 | `EXPIRE` | `EXPIRE key seconds` | Set a timeout on a key. |
 | `TTL` | `TTL key` | Get the time to live for a key in seconds. |
+
 ### 📝 Key-Value Operations

Apply similar changes around lines 33 and 40.

src/test/java/com/redis/storage/ExpiryBugTest.java (1)

6-55: Test isolation concern with singleton usage.

Both tests use RedisDatabase.getInstance() which returns a shared singleton. This can cause test pollution if tests run in parallel or if state leaks between tests. Consider adding a @BeforeEach cleanup method to delete the test keys, or use unique key prefixes per test run.

Also, consider adding @Timeout annotations to prevent tests from hanging indefinitely if the sleep-based assertions fail.

♻️ Suggested improvement for test isolation
 public class ExpiryBugTest {
 
+    `@org.junit.jupiter.api.BeforeEach`
+    public void cleanup() {
+        RedisDatabase db = RedisDatabase.getInstance();
+        db.remove("bugKey");
+        db.remove("bugKeyDelete");
+    }
+
     `@Test`
+    `@org.junit.jupiter.api.Timeout`(5)
     public void testReAddWithoutTTLAfterTTL() throws InterruptedException {
src/main/java/com/redis/commands/ExpireCommand.java (2)

8-13: Docstring mentions unimplemented options.

The docstring references [NX | XX | GT | LT] options that aren't implemented. Either remove these from the docstring or add a note that they're not supported in this implementation to avoid confusion.

📝 Suggested docstring fix
 /**
- * EXPIRE key seconds [NX | XX | GT | LT]
+ * EXPIRE key seconds
  * Set a timeout on key. After the timeout has expired, the key will automatically be deleted.
- * 
- * Simple implementation supporting seconds.
+ *
+ * Simple implementation supporting seconds only. NX/XX/GT/LT options are not implemented.
  */

19-28: Missing upper bound validation for argument count.

The validation args.size() < 2 catches too few arguments, but extra arguments are silently ignored. Consider strict validation to match Redis behavior, which returns an error for wrong argument count.

♻️ Suggested fix
     `@Override`
     public String execute(List<String> args, ChannelHandlerContext ctx) {
-        if (args.size() < 2) return ERR_WRONG_ARGS;
+        if (args.size() != 2) return ERR_WRONG_ARGS;
 
         String key = args.get(0);
src/main/test/integration_tests.sh (2)

19-19: Unquoted array assignment can cause issues with special characters.

The word splitting on $cmd can produce unexpected results if arguments contain spaces or glob characters. Use read -a for safer parsing.

♻️ Suggested fix
-    local args=($cmd)
+    local args
+    read -ra args <<< "$cmd"

1-5: Non-standard test directory location.

The file is located at src/main/test/ but Maven convention places tests under src/test/. This may cause the script to be excluded from standard test discovery or included in production artifacts.

src/main/java/com/redis/commands/LPopCommand.java (1)

71-72: Minor inconsistency: Consider using asList() for type-safe access.

After verifying the type is LIST, you cast via getData() directly. While this works, using existing.asList() would be more consistent with how LLenCommand accesses the list and provides built-in type safety.

♻️ Suggested change
-            `@SuppressWarnings`("unchecked")
-            List<String> existingList = (List<String>) existing.getData();
+            List<String> existingList = existing.asList();
src/test/java/com/redis/commands/LPopCommandTest.java (1)

6-29: Prefer MockitoExtension to manage mock lifecycle.
MockitoAnnotations.openMocks should be closed; using the JUnit 5 extension avoids leaks and boilerplate.

♻️ Proposed refactor
+import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.jupiter.MockitoExtension;

+@ExtendWith(MockitoExtension.class)
 class LPopCommandTest {

@@
     `@BeforeEach`
     void setUp() {
-        MockitoAnnotations.openMocks(this);
         command = new LPopCommand();
         db = RedisDatabase.getInstance();
     }
src/test/java/com/redis/commands/BLPopCommandTest.java (2)

69-79: Time-based assertions can be flaky under CI jitter.
Consider widening bounds to reduce intermittent failures.

🧪 Suggested tweak
-        assertTrue(elapsed >= 80 && elapsed < 500, "Should wait approximately 100ms");
+        assertTrue(elapsed >= 50 && elapsed < 1500, "Should wait approximately 100ms");
@@
-        assertTrue(elapsed >= 150 && elapsed < 500);
+        assertTrue(elapsed >= 150 && elapsed < 1500);

Also applies to: 186-194


196-218: Ensure executor is always shut down (even on assertion failure).
Use a try/finally to avoid thread leaks in failing tests.

♻️ Suggested refactor
-        ExecutorService executor = Executors.newSingleThreadExecutor();
-        Future<String> future = executor.submit(() ->
-            command.execute(List.of("blocktest", "2"), ctx)
-        );
-
-        // Wait a bit, then push data
-        Thread.sleep(100);
-        LPushCommand lpush = new LPushCommand();
-        lpush.execute(List.of("blocktest", "arrived"), ctx);
-
-        // BLPOP should return with the data
-        String result = future.get(2, TimeUnit.SECONDS);
-        assertTrue(result.contains("blocktest"));
-        assertTrue(result.contains("arrived"));
-
-        executor.shutdown();
-        db.remove("blocktest");
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        try {
+            Future<String> future = executor.submit(() ->
+                command.execute(List.of("blocktest", "2"), ctx)
+            );
+
+            // Wait a bit, then push data
+            Thread.sleep(100);
+            LPushCommand lpush = new LPushCommand();
+            lpush.execute(List.of("blocktest", "arrived"), ctx);
+
+            // BLPOP should return with the data
+            String result = future.get(2, TimeUnit.SECONDS);
+            assertTrue(result.contains("blocktest"));
+            assertTrue(result.contains("arrived"));
+        } finally {
+            executor.shutdownNow();
+            db.remove("blocktest");
+        }

Comment thread .gitignore
Comment on lines +9 to +10
# Maven
target/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add build/ directory to .gitignore.

The build/ directory containing compiled classes and Maven metadata is missing from the ignore patterns. This PR includes committed build artifacts under build/classes/ that should be excluded.

Proposed fix
 # Maven
 target/
+build/
 pom.xml.tag
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Maven
target/
# Maven
target/
build/
🤖 Prompt for AI Agents
In @.gitignore around lines 9 - 10, Add the missing ignore for the build/
directory to .gitignore and remove the currently committed build artifacts from
the repo index so they stop being tracked; specifically, add the pattern
"build/" to .gitignore, run a cached removal of the tracked build files (e.g.,
remove build/classes/ and related build outputs from the index), and commit both
the updated .gitignore and the removal so future builds are untracked. Ensure
you verify the repo no longer lists build/ files as changes after committing.

Comment thread build/classes/application.properties Outdated
Comment on lines +1 to +19
# Redis Java Server Configuration
# Default values are provided in RedisConfig class

# Server port
redis.port=6379

# Netty Boss Group thread count (accepts connections)
redis.boss.threads=1

# Netty Worker Group thread count (handles I/O)
# Single-threaded model: 1 thread ensures sequential command processing like real Redis
redis.worker.threads=1

# Cleanup interval in milliseconds (used for periodic safety checks)
# Note: Main expiry is DelayQueue-based (zero-polling), this is just a backup
redis.cleanup.interval.ms=5000

# Enable expiry management for TTL keys
redis.expiry.enabled=true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for the source application.properties file and .gitignore configuration

# Check if source file exists
echo "=== Checking for source file ==="
fd -t f "application.properties" src/main/resources/ 2>&1

# Check .gitignore for build directory exclusions
echo -e "\n=== Checking .gitignore for build directory exclusions ==="
if [ -f .gitignore ]; then
  echo "Searching for 'build/' in .gitignore:"
  rg -n "build/" .gitignore || echo "Warning: 'build/' not found in .gitignore"
  echo -e "\nSearching for 'target/' in .gitignore:"
  rg -n "target/" .gitignore || echo "Warning: 'target/' not found in .gitignore"
else
  echo "Warning: .gitignore file not found"
fi

# List all application.properties files in the repository
echo -e "\n=== All application.properties files in repository ==="
fd -t f "application.properties"

# Check if build directory exists
echo -e "\n=== Checking build directory ==="
ls -la build/ 2>&1 | head -20

Repository: unikdahal/redis-java

Length of output: 723


Remove build artifact from version control and update .gitignore.

The file build/classes/application.properties should not be committed. This is a build output directory containing compiled artifacts. The correct source file already exists at src/main/resources/application.properties.

Actions required:

  1. Remove build/classes/application.properties from version control
  2. Add build/ to .gitignore (currently only target/ is excluded)
🤖 Prompt for AI Agents
In `@build/classes/application.properties` around lines 1 - 19, Remove the
committed build artifact application.properties from version control (the
compiled file in your build output) by running git rm --cached on that file and
commit the removal, and update your .gitignore to include the build/ directory
(add a line "build/") so future build outputs are ignored; ensure the canonical
source application.properties in src/main/resources remains tracked.

Comment thread build/classes/META-INF/INDEX.LIST Outdated
Comment on lines +1 to +8
JarIndex-Version: 1.0

netty-all-4.1.94.Final.jar
META-INF
META-INF/maven
META-INF/maven/io.netty
META-INF/maven/io.netty/netty-all

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Build artifact with version mismatch should be removed.

This file references netty-all-4.1.94.Final.jar but the pom.xml specifies version 4.1.118.Final. Additionally, the pom.xml now uses modular Netty dependencies instead of netty-all.

This build/classes/ directory contains build output that should not be tracked in version control. Remove this directory and add it to .gitignore.

🤖 Prompt for AI Agents
In `@build/classes/META-INF/INDEX.LIST` around lines 1 - 8, Remove the tracked
build output and the stale jar index referencing netty-all-4.1.94.Final.jar by
deleting the build/classes/ directory (which contains META-INF/INDEX.LIST) from
the repo and committing that deletion; then update .gitignore to include
build/classes/ (and any other build output patterns) so generated artifacts
aren’t re-added, and verify pom.xml's modular Netty dependencies (not netty-all)
remain as the source of truth.

Comment on lines +1 to +9
#Generated by netty-parent/pom.xml
#Tue, 20 Jun 2023 15:35:15 +0200

netty-all.version=4.1.94.Final
netty-all.buildDate=2023-06-20 15\:35\:15 +0200
netty-all.commitDate=2023-06-20 12\:54\:19 +0000
netty-all.shortCommitHash=da1a448d5b
netty-all.longCommitHash=da1a448d5bc4f36cc1744db93fcaf64e198db2bd
netty-all.repoStatus=clean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Build artifact should not be committed.

This Netty-generated metadata file is a build artifact. Remove it along with the other files under build/classes/ and ensure the build/ directory is added to .gitignore.

🤖 Prompt for AI Agents
In `@build/classes/META-INF/io.netty.versions.properties` around lines 1 - 9, The
checked-in Netty metadata file io.netty.versions.properties under build/classes/
is a generated build artifact and should be removed from the repo; delete
io.netty.versions.properties and any other files under build/classes/, add the
build/ directory (or at minimum build/classes/) to .gitignore so generated files
are not tracked, and commit a removal commit that updates the repository state
accordingly.

Comment thread build/classes/META-INF/MANIFEST.MF Outdated
Comment on lines +1 to +27
Manifest-Version: 1.0
Implementation-Title: Netty/All-in-One
Bundle-Description: Netty is an asynchronous event-driven network appl
ication framework for rapid development of maintainable high perfo
rmance protocol servers and clients.
Automatic-Module-Name: io.netty.all
Bundle-License: https://www.apache.org/licenses/LICENSE-2.0
Bundle-SymbolicName: io.netty.all
Implementation-Version: 4.1.94.Final
Built-By: norman
Bnd-LastModified: 1687268117627
Bundle-ManifestVersion: 2
Implementation-Vendor-Id: io.netty
Bundle-DocURL: https://netty.io/
Bundle-Vendor: The Netty Project
Import-Package: sun.nio.ch;resolution:=optional,org.eclipse.jetty.npn;
version="[1,2)";resolution:=optional,org.eclipse.jetty.alpn;version="
[1,2)";resolution:=optional
Tool: Bnd-6.3.1.202206071316
Implementation-Vendor: The Netty Project
Bundle-Name: Netty/All-in-One
Bundle-Version: 4.1.94.Final
Build-Jdk-Spec: 1.8
Created-By: Apache Maven Bundle Plugin 5.1.8
Build-Jdk: 1.8.0_252
Implementation-URL: https://netty.io/netty-all/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stale Netty build artifact should be removed.

This manifest is from Netty 4.1.94.Final (built with JDK 1.8), but the project now uses Netty 4.1.118.Final with Java 25. This appears to be an extracted dependency artifact that was accidentally committed.

Remove this file along with the entire build/ directory from version control.

🤖 Prompt for AI Agents
In `@build/classes/META-INF/MANIFEST.MF` around lines 1 - 27, The committed
MANIFEST.MF contains stale Netty artifact metadata (e.g.,
Implementation-Version: 4.1.94.Final, Build-Jdk: 1.8.0_252, Bundle-Name:
Netty/All-in-One) and should be removed: delete this MANIFEST.MF and the entire
build/ directory from version control, add an entry to ignore build/ in
.gitignore (or similar VCS ignore) to prevent re-committing, and commit the
removal with a brief message like “remove stale Netty build artifacts.”

Comment on lines +18 to +20
public String execute(List<String> args, ChannelHandlerContext ctx) {
if (args.isEmpty()) return ERR_WRONG_ARGS;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix TTL arity validation (extra args currently ignored).
Redis expects exactly one argument; accepting more silently deviates from spec and can mask client errors.

🐛 Proposed fix
-        if (args.isEmpty()) return ERR_WRONG_ARGS;
+        if (args.size() != 1) return ERR_WRONG_ARGS;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public String execute(List<String> args, ChannelHandlerContext ctx) {
if (args.isEmpty()) return ERR_WRONG_ARGS;
public String execute(List<String> args, ChannelHandlerContext ctx) {
if (args.size() != 1) return ERR_WRONG_ARGS;
🤖 Prompt for AI Agents
In `@src/main/java/com/redis/commands/TtlCommand.java` around lines 18 - 20, The
execute method in TtlCommand currently only checks for empty args and ignores
extra arguments; update the arity validation in
com.redis.commands.TtlCommand.execute to require exactly one argument
(args.size() != 1) and return ERR_WRONG_ARGS when the count is not one, so extra
arguments are rejected per Redis spec; keep the rest of the method logic and
ChannelHandlerContext usage unchanged.

Comment on lines +63 to 105
private boolean parseRespArray(ByteBuf buf, List<String> result) {
try {
if (buf.readableBytes() < 1) return false;

// Check for Array Start (*)
if (buf.readByte() != '*') {
return result;
return false;
}

// Read Array Length efficiently (no object allocation)
// Read Array Length
int numArgs = readInteger(buf);
if (numArgs <= 0) {
return result;
}

if (numArgs == INCOMPLETE) return false;
if (numArgs < 0) return true; // Nil array (*) or empty

// Read All Arguments
for (int i = 0; i < numArgs; i++) {
if (buf.readableBytes() < 1) return false;

// Check for Bulk String Start ($)
byte marker = buf.readByte();
if (marker != '$') {
break; // Malformed
}
if (marker != '$') return false;

// Read String Length efficiently
// Read String Length
int strLen = readInteger(buf);
if (strLen == INCOMPLETE) return false;
if (strLen < 0) {
break; // Nil bulk string
result.add(null);
continue;
}

// Read the actual String data with charset conversion
if (buf.readableBytes() < strLen + 2) return false;

// Read the actual String data
CharSequence arg = buf.readCharSequence(strLen, StandardCharsets.UTF_8);
result.add(arg.toString());

// Skip trailing \r\n
buf.skipBytes(2);
}
return true;
} catch (Exception e) {
// Log parse error but don't crash
result.clear();
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Malformed RESP frames currently stall the connection indefinitely.
parseRespArray returns false for malformed input, causing the decoder to reset and wait forever on the same bad byte. Consider throwing on malformed frames so the channel closes via exceptionCaught.

🐛 Suggested fix to surface protocol errors
-    private boolean parseRespArray(ByteBuf buf, List<String> result) {
-        try {
-            if (buf.readableBytes() < 1) return false;
-            
-            // Check for Array Start (*)
-            if (buf.readByte() != '*') {
-                return false;
-            }
+    private boolean parseRespArray(ByteBuf buf, List<String> result) {
+        if (buf.readableBytes() < 1) return false;
+
+        // Check for Array Start (*)
+        byte first = buf.readByte();
+        if (first != '*') {
+            throw new IllegalArgumentException("Malformed RESP: expected array");
+        }

@@
-                byte marker = buf.readByte();
-                if (marker != '$') return false;
+                byte marker = buf.readByte();
+                if (marker != '$') {
+                    throw new IllegalArgumentException("Malformed RESP: expected bulk string");
+                }

@@
-            return true;
-        } catch (Exception e) {
-            return false;
-        }
+        return true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private boolean parseRespArray(ByteBuf buf, List<String> result) {
try {
if (buf.readableBytes() < 1) return false;
// Check for Array Start (*)
if (buf.readByte() != '*') {
return result;
return false;
}
// Read Array Length efficiently (no object allocation)
// Read Array Length
int numArgs = readInteger(buf);
if (numArgs <= 0) {
return result;
}
if (numArgs == INCOMPLETE) return false;
if (numArgs < 0) return true; // Nil array (*) or empty
// Read All Arguments
for (int i = 0; i < numArgs; i++) {
if (buf.readableBytes() < 1) return false;
// Check for Bulk String Start ($)
byte marker = buf.readByte();
if (marker != '$') {
break; // Malformed
}
if (marker != '$') return false;
// Read String Length efficiently
// Read String Length
int strLen = readInteger(buf);
if (strLen == INCOMPLETE) return false;
if (strLen < 0) {
break; // Nil bulk string
result.add(null);
continue;
}
// Read the actual String data with charset conversion
if (buf.readableBytes() < strLen + 2) return false;
// Read the actual String data
CharSequence arg = buf.readCharSequence(strLen, StandardCharsets.UTF_8);
result.add(arg.toString());
// Skip trailing \r\n
buf.skipBytes(2);
}
return true;
} catch (Exception e) {
// Log parse error but don't crash
result.clear();
return false;
}
private boolean parseRespArray(ByteBuf buf, List<String> result) {
if (buf.readableBytes() < 1) return false;
// Check for Array Start (*)
byte first = buf.readByte();
if (first != '*') {
throw new IllegalArgumentException("Malformed RESP: expected array");
}
// Read Array Length
int numArgs = readInteger(buf);
if (numArgs == INCOMPLETE) return false;
if (numArgs < 0) return true; // Nil array (*) or empty
// Read All Arguments
for (int i = 0; i < numArgs; i++) {
if (buf.readableBytes() < 1) return false;
// Check for Bulk String Start ($)
byte marker = buf.readByte();
if (marker != '$') {
throw new IllegalArgumentException("Malformed RESP: expected bulk string");
}
// Read String Length
int strLen = readInteger(buf);
if (strLen == INCOMPLETE) return false;
if (strLen < 0) {
result.add(null);
continue;
}
if (buf.readableBytes() < strLen + 2) return false;
// Read the actual String data
CharSequence arg = buf.readCharSequence(strLen, StandardCharsets.UTF_8);
result.add(arg.toString());
// Skip trailing \r\n
buf.skipBytes(2);
}
return true;
}
🤖 Prompt for AI Agents
In `@src/main/java/com/redis/server/RedisCommandHandler.java` around lines 63 -
105, parseRespArray currently swallows protocol errors by returning false, which
makes the decoder re-read the same bad byte and stall; change it so only
incomplete data (readInteger == INCOMPLETE or buf.readableBytes checks) returns
false, but any malformed frame conditions (e.g., missing '*' marker check after
buf.readByte(), unexpected marker != '$', negative lengths that are not the
defined RESP nil semantics, or inconsistent readableBytes < strLen+2) throw a
protocol exception (e.g., CorruptedFrameException or a custom ProtocolException)
instead of returning false; also remove or narrow the broad try/catch so
exceptions propagate to exceptionCaught for proper channel close. Ensure
references: parseRespArray, readInteger, INCOMPLETE, and exceptionCaught when
making the change.

Comment on lines +38 to +47
// TTL should be around 2
String ttlResult = ttl.execute(List.of(key), ctx);
assertTrue(ttlResult.equals(":1\r\n") || ttlResult.equals(":2\r\n"));

// Wait 3 seconds
Thread.sleep(3000);

// TTL should be -2 (expired)
assertEquals(":-2\r\n", ttl.execute(List.of(key), ctx));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid fixed sleeps for expiry timing.

The fixed 3s sleep can be flaky if the expiry worker lags. Prefer polling until TTL reports -2 with a timeout.

✅ Suggested polling approach
-        // Wait 3 seconds
-        Thread.sleep(3000);
-
-        // TTL should be -2 (expired)
-        assertEquals(":-2\r\n", ttl.execute(List.of(key), ctx));
+        // Wait until TTL reports expired (max 5s)
+        long deadline = System.currentTimeMillis() + 5000;
+        String ttlAfter;
+        do {
+            Thread.sleep(100);
+            ttlAfter = ttl.execute(List.of(key), ctx);
+        } while (!":-2\r\n".equals(ttlAfter) && System.currentTimeMillis() < deadline);
+        assertEquals(":-2\r\n", ttlAfter);
🤖 Prompt for AI Agents
In `@src/test/java/com/redis/commands/ExpireTtlCommandTest.java` around lines 38 -
47, Replace the fixed Thread.sleep(3000) in ExpireTtlCommandTest with a polling
loop that repeatedly calls ttl.execute(List.of(key), ctx) until it returns
":-2\r\n" or a short timeout elapses (e.g., 5 seconds); implement the loop using
System.currentTimeMillis() to check elapsed time and Thread.sleep with a small
interval (e.g., 50–100ms) between polls, then assert that the final ttl.execute
result equals ":-2\r\n" to fail the test if the timeout is reached.

Comment on lines +77 to +96
@Test
void testConcurrentSafePreserveTTLWithLPush() throws InterruptedException {
// Set a list with TTL and perform LPUSH (uses compute) and ensure TTL preserved
db.remove("ttl-list");
LinkedList<String> list = new LinkedList<>(List.of("x"));
db.put("ttl-list", RedisValue.list(list), 2000); // 2 seconds

// perform LPUSH via command which uses compute
LPushCommand lpush = new LPushCommand();
String result = lpush.execute(List.of("ttl-list", "y"), ctx);
assertTrue(result.startsWith(":"));

// Immediately check value still has TTL (can't access expiry directly, but expiry manager will remove after TTL)
// Sleep slightly less than TTL and ensure key still exists
Thread.sleep(1000);
assertNotNull(db.getValue("ttl-list"));

// Sleep until after TTL
Thread.sleep(1500);
assertNull(db.getValue("ttl-list"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reduce test flakiness from fixed sleeps.

This test can intermittently fail on slow CI due to timing jitter around the 2s TTL. Consider polling with a timeout or increasing TTL and using a bounded wait loop.

✅ More reliable wait loop
-        db.put("ttl-list", RedisValue.list(list), 2000); // 2 seconds
+        db.put("ttl-list", RedisValue.list(list), 5000); // 5 seconds

         // perform LPUSH via command which uses compute
         LPushCommand lpush = new LPushCommand();
         String result = lpush.execute(List.of("ttl-list", "y"), ctx);
         assertTrue(result.startsWith(":"));

         // Immediately check value still has TTL (can't access expiry directly, but expiry manager will remove after TTL)
         // Sleep slightly less than TTL and ensure key still exists
-        Thread.sleep(1000);
+        Thread.sleep(1000);
         assertNotNull(db.getValue("ttl-list"));

-        // Sleep until after TTL
-        Thread.sleep(1500);
-        assertNull(db.getValue("ttl-list"));
+        // Wait until after TTL (poll with timeout)
+        long deadline = System.currentTimeMillis() + 6000;
+        while (db.getValue("ttl-list") != null && System.currentTimeMillis() < deadline) {
+            Thread.sleep(100);
+        }
+        assertNull(db.getValue("ttl-list"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void testConcurrentSafePreserveTTLWithLPush() throws InterruptedException {
// Set a list with TTL and perform LPUSH (uses compute) and ensure TTL preserved
db.remove("ttl-list");
LinkedList<String> list = new LinkedList<>(List.of("x"));
db.put("ttl-list", RedisValue.list(list), 2000); // 2 seconds
// perform LPUSH via command which uses compute
LPushCommand lpush = new LPushCommand();
String result = lpush.execute(List.of("ttl-list", "y"), ctx);
assertTrue(result.startsWith(":"));
// Immediately check value still has TTL (can't access expiry directly, but expiry manager will remove after TTL)
// Sleep slightly less than TTL and ensure key still exists
Thread.sleep(1000);
assertNotNull(db.getValue("ttl-list"));
// Sleep until after TTL
Thread.sleep(1500);
assertNull(db.getValue("ttl-list"));
`@Test`
void testConcurrentSafePreserveTTLWithLPush() throws InterruptedException {
// Set a list with TTL and perform LPUSH (uses compute) and ensure TTL preserved
db.remove("ttl-list");
LinkedList<String> list = new LinkedList<>(List.of("x"));
db.put("ttl-list", RedisValue.list(list), 5000); // 5 seconds
// perform LPUSH via command which uses compute
LPushCommand lpush = new LPushCommand();
String result = lpush.execute(List.of("ttl-list", "y"), ctx);
assertTrue(result.startsWith(":"));
// Immediately check value still has TTL (can't access expiry directly, but expiry manager will remove after TTL)
// Sleep slightly less than TTL and ensure key still exists
Thread.sleep(1000);
assertNotNull(db.getValue("ttl-list"));
// Wait until after TTL (poll with timeout)
long deadline = System.currentTimeMillis() + 6000;
while (db.getValue("ttl-list") != null && System.currentTimeMillis() < deadline) {
Thread.sleep(100);
}
assertNull(db.getValue("ttl-list"));
🤖 Prompt for AI Agents
In `@src/test/java/com/redis/commands/LLenCommandTest.java` around lines 77 - 96,
The test testConcurrentSafePreserveTTLWithLPush uses fixed Thread.sleep calls
causing flakiness; replace them with bounded polling loops that repeatedly check
db.getValue("ttl-list") with short sleeps (e.g., 50–100ms) and a hard timeout
instead of the fixed Thread.sleep durations. Specifically, after calling
LPushCommand.execute(...) assert the key still exists by looping until either
db.getValue("ttl-list") != null or a 1.5s timeout elapses; then for the
post-expiry check loop until db.getValue("ttl-list") == null or a 3s timeout
elapses. Keep references to db.put(..., 2000), LPushCommand, and
db.getValue("ttl-list") so the replacement logic is applied in the same test
method.

Comment on lines +25 to +29
ByteBuf response1 = channel.readOutbound();
ByteBuf response2 = channel.readOutbound();

assertEquals("+PONG\r\n", response1.toString(StandardCharsets.UTF_8));
assertEquals("+PONG\r\n", response2.toString(StandardCharsets.UTF_8), "Second pipelined command should be processed");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "PipeliningTest.java" -type f

Repository: unikdahal/redis-java

Length of output: 116


🏁 Script executed:

head -60 src/test/java/com/redis/server/PipeliningTest.java

Repository: unikdahal/redis-java

Length of output: 1848


🏁 Script executed:

wc -l src/test/java/com/redis/server/PipeliningTest.java

Repository: unikdahal/redis-java

Length of output: 117


🏁 Script executed:

find src/test -name "*.java" -type f | head -20

Repository: unikdahal/redis-java

Length of output: 875


🏁 Script executed:

rg "EmbeddedChannel" src/test --type java -A 5 -B 2

Repository: unikdahal/redis-java

Length of output: 2218


🏁 Script executed:

rg "ReferenceCountUtil|release\(" src/test --type java | head -20

Repository: unikdahal/redis-java

Length of output: 46


🏁 Script executed:

cat pom.xml | grep -A 5 -B 5 "netty\|leak" | head -40

Repository: unikdahal/redis-java

Length of output: 1399


🏁 Script executed:

rg "javaagent|ByteBufUtil|leak" src --type java

Repository: unikdahal/redis-java

Length of output: 46


🏁 Script executed:

find . -name "*.xml" -o -name "*.properties" | xargs grep -l "leak" 2>/dev/null | head -5

Repository: unikdahal/redis-java

Length of output: 46


🏁 Script executed:

find . -name "*.xml" -o -name "*.gradle" -o -name "*.properties" | xargs grep -l "io.netty.leakDetection" 2>/dev/null

Repository: unikdahal/redis-java

Length of output: 48


🏁 Script executed:

rg "finalize|close\(" src/test --type java -B 2 -A 2 | head -40

Repository: unikdahal/redis-java

Length of output: 46


🏁 Script executed:

python3 << 'EOF'
# Check Netty EmbeddedChannel documentation understanding
# Based on Netty 4.1.118.Final behavior

print("Netty EmbeddedChannel behavior:")
print("- EmbeddedChannel is a test utility for testing ChannelHandler")
print("- readOutbound() returns the next outbound message without removing it from the queue initially")
print("- ByteBuf objects returned have reference count >= 1")
print("- EmbeddedChannel.finishAndReleaseAll() releases all pending outbound messages")
print("- Individual ByteBuf objects should be released if not consuming them")
print("")
print("Leak detection:")
print("- Netty has ResourceLeakDetector which detects leaked ByteBuf instances")
print("- Leak detection is disabled by default in tests")
print("- Can be enabled via: -Dio.netty.leakDetection.level=paranoid")
print("- Tests should follow RAII pattern or explicitly release ByteBuf")
EOF

Repository: unikdahal/redis-java

Length of output: 150


🏁 Script executed:

grep -r "finishAndReleaseAll" src --include="*.java"

Repository: unikdahal/redis-java

Length of output: 46


🏁 Script executed:

# Check if EmbeddedChannel is closed in the tests
rg "channel\." src/test/java/com/redis/server/PipeliningTest.java

Repository: unikdahal/redis-java

Length of output: 540


🏁 Script executed:

# Check other Netty test patterns in the codebase
find src -name "*.java" -type f -path "*/test/*" -exec grep -l "ByteBuf\|EmbeddedChannel" {} \;

Repository: unikdahal/redis-java

Length of output: 114


Release ByteBufs to avoid resource leaks in EmbeddedChannel tests.

The outbound ByteBuf objects from readOutbound() should be explicitly released. Without proper cleanup, Netty's EmbeddedChannel may hold references to these buffers, and the test methods don't invoke finishAndReleaseAll() or close() to ensure cleanup.

🧹 Release buffers
         ByteBuf response1 = channel.readOutbound();
         ByteBuf response2 = channel.readOutbound();
-
-        assertEquals("+PONG\r\n", response1.toString(StandardCharsets.UTF_8));
-        assertEquals("+PONG\r\n", response2.toString(StandardCharsets.UTF_8), "Second pipelined command should be processed");
+        try {
+            assertEquals("+PONG\r\n", response1.toString(StandardCharsets.UTF_8));
+            assertEquals("+PONG\r\n", response2.toString(StandardCharsets.UTF_8), "Second pipelined command should be processed");
+        } finally {
+            io.netty.util.ReferenceCountUtil.release(response1);
+            io.netty.util.ReferenceCountUtil.release(response2);
+        }
         ByteBuf finalResponse = channel.readOutbound();
-        assertEquals("+PONG\r\n", finalResponse.toString(StandardCharsets.UTF_8));
+        try {
+            assertEquals("+PONG\r\n", finalResponse.toString(StandardCharsets.UTF_8));
+        } finally {
+            io.netty.util.ReferenceCountUtil.release(finalResponse);
+        }
🤖 Prompt for AI Agents
In `@src/test/java/com/redis/server/PipeliningTest.java` around lines 25 - 29, The
test reads outbound ByteBufs (response1 and response2) from
channel.readOutbound() but never releases them; update the test to release each
ByteBuf after asserting (use response1.release()/response2.release() or
ReferenceCountUtil.release(...)) and/or call channel.finishAndReleaseAll() or
channel.close() in the test teardown to ensure EmbeddedChannel cleans up
resources; locate the usages of response1, response2, and channel.readOutbound()
in PipeliningTest and add the releases/final cleanup accordingly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/redis/storage/RedisDatabase.java (1)

21-21: Missing volatile on INSTANCE field breaks double-checked locking.

The double-checked locking pattern requires the singleton field to be volatile to ensure proper visibility across threads. Without it, a thread could see a partially constructed RedisDatabase instance due to instruction reordering.

Proposed fix
-    private static RedisDatabase INSTANCE;
+    private static volatile RedisDatabase INSTANCE;

Also applies to: 48-57

🤖 Fix all issues with AI agents
In `@src/main/java/com/redis/commands/BLPopCommand.java`:
- Around line 29-36: The Javadoc for execute() is currently appended to the
MAX_TIMEOUT_SECONDS declaration, so separate them by ending the
MAX_TIMEOUT_SECONDS declaration cleanly and move the multi-line Javadoc to
immediately precede the execute(...) method signature; specifically, remove the
Javadoc text from the same line as the MAX_TIMEOUT_SECONDS field, ensure
MAX_TIMEOUT_SECONDS remains a proper field (private static final long
MAX_TIMEOUT_SECONDS = 60 * 60 * 24;), and place the Javadoc block directly above
the execute(...) method to associate it with execute() instead of the field.

In `@src/main/java/com/redis/commands/PingCommand.java`:
- Around line 19-21: The RESP bulk string length is computed using Java char
count (msg.length()) which is incorrect for UTF-8; in the PingCommand code path
that builds the response (the block that checks args != null && args.size() == 1
and returns the "$...\\r\\n" string), replace the char-length call with the
UTF-8 byte length (e.g. use msg.getBytes(StandardCharsets.UTF_8).length) and
ensure StandardCharsets.UTF_8 is imported/used so multi-byte characters produce
the correct RESP length.

In `@src/main/java/com/redis/server/RedisCommandHandler.java`:
- Around line 147-157: The integer parsing in RedisCommandHandler (the block
using buf, rIndex, readerIndex, value and reading bytes b) currently ignores
non-digit characters and yields incorrect values; change the logic to validate
the entire token between the initial byte and the '\r' terminator: allow an
optional leading '-' then require every subsequent byte up to '\r' to be in
'0'..'9', and if any other byte is seen throw a protocol exception (e.g.,
ProtocolException or a Redis protocol error) instead of silently skipping;
update the initial branch that handles the first byte (b) to detect '-' and
digits and set a flag for negativity, and enforce digit-only bytes in the while
loop before converting to the integer value.

In `@src/main/test/integration_tests.sh`:
- Line 19: Replace the unquoted array assignment that risks
word-splitting/globbing: instead of using local args=($cmd) use Bash's read -ra
to parse the contents of the variable safely into the args array (e.g., read -ra
args <<< "$cmd"), ensuring the variable names cmd and args are used and quoting
the source "$cmd" so expansions are not subject to globbing or word splitting.
♻️ Duplicate comments (7)
src/main/java/com/redis/commands/PingCommand.java (1)

17-24: Missing validation for excess arguments.

Redis PING accepts at most one argument. When more than one argument is provided, Redis returns an error. Currently, multiple arguments silently fall through to return PONG.

Proposed fix
 `@Override`
 public String execute(List<String> args, ChannelHandlerContext ctx) {
     // PING [message] - if message provided, echo it as bulk string
+    if (args != null && args.size() > 1) {
+        return "-ERR wrong number of arguments for 'ping' command\r\n";
+    }
     if (args != null && args.size() == 1) {
         String msg = args.get(0);
         return "$" + msg.length() + "\r\n" + msg + "\r\n";
     }
     return RESP_PONG;
 }
src/main/java/com/redis/commands/EchoCommand.java (1)

20-21: Use byte length for RESP bulk strings.

Line 21 uses msg.length(), which counts UTF-16 code units, not bytes. Multi-byte UTF-8 input will corrupt RESP framing.

Proposed fix
+import java.nio.charset.StandardCharsets;
 
         String msg = args.get(0);
-        return "$" + msg.length() + "\r\n" + msg + "\r\n";
+        int byteLen = msg.getBytes(StandardCharsets.UTF_8).length;
+        return "$" + byteLen + "\r\n" + msg + "\r\n";
run_all_tests.sh (1)

7-8: Make the script portable (avoid hard-coded path).

Line 7 hard-codes a local path, which will fail on other machines or CI environments. Derive from the script location or allow override via environment variable.

Proposed fix
-PROJECT_DIR="/home/unik/Coding/backend/redis-java"
+PROJECT_DIR="${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
 cd "$PROJECT_DIR"
src/main/java/com/redis/server/RedisCommandHandler.java (1)

82-125: Malformed RESP frames still cause connection stalls.

The parsing logic returns false for both incomplete and malformed input (e.g., line 88, 102). When malformed data arrives, the decoder resets the reader index and waits forever for "more data" that won't fix the protocol violation. Consider throwing an exception for definite protocol errors so exceptionCaught can close the channel.

src/main/java/com/redis/commands/TtlCommand.java (1)

29-30: TTL arity validation still allows extra arguments.

The validation only rejects empty args but accepts TTL key extra_arg. Redis expects exactly one argument for TTL.

🐛 Proposed fix
-        if (args.isEmpty()) return ERR_WRONG_ARGS;
+        if (args.size() != 1) return ERR_WRONG_ARGS;
src/main/java/com/redis/commands/BLPopCommand.java (2)

70-99: Thread.sleep() still blocks Netty's event loop.

Based on learnings, command execution runs on Netty's single-threaded event loop per channel. The blocking Thread.sleep() call freezes all client processing during the BLPOP wait period. This should be replaced with async scheduling using ctx.executor().schedule().


146-150: WRONGTYPE handling deviates from Redis specification.

The comment on line 146-147 is incorrect. Per Redis documentation, BLPOP returns a WRONGTYPE error immediately upon encountering any key holding a non-list value—it does not skip to the next key. The current implementation silently ignores wrong-type keys.

🧹 Nitpick comments (3)
src/main/java/com/redis/commands/LPopCommand.java (1)

89-90: Prefer using the type-safe asList() accessor.

RedisValue provides a type-safe asList() method that throws IllegalStateException on type mismatch. Since the type is already validated on line 84, you could simplify this and avoid the unchecked cast warning.

♻️ Suggested simplification
-            `@SuppressWarnings`("unchecked")
-            List<String> existingList = (List<String>) existing.getData();
+            List<String> existingList = existing.asList();
src/main/java/com/redis/commands/LLenCommand.java (1)

9-9: Minor formatting: missing space before {.

Consider adding a space for consistency with Java style conventions.

♻️ Formatting fix
-public class LLenCommand implements ICommand{
+public class LLenCommand implements ICommand {
src/main/java/com/redis/commands/BLPopCommand.java (1)

128-129: Prefer using the type-safe asList() accessor.

Same as in LPopCommand, you can use existing.asList() instead of the unchecked cast since the type is already validated.

♻️ Suggested simplification
-            `@SuppressWarnings`("unchecked")
-            List<String> existingList = (List<String>) existing.getData();
+            List<String> existingList = existing.asList();

Comment thread src/main/java/com/redis/commands/BLPopCommand.java
Comment on lines +19 to +21
if (args != null && args.size() == 1) {
String msg = args.get(0);
return "$" + msg.length() + "\r\n" + msg + "\r\n";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use byte length for RESP bulk strings.

Line 21 uses msg.length(), which counts UTF-16 code units, not bytes. Multi-byte UTF-8 characters will produce incorrect RESP framing.

Proposed fix
+import java.nio.charset.StandardCharsets;
 
     if (args != null && args.size() == 1) {
         String msg = args.get(0);
-        return "$" + msg.length() + "\r\n" + msg + "\r\n";
+        int byteLen = msg.getBytes(StandardCharsets.UTF_8).length;
+        return "$" + byteLen + "\r\n" + msg + "\r\n";
     }
🤖 Prompt for AI Agents
In `@src/main/java/com/redis/commands/PingCommand.java` around lines 19 - 21, The
RESP bulk string length is computed using Java char count (msg.length()) which
is incorrect for UTF-8; in the PingCommand code path that builds the response
(the block that checks args != null && args.size() == 1 and returns the
"$...\\r\\n" string), replace the char-length call with the UTF-8 byte length
(e.g. use msg.getBytes(StandardCharsets.UTF_8).length) and ensure
StandardCharsets.UTF_8 is imported/used so multi-byte characters produce the
correct RESP length.

Comment on lines +147 to 157
} else if (b >= '0' && b <= '9') {
value = b - '0';
}

while (b != '\r') {
value = value * 10 + (b - '0');
while (buf.readerIndex() <= rIndex) {
b = buf.readByte();
if (b == '\r') break;
if (b >= '0' && b <= '9') {
value = value * 10 + (b - '0');
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-digit characters in integer fields are silently ignored.

If malformed input contains non-digit characters (e.g., *abc\r\n), the parser silently ignores them and produces an incorrect result (0 in this case). Consider validating that all characters between the start and \r are digits (or a leading -), and throwing a protocol exception otherwise.

🐛 Suggested fix to validate integer format
         while (buf.readerIndex() <= rIndex) {
             b = buf.readByte();
             if (b == '\r') break;
-            if (b >= '0' && b <= '9') {
-                value = value * 10 + (b - '0');
+            if (b < '0' || b > '9') {
+                throw new IllegalArgumentException("Malformed RESP: invalid integer");
             }
+            value = value * 10 + (b - '0');
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (b >= '0' && b <= '9') {
value = b - '0';
}
while (b != '\r') {
value = value * 10 + (b - '0');
while (buf.readerIndex() <= rIndex) {
b = buf.readByte();
if (b == '\r') break;
if (b >= '0' && b <= '9') {
value = value * 10 + (b - '0');
}
}
} else if (b >= '0' && b <= '9') {
value = b - '0';
}
while (buf.readerIndex() <= rIndex) {
b = buf.readByte();
if (b == '\r') break;
if (b < '0' || b > '9') {
throw new IllegalArgumentException("Malformed RESP: invalid integer");
}
value = value * 10 + (b - '0');
}
🤖 Prompt for AI Agents
In `@src/main/java/com/redis/server/RedisCommandHandler.java` around lines 147 -
157, The integer parsing in RedisCommandHandler (the block using buf, rIndex,
readerIndex, value and reading bytes b) currently ignores non-digit characters
and yields incorrect values; change the logic to validate the entire token
between the initial byte and the '\r' terminator: allow an optional leading '-'
then require every subsequent byte up to '\r' to be in '0'..'9', and if any
other byte is seen throw a protocol exception (e.g., ProtocolException or a
Redis protocol error) instead of silently skipping; update the initial branch
that handles the first byte (b) to detect '-' and digits and set a flag for
negativity, and enforce digit-only bytes in the while loop before converting to
the integer value.

Comment thread src/main/test/integration_tests.sh Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds significant new functionality to the Redis Java server by implementing 7 new commands (BLPOP, ECHO, PING, LPOP with count support, EXPIRE, TTL, LLEN) and refactoring the command handler to support pipelining and fragmentation. The changes also include critical bug fixes for expiry management and comprehensive test coverage.

Changes:

  • Refactored RedisCommandHandler from ChannelInboundHandlerAdapter to ByteToMessageDecoder for proper pipelining and fragmentation support
  • Added 7 new Redis commands with full unit test coverage (except PING and ECHO)
  • Fixed critical expiry bugs by adding clearExpiry() calls when keys are deleted or overwritten
  • Enhanced pom.xml with BOM-based dependency management and plugin version centralization
  • Added comprehensive integration test suite with 200+ tests
  • Improved documentation with detailed command reference and architecture diagrams

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
src/main/java/com/redis/server/RedisCommandHandler.java Refactored to ByteToMessageDecoder for pipelining support
src/main/java/com/redis/storage/RedisDatabase.java Added expiry management methods and clearExpiry() calls to fix bugs
src/main/java/com/redis/storage/ExpiryManager.java Added clearExpiry() method for proper expiry cleanup
src/main/java/com/redis/commands/*.java Added 7 new command implementations
src/test/java/com/redis/commands/*.java Added unit tests for new commands
src/test/java/com/redis/storage/ExpiryBugTest.java Regression tests for expiry bug fixes
src/test/java/com/redis/server/PipeliningTest.java Tests for pipelining and fragmentation support
src/main/test/integration_tests.sh Comprehensive integration test suite
run_all_tests.sh Test runner script
pom.xml Refactored with BOM management and updated versions
README.md Enhanced documentation with badges, mermaid diagrams, and detailed command reference
build/classes/* Build artifacts incorrectly committed to repository

Comment thread pom.xml Outdated
Comment thread pom.xml
<configuration>
<source>25</source>
<target>25</target>
<parameters>true</parameters>

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The <parameters>true</parameters> configuration (line 133) enables the compiler to include parameter names in bytecode. While useful for debugging and reflection, it's not clear if this project needs this feature. If not required, consider removing it to slightly reduce JAR size and compilation time.

Suggested change
<parameters>true</parameters>

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +30
// 4. Check if the key still exists.
// BUG: In the current implementation, it will be null because the background
// cleaner task for the first 'put' will see that keyExpiryMap still has
// the old expiry time (because it wasn't cleared) and remove the key.
assertEquals(val2, db.get(key), "Key should still exist after overwriting without TTL");

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The test comments at lines 27-29 describe a bug scenario but the test is not marked with @disabled or similar annotations. This is actually correct - the test is validating that the bug has been fixed by the changes in this PR (specifically the clearExpiry() calls added to RedisDatabase). However, the comment wording ("BUG: In the current implementation") suggests the bug still exists. Update the comment to clarify that this test validates the fix, e.g., "This test ensures that the expiry bug is fixed: when a key with TTL is overwritten without TTL, it should not expire."

Copilot uses AI. Check for mistakes.
Comment thread src/main/test/integration_tests.sh Outdated
# Comprehensive integration tests for Redis Java server.
# Uses redis-cli style inline commands via netcat.

set -euo pipefail

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The script uses set -euo pipefail (line 5) which will cause the script to exit on any error. However, the test functions run_test and run_test_contains return non-zero on failure (lines 55, 90). With -e set, the first test failure will terminate the entire script instead of continuing to run remaining tests. Consider removing the -e flag or wrapping test calls with || true to allow all tests to run even if some fail.

Suggested change
set -euo pipefail
set -uo pipefail

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
- **💾 In-Memory Storage**: Optimized data structures using `ConcurrentHashMap` for thread-safe, lock-free reads.
- **🔌 Redis Protocol (RESP)**: Full support for the Redis Serialization Protocol, compatible with any standard Redis client (`redis-cli`, `jedis`, `redis-py`, etc.).
- **⏳ Advanced Expiration**: Dual-strategy expiration (Lazy + Active background cleanup via `DelayQueue`).
- **🎯 Single-Threaded Execution**: Mimics Redis's atomic command processing model for data consistency.

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The claim "Mimics Redis's atomic command processing model for data consistency" (line 17) is misleading given that BLPOP blocks the I/O thread (as noted in comment ID 007). When BLPOP is executed, other clients cannot be served, which does NOT match Redis's behavior. Redis uses event-driven I/O to handle multiple blocking clients efficiently. Consider adding a caveat or removing this claim until BLPOP is refactored to use non-blocking I/O.

Suggested change
- **🎯 Single-Threaded Execution**: Mimics Redis's atomic command processing model for data consistency.
- **🎯 Single-Threaded Execution**: Primarily single-threaded command execution for predictable behavior; note that some blocking commands (e.g., `BLPOP`) currently block the I/O thread and do **not** yet match Redis's non-blocking event-driven handling.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +101
// Poll until we find an element or timeout
while (System.currentTimeMillis() < deadline) {
// Try each key in order
for (String key : keys) {
String result = tryPopFromKey(db, key);
if (result != null) {
// Return [key, element] as RESP array
return formatResult(key, result);
}
}

// No element found, sleep before next poll
if (timeoutMs > 0) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
break;
}
long sleepTime = Math.min(POLL_INTERVAL_MS, remaining);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return RESP_NIL;
}
} else {
// Zero timeout means block indefinitely - but check once
// For safety, we return nil immediately if nothing found with 0 timeout
// Real Redis would block forever, but that's not practical
break;
}
}

// Timeout expired

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The BLPOP implementation uses Thread.sleep() for blocking (lines 75-92), which blocks the Netty I/O thread. This is problematic because Netty's event loop is single-threaded and blocking it will prevent processing other clients' requests. Consider using Netty's ScheduledExecutorService or offloading blocking operations to a separate thread pool. The comment at line 89-90 acknowledges the issue with timeout=0 but doesn't address the broader blocking problem.

Suggested change
// Poll until we find an element or timeout
while (System.currentTimeMillis() < deadline) {
// Try each key in order
for (String key : keys) {
String result = tryPopFromKey(db, key);
if (result != null) {
// Return [key, element] as RESP array
return formatResult(key, result);
}
}
// No element found, sleep before next poll
if (timeoutMs > 0) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
break;
}
long sleepTime = Math.min(POLL_INTERVAL_MS, remaining);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return RESP_NIL;
}
} else {
// Zero timeout means block indefinitely - but check once
// For safety, we return nil immediately if nothing found with 0 timeout
// Real Redis would block forever, but that's not practical
break;
}
}
// Timeout expired
// Non-blocking behavior: try each key once and return immediately.
// This avoids blocking the Netty I/O thread with Thread.sleep.
for (String key : keys) {
String result = tryPopFromKey(db, key);
if (result != null) {
// Return [key, element] as RESP array
return formatResult(key, result);
}
}
// No element available at this moment (or timeout semantics are not enforced to avoid blocking)

Copilot uses AI. Check for mistakes.
Comment thread README.md Outdated
Comment on lines +22 to +30
* 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
*/
@Override
public String execute(List<String> args, ChannelHandlerContext ctx) {

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The EXPIRE command doesn't validate for negative seconds values. While Redis accepts negative values (which causes immediate expiration), this implementation passes negative seconds to System.currentTimeMillis() + (seconds * 1000L), resulting in a past timestamp. This may work correctly with the expiry system, but it's not explicitly tested or documented. Consider either rejecting negative values with an error or adding a comment explaining the behavior.

Copilot uses AI. Check for mistakes.
Comment thread run_all_tests.sh Outdated
Comment on lines +22 to +25
public class RedisCommandHandler extends ByteToMessageDecoder {
// Preallocate list to avoid allocations for small commands
private static final int INITIAL_ARGS_CAPACITY = 16;
private final List<String> argsBuffer = new ArrayList<>(INITIAL_ARGS_CAPACITY);

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The refactoring from ChannelInboundHandlerAdapter to ByteToMessageDecoder is a significant architectural change that improves pipelining and fragmentation support. However, there's a potential issue: the argsBuffer is now a class field (line 25) that's reused across calls. While decode() is called sequentially within a channel, ensure that this handler is not shared across multiple channels (which would cause thread-safety issues). Verify that the handler is instantiated per-channel in the server pipeline setup.

Copilot uses AI. Check for mistakes.
unikdahal and others added 3 commits January 26, 2026 02:18
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@unikdahal

Copy link
Copy Markdown
Owner Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copilot AI commented Jan 25, 2026

Copy link
Copy Markdown
Contributor

@unikdahal I've opened a new pull request, #43, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 3 commits January 25, 2026 20:54
Co-authored-by: unikdahal <61407386+unikdahal@users.noreply.github.com>
Co-authored-by: unikdahal <61407386+unikdahal@users.noreply.github.com>
[WIP] Refactor and add new commands for improved functionality
@unikdahal

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 31 changed files in this pull request and generated 12 comments.

Comment on lines +9 to +12
* EXPIRE key seconds [NX | XX | GT | LT]
* Set a timeout on key. After the timeout has expired, the key will automatically be deleted.
*
* Simple implementation supporting seconds.

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The class javadoc on line 9 mentions that EXPIRE supports optional flags [NX | XX | GT | LT], but the implementation (line 31) does not validate or implement these flags. If more than 2 arguments are provided, they are silently ignored. The documentation should either be updated to reflect that these flags are not yet implemented, or the implementation should validate the argument count and return an error if unsupported flags are provided.

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines +59 to +60
# Clone and build (replace REPO_URL with the HTTPS or SSH URL of this repository or your fork)
git clone REPO_URL

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The README states "Clone and build (replace REPO_URL with the HTTPS or SSH URL of this repository or your fork)" but the actual git clone command uses a placeholder "REPO_URL" that will fail if users copy-paste it directly. Consider either using the actual repository URL or providing a clearer example with a commented-out placeholder, such as: git clone <your-repo-url> or # git clone https://github.com/username/redis-java.git

Suggested change
# Clone and build (replace REPO_URL with the HTTPS or SSH URL of this repository or your fork)
git clone REPO_URL
# Clone and build (replace <your-repo-url> with the HTTPS or SSH URL of this repository or your fork)
git clone <your-repo-url>
# Example:
# git clone https://github.com/username/redis-java.git

Copilot uses AI. Check for mistakes.
assertEquals(RedisValue.Type.LIST, value.getType());
assertEquals(1, value.asList().size());
assertEquals("value1", value.asList().getFirst());
assertEquals("value1", value.asList().get(0));

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The change from getFirst() to get(0) suggests this code was previously using Java 21+ SequencedCollection methods. Since the pom.xml now targets Java 17 (which doesn't have getFirst()), this change is necessary for compatibility. However, this is inconsistent with the README which still claims "JDK 25 or higher" is required. All such changes should be verified to work with Java 17, and the documentation should be updated accordingly.

Copilot uses AI. Check for mistakes.
Comment thread src/main/java/com/redis/commands/PingCommand.java Outdated
Comment thread run_all_tests.sh
@@ -0,0 +1,147 @@
#!/bin/bash
# Test Execution Script for Redis Java Integration Tests
# This script runs all test suites and generates a report

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The shebang line uses #!/bin/bash but the script is executed with bash run_all_tests.sh in the execution context. For portability and correctness, ensure the script has execute permissions (chmod +x run_all_tests.sh) and can be invoked directly as ./run_all_tests.sh, or document that it should be run with bash run_all_tests.sh.

Suggested change
# This script runs all test suites and generates a report
# This script runs all test suites and generates a report
#
# Usage:
# # Make the script executable and invoke via the shebang:
# # chmod +x run_all_tests.sh
# # ./run_all_tests.sh
# #
# # Or invoke explicitly with bash (as used in some environments):
# # bash run_all_tests.sh

Copilot uses AI. Check for mistakes.
Comment thread pom.xml
Comment on lines +12 to +14
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The PR description claims "Rewritten README with concise architecture, command reference, and usage guide" but doesn't mention the critical Java version downgrade from 25 to 17 in pom.xml. This is a significant breaking change that affects compatibility and may impact developers who have Java 25-specific code or dependencies. The PR description should explicitly mention this major configuration change.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +98
while (System.currentTimeMillis() < deadline) {
// Try each key in order
for (String key : keys) {
String result = tryPopFromKey(db, key);
if (result != null) {
// Return [key, element] as RESP array
return formatResult(key, result);
}
}

// No element found, sleep before next poll
if (timeoutMs > 0) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
break;
}
long sleepTime = Math.min(POLL_INTERVAL_MS, remaining);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return RESP_NIL;
}

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The BLPOP implementation uses Thread.sleep() which blocks the Netty I/O thread, preventing the server from processing other clients' requests during BLPOP execution. While there is a warning comment acknowledging this (lines 69-73), this is a critical operational issue that makes the implementation unsuitable for production use. The comment suggests using "Netty's event loop scheduling or a separate executor thread pool", which should be implemented before merging this blocking implementation. This blocking behavior violates Redis's asynchronous architecture and will cause severe performance degradation under load.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment thread README.md

### Running the Server
### Prerequisites
- **JDK 25** or higher (with `--enable-preview`)

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The README states "JDK 25 or higher (with --enable-preview)" is required on line 54, but the pom.xml has been changed to target Java 17 (line 12). This creates a critical inconsistency - developers following the README will install Java 25 unnecessarily, while the actual build configuration targets Java 17. Either revert the pom.xml changes to use Java 25 with preview features as originally intended, or update this line to read "JDK 17 or higher".

Suggested change
- **JDK 25** or higher (with `--enable-preview`)
- **JDK 17** or higher

Copilot uses AI. Check for mistakes.
Comment thread README.md

```sh
# Start the server
java --enable-preview -jar target/redis-server.jar

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

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

The command java --enable-preview -jar target/redis-server.jar uses the --enable-preview flag, but since the pom.xml now targets Java 17 (which doesn't require or support preview features in this context), this flag is unnecessary and may cause confusion. Either remove the flag from this command or revert to Java 25 configuration in pom.xml.

Copilot uses AI. Check for mistakes.
Comment thread pom.xml
unikdahal and others added 2 commits January 26, 2026 15:54
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

Copilot AI commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

@unikdahal I've opened a new pull request, #44, to work on those changes. Once the pull request is ready, I'll request review from you.

unikdahal and others added 3 commits January 26, 2026 15:56
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…leep

Co-authored-by: unikdahal <61407386+unikdahal@users.noreply.github.com>
[WIP] WIP to address feedback on refactored commands PR
@unikdahal
unikdahal merged commit e0e3bd0 into main Jan 26, 2026
1 of 2 checks passed
@unikdahal
unikdahal deleted the feature/LPushCommand branch January 26, 2026 11:55
@coderabbitai coderabbitai Bot mentioned this pull request Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants