Skip to content

Commit 871b327

Browse files
authored
feat: add multi-port support, interceptors, scenarios, cluster (#3)
* Add example cluster config * refactor according to proxy changes * add support for hardcoded scenarios * expose interceptors api * add option to provide multiple listenPorts * fixes * Switch data endpoints to accept JSON payloads * Simplify LISTEN_PORT parsing and validation Update docker-compose and docs for multi-port setup Remove legacy cluster node registration logic * fix lint/formatting * fix tests * Revert "Switch data endpoints to accept JSON payloads" This reverts commit c13075e. * add examples * address PR comments * fix formatting
1 parent 60a2b74 commit 871b327

File tree

12 files changed

+871
-171
lines changed

12 files changed

+871
-171
lines changed

README.md

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const response = await fetch('http://localhost:3000/send-to-all-clients?encoding
5050
});
5151

5252
const result = await response.json();
53-
console.log(result.success ? 'Injected' : 'Failed');
53+
console.log(result.results.length > 0 ? 'Injected' : 'Failed');
5454
```
5555

5656
**Go Example:**
@@ -69,7 +69,7 @@ func main() {
6969
defer resp.Body.Close()
7070

7171
body, _ := io.ReadAll(resp.Body)
72-
if strings.Contains(string(body), `"success":true`) {
72+
if strings.Contains(string(body), `"results"`) {
7373
println("Injected")
7474
}
7575
}
@@ -91,7 +91,7 @@ public class RespProxyClient {
9191

9292
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
9393

94-
if (response.body().contains("\"success\":true")) {
94+
if (response.body().contains("\"results\"")) {
9595
System.out.println("Injected");
9696
}
9797
}
@@ -108,7 +108,7 @@ req = urllib.request.Request("http://localhost:3000/send-to-all-clients?encoding
108108

109109
with urllib.request.urlopen(req) as response:
110110
result = json.loads(response.read())
111-
print("Injected" if result["success"] else "Failed")
111+
print("Injected" if len(result["results"]) > 0 else "Failed")
112112
```
113113

