A reference for the CacheNet simulator: the event engine, what each agent does, the telemetry contract the dashboard reads, and how to run the whole thing locally.
CacheNet is an agent-based model of a distributed cache sitting in front of a database. Nothing in it is scripted: clients, cache nodes, a load balancer, a network and an origin database each follow their own rules, exchange messages, and the behaviour you watch on the dashboard is what falls out of that.
Every agent runs on one shared clock. There is no wall-clock time and no threading — the engine advances to the timestamp of the next scheduled event, which makes a run deterministic and repeatable for a given configuration.
load_balancer
Routes each request to a node by key hash so a key keeps landing on the same node.
service_node
Holds an LRU cache, answers reads, applies TTL and honours invalidation messages.
client
Generates the read and write traffic that drives the model.
database
Source of truth. Serves misses and issues invalidations on every write.
network
Adds latency and possible loss to every message that crosses it.
chaos_monkey
Terminates or degrades nodes mid-run so recovery can be observed.
observer
Samples hits, misses and latency and pushes them to the interface.
The engine is a discrete-event simulator. Events sit in a priority queue keyed by their scheduled time; the loop pops the earliest, moves the clock to it, and lets the receiving agent react — which usually schedules further events.
Queue
Python heapq, O(log n) push and pop
Ordering
By event time, ties broken by monotonic event id
Clock
Model time in milliseconds, advanced event to event
Concurrency
None — one event is handled at a time, by design
A single write followed by a read produces the sequence below. It is the worked example the engine ships with, and the reason a read can miss even though the key was cached moments earlier.
t=0 client WRITE key=test_key t=5 database recv WRITE → invalidate all nodes holding key t=10 node_* INVALIDATE key=test_key t=15 client READ key=test_key t=20 node_1 CACHE_MISS → fetch from database t=30 client RESPONSE key=test_key value=v1
Each node keeps its own cache backed by an ordered map, giving O(1) lookup, insertion and eviction. Reads move a key to the most-recently-used end; when the node is at capacity the least-recently-used key is dropped.
Lookup
Hash lookup on the local map — no scan, no coordination with other nodes.
Eviction
Least recently used, triggered when the node hits its configured size.
TTL
Entries carry an expiry; an expired entry is treated as a miss.
Invalidation
The database pushes invalidations on write, so nodes never serve a value it has superseded.
Consistency here is invalidation-based, not write-through. Between the write landing and the invalidation arriving, a node can still answer with the old value — that window is real, and it is visible in the log.
With the Chaos Monkey armed, the model kills a node partway through a run. The interesting part is not the failure — it is what the remaining nodes do next, and how long the cluster takes to get its hit ratio back.
Termination
A node stops accepting traffic and its cached keys are gone.
Re-hash
The balancer redistributes that node’s share of the keyspace across survivors.
Cold period
Re-routed keys are not cached on their new node, so every one of them misses.
Re-warm
Misses fill the survivors, and the hit ratio climbs back toward its previous level.
The recovery is caused by re-warming, not by repair. That distinction is the single most useful thing the model teaches.
The backend holds one WebSocket open at /ws/simulation. The client sends commands over it and receives three kinds of message back.
SIM_UPDATE
Aggregate metrics, per-agent stats, alive flags, model time and progress.
LOG
One event line: model time, log type and a human-readable message.
SIM_FINISHED
Final metrics and agent state; the dashboard stores this as a run.
{
"type": "SIM_UPDATE",
"time": 61.4,
"progress": 62.0,
"metrics": { "hits": 1121, "misses": 163, "unique_keys": 240 },
"agent_states": { "node_0": true, "node_2": false },
"agent_stats": { "node_0": { "hits": 142, "misses": 18 } }
}Commands travel the other way as { type: "START_SIM", config } and { type: "STOP_SIM" }, where config carries node count, cache size, duration and the chaos flag.
The dashboard can hand the current metrics envelope to a language model and ask it to explain the run. The request is a plain POST; the model sees exactly the numbers on your screen and nothing else.
Endpoint
POST /ai/analyze
Body
{ "query": string, "metrics": object }
Provider
Groq via LangChain
Model
Configured via GROQ_MODEL
Key
GROQ_API_KEY in the backend environment
Without a key the endpoint still answers, but the analyst panel will tell you it could not reach a model rather than inventing an explanation.
Two processes: the Python engine and this interface.
cd backend python -m venv venv && source venv/bin/activate pip install -r requirements.txt python src/main.py
cd frontend npm install npm run dev
The dashboard reads NEXT_PUBLIC_WS_URL and NEXT_PUBLIC_API_URL, falling back to localhost:8000. If the badge in the header reads “Engine offline”, the backend is not up or those variables point somewhere else.