Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions spring-aerospike/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
keploy/
8 changes: 8 additions & 0 deletions spring-aerospike/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM eclipse-temurin:17-jdk
WORKDIR /app
RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/*
COPY pom.xml /app/
COPY src /app/src
RUN mvn -q -DskipTests package
EXPOSE 8090
ENTRYPOINT ["java", "-jar", "target/spring-aerospike.jar"]
144 changes: 144 additions & 0 deletions spring-aerospike/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# spring-aerospike — Aerospike-Java sample with Keploy record/replay

A Spring Boot 2.7 service that talks to Aerospike CE over the
clear-text service port (3000) using the official
`aerospike-client-jdk8`. Recorded and replayed end-to-end with
Keploy via three bundled scripts that mirror the
`keploy/samples-go/aerospike-tls` shape one-to-one — same endpoints,
same test-set layout, same record-then-replay loop.

What the sample demonstrates:

* **Keploy records binary Aerospike protocol traffic** — Info,
AS_MSG (single-record PUT/GET/TOUCH/DELETE), BATCH_READ/WRITE,
SCAN, QUERY, UDF, CDT — and replays them from `mocks.yaml`
without needing the real cluster.
* **Replay stays deterministic at any concurrency the app exposes** —
single-client `/parallel`, multi-client round-robin, and per-
request fresh-client construction all pass cleanly.
* **A pipeline-friendly shape.** Three `scripts/script-{1,2,3}.sh`
entry points each record and replay one test-set independently,
so a CI matrix can call them as separate steps.

## Layout

```
spring-aerospike/
├── pom.xml # Spring Boot 2.7 + aerospike-client-jdk8
├── src/main/java/com/example/aerospike/
│ ├── SpringAerospikeApplication.java
│ ├── config/ # client + multi-client pool, warmup, policies
│ └── controller/ # one @RestController per endpoint group
├── src/main/resources/
│ └── application.properties # port + Aerospike host/namespace/pool sizing
├── aerospike-conf/
│ └── aerospike.conf # CE config: clear-text on 3000
├── docker-compose.yml # Aerospike CE + the Spring Boot app
├── Dockerfile # eclipse-temurin 17 + mvn package
├── keploy.yml # Keploy CLI config (command, ports)
└── scripts/
├── common.sh # shared boot/build/record/replay/normalise
├── script-1.sh # records + replays test-set-0 (CRUD)
├── script-2.sh # records + replays test-set-1 (/parallel)
└── script-3.sh # records + replays test-set-2 (/multiclient + /freshclient)
```

There is no committed `keploy/` directory — the scripts produce it
from scratch every run. Each CI run validates the full
record-then-replay loop instead of replaying stale captures.

## Endpoints

| Method | Path | What it does |
| ------ | -------------------------- | ---------------------------------------------------------------------------- |
| GET | `/health` | `info build + namespaces` |
| POST | `/put` | single-record PUT |
| GET | `/get/{key}` | single-record GET |
| POST | `/batch/put` | sequential write loop |
| GET | `/batch/get?k=a&k=b` | BATCH_READ |
| POST | `/scan` | full namespace scan |
| POST | `/query` | secondary-index range query |
| POST | `/udf` | UDF_EXECUTE |
| POST | `/cdt/list/append` | CDT list append |
| POST | `/cdt/map/put` | CDT map put |
| POST | `/touch/{key}` | TOUCH |
| DELETE | `/key/{key}` | DELETE |
| POST | `/parallel?n=N&prefix=P` | fans out N threads, each PUT+GET on a unique key — **one shared client** |
| POST | `/multiclient?n=N&prefix=P`| same, but round-robins across **4 pre-built `AerospikeClient` instances** |
| POST | `/freshclient?n=N&prefix=P`| **each thread builds its own `AerospikeClient`** inside the request |

## Run it manually

```bash
# 1) Boot Aerospike CE on clear-text 3000.
docker compose up -d aerospike

# 2) Build + run the Spring Boot app.
mvn -q -DskipTests package
java -jar target/spring-aerospike.jar

# 3) Hit it.
curl -s localhost:8090/health
curl -s -XPOST localhost:8090/put -H 'Content-Type: application/json' \
-d '{"key":"alice","bins":{"age":30}}'
curl -s localhost:8090/get/alice
curl -s -XPOST 'localhost:8090/parallel?n=24&prefix=run1'
curl -s -XPOST 'localhost:8090/multiclient?n=24&prefix=mc1'
curl -s -XPOST 'localhost:8090/freshclient?n=8&prefix=fc1'
```

## Record + replay with the scripts

```bash
# Each script is self-contained: brings up Aerospike, builds the
# JAR, records, replays. Exit code is non-zero if any case fails on
# replay.
sudo ./scripts/script-1.sh # test-set-0: single-endpoint CRUD
sudo ./scripts/script-2.sh # test-set-1: /parallel n = 4..24
sudo ./scripts/script-3.sh # test-set-2: /multiclient + /freshclient
```

Pipeline-friendly knobs (env vars):

| Var | Default | What it does |
|--------------|---------------|---------------------------------------------------------------|
| `KEPLOY` | `sudo keploy` | binary + auth invocation. Override to `keploy` if root |
| `PORT` | `8090` | HTTP port the recorded sample listens on |
| `LOG_DIR` | `/tmp` | where to drop the keploy record log |
| `SKIP_DOCKER`| (unset) | `=1` skips `docker compose up -d aerospike` (already running) |
| `SKIP_BUILD` | (unset) | `=1` skips `mvn package` (JAR already in target/) |

## Concurrency notes — why the warmup + retry matter

Mocked replay through Keploy is roughly 10–20× faster than real
Aerospike for the same op. A burst of N concurrent threads on a
cold client pool then races to open N fresh sockets, and the
thread that loses the race surfaces as `MAX_RETRIES_EXCEEDED` at
the application — even though every peer in the same burst
succeeds.

`AerospikeConfig` paints over this with four layered changes;
together they make `/parallel?n=24`, `/multiclient?n=24`, and
`/freshclient?n=8` replay clean on every run:

1. **Sized pool** — `ClientPolicy.maxConnsPerNode = 256`. The
`OpeningConnectionThreshold` analogue is kept low (16) so a
sudden burst doesn't outpace upstream connect rate.
2. **Tolerant per-op policy** — `Policies.parallelWrite()` and
`Policies.parallelRead()` set `socketTimeout 10s`, `totalTimeout
30s`, `maxRetries 10`, `sleepBetweenRetries 5ms`.
3. **Two-phase warmup** on the main client at startup: a sequential
prelude that walks the cluster past cold-start latencies,
followed by a parallel fill that puts idle connections in the
pool before the HTTP server accepts the first request.
4. **App-level retry wrapper** (`RetryHelper.doOp`) around each PUT
and GET in `/parallel`, `/multiclient`, and `/freshclient`.

`/multiclient`'s extra clients are deliberately NOT warmed at
startup — a hundred concurrent dials at boot can stall a record-
time proxy. The retry wrapper covers their first burst instead.

This sample is the Java counterpart of
[`keploy/samples-go/aerospike-tls`](https://github.com/keploy/samples-go/tree/main/aerospike-tls);
the script set is byte-for-byte the same shape so a single CI
matrix can drive both languages with the same harness.
45 changes: 45 additions & 0 deletions spring-aerospike/aerospike-conf/aerospike.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Aerospike CE config — clear-text on port 3000.

service {
proto-fd-max 15000
cluster-name spring-aerospike-sample
}

logging {
console {
context any info
}
}

network {
service {
address any
port 3000
}

heartbeat {
mode mesh
address local
port 3002
interval 150
timeout 10
}

fabric {
address local
port 3001
}

info {
port 3003
}
}

namespace test {
replication-factor 1
default-ttl 30d
nsup-period 120
storage-engine memory {
data-size 1G
}
}
42 changes: 42 additions & 0 deletions spring-aerospike/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Aerospike CE on clear-text 3000 + the Spring Boot sample on 8090.
services:
aerospike:
image: aerospike/aerospike-server:7.2.0.1
container_name: aerospike
networks:
- keploy-network
ports:
- "3000:3000"
volumes:
- ./aerospike-conf/aerospike.conf:/etc/aerospike/aerospike.conf:ro
entrypoint: ["/usr/bin/asd", "--foreground", "--config-file", "/etc/aerospike/aerospike.conf"]
command: []
ulimits:
nofile:
soft: 65536
hard: 65536
healthcheck:
test: ["CMD", "asinfo", "-h", "127.0.0.1", "-p", "3000", "-v", "build"]
interval: 5s
timeout: 3s
retries: 20

app:
build:
context: .
dockerfile: Dockerfile
container_name: spring-aerospike
depends_on:
aerospike:
condition: service_healthy
environment:
AEROSPIKE_HOST: aerospike
AEROSPIKE_PORT: "3000"
LISTEN_PORT: "8090"
ports:
- "8090:8090"
networks:
- keploy-network

networks:
keploy-network:
51 changes: 51 additions & 0 deletions spring-aerospike/keploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
path: ""
appId: 0
appName: spring-aerospike
command: java -jar target/spring-aerospike.jar
templatize:
testSets: []
port: 0
dnsPort: 26789
proxyPort: 16789
incomingProxyPort: 36789
debug: false
disableTele: false
disableANSI: false
containerName: ""
networkName: ""
buildDelay: 30
test:
selectedTests: {}
globalNoise:
global: {}
test-sets: {}
delay: 15
apiTimeout: 5
skipCoverage: false
coverageReportPath: ""
ignoreOrdering: true
mongoPassword: default@123
language: ""
removeUnusedMocks: false
fallBackOnMiss: false
jacocoAgentPath: ""
basePath: ""
mocking: true
ignoredTests: {}
strictMockWindow: true
record:
filters: []
basePath: ""
recordTimer: 0s
metadata: ""
testCaseNaming: descriptive
report:
selectedTestSets: {}
showFullBody: false
reportPath: ""
summary: false
testCaseIDs: []
format: ""
keployContainer: keploy-v3
keployNetwork: keploy-network
cmdType: native
45 changes: 45 additions & 0 deletions spring-aerospike/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>spring-aerospike</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-aerospike</name>
<description>Aerospike-Java sample for Keploy record/replay</description>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.aerospike</groupId>
<artifactId>aerospike-client-jdk8</artifactId>
<version>9.0.5</version>
</dependency>
</dependencies>

<build>
<finalName>spring-aerospike</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Loading
Loading