114114
Key Endpoints: `POST /send-to-client/{id}`, `POST /send-to-all-clients`, `GET /connections`, `GET /stats`
@@ -318,6 +318,109 @@ Forcefully close a specific client connection.
318318
}
319319
```
320320

321+
#### Add Interceptor
322+
```http
323+
POST /interceptors
324+
```
325+
Add a custom interceptor to match commands and return custom responses.
326+
327+
**Example:**
328+
```bash
329+
curl -X POST "http://localhost:3000/interceptors" \
330+
-H "Content-Type: application/json" \
331+
-d '{"name":"ping-interceptor","match":"*1\r\n$4\r\nPING\r\n","response":"+CUSTOM PONG\r\n","encoding":"raw"}'
332+
```
333+
334+
**Response:**
335+
```json
336+
{
337+
"success": true,
338+
"name": "ping-interceptor"
339+
}
340+
```
341+
342+
#### Create Scenario
343+
```http
344+
POST /scenarios
345+
```
346+
Set up automated response sequence for testing.
347+
348+
**Example:**
349+
```bash
350+
curl -X POST "http://localhost:3000/scenarios" \
351+
-H "Content-Type: application/json" \
352+
-d '{"responses":["+OK\r\n",":42\r\n"],"encoding":"raw"}'
353+
```
354+
355+
**Response:**
356+
```json
357+
{
358+
"success": true,
359+
"totalResponses": 2
360+
}
361+
```
362+
363+
#### Get Nodes
364+
```http
365+
GET /nodes
366+
```
367+
List all proxy node IDs.
368+
369+
**Example:**
370+
```bash
371+
curl http://localhost:3000/nodes
372+
```
373+
374+
**Response:**
375+
```json
376+
{
377+
"ids": ["localhost:6379:6379", "localhost:6379:6380"]
378+
}
379+
```
380+
381+
#### Add Node
382+
```http
383+
POST /nodes
384+
```
385+
Add a new proxy node dynamically.
386+
387+
**Example:**
388+
```bash
389+
curl -X POST "http://localhost:3000/nodes" \
390+
-H "Content-Type: application/json" \
391+
-d '{"listenPort":6380,"targetHost":"localhost","targetPort":6379}'
392+
```
393+
394+
**Response:**
395+
```json
396+
{
397+
"success": true,
398+
"cfg": {
399+
"listenPort": 6380,
400+
"targetHost": "localhost",
401+
"targetPort": 6379
402+
}
403+
}
404+
```
405+
406+
#### Delete Node
407+
```http
408+
DELETE /nodes/{nodeId}
409+
```
410+
Remove a proxy node.
411+
412+
**Example:**
413+
```bash
414+
curl -X DELETE "http://localhost:3000/nodes/localhost:6379:6380"
415+
```
416+
417+
**Response:**
418+
```json
419+
{
420+
"success": true
421+
}
422+
```
423+
321424
## Use Cases
322425

323426
### Testing Redis Applications

bun.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dependencies": {
77
"@hono/zod-validator": "^0.7.2",
88
"hono": "^4.8.5",
9-
"redis-monorepo": "github:redis/node-redis#master",
9+
"redis-monorepo": "github:nkaradzhov/node-redis#proxy-improvements",
1010
"zod": "^4.0.8",
1111
},
1212
"devDependencies": {
@@ -66,7 +66,7 @@
6666

6767
"redis": ["[email protected]", "", { "dependencies": { "@redis/bloom": "5.8.3", "@redis/client": "5.8.3", "@redis/json": "5.8.3", "@redis/search": "5.8.3", "@redis/time-series": "5.8.3" } }, "sha512-MfSrfV6+tEfTw8c4W0yFp6XWX8Il4laGU7Bx4kvW4uiYM1AuZ3KGqEGt1LdQHeD1nEyLpIWetZ/SpY3kkbgrYw=="],
6868

69-
"redis-monorepo": ["redis-monorepo@github:redis/node-redis#e6025b1", {}, "redis-node-redis-e6025b1"],
69+
"redis-monorepo": ["redis-monorepo@github:nkaradzhov/node-redis#b5c9b07", {}, "nkaradzhov-node-redis-b5c9b07"],
7070

7171
"undici-types": ["[email protected]", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
7272

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
networks:
2+
redis-net:
3+
driver: bridge
4+
5+
services:
6+
redis:
7+
image: redislabs/client-libs-test:8.2
8+
container_name: redis
9+
ports:
10+
- "3000:3000"
11+
networks:
12+
- redis-net
13+
healthcheck:
14+
test: ["CMD", "redis-cli", "-p", "3000", "PING"]
15+
interval: 10s
16+
timeout: 3s
17+
retries: 5
18+
19+
resp-proxy:
20+
image: redislabs/client-resp-proxy
21+
container_name: resp-proxy
22+
environment:
23+
LISTEN_HOST: "0.0.0.0"
24+
LISTEN_PORT: "6379,6380,6381"
25+
TARGET_HOST: "redis"
26+
TARGET_PORT: "3000"
27+
API_PORT: "4000"
28+
ENABLE_LOGGING: true
29+
SIMULATE_CLUSTER: true
30+
ports:
31+
- "6379:6379"
32+
- "6380:6380"
33+
- "6381:6381"
34+
- "4000:4000"
35+
depends_on:
36+
- redis
37+
networks:
38+
- redis-net
39+
healthcheck:
40+
test: ["CMD", "sh", "-c", "wget -qO- http://localhost:4000/stats || exit 1"]
41+
interval: 10s
42+
timeout: 3s
43+
retries: 5
44+
45+
# debug:
46+
# image: nicolaka/netshoot:latest
47+
# container_name: debug
48+
# depends_on:
49+
# - resp-proxy
50+
# networks:
51+
# - redis-net
52+
# command: sleep infinity
53+
# stdin_open: true
54+
# tty: true

examples/cluster/readme.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Cluster Example
2+
3+
Short example demonstrating how to use the Proxy in cluster simulating mode in front of a standalone Redis.
4+
5+
## Step 1: Setup redis + proxy
6+
7+
- Option 1: Using Docker Compose
8+
9+
Running the provided `docker-compose.yml` file:
10+
11+
```bash
12+
docker-compose up
13+
```
14+
15+
- Option 2: Using external Redis server
16+
17+
1. Start a standalone Redis server on port 3000
18+
19+
2. Run the proxy in cluster mode:
20+
```bash
21+
docker run \
22+
-p 6379:6379 -p 6380:6380 -p 6381:6381 -p 4000:4000 \
23+
-e TARGET_HOST=<redis-host> \
24+
-e TARGET_PORT=<redis-port> \
25+
-e TIMEOUT=0 \
26+
-e API_PORT=4000 \
27+
-e SIMULATE_CLUSTER=yes \
28+
redislabs/client-resp-proxy
29+
30+
```
31+
This will start a Proxy instance (ports 6379, 6380 and 6381 for proxying and 4000 for the REST API).
32+
The proxy will simulate a cluster with 3 nodes running on ports 6379, 6479 and 6579 by intercepting the `cluster slots` command and returning a fake response.
33+
34+
## Step 2: Check if `cluster slots` reports correctly
35+
36+
Open a separate terminal
37+
38+
```bash
39+
redis-cli cluster slots
40+
```
41+
42+
Response should be similar to the following, where the ports are the proxy listen ports ( 6379, 6479 and 6579 ):
43+
```
44+
1) 1) (integer) 0
45+
2) (integer) 5460
46+
3) 1) "0.0.0.0"
47+
2) (integer) 6379
48+
3) "proxy-id-6379"
49+
2) 1) (integer) 5461
50+
2) (integer) 10922
51+
3) 1) "0.0.0.0"
52+
2) (integer) 6380
53+
3) "proxy-id-6380"
54+
3) 1) (integer) 10923
55+
2) (integer) 16383
56+
3) 1) "0.0.0.0"
57+
2) (integer) 6381
58+
3) "proxy-id-6381"
59+
```
60+
61+
### Step 3: Test push
62+
63+
```bash
64+
redis-cli subscribe foo
65+
```
66+
67+
Open another terminal
68+
69+
Push a message to all connected clients
70+
```bash
71+
echo '>3\r\n$7\r\nmessage\r\n$3\r\nfoo\r\n$4\r\neeee\r' | base64
72+
# PjMNCiQ3DQptZXNzYWdlDQokMw0KZm9vDQokNA0KZWVlZQ0K
73+
curl -X POST "http://localhost:4000/send-to-all-clients?encoding=base64" -d "PjMNCiQ3DQptZXNzYWdlDQokMw0KZm9vDQokNA0KZWVlZQ0K"
74+
```
75+
76+
You should see the following message in the `redis-cli subscribe` terminal:
77+
```
78+
1) "message"
79+
2) "foo"
80+
3) "eeee"
81+
```
82+
83+
### Step 4: Test topology change
84+
85+
Changing cluster topology is done by adding an interceptor that will catch the `cluster slots` command and return a different response. In this case we swapped the ports of node 2 and node 3.
86+
```
87+
curl -X POST "http://localhost:4000/interceptors" -H 'Content-Type: application/json' -d '{"name":"test", "match":"*2\r\n$7\r\ncluster\r\n$5\r\nslots\r\n", "response":"*3\r\n*3\r\n:0\r\n:5460\r\n*3\r\n$9\r\n127.0.0.1\r\n:6381\r\n$13\r\nproxy-id-6379\r\n*3\r\n:5461\r\n:10921\r\n*3\r\n$9\r\n127.0.0.1\r\n:6380\r\n$13\r\nproxy-id-6380\r\n*3\r\n:10922\r\n:16383\r\n*3\r\n$9\r\n127.0.0.1\r\n:6379\r\n$13\r\nproxy-id-6381\r\n", "encoding":"raw"}'
88+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"dependencies": {
2323
"@hono/zod-validator": "^0.7.2",
2424
"hono": "^4.8.5",
25-
"redis-monorepo": "github:redis/node-redis#master",
25+
"redis-monorepo": "github:nkaradzhov/node-redis#proxy-improvements",
2626
"zod": "^4.0.8"
2727
},
2828
"devDependencies": {

0 commit comments

Comments
 (0)