Distributed Cache System in JavaScript
Building a distributed cache system is a common requirement in modern applications to improve performance and reduce database load. This challenge asks you to design and implement a simplified distributed cache using JavaScript, focusing on core functionalities like setting, getting, and deleting cached data across multiple nodes. Successfully completing this challenge demonstrates understanding of distributed systems concepts and basic network communication.
Problem Description
You are tasked with building a rudimentary distributed cache system. The system should consist of multiple "cache nodes" that collectively store cached data. The system should support the following operations:
set(key, value, ttl): Stores avalueassociated with akeyin the cache. Thettl(time-to-live) is in milliseconds and specifies how long the entry should remain in the cache before expiring. Thesetoperation should distribute the key-value pair across the available cache nodes using a consistent hashing strategy (explained in Notes).get(key): Retrieves thevalueassociated with akeyfrom the cache. Thegetoperation should query the appropriate cache node(s) based on the consistent hashing strategy. If the key is not found or has expired, it should returnnull.delete(key): Removes the entry associated with akeyfrom the cache. Thedeleteoperation should target the cache node(s) holding the key.
Key Requirements:
- Multiple Cache Nodes: The system should be designed to work with multiple cache nodes. You'll simulate these nodes using simple JavaScript objects.
- Consistent Hashing: Implement a simple consistent hashing algorithm to distribute keys across the cache nodes. This ensures that keys are consistently mapped to the same node(s) unless the node set changes.
- Time-to-Live (TTL): Implement TTL functionality for cached entries. Entries should automatically expire after the specified TTL.
- Error Handling: Handle cases where a cache node is unavailable or a key is not found.
- Asynchronous Operations (Simulated): Simulate asynchronous network calls to cache nodes. Use
setTimeoutto mimic network latency.
Expected Behavior:
setoperations should distribute keys across nodes as determined by the consistent hashing function.getoperations should return the cached value if it exists and hasn't expired.deleteoperations should remove the key-value pair from the appropriate node(s).- Expired entries should not be returned by
getoperations.
Edge Cases to Consider:
- Empty cache node list.
- TTL of 0 (entry expires immediately).
- Key collisions (though unlikely with a good hashing function).
- Cache node failures (simulated by returning errors from
getordelete). - Large TTL values.
Examples
Example 1:
Input:
cacheNodes = [{id: 1}, {id: 2}, {id: 3}];
cache = new DistributedCache(cacheNodes);
cache.set("user1", {name: "Alice"}, 5000);
cache.set("user2", {name: "Bob"}, 10000);
setTimeout(() => {
console.log(cache.get("user1")); // Output: {name: "Alice"}
}, 6000);
setTimeout(() => {
console.log(cache.get("user1")); // Output: null (expired)
}, 11000);
Explanation: The first get call retrieves the value for "user1" before it expires. The second get call returns null because the entry has expired.
Example 2:
Input:
cacheNodes = [{id: 1}, {id: 2}];
cache = new DistributedCache(cacheNodes);
cache.set("product1", {price: 20}, 2000);
console.log(cache.get("product1")); // Output: {price: 20}
cache.delete("product1");
console.log(cache.get("product1")); // Output: null
Explanation: The set operation stores the product information. The first get retrieves it. The delete operation removes the entry, and the second get returns null.
Example 3: (Edge Case - Empty Cache Nodes)
Input:
cacheNodes = [];
cache = new DistributedCache(cacheNodes);
cache.set("key1", "value1", 1000);
console.log(cache.get("key1")); // Output: null
Explanation: With no cache nodes, all operations should effectively fail, returning null for get.
Constraints
- Number of Cache Nodes: The number of cache nodes can range from 1 to 10.
- Key Length: Key strings should be no longer than 64 characters.
- Value Size: Values can be any JavaScript data type (objects, strings, numbers, etc.). Assume a reasonable size limit (e.g., no more than 1MB).
- TTL Range: TTL values should be non-negative integers.
- Performance: While not a primary focus, avoid excessively complex algorithms that would significantly degrade performance. The simulation of asynchronous operations will inherently introduce latency.
Notes
- Consistent Hashing: A simple consistent hashing implementation is sufficient. You can use a modulo operation (
%) to map keys to nodes. For example,nodeIndex = hash(key) % numNodes. Thehashfunction can be a simple string hashing algorithm. - Cache Node Simulation: Each cache node can be represented as a JavaScript object with a
storeproperty (an object to hold key-value pairs) and anid. - Asynchronous Simulation: Use
setTimeoutto simulate network latency when interacting with cache nodes. A delay of 50-150ms is reasonable. - TTL Implementation: Use
setTimeoutto schedule the expiration of entries. Store the expiration timestamp along with the value in the cache node. - Error Handling: Simulate cache node failures by occasionally returning
nullor throwing an error fromgetordeleteoperations. - Class Structure: Organize your code into a
DistributedCacheclass. - Focus: The primary focus is on demonstrating the core concepts of a distributed cache, not on building a production-ready system. Error handling and robustness can be simplified.