isCache is a distributed, fault-tolerant cache framework for Python that provides in-memory caching with multiple eviction strategies across a cluster of nodes. The framework ensures cache consistency and reliability during node failures through data replication and consistent hashing for efficient key distribution.
isCache is designed as an in-process distributed cache, where multiple cache nodes run within the same Python process but are managed as a logical cluster with consistent hashing, replication, and failover capabilities.
- Multiple Eviction Policies: Support for LRU (Least Recently Used), LFU (Least Frequently Used), and TTL (Time-To-Live) eviction strategies
- Consistent Hashing: Minimizes data redistribution when nodes are added or removed from the cluster
- Data Replication: Configurable replication factor ensures data availability during node failures
- Automatic Failover: Client automatically fails over to replica nodes when primary nodes are unavailable
- Dynamic Topology Management: Add or remove nodes at runtime without restarting the cache
- Topology Broadcast: Registered listeners are notified immediately when the cluster topology changes
- Thread-Safe Operations: All cache operations are thread-safe with fine-grained locking
- Capacity Management: Per-node capacity limits with automatic eviction
- Statistics Tracking: Monitor cache performance with hit/miss/eviction metrics
- Background TTL Cleanup: Automatic removal of expired entries for TTL policy
┌──────────────────────────────────────────────────────────────────────┐
│ CacheClient │
│ ┌───────────────┐ ┌──────────────────┐ ┌────────────────────────┐│
│ │ Hash Ring │ │ Replication Mgr │ │ Cluster Manager ││
│ │ (Consistent │ │ (Replication │ │ add_node / remove_node││
│ │ Hashing) │ │ Factor: N) │ │ get_topology ││
│ └───────────────┘ └──────────────────┘ └────────────────────────┘│
└──────────────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│──────────│ │──────────│ │──────────│
│ Storage │ │ Storage │ │ Storage │
│ LRU/LFU │ │ LRU/LFU │ │ LRU/LFU │
│ TTL │ │ TTL │ │ TTL │
│ Stats │ │ Stats │ │ Stats │
└──────────┘ └──────────┘ └──────────┘
- CacheClient: Main interface for interacting with the distributed cache. Handles routing, replication, failover, and dynamic topology management.
- ClusterManager: Manages live cluster topology — adding/removing nodes, broadcasting changes to listeners, and providing topology snapshots.
- ClusterTopology: Immutable snapshot of the cluster state at a point in time (nodes, version, timestamp, hash ring state).
- CacheNode: Individual cache storage unit with configurable eviction policy and capacity.
- ConsistentHashRing: Implements consistent hashing algorithm with virtual nodes for uniform key distribution.
- ReplicationManager: Coordinates replication of data across multiple nodes.
- CacheEntry: Data model representing a cached item with metadata (timestamps, access count, expiration).
- EvictionPolicy: Enumeration of supported eviction strategies (LRU, LFU, TTL).
- Python 3.8 or higher
# Clone or navigate to the project directory
cd isCache
# Create and activate a virtual environment (recommended)
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install in development mode
pip install -e .
# Install test dependencies
pip install pytest hypothesisHere's a simple example to get started with isCache:
from iscache import CacheClient
# Create a cache client with 3 nodes and replication factor of 2
client = CacheClient(
node_addresses=["node1", "node2", "node3"],
replication_factor=2
)
# Store data
client.set("user:1", {"name": "Alice", "age": 30})
client.set("user:2", {"name": "Bob", "age": 25})
# Retrieve data
user1 = client.get("user:1")
print(user1) # {'name': 'Alice', 'age': 30}
# Update data
client.set("user:1", {"name": "Alice", "age": 31})
# Delete data
client.delete("user:2")
# Verify deletion
user2 = client.get("user:2")
print(user2) # NoneThe CacheClient is the main entry point for all cache operations:
from iscache import CacheClient
# Single node (no replication)
client = CacheClient(["node1"], replication_factor=1)
# Multi-node with replication
client = CacheClient(
["node1", "node2", "node3", "node4"],
replication_factor=3
)Parameters:
node_addresses: List of node identifiers (strings)replication_factor: Number of copies to maintain (default: 2, must be >= 1)
# Set a value (supports any Python object)
client.set("key", "value")
client.set("user:123", {"name": "Charlie", "email": "charlie@example.com"})
client.set("counter", 42)
client.set("items", [1, 2, 3, 4, 5])
# Get a value
value = client.get("key")
user = client.get("user:123")
count = client.get("counter")
# Non-existent keys return None
missing = client.get("nonexistent") # None# Delete a key from all replicas
result = client.delete("key") # Returns True
# Deleting a non-existent key also returns True
result = client.delete("nonexistent") # Returns TrueFor advanced use cases, you can work directly with CacheNode instances:
from iscache.node import CacheNode
from iscache.models import EvictionPolicy
# Create a node with LRU policy
node = CacheNode(
node_id="my_node",
capacity=1000,
eviction_policy=EvictionPolicy.LRU
)
# Use the node
node.set("key", "value")
value = node.get("key")
node.delete("key")
# Get statistics
stats = node.get_stats()
print(stats) # {'hits': 1, 'misses': 0, 'evictions': 0, 'size': 0}Evicts the entry that was accessed (read or written) least recently.
from iscache.node import CacheNode
from iscache.models import EvictionPolicy
node = CacheNode("node1", capacity=3, eviction_policy=EvictionPolicy.LRU)
node.set("a", 1)
node.set("b", 2)
node.set("c", 3)
# Access 'a' to make it recently used
node.get("a")
# Adding 'd' will evict 'b' (least recently accessed)
node.set("d", 4)
print(node.get("a")) # 1 (still exists)
print(node.get("b")) # None (evicted)
print(node.get("c")) # 3 (still exists)
print(node.get("d")) # 4 (newly added)Best for: General-purpose caching where recent access patterns are good predictors of future access.
Evicts the entry with the lowest access count (frequency).
from iscache.node import CacheNode
from iscache.models import EvictionPolicy
node = CacheNode("node1", capacity=3, eviction_policy=EvictionPolicy.LFU)
node.set("a", 1)
node.set("b", 2)
node.set("c", 3)
# Access 'a' multiple times
for _ in range(5):
node.get("a")
# Access 'c' twice
node.get("c")
node.get("c")
# Don't access 'b' at all
# Adding 'd' will evict 'b' (lowest frequency: 0)
node.set("d", 4)
print(node.get("a")) # 1 (still exists, freq=5)
print(node.get("b")) # None (evicted, freq=0)
print(node.get("c")) # 3 (still exists, freq=2)
print(node.get("d")) # 4 (newly added, freq=0)Best for: Caching scenarios where access frequency is more important than recency (e.g., popular content).
Automatically evicts entries after a specified duration. Expired entries are removed lazily on access and proactively via background cleanup.
from iscache.node import CacheNode
from iscache.models import EvictionPolicy
import time
# Create node with 2-second TTL
node = CacheNode(
"node1",
capacity=10,
eviction_policy=EvictionPolicy.TTL,
ttl_seconds=2
)
node.set("temp", "temporary data")
print(node.get("temp")) # "temporary data"
# Wait for expiration
time.sleep(2.1)
print(node.get("temp")) # None (expired)
# Stop the background cleanup thread when done
node.stop()Best for: Caching time-sensitive data like session tokens, temporary credentials, or rate-limiting counters.
Features:
- Expired entries return
Noneon access and are automatically removed - Background thread cleans up expired entries every 60 seconds
- When cache is full, expired entries are prioritized for eviction
isCache provides data replication to ensure availability during node failures:
from iscache import CacheClient
# Create cluster with 4 nodes and replication factor 3
# Each key will be stored on 3 nodes
client = CacheClient(
["node1", "node2", "node3", "node4"],
replication_factor=3
)
# Data is automatically replicated
client.set("important_data", {"value": 42})
# If primary node fails, client automatically fails over to replicas
# The get operation will try primary first, then replicas in order
data = client.get("important_data") # Still works even if primary is down- Key Routing: Consistent hashing determines the primary node for each key
- Replication: Data is replicated to N-1 additional nodes (clockwise on the hash ring)
- Read Operations: Client tries primary first, falls back to replicas if needed
- Write Operations: Data is written to all replica nodes
- Failover: Automatic and transparent to the application
from iscache import CacheClient, NodeUnreachableException
client = CacheClient(["node1", "node2", "node3"], replication_factor=2)
try:
value = client.get("key")
except NodeUnreachableException:
# All nodes (primary and replicas) are unreachable
print("Cache is unavailable")isCache supports adding and removing nodes at runtime without stopping the cache. The ClusterManager coordinates these changes, updates the hash ring, and notifies any registered listeners.
from iscache import CacheClient
client = CacheClient(["node1", "node2"], replication_factor=2)
# Add a new node with default settings (capacity=1000, LRU)
client.add_node("node3")
# Add a node with custom settings
client.add_node("node4", capacity=5000, eviction_policy=EvictionPolicy.LFU)
# Cache operations immediately use the new node
client.set("key", "value")# Remove a node — hash ring is updated, existing operations continue
client.remove_node("node2")
# Raises ConfigurationException if node does not exist
from iscache import ConfigurationException
try:
client.remove_node("ghost")
except ConfigurationException as e:
print(e)from iscache import CacheClient, ClusterTopology
client = CacheClient(["node1", "node2", "node3"], replication_factor=2)
topo: ClusterTopology = client.get_topology()
print(topo.version) # 1
print(topo.nodes) # {"node1": "active", "node2": "active", "node3": "active"}
print(topo.hash_ring_state) # ["node1", "node2", "node3"]
print(topo.timestamp) # Unix timestamp of the snapshotRegister a callback to be notified whenever a node is added or removed:
def on_topology_change(topo: ClusterTopology):
print(f"Topology changed! version={topo.version}, nodes={list(topo.nodes.keys())}")
client.cluster_manager.register_topology_listener(on_topology_change)
client.add_node("node4") # triggers: "Topology changed! version=2, nodes=[...]"
client.remove_node("node1") # triggers: "Topology changed! version=3, nodes=[...]"- After
add_node, theReplicationManageris automatically rebuilt to include the new node. - After
remove_node, the node's TTL cleanup thread (if any) is stopped cleanly. add_noderaisesConfigurationExceptionif a node with the same ID already exists.remove_noderaisesConfigurationExceptionif the node ID is not found.- All topology operations are thread-safe.
Install the CLI extras and launch the Redis-style REPL:
pip install iscache[cli]
iscache-cli# In-process (default) — creates a local in-memory cache
iscache-cli
# Specify nodes explicitly
iscache-cli --nodes node1,node2,node3
# Connect to a running iscache-daemon
iscache-cli --host localhost --port 6380| Command | Description |
|---|---|
SET key value [ttl] |
Store a value (optional TTL in seconds) |
GET key |
Retrieve a value |
DEL key |
Delete a key |
EXISTS key |
Check if a key exists (returns 1 or 0) |
STATS |
Show hit/miss/eviction counts per node |
NODES |
Show eviction policy, capacity, and size per node |
TOPOLOGY |
Show cluster version and hash ring state |
FLUSH |
Delete all keys from all nodes |
PING |
Check connectivity (returns PONG) |
HELP |
List all commands |
EXIT |
Exit the REPL |
isCache CLI
Mode: in-process
Type HELP for available commands.
iscache> SET user:1 Alice
OK
iscache> GET user:1
Alice
iscache> STATS
NODE HITS MISSES EVICTIONS
--------------------------------------------------
default-node 1 0 0
iscache> TOPOLOGY
Version : 1
Hash ring: default-node
iscache> EXIT
The dashboard gives you live visibility into cache performance across all connected applications.
# 1. Install dashboard extras
pip install iscache[dashboard]
# 2. Start the metrics daemon (receives telemetry from your apps)
iscache-daemon
# 3. Start the dashboard (opens browser automatically)
iscache-dashboardThe dashboard opens at http://localhost:6381 and auto-refreshes every 5 seconds.
Add one line to your code to connect it to the daemon:
from iscache import CacheClient
from iscache.telemetry import connect_telemetry
client = CacheClient(["node1", "node2", "node3"], replication_factor=2)
# Register with the daemon — metrics are pushed every 10s automatically
connect_telemetry(client, app_name="my-service")The telemetry agent:
- Registers your app with the daemon on startup
- Pushes hit/miss/eviction metrics every 10 seconds in a background thread
- Sends heartbeats every 5 seconds (app shown as "stale" if missed for 15s)
- Deregisters cleanly on process exit
| Panel | Description |
|---|---|
| Overview | Total hits, misses, evictions, connected apps count |
| Hit Rate Chart | Hit rate % per minute over 60 minutes |
| Operations Chart | GET/SET/DEL counts per minute |
| Connected Applications | All registered apps with PID, host, status |
| Node Distribution | Which nodes each app is using |
# Custom ports
iscache-daemon --port 6380
iscache-dashboard --port 6381 --daemon-url http://localhost:6380| Install command | What you get |
|---|---|
pip install iscache |
Core cache engine, zero dependencies |
pip install iscache[cli] |
Core + interactive CLI |
pip install iscache[dashboard] |
Core + web dashboard + telemetry SDK |
pip install iscache[all] |
Everything |
Constructor:
CacheClient(node_addresses: List[str], replication_factor: int = 2)Methods:
get(key: str) -> Optional[Any]: Retrieve value for keyset(key: str, value: Any, ttl: Optional[int] = None) -> bool: Store key-value pairdelete(key: str) -> bool: Remove key from cacheadd_node(node_id, capacity=1000, eviction_policy=LRU, ttl_seconds=None) -> bool: Add a new node to the cluster at runtimeremove_node(node_id) -> bool: Remove a node from the cluster at runtimeget_topology() -> ClusterTopology: Return current cluster topology snapshot
Raises:
ConfigurationException: Invalid initialization parametersNodeUnreachableException: All nodes unreachable for a keyCacheException: General cache operation failure
Constructor:
CacheNode(
node_id: str,
capacity: int,
eviction_policy: EvictionPolicy,
ttl_seconds: Optional[int] = None
)Methods:
get(key: str) -> Optional[Any]: Retrieve valueset(key: str, value: Any) -> bool: Store key-value pairdelete(key: str) -> bool: Remove keyget_stats() -> Dict[str, int]: Get cache statisticsstop() -> None: Stop background cleanup thread (TTL only)
Statistics: Returns dictionary with:
hits: Number of successful getsmisses: Number of failed getsevictions: Number of evicted entriessize: Current number of entries
from iscache.models import EvictionPolicy
EvictionPolicy.LRU # Least Recently Used
EvictionPolicy.LFU # Least Frequently Used
EvictionPolicy.TTL # Time-To-LiveConstructor:
ClusterManager(hash_ring: ConsistentHashRing, nodes: Dict[str, CacheNode])Normally accessed via
client.cluster_manager— not created directly.
Methods:
add_node(node_id, capacity=1000, eviction_policy=LRU, ttl_seconds=None) -> bool: Add a node to the clusterremove_node(node_id) -> bool: Remove a node from the clusterget_topology() -> ClusterTopology: Return a snapshot of current cluster stateregister_topology_listener(callback) -> None: Subscribe to topology change notifications
Raises:
ConfigurationException: Duplicate node_id on add, or unknown node_id on remove
A read-only dataclass snapshot of the cluster at a point in time.
Fields:
nodes: Dict[str, str]— mapping of node_id → status ("active")version: int— monotonically increasing version numbertimestamp: float— Unix timestamp when snapshot was createdhash_ring_state: List[str]— list of active node_ids currently in the ring
from iscache import ClusterTopology
topo = client.get_topology()
print(topo.version) # 3
print(topo.nodes) # {"node1": "active", "node3": "active"}
print(topo.hash_ring_state) # ["node1", "node3"]- Get Operations: O(log N) hash ring lookup + O(1) node access
- Set Operations: O(log N) hash ring lookup + O(1) node write + O(R) replication
- Delete Operations: O(log N) hash ring lookup + O(R) replication
- Eviction: O(N) scan for victim selection (where N is entries per node)
- Memory Usage: O(C × N) where C is capacity per node and N is number of nodes
Where:
- N = number of nodes
- R = replication factor
- C = capacity per node
All cache operations are thread-safe:
CacheNodeusesthreading.Lockfor all shared state accessConsistentHashRingusesthreading.RLockfor thread safety- Statistics counters are protected by locks
- TTL cleanup runs in a daemon thread
This project is provided as-is for educational and development purposes.
Contributions are welcome! Areas for improvement:
- Additional eviction policies (ARC, SLRU, etc.)
- Network-based distribution
- Persistence layer
- Performance optimizations
- Additional test coverage
For issues, questions, or contributions, please refer to the project repository or documentation.