package kr.pe.kwonnam.hibernate4memcached.spymemcached;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer;
import de.javakaffee.kryoserializers.KryoReflectionFactorySupport;
import kr.pe.kwonnam.hibernate4memcached.util.IntToBytesUtils;
import kr.pe.kwonnam.hibernate4memcached.util.Lz4CompressUtils;
import kr.pe.kwonnam.hibernate4memcached.util.OverridableReadOnlyProperties;
import net.spy.memcached.CachedData;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Default transcoder for {@link SpyMemcachedAdapter}.
*
* This transcoder uses Kryo Serializer and compress data with Lz4 when data size is greater than compression
* threashold.
*
* @author KwonNam Son (kwon37xi@gmail.com)
*/
public class KryoTranscoder implements InitializableTranscoder<Object> {
private Logger log = LoggerFactory.getLogger(KryoTranscoder.class);
public static final String COMPRESSION_THREASHOLD_PROPERTY_KEY = SpyMemcachedAdapter.PROPERTY_KEY_PREFIX + ".kryotranscoder.compression.threashold.bytes";
public static final int BASE_FLAG = 0;
public static final int COMPRESS_FLAG = 4; // 0b0100
public static final int DEFAULT_BUFFER_SIZE = 1024 * 20; // 20kb
public static final int DECOMPRESSED_SIZE_STORE_BYTES_LENGTH = 4;
private OverridableReadOnlyProperties properties;
private int bufferSize = DEFAULT_BUFFER_SIZE;
private int compressionThreasholdBytes;
@Override
public void init(OverridableReadOnlyProperties properties) {
this.properties = properties;
String compressionThreasholdBytesProperty = properties.getRequiredProperty(COMPRESSION_THREASHOLD_PROPERTY_KEY);
compressionThreasholdBytes = Integer.parseInt(compressionThreasholdBytesProperty);
}
@Override
public boolean asyncDecode(CachedData d) {
return false;
}
/**
* Override this method to change serialization rule.
*
* @return Kryo serializer instance.
*/
protected Kryo createKryo() {
Kryo kryo = new KryoReflectionFactorySupport();
kryo.setDefaultSerializer(CompatibleFieldSerializer.class);
return kryo;
}
@Override
public CachedData encode(Object object) {
int flags = BASE_FLAG;
byte[] encodedBytes = kryoEncode(object);
boolean compressionRequired = encodedBytes.length > compressionThreasholdBytes;
if (compressionRequired) {
int beforeSize = encodedBytes.length;
encodedBytes = compress(encodedBytes);
log.debug("kryotranscoder compress required : {}, original {} bytes -> compressed {} bytes", compressionRequired, beforeSize, encodedBytes.length);
flags = flags | COMPRESS_FLAG;
}
return new CachedData(flags, encodedBytes, getMaxSize());
}
private byte[] kryoEncode(Object o) {
Kryo kryo = createKryo();
Output output = new Output(bufferSize, getMaxSize());
kryo.writeClassAndObject(output, o);
return output.toBytes();
}
byte[] compress(byte[] encodedBytes) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(encodedBytes.length);
try {
baos.write(IntToBytesUtils.intToBytes(encodedBytes.length));
byte[] compressedBytes = Lz4CompressUtils.compress(encodedBytes);
baos.write(compressedBytes);
} catch (IOException e) {
throw new IllegalStateException("Failed do compress kryo serialized data.", e);
}
return baos.toByteArray();
}
@Override
public Object decode(CachedData data) {
int flags = data.getFlags();
byte[] decodedBytes = data.getData();
boolean compressed = (flags & COMPRESS_FLAG) > 0;
if (compressed) {
decodedBytes = decompress(decodedBytes);
}
Kryo kryo = createKryo();
return kryo.readClassAndObject(new Input(decodedBytes));
}
private byte[] decompress(byte[] decodedBytes) {
int decompressedSize = IntToBytesUtils.bytesToInt(ArrayUtils.subarray(decodedBytes, 0, DECOMPRESSED_SIZE_STORE_BYTES_LENGTH));
return Lz4CompressUtils.decompressFast(decodedBytes, DECOMPRESSED_SIZE_STORE_BYTES_LENGTH, decompressedSize);
}
@Override
public int getMaxSize() {
return CachedData.MAX_SIZE;
}
}