Skip to content

Commit 0248397

Browse files
committed
updated readme and dashboard UI/UX
1 parent 2be0322 commit 0248397

5 files changed

Lines changed: 551 additions & 288 deletions

File tree

README.md

Lines changed: 70 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,179 +1,118 @@
11
# Shudhi
22

3-
A sidecar for managing in-memory caches across services. Deploy alongside any app that implements the [InMem Management Protocol](#app-contract), and get cross-service cache visibility, targeted key lookups, and coordinated cache invalidation — all through a shared Redis.
4-
5-
## Architecture
3+
A sidecar for managing in-memory caches across services. Deploy it alongside your app to get cache visibility, key lookups, and coordinated invalidation — all through Redis.
64

75
```
8-
┌──────────────────────────┐
9-
│ Redis │
10-
│ │
11-
│ inmem:pod:<svc>:<pod> │ pod liveness (TTL 60s)
12-
│ inmem:keys:<svc>:<pod> │ key registry (TTL 3d)
13-
│ inmem:<svc> (pubsub) │ broadcast refresh
14-
│ inmem:req:<svc>:<pod> │ targeted get (pubsub RPC)
15-
└────────┬─────────────────┘
16-
17-
┌──────────────┼──────────────┐
18-
│ │ │
19-
Sidecar A Sidecar B Sidecar C
20-
(svc: rider) (svc: rider) (svc: driver)
21-
│ │ │
22-
App Pod A App Pod B App Pod C
6+
Redis (shared)
7+
8+
┌──────────┼──────────┐
9+
│ │ │
10+
Sidecar A Sidecar B Sidecar C
11+
│ │ │
12+
App Pod A App Pod B App Pod C
2313
```
2414

25-
Every sidecar connects to the same Redis. Discovery (list services, pods, keys) works from any sidecar. Interaction (get value, refresh) routes to the right place via Redis pub/sub — no direct sidecar-to-sidecar HTTP required.
26-
27-
## Configuration
28-
29-
| Env Var | Default | Description |
30-
|---------|---------|-------------|
31-
| `APP_URL` | `http://localhost:8080` | Local app's base URL |
32-
| `SIDECAR_PORT` | `8900` | Port the sidecar listens on |
33-
| `REDIS_URL` | `localhost:6379` | Redis address |
34-
| `POD_IP` | `127.0.0.1` | Pod IP (from k8s downward API) |
35-
| `INMEM_TOKEN` | _(empty)_ | If set, sent as `x-inmem-token` header on all calls to the app and between sidecars |
36-
37-
## Running
15+
## Quick Start
3816

3917
```bash
40-
# Binary
4118
APP_URL=http://localhost:8080 REDIS_URL=localhost:6379 go run .
42-
43-
# Docker
44-
docker run -e APP_URL=http://localhost:8080 -e REDIS_URL=redis:6379 ghcr.io/<owner>/shudhi
4519
```
4620

47-
## API
48-
49-
All endpoints are served by the sidecar. The dashboard (or any client) only needs to reach one sidecar to interact with any service.
21+
| Env Var | Default | Description |
22+
|---------|---------|-------------|
23+
| `APP_URL` | `http://localhost:8080` | Your app's base URL |
24+
| `SIDECAR_PORT` | `8900` | Sidecar listen port |
25+
| `REDIS_URL` | `localhost:6379` | Redis address |
26+
| `POD_IP` | `127.0.0.1` | Pod IP (k8s downward API) |
27+
| `INMEM_TOKEN` | _(empty)_ | Shared auth token between sidecar and app |
5028

51-
### Discovery (reads from Redis, works from any sidecar)
29+
## Integrating Your App
5230

53-
#### `GET /api/services`
31+
Your app needs to expose 3 endpoints and make 1 optional call:
5432

55-
List all registered services.
33+
### 1. Tell the sidecar who you are (required)
5634

57-
```json
58-
{ "services": ["rider-app", "driver-app", "payment-service"] }
5935
```
60-
61-
#### `GET /api/pods?service=rider-app`
62-
63-
List live pods for a service.
64-
65-
```json
66-
{
67-
"pods": [
68-
{ "podName": "rider-app-7b4f8d6c9-x2k4m", "sidecarUrl": "http://10.0.3.42:8900" }
69-
]
70-
}
36+
GET /internal/inMem/serverInfo
37+
→ { "serviceName": "rider-app", "podName": "rider-app-7b4f8d6c9-x2k4m" }
7138
```
7239

73-
#### `GET /api/keys?service=rider-app&pod=<optional>`
40+
Called once at startup. The sidecar uses this to register itself in Redis.
7441

75-
List registered cache keys. If `pod` is omitted, returns keys across all pods.
42+
### 2. Return a cached value on demand (required)
7643

77-
```json
78-
{
79-
"keys": [
80-
{
81-
"keyName": "RouteByRouteId:config-id:route-123",
82-
"keySchema": null,
83-
"ttlInSeconds": 3600,
84-
"registeredAt": "2026-04-24T10:30:00Z",
85-
"podName": "rider-app-7b4f8d6c9-x2k4m"
86-
}
87-
]
88-
}
8944
```
90-
91-
### Interaction (routes to the correct service/pod)
92-
93-
#### `POST /api/pod/get`
94-
95-
Get a cached value from a specific pod. Tries direct HTTP to the target sidecar, falls back to Redis pub/sub RPC.
96-
97-
```json
98-
// Request
99-
{
100-
"serviceName": "rider-app",
101-
"podName": "rider-app-7b4f8d6c9-x2k4m",
102-
"key": "RouteByRouteId:config-id:route-123"
103-
}
104-
105-
// Response (proxied from app)
106-
{
107-
"found": true,
108-
"value": { "vehicleType": "SUV", "category": "premium" }
109-
}
45+
POST /internal/inMem/get
46+
← { "key": "RouteByRouteId:config-id:route-123" }
47+
→ { "found": true, "value": { "vehicleType": "SUV" } }
11048
```
11149

112-
#### `POST /api/refresh`
113-
114-
Invalidate cache entries across all pods of a service. Publishes to the service's Redis pub/sub channel.
50+
### 3. Clear cache entries on demand (required)
11551

116-
```json
117-
// Request
118-
{
119-
"serviceName": "rider-app",
120-
"keyInfix": "RouteByRouteId" // null to clear all
121-
}
122-
123-
// Response
124-
{ "published": true, "service": "rider-app" }
52+
```
53+
POST /internal/inMem/refresh
54+
← { "keyInfix": "RouteByRouteId" } // or null to clear all
55+
→ 200 OK
12556
```
12657

127-
### App-facing
58+
Delete any cached entries whose key contains the given `keyInfix`. If `null`, clear everything.
12859

129-
#### `POST /api/registerKey`
60+
### 4. Register cached keys (optional, enables dashboard visibility)
13061

131-
Called by the app to register a cached key with the sidecar.
62+
Whenever your app caches something, tell the sidecar:
13263

133-
```json
64+
```
65+
POST http://localhost:8900/api/registerKey
13466
{
13567
"keyName": "RouteByRouteId:config-id:route-123",
136-
"keySchema": { "type": "object", "properties": { "vehicleType": { "type": "string" } } },
68+
"keySchema": null,
13769
"ttlInSeconds": 3600
13870
}
13971
```
14072

141-
Stored in Redis hash `inmem:keys:<serviceName>:<podName>` with a 3-day TTL.
73+
This makes the key visible in the dashboard. If the sidecar isn't ready yet, the call is silently accepted.
14274

143-
### Infra
75+
> If `INMEM_TOKEN` is set, the sidecar sends it as `x-inmem-token` header on all calls to your app. Use it to verify requests come from a trusted sidecar.
14476
145-
#### `GET /api/health`
77+
## Sidecar API
14678

147-
```json
148-
{ "redis": true, "app": true }
149-
```
79+
All endpoints work from any sidecar — the dashboard only needs to reach one.
80+
81+
| Method | Endpoint | What it does |
82+
|--------|----------|-------------|
83+
| `GET` | `/api/services` | List all services |
84+
| `GET` | `/api/pods?service=X` | List live pods for a service |
85+
| `GET` | `/api/keys?service=X&pod=Y` | List registered cache keys (pod optional) |
86+
| `POST` | `/api/pod/get` | Get a cached value from a specific pod |
87+
| `POST` | `/api/refresh` | Clear cache entries across all pods of a service |
88+
| `POST` | `/api/registerKey` | Register a cache key (called by your app) |
89+
| `GET` | `/api/health` | Health check (`{ "redis": bool, "app": bool }`) |
90+
91+
## How It Works
92+
93+
**Startup**: The sidecar calls your app's `/serverInfo` once to learn its service name and pod name. It then registers itself in Redis (`inmem:pod:<svc>:<pod>` with a 60s TTL) and subscribes to the service's pub/sub channel. A heartbeat refreshes the TTL every 30s. On `SIGTERM`, the key is deleted immediately; on crash, it auto-expires within 60s.
15094

151-
Returns `503` if either Redis or the app is unreachable.
95+
**Getting a value**: When the dashboard (or any client) asks for a key from a specific pod, the sidecar looks up the target pod's URL from Redis and proxies the request directly. If direct HTTP fails (network policy, pod restarting), it falls back to a Redis pub/sub RPC — publishes a request to the target pod's channel and waits for a reply on an ephemeral channel.
15296

153-
## App Contract
97+
**Refreshing / clearing cache**: A refresh request publishes a message to the service's broadcast channel in Redis. Every sidecar subscribed to that channel receives it and forwards the clear command to its local app. The originating pod skips the broadcast (it already cleared locally). Retries up to 3 times with backoff if the app returns an error.
15498

155-
The app must expose these endpoints under `/internal/inMem/`:
99+
**Key registration**: When your app calls `/api/registerKey`, the sidecar writes the key metadata to a Redis hash (`inmem:keys:<svc>:<pod>`, 3-day TTL). The dashboard reads these hashes to show what's cached where. This is the only way keys appear in the dashboard — if you don't register, the sidecar still works for get/refresh, you just lose visibility.
156100

157-
| Method | Endpoint | Purpose |
158-
|--------|----------|---------|
159-
| `GET` | `/internal/inMem/serverInfo` | Returns `{ "serviceName": "...", "podName": "..." }` |
160-
| `POST` | `/internal/inMem/get` | Returns `{ "found": bool, "value": json }` for a given key |
161-
| `POST` | `/internal/inMem/refresh` | Deletes cached entries by prefix (or all) |
101+
**Nothing piles up in Redis.** Pub/Sub messages are fire-and-forget — delivered to active subscribers and immediately discarded. The only persistent keys are pod liveness (60s TTL) and key registrations (3-day TTL), both self-cleaning.
162102

163-
The sidecar calls `/serverInfo` once at startup to learn its identity. `/get` and `/refresh` are called on demand.
103+
## Use Cases
164104

165-
If `INMEM_TOKEN` is set, every request to these endpoints includes the `x-inmem-token` header. The app can use this to verify that requests are coming from a trusted sidecar.
105+
**"A config changed in the DB, clear it from all pods"**
106+
You updated a pricing config in the database, but 12 pods still have the old value in memory. Instead of restarting the deployment, hit the dashboard's Clear button (or call `/api/refresh`) and every pod drops the stale entry. Next request fetches fresh from DB.
166107

167-
## Pod Liveness
108+
**"Why is this user seeing stale data?"**
109+
A customer reports seeing outdated information. Open the dashboard, find the service, pick the user's pod, and click Get on the relevant cache key to see exactly what's in memory on that pod right now. No port-forwarding, no kubectl exec, no guesswork.
168110

169-
Each sidecar registers itself in Redis with a 60s TTL and refreshes every 30s. On graceful shutdown (`SIGTERM`), it deletes its key immediately. On crash, the key auto-expires within 60s. If the key exists, the pod is alive — no filtering logic needed.
111+
**"We need cache visibility across 5 microservices"**
112+
Each team owns their own service with their own in-memory cache. Deploy Shudhi as a sidecar to each, point them at the same Redis, and the dashboard shows all services, all pods, all cached keys in one place. Platform team gets visibility without each service team building custom tooling.
170113

171-
## Redis Keys
114+
**"Rolling out a feature flag change"**
115+
Feature flags cached in memory across pods. Instead of waiting for TTL expiry (could be hours), trigger a targeted refresh for the flag's cache key and every pod picks up the new value within seconds.
172116

173-
| Key Pattern | Type | TTL | Purpose |
174-
|-------------|------|-----|---------|
175-
| `inmem:pod:<svc>:<pod>` | STRING | 60s | Pod liveness + sidecar URL |
176-
| `inmem:keys:<svc>:<pod>` | HASH | 3 days | Registered cache keys |
177-
| `inmem:<svc>` | PUBSUB || Broadcast channel (refresh) |
178-
| `inmem:req:<svc>:<pod>` | PUBSUB || Targeted request channel (get) |
179-
| `inmem:reply:<pod>:<nonce>` | PUBSUB || Ephemeral reply channel for pub/sub RPC |
117+
**"Debugging cache inconsistency between pods"**
118+
Pod A returns one value, Pod B returns another for the same key. Use the dashboard to Get the value from each pod side by side and see exactly what diverged — no need to reproduce the issue or add temporary logging.

config.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,21 @@ type AppInfo struct {
4949
}
5050

5151
type Sidecar struct {
52-
Config Config
53-
Redis *redis.Client
54-
AppInfo AppInfo
55-
HTTP *http.Client
56-
ready atomic.Bool // true once app info is fetched and registered
52+
Config Config
53+
Redis *redis.Client
54+
AppInfo AppInfo
55+
HTTP *http.Client
56+
ProxyHTTP *http.Client // shorter timeout for sidecar-to-sidecar calls
57+
ready atomic.Bool // true once app info is fetched and registered
5758
}
5859

5960
func NewSidecar(cfg Config) *Sidecar {
6061
rdb := redis.NewClient(&redis.Options{Addr: cfg.RedisURL, DB: cfg.RedisDB})
6162
return &Sidecar{
62-
Config: cfg,
63-
Redis: rdb,
64-
HTTP: &http.Client{Timeout: 5 * time.Second},
63+
Config: cfg,
64+
Redis: rdb,
65+
HTTP: &http.Client{Timeout: 5 * time.Second},
66+
ProxyHTTP: &http.Client{Timeout: 2 * time.Second},
6567
}
6668
}
6769

0 commit comments

Comments
 (0)