Skip to content

Commit

Permalink
Remove dependency on subzero
Browse files Browse the repository at this point in the history
  • Loading branch information
lbwexler committed Apr 7, 2024
1 parent ea2f81a commit a494b1e
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 8 deletions.
4 changes: 1 addition & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ dependencies {
api 'org.jasypt:jasypt:1.9.3'
api "commons-io:commons-io:2.8.0"
api "org.owasp.encoder:encoder:1.2.3"

api "com.esotericsoftware:kryo:5.5.0"
api "info.jerrinot:subzero-core:0.11"
api "com.esotericsoftware:kryo:5.6.0"

}

Expand Down
9 changes: 5 additions & 4 deletions grails-app/init/io/xh/hoist/ClusterConfig.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import com.hazelcast.collection.ISet
import com.hazelcast.config.MaxSizePolicy
import com.hazelcast.config.NearCacheConfig
import grails.core.GrailsClass
import info.jerrinot.subzero.SubZero
import io.xh.hoist.cache.Entry
import io.xh.hoist.cluster.ClusterResponse
import io.xh.hoist.cluster.ReplicatedValueEntry
import io.xh.hoist.kryo.KryoSupport
import io.xh.hoist.util.Utils

import static io.xh.hoist.util.InstanceConfigUtils.getInstanceConfig

