Skip to content

Commit 6593774

Browse files
Volchkov AndreyFameing
Volchkov Andrey
authored andcommitted
feat: added module embedded aerospike enterprise with configured mandatory durable deletes by default
1 parent 3c1bf21 commit 6593774

20 files changed

+570
-2
lines changed
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
=== embedded-aerospike-enterprise
2+
3+
TIP: This module provides integration with https://github.com/Shopify/toxiproxy[ToxiProxy] out of the box.
4+
ToxiProxy is a great tool for simulating network conditions, meaning that you can test your application's resiliency.
5+
6+
==== Difference with `embedded-aerospike` module
7+
8+
* Aerospike Enterprise container version must be not less then 6.3.0, because of option for disallow non-durable deletes.
9+
* By default disallow [non-durable deletes](#Disallow non-durable deletes).
10+
11+
==== Maven dependency
12+
13+
.pom.xml
14+
[source,xml]
15+
----
16+
<dependency>
17+
<groupId>com.playtika.testcontainers</groupId>
18+
<artifactId>embedded-aerospike-enterprise</artifactId>
19+
<scope>test</scope>
20+
</dependency>
21+
----
22+
23+
==== Consumes (via `bootstrap.properties`)
24+
25+
* `embedded.aerospike.enabled` `(true|false, default is 'true')`
26+
* `embedded.aerospike.reuseContainer` `(true|false, default is 'false')`
27+
* `embedded.aerospike.dockerImage` `(default is set to 'aerospike/aerospike-server-enterprise:6.3.0.16_1')`
28+
** Aerospike Enterprise version must be not less then 6.3.0
29+
* `embedded.aerospike.featureKey` base64 of a feature-key-file https://aerospike.com/docs/server/operations/configure/feature-key, default is null.
30+
**Warning: if not provided, the Aerospike Database EE evaluation feature key file will be used. That means you can use it internally only for Evaluation
31+
purposes only during the Evaluation Period**. See https://github.com/aerospike/aerospike-server.docker/blob/master/enterprise/ENTERPRISE_LICENSE`
32+
* `embedded.aerospike.waitTimeoutInSeconds` `(default is 60 seconds)`
33+
* `embedded.toxiproxy.proxies.aerospike.enabled` Enables both creation of the container with ToxiProxy TCP proxy and a proxy to the `embedded-aerospike` container.
34+
* `embedded.aerospike.time-travel.enabled` Enables time travel to clean expired documents by time. Does not work on ARM(mac m1) because of LUA scripts are not supported on ARM.
35+
* `embedded.aerospike.enterprise.durableDeletes` Enables disallow non-durable deletes for Aerospike Enterprise Server. By default is true.
36+
* https://mvnrepository.com/artifact/com.aerospike/aerospike-client[aerospike client library]
37+
38+
==== Produces
39+
40+
* `embedded.aerospike.host`
41+
* `embedded.aerospike.port`
42+
* `embedded.aerospike.namespace`
43+
* `embedded.aerospike.toxiproxy.host`
44+
* `embedded.aerospike.toxiproxy.port`
45+
* `embedded.aerospike.networkAlias`
46+
* `embedded.aerospike.internalPort`
47+
* Bean `AerospikeTestOperations aerospikeTestOperations`
48+
* Bean `ToxiproxyContainer.ContainerProxy aerospikeContainerProxy`
49+
50+
==== Example
51+
52+
See `embedded-aerospike` module readme for examples.
53+
54+
==== Enterprise features
55+
56+
===== Disallow non-durable deletes
57+
58+
Aerospike server never delete record from disk(SSD), but the index in memory that points to the record is removed.
59+
If the location on disk of the deleted record was not overwritten prior to reboot, the record will be indexed during cold restart.
60+
The record then returns from the disk to the database as a zombie record.
61+
62+
To avoid this, Aerospike provide a feature called https://aerospike.com/docs/server/guide/durable_deletes[Durable Deletes].
63+
Durable deletes typically free storage when they generate a tombstone, a record without any bins that contains all metadata including the key.
64+
Tombstones correctly resolve conflicts and prevent previously persisted versions of deleted objects from resurrecting when the index is repopulated.
65+
This feature is available only for Aerospike Enterprise Edition.
66+
67+
This library reconfigure com.playtika.testcontainers:embedded-aerospike
68+
to use Aerospike Enterprise Edition docker image, and set up Durable Deletes feature as mandatory.
69+
If the client call delete operation without setting WritePolicy.durableDeletes to true, the operation
70+
will fail with Aerospike Error 22 (forbidden). This is done by configuring aerospike server with option
71+
https://aerospike.com/docs/server/reference/configuration#disallow-expunge[disallow-expunge].

embedded-aerospike-enterprise/pom.xml

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<artifactId>testcontainers-spring-boot-parent</artifactId>
9+
<groupId>com.playtika.testcontainers</groupId>
10+
<version>3.1.1</version>
11+
<relativePath>../testcontainers-spring-boot-parent</relativePath>
12+
</parent>
13+
14+
<artifactId>embedded-aerospike-enterprise</artifactId>
15+
16+
<properties>
17+
<aerospike-client.version>7.2.0</aerospike-client.version>
18+
</properties>
19+
20+
<dependencies>
21+
<!--
22+
aerospike client is provided since we want users to pick own version,
23+
and not rely on test library
24+
-->
25+
<dependency>
26+
<groupId>com.aerospike</groupId>
27+
<artifactId>aerospike-client</artifactId>
28+
<version>${aerospike-client.version}</version>
29+
<scope>provided</scope>
30+
</dependency>
31+
<dependency>
32+
<groupId>com.playtika.testcontainers</groupId>
33+
<artifactId>embedded-aerospike</artifactId>
34+
</dependency>
35+
<dependency>
36+
<groupId>org.assertj</groupId>
37+
<artifactId>assertj-core</artifactId>
38+
<scope>test</scope>
39+
</dependency>
40+
</dependencies>
41+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.playtika.testcontainers.aerospike.enterprise;
2+
3+
import com.playtika.testcontainer.aerospike.AerospikeProperties;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.testcontainers.containers.Container;
7+
import org.testcontainers.containers.GenericContainer;
8+
9+
import java.io.IOException;
10+
11+
@RequiredArgsConstructor
12+
@Slf4j
13+
public class AerospikeEnterpriseConfigurer {
14+
15+
private final AerospikeProperties aerospikeProperties;
16+
private final AerospikeEnterpriseProperties enterpriseProperties;
17+
18+
public void configure(GenericContainer<?> aerospikeContainer) throws IOException, InterruptedException {
19+
if (aerospikeProperties.getFeatureKey() == null || aerospikeProperties.getFeatureKey().isBlank()) {
20+
log.warn("Evaluation feature key file not provided by 'embedded.aerospike.featureKey' property. " +
21+
"Pay attention to license details: https://github.com/aerospike/aerospike-server.docker/blob/master/enterprise/ENTERPRISE_LICENSE");
22+
}
23+
24+
setupDisallowExpunge(aerospikeContainer);
25+
}
26+
27+
private void setupDisallowExpunge(GenericContainer<?> aerospikeContainer) throws IOException, InterruptedException {
28+
if (!enterpriseProperties.isDurableDeletes()) {
29+
return;
30+
}
31+
log.info("Setting up 'disallow-expunge' to true...");
32+
String namespace = aerospikeProperties.getNamespace();
33+
Container.ExecResult result = aerospikeContainer.execInContainer("asadm", "-e",
34+
String.format("enable; manage config namespace %s param disallow-expunge to true", namespace));
35+
if (result.getStderr().length() > 0) {
36+
throw new IllegalStateException("Failed to set up 'disallow-expunge' to true: " + result.getStderr());
37+
}
38+
log.info("Set up 'disallow-expunge' to true: {}", result.getStdout());
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.playtika.testcontainers.aerospike.enterprise;
2+
3+
import lombok.Data;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
6+
@Data
7+
@ConfigurationProperties("embedded.aerospike.enterprise")
8+
public class AerospikeEnterpriseProperties {
9+
10+
boolean durableDeletes = true;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.playtika.testcontainers.aerospike.enterprise;
2+
3+
import com.aerospike.client.AerospikeClient;
4+
import com.playtika.testcontainer.aerospike.AerospikeExpiredDocumentsCleaner;
5+
import com.playtika.testcontainer.aerospike.AerospikeProperties;
6+
import com.playtika.testcontainer.aerospike.EmbeddedAerospikeTestOperationsAutoConfiguration;
7+
import com.playtika.testcontainer.aerospike.ExpiredDocumentsCleaner;
8+
import org.springframework.boot.autoconfigure.AutoConfiguration;
9+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12+
import org.springframework.context.annotation.Bean;
13+
14+
@AutoConfiguration(afterName = "org.springframework.boot.autoconfigure.aerospike.AerospikeAutoConfiguration",
15+
before = EmbeddedAerospikeTestOperationsAutoConfiguration.class)
16+
@ConditionalOnExpression("${embedded.containers.enabled:true}")
17+
@ConditionalOnBean({AerospikeClient.class, AerospikeProperties.class})
18+
@ConditionalOnProperty(value = "embedded.aerospike.enabled", matchIfMissing = true)
19+
public class EnterpriseAerospikeTestOperationsAutoConfiguration {
20+
21+
@Bean
22+
@ConditionalOnProperty(value = "embedded.aerospike.time-travel.enabled", havingValue = "true", matchIfMissing = true)
23+
public ExpiredDocumentsCleaner expiredDocumentsCleaner(AerospikeClient client,
24+
AerospikeProperties properties) {
25+
return new AerospikeExpiredDocumentsCleaner(client, properties.getNamespace(), true);
26+
}
27+
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.playtika.testcontainers.aerospike.enterprise;
2+
3+
import lombok.NonNull;
4+
5+
import java.util.Comparator;
6+
7+
record ImageVersion (int major, int minor) implements Comparable<ImageVersion> {
8+
9+
static ImageVersion parse(String version) {
10+
String[] parts = version.split("\\.");
11+
if (parts.length < 2) {
12+
throw new IllegalArgumentException("Invalid version: " + version);
13+
}
14+
try {
15+
int major = Integer.parseInt(parts[0]);
16+
int minor = Integer.parseInt(parts[1]);
17+
return new ImageVersion(major, minor);
18+
} catch (NumberFormatException e) {
19+
throw new IllegalArgumentException("Invalid version: " + version, e);
20+
}
21+
}
22+
23+
@Override
24+
public int compareTo(@NonNull ImageVersion o) {
25+
return Comparator.comparingInt(ImageVersion::major)
26+
.thenComparingInt(ImageVersion::minor)
27+
.compare(this, o);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.playtika.testcontainers.aerospike.enterprise;
2+
3+
import com.playtika.testcontainer.aerospike.AerospikeProperties;
4+
import com.playtika.testcontainer.aerospike.EmbeddedAerospikeBootstrapConfiguration;
5+
import jakarta.annotation.PostConstruct;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.beans.factory.annotation.Qualifier;
9+
import org.springframework.boot.autoconfigure.AutoConfiguration;
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
12+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
13+
import org.springframework.context.annotation.PropertySource;
14+
import org.springframework.core.env.Environment;
15+
import org.testcontainers.containers.GenericContainer;
16+
17+
import java.io.IOException;
18+
19+
@Slf4j
20+
@AutoConfiguration(after = EmbeddedAerospikeBootstrapConfiguration.class)
21+
@ConditionalOnExpression("${embedded.containers.enabled:true}")
22+
@ConditionalOnProperty(value = "embedded.aerospike.enabled", matchIfMissing = true)
23+
@EnableConfigurationProperties(AerospikeEnterpriseProperties.class)
24+
@PropertySource("classpath:/embedded-enterprise-aerospike.properties")
25+
public class SetupEnterpriseAerospikeBootstrapConfiguration {
26+
27+
private static final String DOCKER_IMAGE = "aerospike/aerospike-server-enterprise:6.3.0.16_1";
28+
private static final String AEROSPIKE_DOCKER_IMAGE_PROPERTY = "embedded.aerospike.dockerImage";
29+
private static final ImageVersion SUITABLE_IMAGE_VERSION = new ImageVersion(6, 3);
30+
private static final String TEXT_TO_DOCUMENTATION = "Documentation: https://github.com/PlaytikaOSS/testcontainers-spring-boot/blob/develop/embedded-aerospike-enterprise/README.adoc";
31+
32+
private GenericContainer<?> aerospikeContainer;
33+
private AerospikeProperties aerospikeProperties;
34+
private AerospikeEnterpriseProperties aerospikeEnterpriseProperties;
35+
private Environment environment;
36+
37+
@Autowired
38+
public void setEnvironment(Environment environment) {
39+
this.environment = environment;
40+
}
41+
42+
@Autowired
43+
@Qualifier(AerospikeProperties.BEAN_NAME_AEROSPIKE)
44+
public void setAerospikeContainer(GenericContainer<?> aerospikeContainer) {
45+
this.aerospikeContainer = aerospikeContainer;
46+
}
47+
48+
@Autowired
49+
public void setAerospikeProperties(AerospikeProperties aerospikeProperties) {
50+
this.aerospikeProperties = aerospikeProperties;
51+
}
52+
53+
@Autowired
54+
public void setAerospikeEnterpriseProperties(AerospikeEnterpriseProperties aerospikeEnterpriseProperties) {
55+
this.aerospikeEnterpriseProperties = aerospikeEnterpriseProperties;
56+
}
57+
58+
@PostConstruct
59+
public void setupEnterpriseAerospike() throws IOException, InterruptedException {
60+
verifyAerospikeImage();
61+
AerospikeEnterpriseConfigurer aerospikeEnterpriseConfigurer = new AerospikeEnterpriseConfigurer(aerospikeProperties, aerospikeEnterpriseProperties);
62+
aerospikeEnterpriseConfigurer.configure(aerospikeContainer);
63+
}
64+
65+
private void verifyAerospikeImage() {
66+
log.info("Verify Aerospike Enterprise Image");
67+
68+
String dockerImage = environment.getProperty(AEROSPIKE_DOCKER_IMAGE_PROPERTY);
69+
if (dockerImage == null) {
70+
throw new IllegalStateException("Aerospike enterprise docker image not provided, set up 'embedded.aerospike.dockerImage' property.\n"
71+
+ TEXT_TO_DOCUMENTATION);
72+
}
73+
74+
if (!isEnterpriseImage(dockerImage)) {
75+
throw illegalAerospikeImageException();
76+
}
77+
}
78+
79+
private IllegalStateException illegalAerospikeImageException() {
80+
return new IllegalStateException(
81+
"You should use enterprise image for the Aerospike container with equal or higher version: " + DOCKER_IMAGE + ". "
82+
+ "Container enable 'disallow-expunge' config option to prevent non-durable deletes, and this config option is available starting with version 6.3. "
83+
+ "Enterprise image is required, as non-durable deletes are not available in Community."
84+
+ TEXT_TO_DOCUMENTATION
85+
);
86+
}
87+
88+
private boolean isEnterpriseImage(String dockerImage) {
89+
return dockerImage.contains("enterprise")
90+
&& isSuitableVersion(dockerImage);
91+
}
92+
93+
private boolean isSuitableVersion(String dockerImage) {
94+
int index = dockerImage.indexOf(":");
95+
if (index == -1) {
96+
throw new IllegalStateException("Invalid docker image version format: " + dockerImage + ".\n"
97+
+ TEXT_TO_DOCUMENTATION);
98+
}
99+
String version = dockerImage.substring(index + 1);
100+
ImageVersion imageVersion = ImageVersion.parse(version);
101+
return imageVersion.compareTo(SUITABLE_IMAGE_VERSION) >= 0;
102+
}
103+
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
2+
com.playtika.testcontainers.aerospike.enterprise.SetupEnterpriseAerospikeBootstrapConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.playtika.testcontainers.aerospike.enterprise.EnterpriseAerospikeTestOperationsAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
embedded.aerospike.dockerImage=aerospike/aerospike-server-enterprise:6.3.0.16_1

0 commit comments

Comments
 (0)