Skip to content

Commit

Permalink
add using Kryo to serialize cached objects
Browse files Browse the repository at this point in the history
  • Loading branch information
agrgr committed Jun 16, 2024
1 parent 4c9014e commit 6bb851b
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 20 deletions.
18 changes: 16 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<logback.test>1.5.6</logback.test>
<hibernate.validator>8.0.1.Final</hibernate.validator>
<netty.version>4.1.110.Final</netty.version>
<kryo.version>5.6.0</kryo.version>
</properties>

<licenses>
Expand Down Expand Up @@ -210,6 +211,11 @@
<artifactId>lombok</artifactId>
<version>${lombok}</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>${kryo.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down Expand Up @@ -243,6 +249,10 @@
<artifactId>aerospike-reactor-client</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
Expand Down Expand Up @@ -392,9 +402,13 @@
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<argLine>--add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED
<argLine>
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED</argLine>
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,22 @@
import com.aerospike.client.Record;
import com.aerospike.client.policy.RecordExistsAction;
import com.aerospike.client.policy.WritePolicy;
import com.esotericsoftware.kryo.Kryo;
import org.objenesis.strategy.StdInstantiatorStrategy;
import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.data.aerospike.convert.AerospikeConverter;
import org.springframework.data.aerospike.convert.AerospikeReadData;
import org.springframework.data.aerospike.convert.AerospikeWriteData;
import org.springframework.data.aerospike.core.WritePolicyBuilder;

import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.concurrent.Callable;

import static org.springframework.data.aerospike.cache.CacheUtils.serialize;
import static org.springframework.data.aerospike.cache.CacheUtils.sha256;

/**
* A Cache {@link org.springframework.cache.Cache} implementation backed by Aerospike database as store. Create and
* configure Aerospike cache instances via {@link AerospikeCacheManager}.
Expand All @@ -47,6 +52,7 @@ public class AerospikeCache implements Cache {
private final AerospikeCacheConfiguration cacheConfiguration;
private final WritePolicy createOnly;
private final WritePolicy writePolicyForPut;
private final Kryo kryoInstance = new Kryo();

public AerospikeCache(String name,
IAerospikeClient client,
Expand All @@ -63,6 +69,22 @@ public AerospikeCache(String name,
this.writePolicyForPut = WritePolicyBuilder.builder(client.getWritePolicyDefault())
.expiration(cacheConfiguration.getExpirationInSeconds())
.build();
configureKryo(kryoInstance);
}

/**
* Configuration for Kryo.
* <p>
* Classes of the objects to be cached can be pre-registered if required. Registering in advance is not necessary,
* however it can be done to increase serialization performance. If a class has been pre-registered, the first time
* it is encountered Kryo can just output a numeric reference to it instead of writing fully qualified class name.
*
* @param kryoInstance Instance of Kryo in use
*/
public void configureKryo(Kryo kryoInstance) {
// setting to false means not requiring registration for all the classes of cached objects in advance
kryoInstance.setRegistrationRequired(false);
kryoInstance.setInstantiatorStrategy(new StdInstantiatorStrategy());
}

/**
Expand Down Expand Up @@ -197,11 +219,15 @@ public ValueWrapper putIfAbsent(Object key, Object value) {
}

private Key getKey(Object key) {
int userKey = (key instanceof SimpleKey && key.equals(SimpleKey.EMPTY))
// return hash code of key.toString() (hash code of key itself can be equal to 1) when key is empty
? key.toString().hashCode()
: key.hashCode();
return new Key(cacheConfiguration.getNamespace(), cacheConfiguration.getSet(), userKey);
return new Key(cacheConfiguration.getNamespace(), cacheConfiguration.getSet(), serializeAndHash(key));
}

private String serializeAndHash(Object key) {
try {
return sha256(serialize(key, kryoInstance));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}

private void serializeAndPut(WritePolicy writePolicy, Object key, Object value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.springframework.data.aerospike.cache;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.ByteBufferOutput;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class CacheUtils {

private CacheUtils() {
}

protected static byte[] serialize(Object key, Kryo kryoInstance) {
ByteBufferOutput output = new ByteBufferOutput(1024); // Initial buffer size
kryoInstance.writeClassAndObject(output, key);
output.flush();
return output.toBytes();
}

protected static String sha256(byte[] data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
return bytesToHex(hash);
}

private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@

import com.aerospike.client.IAerospikeClient;
import com.aerospike.client.Key;
import com.esotericsoftware.kryo.Kryo;
import lombok.AllArgsConstructor;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.objenesis.strategy.StdInstantiatorStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
Expand All @@ -29,11 +32,14 @@
import org.springframework.data.aerospike.core.AerospikeOperations;
import org.springframework.data.aerospike.util.AwaitilityUtils;

import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.data.aerospike.cache.CacheUtils.serialize;
import static org.springframework.data.aerospike.cache.CacheUtils.sha256;
import static org.springframework.data.aerospike.util.AwaitilityUtils.awaitTenSecondsUntil;

public class AerospikeCacheManagerIntegrationTests extends BaseBlockingIntegrationTests {
Expand All @@ -44,6 +50,7 @@ public class AerospikeCacheManagerIntegrationTests extends BaseBlockingIntegrati
private static final Map<String, String> MAP_PARAM =
Map.of("1", "val1", "2", "val2", "3", "val3", "4", "val4");
private static final String VALUE = "bar";
private static final Kryo kryoInstance = new Kryo();

@Autowired
IAerospikeClient client;
Expand All @@ -54,27 +61,42 @@ public class AerospikeCacheManagerIntegrationTests extends BaseBlockingIntegrati
@Autowired
AerospikeCacheManager aerospikeCacheManager;

@BeforeAll
public static void beforeAll() {
// setting to false means not requiring registration for all the classes of cached objects in advance
kryoInstance.setRegistrationRequired(false);
kryoInstance.setInstantiatorStrategy(new StdInstantiatorStrategy());
}

@BeforeEach
public void setup() throws NoSuchMethodException {
cachingComponent.reset();
deleteRecords();
}

private void deleteRecords() throws NoSuchMethodException {
List<Integer> hashCodes = List.of(
STRING_PARAM.hashCode(),
STRING_PARAM_THAT_MATCHES_CONDITION.hashCode(),
Long.hashCode(NUMERIC_PARAM),
MAP_PARAM.hashCode(),
new SimpleKey(STRING_PARAM, NUMERIC_PARAM, MAP_PARAM).hashCode(),
SimpleKey.EMPTY.toString().hashCode(),
CachingComponent.class.hashCode(),
CachingComponent.class.getMethod("cacheableMethodWithMethodNameKey").hashCode()
List<Object> params = List.of(
STRING_PARAM,
STRING_PARAM_THAT_MATCHES_CONDITION,
NUMERIC_PARAM,
MAP_PARAM,
new SimpleKey(STRING_PARAM, NUMERIC_PARAM, MAP_PARAM),
SimpleKey.EMPTY,
CachingComponent.class,
CachingComponent.class.getMethod("cacheableMethodWithMethodNameKey")
);
for (int hash : hashCodes) {
client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, hash));
for (Object param : params) {
client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, serializeAndHash(param)));
}
client.delete(null, new Key(getNameSpace(), DIFFERENT_SET_NAME, serializeAndHash(STRING_PARAM)));
}

private String serializeAndHash(Object param) {
try {
return sha256(serialize(param, kryoInstance));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
client.delete(null, new Key(getNameSpace(), DIFFERENT_SET_NAME, STRING_PARAM.hashCode()));
}

@Test
Expand Down Expand Up @@ -321,6 +343,7 @@ public void shouldCacheUsingAnotherCacheManager() {
@Test
public void shouldNotClearCacheClearingDifferentCache() {
assertThat(aerospikeOperations.count(DIFFERENT_SET_NAME)).isEqualTo(0);
assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0);
CachedObject response1 = cachingComponent.cacheableMethod(STRING_PARAM);
assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(1);
aerospikeCacheManager.getCache(DIFFERENT_EXISTING_CACHE).clear();
Expand Down

0 comments on commit 6bb851b

Please sign in to comment.