import static grails.util.Holders.grailsApplication
Expand Down Expand Up @@ -50,8 +51,8 @@ class ClusterConfig {
*
* Defaults to true. Is set to false, Hoist will not create multi-instance clusters and may
* use simpler in-memory data-structures in place of their Hazelcast counterparts. Use this
* for applications that do not require multi-instance and do not wish to pay the serialization penalty of storing
* shared data in Hazelcast.
* for applications that do not require multi-instance and do not wish to pay the serialization
* penalty of storing shared data in Hazelcast.
*
* Applications and plug-ins may set this value explicitly via the `multiInstanceEnabled`
* instance config, or override this method to implement additional logic.
Expand Down Expand Up @@ -101,7 +102,7 @@ class ClusterConfig {
createHibernateConfigs(ret)
createServiceConfigs(ret)

SubZero.useAsGlobalSerializer(ret)
KryoSupport.setAsGlobalSerializer(ret)

return ret
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | [email protected])
*
* Copyright © 2023 Extremely Heavy Industries Inc.
*/

package io.xh.hoist.admin

import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoSerializable
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import io.xh.hoist.BaseService
import io.xh.hoist.log.LogSupport
import io.xh.hoist.pref.PrefService

import static java.lang.System.currentTimeMillis

class SerializationTestService extends BaseService {

private replicatedValue = getReplicatedValue('sniff')
PrefService prefService

void init() {
replicatedValue.set(new TestObject([foo: 'foo', bar: new Date(), baz: [submap:'hi'], biz: null]))
prefService.setBool('darkMode', true)
logInfo(replicatedValue.get().toString())
}
}

class TestObject implements Serializable, LogSupport, KryoSerializable {
private String foo
private Date bar
private Map baz
private Object biz

void write(Kryo kryo, Output output) {
withSingleTrace('Serializing - Hi') {
output.writeString(foo)
kryo.writeObjectOrNull(output, bar, Date)
kryo.writeClassAndObject(output, baz)
kryo.writeClassAndObject(output, biz)
}
}

void read(Kryo kryo, Input input) {
withSingleTrace('Deserializing - Hi!') {
foo = input.readString()
bar = kryo.readObject(input, Date)
baz = kryo.readClassAndObject(input) as Map
biz = kryo.readClassAndObject(input)

}
}

private void withSingleTrace(String msg, Closure c) {
Long start = currentTimeMillis()
c()
logTrace(msg, [_elapsedMs: currentTimeMillis() - start])
}
}
2 changes: 1 addition & 1 deletion src/main/groovy/io/xh/hoist/cluster/ClusterRequest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.xh.hoist.log.LogSupport
import java.util.concurrent.Callable
import static io.xh.hoist.util.Utils.getExceptionHandler

abstract class ClusterRequest<T> implements Callable<ClusterResponse<T>>, Serializable, LogSupport {
abstract class ClusterRequest<T> implements Callable<ClusterResponse<T>>, LogSupport {

ClusterResponse<T> call() {
try {
Expand Down
46 changes: 46 additions & 0 deletions src/main/groovy/io/xh/hoist/kryo/KryoIdGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.xh.hoist.kryo;


import com.hazelcast.core.HazelcastInstance;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
* A thread safe generator for serializer ids.
*
* Simplification of class from https://github.com/jerrinot/subzero
*/
public class KryoIdGenerator {
private static final int BASE_ID = 6000;

private static ConcurrentHashMap<HazelcastInstance, IdSequence> counterMap = new ConcurrentHashMap<>();

public static int globalId(HazelcastInstance hz) {
IdSequence idSequence = getOrCreateSequence(hz);
return idSequence.idFor(hz.getClass());
}

public static void instanceDestroyed(HazelcastInstance hz) {
counterMap.remove(hz);
}

private static IdSequence getOrCreateSequence(HazelcastInstance hazelcastInstance) {
IdSequence currentSequence = counterMap.get(hazelcastInstance);
if (currentSequence != null) {
return currentSequence;
}
IdSequence newSequence = new IdSequence();
currentSequence = counterMap.putIfAbsent(hazelcastInstance, newSequence);
return currentSequence == null ? newSequence : currentSequence;
}

private static class IdSequence {
private final ConcurrentMap<Class<?>, Integer> knownTypes = new ConcurrentHashMap<>();
private final AtomicInteger counter = new AtomicInteger(BASE_ID);

private int idFor(Class<?> clazz) {
return knownTypes.computeIfAbsent(clazz, (ignored) -> counter.incrementAndGet());
}
}
}
107 changes: 107 additions & 0 deletions src/main/groovy/io/xh/hoist/kryo/KryoSerializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.xh.hoist.kryo;


import com.esotericsoftware.kryo.ClassResolver;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.ReferenceResolver;
import com.esotericsoftware.kryo.io.InputChunked;
import com.esotericsoftware.kryo.io.OutputChunked;
import com.esotericsoftware.kryo.util.DefaultClassResolver;
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
import com.esotericsoftware.kryo.util.MapReferenceResolver;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.HazelcastInstanceAware;
import com.hazelcast.internal.nio.ClassLoaderUtil;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.nio.serialization.StreamSerializer;
import com.hazelcast.nio.ObjectDataInput;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.InputStream;
import java.io.OutputStream;

import static java.lang.ThreadLocal.withInitial;

/**
* A Hazelcast Serializer that uses Kryo.
*
* Simplification of strategy from https://github.com/jerrinot/subzero
*/
class KryoSerializer<T> implements StreamSerializer<T>, HazelcastInstanceAware {

private int typeId;
private HazelcastInstance hzInstance;
private final ThreadLocal<KryoContext> ctx = withInitial(() -> new KryoContext(hzInstance));

//-----------------------------------
// Hazelcast Overrides for Serializer
//-----------------------------------
public int getTypeId() {
return typeId;
}

public void destroy() {
KryoIdGenerator.instanceDestroyed(hzInstance);
}

public void setHazelcastInstance(HazelcastInstance instance) {
hzInstance = instance;
typeId = KryoIdGenerator.globalId(hzInstance);
}

public void write(ObjectDataOutput out, T object) {
KryoContext kryoContext = ctx.get();
OutputChunked output = kryoContext.outputChunked;
output.setOutputStream((OutputStream) out);
kryoContext.kryo.writeClassAndObject(output, object);
output.endChunk();
output.flush();
}

public T read(ObjectDataInput in) {
KryoContext kryoContext = ctx.get();
InputChunked input = kryoContext.inputChunked;
input.setInputStream((InputStream) in);
return (T) kryoContext.kryo.readClassAndObject(input);
}


//-------------------------
// Implementation
//--------------------------
private static class KryoContext {
final Kryo kryo;
final InputChunked inputChunked = new InputChunked(16*1024);
final OutputChunked outputChunked = new OutputChunked(16*1024);

KryoContext(HazelcastInstance hzInstance) {
ClassResolver classResolver = new HzClassResolver(hzInstance);
ReferenceResolver referenceResolver = new MapReferenceResolver();

Kryo kryo = new Kryo(classResolver, referenceResolver);
kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
kryo.setRegistrationRequired(false);
this.kryo = kryo;
}
}

private static class HzClassResolver extends DefaultClassResolver {

private final ClassLoader classLoader;

public HzClassResolver(HazelcastInstance hzInstance) {
classLoader = hzInstance.getConfig().getClassLoader();
}

protected Class<?> getTypeByName(String className) {
try {
return ClassLoaderUtil.loadClass(classLoader, className);
} catch (ClassNotFoundException e) {
return null;
}
}
}
}



19 changes: 19 additions & 0 deletions src/main/groovy/io/xh/hoist/kryo/KryoSupport.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.xh.hoist.kryo

import com.hazelcast.config.Config
import com.hazelcast.config.GlobalSerializerConfig

/**
* Support for serialization via Kryo.
*/
class KryoSupport {
static setAsGlobalSerializer(Config config) {
def gsc = config.serializationConfig.globalSerializerConfig ?= new GlobalSerializerConfig()
gsc.className = KryoSerializer.class.name

// Avoid stomping on Hibernate Cache Serialization which fails with Kryo
// Consider replacing this with an *explicit* exclusion.
gsc.overrideJavaSerialization = false
}
}

0 comments on commit a494b1e

Please sign in to comment.