Skip to content

Commit c48dd78

Browse files
feat: instrument RedisCluster and ClusterPipeline for record/replay (#91)
1 parent 46a8936 commit c48dd78

File tree

4 files changed

+331
-5
lines changed

4 files changed

+331
-5
lines changed

drift/instrumentation/redis/e2e-tests/docker-compose.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,46 @@ services:
77
timeout: 5s
88
retries: 5
99

10+
redis-node-1:
11+
image: redis:7-alpine
12+
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-hostname redis-node-1 --appendonly yes
13+
healthcheck:
14+
test: ["CMD", "redis-cli", "ping"]
15+
interval: 5s
16+
timeout: 5s
17+
retries: 5
18+
19+
redis-node-2:
20+
image: redis:7-alpine
21+
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-hostname redis-node-2 --appendonly yes
22+
healthcheck:
23+
test: ["CMD", "redis-cli", "ping"]
24+
interval: 5s
25+
timeout: 5s
26+
retries: 5
27+
28+
redis-node-3:
29+
image: redis:7-alpine
30+
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --cluster-announce-hostname redis-node-3 --appendonly yes
31+
healthcheck:
32+
test: ["CMD", "redis-cli", "ping"]
33+
interval: 5s
34+
timeout: 5s
35+
retries: 5
36+
37+
redis-cluster-init:
38+
image: redis:7-alpine
39+
depends_on:
40+
redis-node-1:
41+
condition: service_healthy
42+
redis-node-2:
43+
condition: service_healthy
44+
redis-node-3:
45+
condition: service_healthy
46+
command: >
47+
sh -c "redis-cli --cluster create redis-node-1:6379 redis-node-2:6379 redis-node-3:6379 --cluster-replicas 0 --cluster-yes"
48+
restart: "no"
49+
1050
app:
1151
build:
1252
context: ../../../..
@@ -21,10 +61,14 @@ services:
2161
depends_on:
2262
redis:
2363
condition: service_healthy
64+
redis-cluster-init:
65+
condition: service_completed_successfully
2466
environment:
2567
- PORT=8000
2668
- REDIS_HOST=redis
2769
- REDIS_PORT=6379
70+
- REDIS_CLUSTER_HOST=redis-node-1
71+
- REDIS_CLUSTER_PORT=6379
2872
- TUSK_ANALYTICS_DISABLED=1
2973
- PYTHONUNBUFFERED=1
3074
- TUSK_USE_RUST_CORE=${TUSK_USE_RUST_CORE:-0}

drift/instrumentation/redis/e2e-tests/src/app.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,27 @@
1515

1616
app = Flask(__name__)
1717

18-
# Initialize Redis client
18+
# Initialize Redis client (standalone)
1919
redis_client = redis.Redis(
2020
host=os.getenv("REDIS_HOST", "redis"), port=int(os.getenv("REDIS_PORT", "6379")), db=0, decode_responses=True
2121
)
2222

23+
# Lazy-initialized RedisCluster client.
24+
# Initialized on first use so that cluster connection failures don't prevent
25+
# the rest of the app from starting (avoids breaking non-cluster tests).
26+
_cluster_client = None
27+
28+
29+
def get_cluster_client():
30+
global _cluster_client
31+
if _cluster_client is None:
32+
_cluster_client = redis.RedisCluster(
33+
host=os.getenv("REDIS_CLUSTER_HOST", "redis-node-1"),
34+
port=int(os.getenv("REDIS_CLUSTER_PORT", "6379")),
35+
decode_responses=True,
36+
)
37+
return _cluster_client
38+
2339

2440
@app.route("/health")
2541
def health():
@@ -245,6 +261,85 @@ def test_transaction_watch():
245261
return jsonify({"error": str(e)}), 500
246262

247263

264+
@app.route("/test/cluster-set-get", methods=["GET"])
265+
def test_cluster_set_get():
266+
"""Test SET/GET through RedisCluster.
267+
268+
RedisCluster.execute_command is a separate method from Redis.execute_command
269+
"""
270+
try:
271+
cluster = get_cluster_client()
272+
cluster.set("test:cluster:key1", "cluster_value1")
273+
cluster.set("test:cluster:key2", "cluster_value2")
274+
val1 = cluster.get("test:cluster:key1")
275+
val2 = cluster.get("test:cluster:key2")
276+
cluster.delete("test:cluster:key1", "test:cluster:key2")
277+
return jsonify({"success": True, "val1": val1, "val2": val2})
278+
except Exception as e:
279+
return jsonify({"error": str(e)}), 500
280+
281+
282+
@app.route("/test/cluster-incr", methods=["GET"])
283+
def test_cluster_incr():
284+
"""Test INCR through RedisCluster.
285+
286+
Exercises the cluster's execute_command path for a simple write operation.
287+
"""
288+
try:
289+
cluster = get_cluster_client()
290+
cluster.set("test:cluster:counter", "0")
291+
cluster.incr("test:cluster:counter")
292+
cluster.incr("test:cluster:counter")
293+
cluster.incr("test:cluster:counter")
294+
val = cluster.get("test:cluster:counter")
295+
cluster.delete("test:cluster:counter")
296+
return jsonify({"success": True, "value": int(val)})
297+
except Exception as e:
298+
return jsonify({"error": str(e)}), 500
299+
300+
301+
@app.route("/test/cluster-pipeline", methods=["GET"])
302+
def test_cluster_pipeline():
303+
"""Test pipeline through RedisCluster (ClusterPipeline).
304+
305+
ClusterPipeline.execute_command is also a separate path.
306+
All keys use the same hash tag {cp} to ensure they land on the same slot,
307+
which is required for cluster pipelines.
308+
"""
309+
try:
310+
cluster = get_cluster_client()
311+
pipe = cluster.pipeline()
312+
pipe.set("{cp}:key1", "pipe_val1")
313+
pipe.set("{cp}:key2", "pipe_val2")
314+
pipe.get("{cp}:key1")
315+
pipe.get("{cp}:key2")
316+
results = pipe.execute()
317+
cluster.delete("{cp}:key1", "{cp}:key2")
318+
return jsonify({"success": True, "results": results})
319+
except Exception as e:
320+
return jsonify({"error": str(e)}), 500
321+
322+
323+
@app.route("/test/cluster-pipeline-transaction", methods=["GET"])
324+
def test_cluster_pipeline_transaction():
325+
"""Test ClusterPipeline with transaction mode.
326+
327+
Uses TransactionStrategy internally. All keys must be on the same slot.
328+
"""
329+
try:
330+
cluster = get_cluster_client()
331+
pipe = cluster.pipeline(transaction=True)
332+
pipe.set("{tx}:key1", "txval1")
333+
pipe.set("{tx}:key2", "txval2")
334+
pipe.get("{tx}:key1")
335+
pipe.get("{tx}:key2")
336+
results = pipe.execute()
337+
cluster.delete("{tx}:key1", "{tx}:key2")
338+
return jsonify({"success": True, "results": results})
339+
except Exception as e:
340+
return jsonify({"error": str(e)}), 500
341+
342+
248343
if __name__ == "__main__":
249344
sdk.mark_app_as_ready()
250345
app.run(host="0.0.0.0", port=8000, debug=False)

drift/instrumentation/redis/e2e-tests/src/test_requests.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,12 @@
4444

4545
make_request("GET", "/test/transaction-watch")
4646

47+
# RedisCluster operations
48+
make_request("GET", "/test/cluster-set-get")
49+
make_request("GET", "/test/cluster-incr")
50+
make_request("GET", "/test/cluster-pipeline")
51+
52+
# RedisCluster
53+
make_request("GET", "/test/cluster-pipeline-transaction")
54+
4755
print_request_summary()

0 commit comments

Comments
 (0)