Skip to content

Commit bffcb88

Browse files
Aditya-eddyclaude
andauthored
Add spring-aerospike sample: Aerospike-Java with Keploy record/replay (#135)
Java counterpart of keploy/samples-go/aerospike-tls. A Spring Boot 2.7 service that talks to Aerospike CE on clear-text :3000 via the aerospike-client-jdk8 driver, recorded and replayed end-to-end with three bundled scripts that mirror the Go sample's shape one-to-one (same endpoints, same test-set layout, same scripts). Endpoints (full parity with the Go sample, 14 total): GET /health POST /put GET /get/{key} POST /batch/put GET /batch/get POST /scan POST /query POST /udf POST /cdt/list/append POST /cdt/map/put POST /touch/{key} DELETE /key/{key} POST /parallel POST /multiclient POST /freshclient main.go's concurrency story is ported one-for-one: * ClientPolicy.maxConnsPerNode = 256, OpeningConnectionThreshold analogue set to 16 so bursts don't outpace upstream connect rate. * parallelWrite / parallelRead policies with socketTimeout 10s, totalTimeout 30s, maxRetries 10, sleepBetweenRetries 5ms. * Two-phase warmup on the main client at startup — sequential prelude walks the cluster past cold-start latencies, then a parallel fill puts idle connections in the pool before the HTTP server accepts the first request. * RetryHelper.doOp wraps each PUT and GET in /parallel, /multiclient, /freshclient. scripts/ matches the Go sample's pipeline shape (common.sh + script-{1,2,3}.sh + same env-var knobs: KEPLOY / PORT / LOG_DIR / SKIP_DOCKER / SKIP_BUILD). Smoke-tested locally with the dev keploy binary that carries the Aerospike parser: script-1.sh → test-set-0: 8/8 pass script-2.sh → test-set-1: 6/6 pass script-3.sh → test-set-2: 8/8 pass Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a84671c commit bffcb88

25 files changed

Lines changed: 1356 additions & 0 deletions

spring-aerospike/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target/
2+
keploy/

spring-aerospike/Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM eclipse-temurin:17-jdk
2+
WORKDIR /app
3+
RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/*
4+
COPY pom.xml /app/
5+
COPY src /app/src
6+
RUN mvn -q -DskipTests package
7+
EXPOSE 8090
8+
ENTRYPOINT ["java", "-jar", "target/spring-aerospike.jar"]

spring-aerospike/README.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# spring-aerospike — Aerospike-Java sample with Keploy record/replay
2+
3+
A Spring Boot 2.7 service that talks to Aerospike CE over the
4+
clear-text service port (3000) using the official
5+
`aerospike-client-jdk8`. Recorded and replayed end-to-end with
6+
Keploy via three bundled scripts that mirror the
7+
`keploy/samples-go/aerospike-tls` shape one-to-one — same endpoints,
8+
same test-set layout, same record-then-replay loop.
9+
10+
What the sample demonstrates:
11+
12+
* **Keploy records binary Aerospike protocol traffic** — Info,
13+
AS_MSG (single-record PUT/GET/TOUCH/DELETE), BATCH_READ/WRITE,
14+
SCAN, QUERY, UDF, CDT — and replays them from `mocks.yaml`
15+
without needing the real cluster.
16+
* **Replay stays deterministic at any concurrency the app exposes**
17+
single-client `/parallel`, multi-client round-robin, and per-
18+
request fresh-client construction all pass cleanly.
19+
* **A pipeline-friendly shape.** Three `scripts/script-{1,2,3}.sh`
20+
entry points each record and replay one test-set independently,
21+
so a CI matrix can call them as separate steps.
22+
23+
## Layout
24+
25+
```
26+
spring-aerospike/
27+
├── pom.xml # Spring Boot 2.7 + aerospike-client-jdk8
28+
├── src/main/java/com/example/aerospike/
29+
│ ├── SpringAerospikeApplication.java
30+
│ ├── config/ # client + multi-client pool, warmup, policies
31+
│ └── controller/ # one @RestController per endpoint group
32+
├── src/main/resources/
33+
│ └── application.properties # port + Aerospike host/namespace/pool sizing
34+
├── aerospike-conf/
35+
│ └── aerospike.conf # CE config: clear-text on 3000
36+
├── docker-compose.yml # Aerospike CE + the Spring Boot app
37+
├── Dockerfile # eclipse-temurin 17 + mvn package
38+
├── keploy.yml # Keploy CLI config (command, ports)
39+
└── scripts/
40+
├── common.sh # shared boot/build/record/replay/normalise
41+
├── script-1.sh # records + replays test-set-0 (CRUD)
42+
├── script-2.sh # records + replays test-set-1 (/parallel)
43+
└── script-3.sh # records + replays test-set-2 (/multiclient + /freshclient)
44+
```
45+
46+
There is no committed `keploy/` directory — the scripts produce it
47+
from scratch every run. Each CI run validates the full
48+
record-then-replay loop instead of replaying stale captures.
49+
50+
## Endpoints
51+
52+
| Method | Path | What it does |
53+
| ------ | -------------------------- | ---------------------------------------------------------------------------- |
54+
| GET | `/health` | `info build + namespaces` |
55+
| POST | `/put` | single-record PUT |
56+
| GET | `/get/{key}` | single-record GET |
57+
| POST | `/batch/put` | sequential write loop |
58+
| GET | `/batch/get?k=a&k=b` | BATCH_READ |
59+
| POST | `/scan` | full namespace scan |
60+
| POST | `/query` | secondary-index range query |
61+
| POST | `/udf` | UDF_EXECUTE |
62+
| POST | `/cdt/list/append` | CDT list append |
63+
| POST | `/cdt/map/put` | CDT map put |
64+
| POST | `/touch/{key}` | TOUCH |
65+
| DELETE | `/key/{key}` | DELETE |
66+
| POST | `/parallel?n=N&prefix=P` | fans out N threads, each PUT+GET on a unique key — **one shared client** |
67+
| POST | `/multiclient?n=N&prefix=P`| same, but round-robins across **4 pre-built `AerospikeClient` instances** |
68+
| POST | `/freshclient?n=N&prefix=P`| **each thread builds its own `AerospikeClient`** inside the request |
69+
70+
## Run it manually
71+
72+
```bash
73+
# 1) Boot Aerospike CE on clear-text 3000.
74+
docker compose up -d aerospike
75+
76+
# 2) Build + run the Spring Boot app.
77+
mvn -q -DskipTests package
78+
java -jar target/spring-aerospike.jar
79+
80+
# 3) Hit it.
81+
curl -s localhost:8090/health
82+
curl -s -XPOST localhost:8090/put -H 'Content-Type: application/json' \
83+
-d '{"key":"alice","bins":{"age":30}}'
84+
curl -s localhost:8090/get/alice
85+
curl -s -XPOST 'localhost:8090/parallel?n=24&prefix=run1'
86+
curl -s -XPOST 'localhost:8090/multiclient?n=24&prefix=mc1'
87+
curl -s -XPOST 'localhost:8090/freshclient?n=8&prefix=fc1'
88+
```
89+
90+
## Record + replay with the scripts
91+
92+
```bash
93+
# Each script is self-contained: brings up Aerospike, builds the
94+
# JAR, records, replays. Exit code is non-zero if any case fails on
95+
# replay.
96+
sudo ./scripts/script-1.sh # test-set-0: single-endpoint CRUD
97+
sudo ./scripts/script-2.sh # test-set-1: /parallel n = 4..24
98+
sudo ./scripts/script-3.sh # test-set-2: /multiclient + /freshclient
99+
```
100+
101+
Pipeline-friendly knobs (env vars):
102+
103+
| Var | Default | What it does |
104+
|--------------|---------------|---------------------------------------------------------------|
105+
| `KEPLOY` | `sudo keploy` | binary + auth invocation. Override to `keploy` if root |
106+
| `PORT` | `8090` | HTTP port the recorded sample listens on |
107+
| `LOG_DIR` | `/tmp` | where to drop the keploy record log |
108+
| `SKIP_DOCKER`| (unset) | `=1` skips `docker compose up -d aerospike` (already running) |
109+
| `SKIP_BUILD` | (unset) | `=1` skips `mvn package` (JAR already in target/) |
110+
111+
## Concurrency notes — why the warmup + retry matter
112+
113+
Mocked replay through Keploy is roughly 10–20× faster than real
114+
Aerospike for the same op. A burst of N concurrent threads on a
115+
cold client pool then races to open N fresh sockets, and the
116+
thread that loses the race surfaces as `MAX_RETRIES_EXCEEDED` at
117+
the application — even though every peer in the same burst
118+
succeeds.
119+
120+
`AerospikeConfig` paints over this with four layered changes;
121+
together they make `/parallel?n=24`, `/multiclient?n=24`, and
122+
`/freshclient?n=8` replay clean on every run:
123+
124+
1. **Sized pool**`ClientPolicy.maxConnsPerNode = 256`. The
125+
`OpeningConnectionThreshold` analogue is kept low (16) so a
126+
sudden burst doesn't outpace upstream connect rate.
127+
2. **Tolerant per-op policy**`Policies.parallelWrite()` and
128+
`Policies.parallelRead()` set `socketTimeout 10s`, `totalTimeout
129+
30s`, `maxRetries 10`, `sleepBetweenRetries 5ms`.
130+
3. **Two-phase warmup** on the main client at startup: a sequential
131+
prelude that walks the cluster past cold-start latencies,
132+
followed by a parallel fill that puts idle connections in the
133+
pool before the HTTP server accepts the first request.
134+
4. **App-level retry wrapper** (`RetryHelper.doOp`) around each PUT
135+
and GET in `/parallel`, `/multiclient`, and `/freshclient`.
136+
137+
`/multiclient`'s extra clients are deliberately NOT warmed at
138+
startup — a hundred concurrent dials at boot can stall a record-
139+
time proxy. The retry wrapper covers their first burst instead.
140+
141+
This sample is the Java counterpart of
142+
[`keploy/samples-go/aerospike-tls`](https://github.com/keploy/samples-go/tree/main/aerospike-tls);
143+
the script set is byte-for-byte the same shape so a single CI
144+
matrix can drive both languages with the same harness.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Aerospike CE config — clear-text on port 3000.
2+
3+
service {
4+
proto-fd-max 15000
5+
cluster-name spring-aerospike-sample
6+
}
7+
8+
logging {
9+
console {
10+
context any info
11+
}
12+
}
13+
14+
network {
15+
service {
16+
address any
17+
port 3000
18+
}
19+
20+
heartbeat {
21+
mode mesh
22+
address local
23+
port 3002
24+
interval 150
25+
timeout 10
26+
}
27+
28+
fabric {
29+
address local
30+
port 3001
31+
}
32+
33+
info {
34+
port 3003
35+
}
36+
}
37+
38+
namespace test {
39+
replication-factor 1
40+
default-ttl 30d
41+
nsup-period 120
42+
storage-engine memory {
43+
data-size 1G
44+
}
45+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Aerospike CE on clear-text 3000 + the Spring Boot sample on 8090.
2+
services:
3+
aerospike:
4+
image: aerospike/aerospike-server:7.2.0.1
5+
container_name: aerospike
6+
networks:
7+
- keploy-network
8+
ports:
9+
- "3000:3000"
10+
volumes:
11+
- ./aerospike-conf/aerospike.conf:/etc/aerospike/aerospike.conf:ro
12+
entrypoint: ["/usr/bin/asd", "--foreground", "--config-file", "/etc/aerospike/aerospike.conf"]
13+
command: []
14+
ulimits:
15+
nofile:
16+
soft: 65536
17+
hard: 65536
18+
healthcheck:
19+
test: ["CMD", "asinfo", "-h", "127.0.0.1", "-p", "3000", "-v", "build"]
20+
interval: 5s
21+
timeout: 3s
22+
retries: 20
23+
24+
app:
25+
build:
26+
context: .
27+
dockerfile: Dockerfile
28+
container_name: spring-aerospike
29+
depends_on:
30+
aerospike:
31+
condition: service_healthy
32+
environment:
33+
AEROSPIKE_HOST: aerospike
34+
AEROSPIKE_PORT: "3000"
35+
LISTEN_PORT: "8090"
36+
ports:
37+
- "8090:8090"
38+
networks:
39+
- keploy-network
40+
41+
networks:
42+
keploy-network:

spring-aerospike/keploy.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
path: ""
2+
appId: 0
3+
appName: spring-aerospike
4+
command: java -jar target/spring-aerospike.jar
5+
templatize:
6+
testSets: []
7+
port: 0
8+
dnsPort: 26789
9+
proxyPort: 16789
10+
incomingProxyPort: 36789
11+
debug: false
12+
disableTele: false
13+
disableANSI: false
14+
containerName: ""
15+
networkName: ""
16+
buildDelay: 30
17+
test:
18+
selectedTests: {}
19+
globalNoise:
20+
global: {}
21+
test-sets: {}
22+
delay: 15
23+
apiTimeout: 5
24+
skipCoverage: false
25+
coverageReportPath: ""
26+
ignoreOrdering: true
27+
mongoPassword: default@123
28+
language: ""
29+
removeUnusedMocks: false
30+
fallBackOnMiss: false
31+
jacocoAgentPath: ""
32+
basePath: ""
33+
mocking: true
34+
ignoredTests: {}
35+
strictMockWindow: true
36+
record:
37+
filters: []
38+
basePath: ""
39+
recordTimer: 0s
40+
metadata: ""
41+
testCaseNaming: descriptive
42+
report:
43+
selectedTestSets: {}
44+
showFullBody: false
45+
reportPath: ""
46+
summary: false
47+
testCaseIDs: []
48+
format: ""
49+
keployContainer: keploy-v3
50+
keployNetwork: keploy-network
51+
cmdType: native

spring-aerospike/pom.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.springframework.boot</groupId>
9+
<artifactId>spring-boot-starter-parent</artifactId>
10+
<version>2.7.18</version>
11+
<relativePath/>
12+
</parent>
13+
14+
<groupId>com.example</groupId>
15+
<artifactId>spring-aerospike</artifactId>
16+
<version>0.0.1-SNAPSHOT</version>
17+
<name>spring-aerospike</name>
18+
<description>Aerospike-Java sample for Keploy record/replay</description>
19+
20+
<properties>
21+
<java.version>17</java.version>
22+
</properties>
23+
24+
<dependencies>
25+
<dependency>
26+
<groupId>org.springframework.boot</groupId>
27+
<artifactId>spring-boot-starter-web</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>com.aerospike</groupId>
31+
<artifactId>aerospike-client-jdk8</artifactId>
32+
<version>9.0.5</version>
33+
</dependency>
34+
</dependencies>
35+
36+
<build>
37+
<finalName>spring-aerospike</finalName>
38+
<plugins>
39+
<plugin>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-maven-plugin</artifactId>
42+
</plugin>
43+
</plugins>
44+
</build>
45+
</project>

0 commit comments

Comments
 (0)