Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 50 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
# Custom HashMap
# Concurrent Key-Value Store (Jmap)

This project is a custom implementation of a thread-safe HashMap in Java. It is designed to be a learning exercise to understand the internal workings of a HashMap and multithreading in java.
A custom, high-performance Concurrent HashMap implementation in Java with Time-To-Live (TTL) support, extensive concurrency strategies, and a built-in interactive Command Line Interface.

## Features

- **Thread-Safe**: The `Jmap` class is thread-safe. It uses bucket-level locking to allow multiple threads to access the map concurrently. This is more efficient than locking the entire map for every operation.
- **Automatic Resizing**: The map automatically resizes itself when the number of elements exceeds the load factor. This is done to maintain a constant time complexity for the basic operations like `put`, `get`, and `remove`.
- **Key-Value Store**: The `KVStore` class provides a simple key-value store that uses the `Jmap` as its underlying data structure. It provides an interactive command-line interface to perform `put`, `get`, and `delete` operations.
- **Persistence**: The `KVStore` class also provides a simple persistence mechanism. It logs all the `put` and `delete` operations to a log file. When the application is started, it rebuilds the in-memory key-value store from the log file.
- **Custom Concurrent Map (`Jmap`)**: Implements a highly concurrent hash map using bucket-level `ReentrantReadWriteLock`s to minimize thread contention down to the specific bucket level.
- **Optimistic Reads**: Utilizes Java's `StampedLock` for fast optimistic reads, scaling up throughput. When validation fails (like during a concurrent resize), it gracefully falls back to secure read locks.
- **Dynamic Resizing**: Automatically handles resizing and re-hashing with minimal disruption when the load factor threshold is reached (default 0.75).
- **Time-To-Live (TTL) Expiration**: Attach an expiration time to any key using the `putexp` command. A background `TTLManager` daemon thread efficiently cleans up expired keys using a `DelayQueue`.

## Methods
## Project Structure

- `PUT`: To add a new key-value pair.
- `GET`: To retrieve the value for a given key.
- `DELETE`: To remove a key-value pair.
- `src/main/java/cache/Jmap.java`: The core concurrent hash map implementation, featuring fine-grained locking and optimistic reads.
- `src/main/java/cache/Jnode.java`: Represents a node/bucket entry in the map.
- `src/main/java/cache/KVStore.java`: The main application class providing the interactive CLI.
- `src/main/java/cache/TTLManager.java`: Manages key expiration and cleanup using a background daemon thread.
- `src/main/java/cache/ExpiryEntry.java`: A supporting class used by `DelayQueue` to track the expiration time of keys.
- `src/main/java/cache/JmapBenchmark.java`: JMH benchmarking setup to compare throughput running with 1, 8, and 16 concurrent threads.
- `src/main/java/cache/StressTest.java`: Multi-threaded integration tests running intensive concurrent operations.
- `src/test/java/cache/TTLTest.java`: Unit tests ensuring correct TTL behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

TTL test path in project structure appears incorrect.

README lists src/test/java/cache/TTLTest.java, but the provided test snippet is from src/test/java/TTLTest.java. Please correct the path so contributors can find tests reliably.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 21, Update the README entry that references the TTL test
path: replace the incorrect path `src/test/java/cache/TTLTest.java` with the
actual path `src/test/java/TTLTest.java` so the description and list entry
correctly point to the TTLTest.java unit test; ensure any other occurrences of
the old path in README.md are updated consistently.


## Usage

mvn exec:java -Dexec.mainClass="KVStore"
You can run the interactive CLI by executing the `KVStore` main class.

### CLI Commands

- `put <key> <value>`: Insert or update a key with a value.
- `putexp <key> <value> <ttl_seconds>`: Insert or update a key with a value and a Time-To-Live (TTL) in seconds.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

putexp TTL unit is documented as seconds but runtime currently uses raw milliseconds.

The command docs say ttl_seconds, but KVStore and Jmap.putexp currently treat that value as a direct millisecond delta. Please align docs and implementation to one unit (preferably convert seconds → ms in KVStore).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 30, The documentation says putexp accepts TTL in seconds
but the code uses raw milliseconds; update the implementation so KVStore and
Jmap.putexp treat the API TTL as seconds by converting seconds to milliseconds
before storing—locate the KVStore.putexp (or KVStore.set/put) code that computes
the expiry and multiply the received ttl_seconds by 1000 (or otherwise convert
to ms) when computing the expiry timestamp, and ensure Jmap.putexp passes the
CLI/API ttl value unchanged (seconds) to KVStore; also add a brief unit comment
near KVStore.putexp to avoid future confusion.

- `get <key>`: Retrieve the value associated with a key.
- `del <key>` (or `remove <key>`): Delete a key from the store.
- `show` (or `display`): Print the internal state and structure of the map.
- `count` (or `size`): Print the total number of non-expired nodes in the map.
- `help`: Display the list of available commands.
- `exit` (or `quit`): Safely shut down the CLI and background threads.

## Build and Run

This project uses Maven for dependency management and builds. Built on Java 21, packing standard tools like JUnit 5 and JMH.

```bash
mvn clean compile
mvn exec:java -Dexec.mainClass="cache.KVStore"
```

### Running Benchmarks

Because the program implements JMH benchmarks, you can package and run them locally to observe throughput comparison against `java.util.concurrent.ConcurrentHashMap`:

