Skip to content

Commit

Permalink
replace hash implementation with a faster one, make serialization and…
Browse files Browse the repository at this point in the history
… hashing methods overridable
  • Loading branch information
agrgr committed Jun 17, 2024
1 parent bd1d7f8 commit b998979
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 90 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<hibernate.validator>8.0.1.Final</hibernate.validator>
<netty.version>4.1.110.Final</netty.version>
<kryo.version>5.6.0</kryo.version>
<xxhash.version>1.8.0</xxhash.version>
</properties>

<licenses>
Expand Down Expand Up @@ -216,6 +217,11 @@
<artifactId>kryo</artifactId>
<version>${kryo.version}</version>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
<version>${xxhash.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down Expand Up @@ -253,6 +259,10 @@
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,16 @@
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.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 @@ -52,7 +46,7 @@ public class AerospikeCache implements Cache {
private final AerospikeCacheConfiguration cacheConfiguration;
private final WritePolicy createOnly;
private final WritePolicy writePolicyForPut;
private final Kryo kryoInstance = new Kryo();
private final AerospikeCacheKeyProcessor cacheKeyProcessor = new AerospikeCacheKeyProcessor();

public AerospikeCache(String name,
IAerospikeClient client,
Expand All @@ -69,22 +63,6 @@ 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 @@ -219,15 +197,9 @@ public ValueWrapper putIfAbsent(Object key, Object value) {
}

private Key getKey(Object key) {
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);
}
AerospikeCacheKey cacheKey = cacheKeyProcessor.serializeAndHash(key);
return new Key(cacheConfiguration.getNamespace(), cacheConfiguration.getSet(),
cacheKey.getValue());
}

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

import com.aerospike.client.Value;
import lombok.Getter;

public class AerospikeCacheKey {

@Getter
private Value value;

private AerospikeCacheKey(String string) {
this.value = new Value.StringValue(string);
}

private AerospikeCacheKey(long number) {
this.value = new Value.LongValue(number);
}

/**
* Instantiate AerospikeCacheKey instance with a String.
*
* @param string String parameter
* @return AerospikeCacheKey
*/
public static AerospikeCacheKey of(String string) {
return new AerospikeCacheKey(string);
}

/**
* Instantiate AerospikeCacheKey instance with a long number.
*
* @param number long number
* @return AerospikeCacheKey
*/
public static AerospikeCacheKey of(long number) {
return new AerospikeCacheKey(number);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.springframework.data.aerospike.cache;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.ByteBufferOutput;
import net.jpountz.xxhash.XXHash64;
import net.jpountz.xxhash.XXHashFactory;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.nio.ByteBuffer;

public class AerospikeCacheKeyProcessor {

private static final Kryo kryoInstance = new Kryo();

public AerospikeCacheKeyProcessor() {
configureKryo();
}

/**
* Configuration for Kryo instance.
* <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.
*/
public void configureKryo() {
// setting to false means not requiring registration for all the classes of cached objects in advance
kryoInstance.setRegistrationRequired(false);
kryoInstance.setInstantiatorStrategy(new StdInstantiatorStrategy());
}

/**
* Serialize the given key and calculate hash based on the serialization result.
*
* @param key Object to be serialized and hashed
* @return AerospikeCacheKey instantiated with either a String or a long number
*/
public AerospikeCacheKey serializeAndHash(Object key) {
return calculateHash(serialize(key));
}

/**
* Serialize the given key.
* <p>
* The default implementation uses Kryo.
* <p>
* The method can be overridden if different serialization implementation is required.
*
* @param key Object to be serialized
* @return byte[]
*/
public byte[] serialize(Object key) {
ByteBufferOutput output = new ByteBufferOutput(1024); // Initial buffer size
kryoInstance.writeClassAndObject(output, key);
output.flush();
return output.toBytes();
}

/**
* Calculate hash based on the given byte array.
* <p>
* The default implementation is 64 bit xxHash.
* <p>
* The method can be overridden if different hashing algorithm or implementation is required.
*
* @param data Byte array to be hashed
* @return AerospikeCacheKey instantiated with either a String or a long number
*/
public static AerospikeCacheKey calculateHash(byte[] data) {
XXHash64 xxHash64 = XXHashFactory.fastestInstance().hash64();
ByteBuffer buffer = ByteBuffer.wrap(data);
return AerospikeCacheKey.of(xxHash64.hash(buffer, 0, data.length, 0));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@

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 @@ -32,26 +29,23 @@
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;

@SuppressWarnings("NewObjectEquality")
public class AerospikeCacheManagerIntegrationTests extends BaseBlockingIntegrationTests {

private final AerospikeCacheKeyProcessor cacheKeyProcessor = new AerospikeCacheKeyProcessor();
private static final String STRING_PARAM = "foo";
private static final String STRING_PARAM_THAT_MATCHES_CONDITION = "abcdef";
private static final long NUMERIC_PARAM = 100L;
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 @@ -62,13 +56,6 @@ 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();
Expand All @@ -87,17 +74,11 @@ private void deleteRecords() throws NoSuchMethodException {
CachingComponent.class.getMethod("cacheableMethodWithMethodNameKey")
);
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);
AerospikeCacheKey cacheKey = cacheKeyProcessor.serializeAndHash(param);
client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, cacheKey.getValue()));
}
client.delete(null, new Key(getNameSpace(), DIFFERENT_SET_NAME,
cacheKeyProcessor.serializeAndHash(STRING_PARAM).getValue()));
}

@Test
Expand Down

0 comments on commit b998979

Please sign in to comment.