```bash
mvn clean install
java -jar target/benchmarks.jar
```
Global ReentrantReadWriteLock became a bottleneck as thread count increased. Although multiple readers can hold the read lock concurrently, acquiring and releasing the read lock is not free. Internally, the lock maintains shared state (such as reader counts) that is updated atomically. This lock state resides in a cache line that may be cached by multiple CPU cores. When a thread acquires or releases the read lock, its core must obtain exclusive ownership of that cache line before modifying it. This causes cache coherence traffic as ownership of the cache line moves between cores. Under high read concurrency, many threads contend on the same lock metadata, causing frequent cache-line bouncing and reducing scalability. As thread count increases, the overhead of maintaining the shared reader count becomes significant even though the protected data itself is only being read.
Future scope:

Might move fom one single global lock to sharding the entire map into smaller pieces and having locks for those smaller shards because stampedlock doesn't always try to acquire a lock but it can also still fail when no. of writes are more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typo in “Future scope” sentence (fomfrom).

Small docs typo in a user-facing section.

🧰 Tools
🪛 LanguageTool

[grammar] ~58-~58: Ensure spelling is correct
Context: ...y being read. Future scope: Might move fom one single global lock to sharding the ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 58, In the README's "Future scope" sentence replace the
typo "fom" with "from" so the sentence reads "Might move from one single global
lock to sharding the entire map into smaller pieces..." — update that exact word
in the "Future scope" paragraph to correct the user-facing docs.

Source: Linters/SAST tools

85 changes: 85 additions & 0 deletions dependency-reduced-pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>customhashmap</groupId>
<artifactId>customhashmap</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>benchmarks</finalName>
<transformers>
<transformer>
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
<transformer />
</transformers>
Comment on lines +23 to +27

Copy link
Copy Markdown

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:

#!/bin/bash
# Verify all shade transformers in dependency-reduced-pom.xml declare implementation classes.
python - <<'PY'
import xml.etree.ElementTree as ET

pom = "dependency-reduced-pom.xml"
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
tree = ET.parse(pom)
root = tree.getroot()

for t in root.findall(".//m:plugin[m:artifactId='maven-shade-plugin']//m:transformer", ns):
    print("transformer attributes:", t.attrib, "| has implementation:", "implementation" in t.attrib)
PY

Repository: RishabhRawat9/cache

Length of output: 173


🌐 Web query:

Maven Shade Plugin dependency-reduced-pom.xml <transformer> without implementation attribute mainClass ServicesResourceTransformer behavior

💡 Result:

The ServicesResourceTransformer in the Maven Shade Plugin is designed specifically to relocate class names and merge multiple entries within META-INF/services resources [1][2]. It does not have, nor does it support, a mainClass attribute or configuration element [1]. The mainClass configuration parameter is specific to the ManifestResourceTransformer, which is used to define the entry point of an executable JAR by modifying the MANIFEST.MF file [3][4]. Attempting to add a mainClass or manifestEntries element to a ServicesResourceTransformer configuration will result in a build error (often reported as "Cannot find '...' in class") because the underlying Java class for the ServicesResourceTransformer does not contain these properties [5]. Regarding the dependency-reduced-pom.xml, this is a separate feature of the shade goal that automatically generates a simplified POM file for the shaded artifact [6]. Its creation is controlled by the createDependencyReducedPom configuration parameter (which defaults to true) and is independent of the transformers specified in the plugin configuration [6][7]. The transformers themselves do not interfere with or require specific handling for this file [1][6]. To correctly configure these elements: 1. Use ServicesResourceTransformer with no additional attributes within the element [2][8]. 2. Use ManifestResourceTransformer if you need to specify a mainClass [4]. 3. The dependency-reduced-pom.xml is managed automatically by the plugin based on its own configuration parameters, not by the transformers [6].

Citations:


Fix missing implementation attributes for Shade transformers in dependency-reduced-pom.xml

dependency-reduced-pom.xml (lines 23-27) contains two <transformer> blocks with no implementation class (one includes <mainClass>). Shade transformers are selected/configured via implementation: <mainClass> belongs to ManifestResourceTransformer, and the empty transformer should correspond to ServicesResourceTransformer; as-is the transformer configuration is incomplete/ignored if Shade is ever re-run using this POM.

🔧 Proposed fix
-            <transformer>
+            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
               <mainClass>org.openjdk.jmh.Main</mainClass>
             </transformer>
-            <transformer />
+            <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
📝 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
<transformer>
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
<transformer />
</transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dependency-reduced-pom.xml` around lines 23 - 27, The two <transformer>
entries are missing their required implementation attributes: the transformer
that contains <mainClass> should be declared as ManifestResourceTransformer
(implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer")
and the empty <transformer/> should be declared as ServicesResourceTransformer
(implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer");
update the <transformer> elements to include these implementation attributes so
Shade will recognize and apply ManifestResourceTransformer and
ServicesResourceTransformer when re-running with the dependency-reduced POM.

</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>cache.StressTest</mainClass>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit-jupiter-api</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
<exclusion>
<artifactId>junit-jupiter-params</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
<exclusion>
<artifactId>junit-jupiter-engine</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
</properties>
</project>
56 changes: 46 additions & 10 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>4.0.0</modelVersion>
http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>customhashmap</groupId>
<artifactId>customhashmap</artifactId>
<version>1.0-SNAPSHOT</version>
Expand All @@ -23,27 +22,63 @@
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</dependency>
</dependencies>

<build>
<plugins>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>benchmarks</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<version>3.13.0</version>
<configuration>
<release>17</release>
<release>21</release>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>


<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>KVStore</mainClass>
<mainClass>cache.StressTest</mainClass>
</configuration>
</plugin>
<plugin>
Expand All @@ -53,4 +88,5 @@
</plugin>
</plugins>
</build>
</project>

</project>
Loading