From 3e322e4d00f2f9d7063d381167b2e9c866023178 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 20 Dec 2015 15:44:15 +0000 Subject: [PATCH] Add redis persisting support - Add new StateMachineContextRepository interface. - Add new redis module having persisting support for StateMachineContext. - Extract kryo serializers into a new kryo module which is then shared with zookeeper and redis modules. - We'll add redis related tests later when we figure out a correct pattern for integration tests. - Fixes #110 --- build.gradle | 43 ++++++- gradle.properties | 2 + settings.gradle | 2 + .../StateMachineContextRepository.java | 47 ++++++++ .../RepositoryStateMachinePersist.java | 54 +++++++++ .../kryo/MessageHeadersSerializer.java | 53 +++++++++ .../kryo/StateMachineContextSerializer.java | 60 ++++++++++ .../statemachine/kryo/UUIDSerializer.java | 50 ++++++++ .../RedisStateMachineContextRepository.java | 110 ++++++++++++++++++ .../ZookeeperStateMachinePersist.java | 72 +----------- 10 files changed, 420 insertions(+), 73 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContextRepository.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/support/RepositoryStateMachinePersist.java create mode 100644 spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/MessageHeadersSerializer.java create mode 100644 spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java create mode 100644 spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/UUIDSerializer.java create mode 100644 spring-statemachine-redis/src/main/java/org/springframework/statemachine/redis/RedisStateMachineContextRepository.java diff --git a/build.gradle b/build.gradle index ca094114..8555b3f9 100644 --- a/build.gradle +++ b/build.gradle @@ -149,11 +149,28 @@ project('spring-statemachine-test') { } } -project('spring-statemachine-zookeeper') { - description = "Spring State Machine Zookeeper" - +project('spring-statemachine-kryo') { + description = "Spring State Machine Kryo" + dependencies { compile project(":spring-statemachine-core") + compile "com.esotericsoftware.kryo:kryo:$kryoVersion" + + testCompile project(":spring-statemachine-test") + testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" + testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + testCompile "junit:junit:$junitVersion" + testRuntime "log4j:log4j:$log4jVersion" + } +} + +project('spring-statemachine-zookeeper') { + description = "Spring State Machine Zookeeper" + + dependencies { + compile project(":spring-statemachine-core") + compile project(":spring-statemachine-kryo") compile "org.apache.curator:curator-recipes:$curatorVersion" compile "com.esotericsoftware.kryo:kryo:$kryoVersion" @@ -163,7 +180,25 @@ project('spring-statemachine-zookeeper') { testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" testCompile "junit:junit:$junitVersion" - testRuntime("log4j:log4j:$log4jVersion") + testRuntime "log4j:log4j:$log4jVersion" + } +} + +project('spring-statemachine-redis') { + description = "Spring State Machine Redis" + + dependencies { + compile project(":spring-statemachine-core") + compile project(":spring-statemachine-kryo") + compile "org.springframework.data:spring-data-redis:$springDataRedisVersion" + compile "redis.clients:jedis:$jedisVersion" + + testCompile project(":spring-statemachine-test") + testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" + testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + testCompile "junit:junit:$junitVersion" + testRuntime "log4j:log4j:$log4jVersion" } } diff --git a/gradle.properties b/gradle.properties index c2a946dd..75c007e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,7 @@ springShellVersion=1.1.0.RELEASE springSecurityVersion=4.0.3.RELEASE +springDataRedisVersion=1.6.2.RELEASE +jedisVersion=2.7.3 junitVersion=4.12 springVersion=4.2.2.RELEASE kryoVersion=2.24.0 diff --git a/settings.gradle b/settings.gradle index 31ce80b2..a426ead2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,9 @@ rootProject.name = 'spring-statemachine' include 'spring-statemachine-core' include 'spring-statemachine-test' +include 'spring-statemachine-kryo' include 'spring-statemachine-zookeeper' +include 'spring-statemachine-redis' include 'spring-statemachine-recipes' diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContextRepository.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContextRepository.java new file mode 100644 index 00000000..11dad4f9 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContextRepository.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine; + +import org.springframework.statemachine.StateMachineContext; + +/** + * Repository interface for saving and retrieving {@link StateMachineContext} objects. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + * @param The type of state machine context + */ +public interface StateMachineContextRepository> { + + /** + * Save a context. + * + * @param context the context + * @param id the id + */ + void save(T context, String id); + + /** + * Gets the context. + * + * @param id the id + * @return the context + */ + T getContext(String id); + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/RepositoryStateMachinePersist.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/RepositoryStateMachinePersist.java new file mode 100644 index 00000000..a07eef98 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/RepositoryStateMachinePersist.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.support; + +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.StateMachineContextRepository; +import org.springframework.statemachine.StateMachinePersist; + +/** + * A {@link StateMachinePersist} using a generic {@link StateMachineContextRepository} + * for persisting {@link StateMachineContext}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class RepositoryStateMachinePersist implements StateMachinePersist { + + private final StateMachineContextRepository> repository; + + /** + * Instantiates a new repository state machine persist. + * + * @param repository the repository + */ + public RepositoryStateMachinePersist(StateMachineContextRepository> repository) { + this.repository = repository; + } + + @Override + public void write(StateMachineContext context, String contextOjb) throws Exception { + repository.save(context, contextOjb); + } + + @Override + public StateMachineContext read(String contextOjb) throws Exception { + return repository.getContext(contextOjb); + } + +} diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/MessageHeadersSerializer.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/MessageHeadersSerializer.java new file mode 100644 index 00000000..fa0c85fe --- /dev/null +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/MessageHeadersSerializer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.kryo; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.messaging.MessageHeaders; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Kryo {@link Serializer} for spring messaging message headers. + * + * @author Janne Valkealahti + * + */ +public class MessageHeadersSerializer extends Serializer { + + @Override + public void write(Kryo kryo, Output output, MessageHeaders object) { + HashMap map = new HashMap(); + for (Entry entry : object.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + kryo.writeClassAndObject(output, map); + } + + @SuppressWarnings("unchecked") + @Override + public MessageHeaders read(Kryo kryo, Input input, Class type) { + Map eventHeaders = (Map) kryo.readClassAndObject(input); + return new MessageHeaders(eventHeaders); + } + +} diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java new file mode 100644 index 00000000..305cb1f0 --- /dev/null +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.kryo; + +import java.util.List; +import java.util.Map; + +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.support.DefaultExtendedState; +import org.springframework.statemachine.support.DefaultStateMachineContext; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Kryo {@link Serializer} for {@link StateMachineContext}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class StateMachineContextSerializer extends Serializer> { + + @Override + public void write(Kryo kryo, Output output, StateMachineContext context) { + kryo.writeClassAndObject(output, context.getEvent()); + kryo.writeClassAndObject(output, context.getState()); + kryo.writeClassAndObject(output, context.getEventHeaders()); + kryo.writeClassAndObject(output, context.getExtendedState().getVariables()); + kryo.writeClassAndObject(output, context.getChilds()); + } + + @SuppressWarnings("unchecked") + @Override + public StateMachineContext read(Kryo kryo, Input input, Class> clazz) { + E event = (E) kryo.readClassAndObject(input); + S state = (S) kryo.readClassAndObject(input); + Map eventHeaders = (Map) kryo.readClassAndObject(input); + Map variables = (Map) kryo.readClassAndObject(input); + List> childs = (List>) kryo.readClassAndObject(input); + return new DefaultStateMachineContext(childs, state, event, eventHeaders, new DefaultExtendedState(variables)); + } + +} diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/UUIDSerializer.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/UUIDSerializer.java new file mode 100644 index 00000000..8bda0da1 --- /dev/null +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/UUIDSerializer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.kryo; + +import java.util.UUID; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Kryo {@link Serializer} for {@link UUID}. + * + * @author Janne Valkealahti + * + */ +public class UUIDSerializer extends Serializer { + + /** + * Instantiates a new UUID serializer. + */ + public UUIDSerializer() { + setImmutable(true); + } + + @Override + public void write(final Kryo kryo, final Output output, final UUID uuid) { + output.writeLong(uuid.getMostSignificantBits()); + output.writeLong(uuid.getLeastSignificantBits()); + } + + @Override + public UUID read(final Kryo kryo, final Input input, final Class uuidClass) { + return new UUID(input.readLong(), input.readLong()); + } +} diff --git a/spring-statemachine-redis/src/main/java/org/springframework/statemachine/redis/RedisStateMachineContextRepository.java b/spring-statemachine-redis/src/main/java/org/springframework/statemachine/redis/RedisStateMachineContextRepository.java new file mode 100644 index 00000000..a8ce58c5 --- /dev/null +++ b/spring-statemachine-redis/src/main/java/org/springframework/statemachine/redis/RedisStateMachineContextRepository.java @@ -0,0 +1,110 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.redis; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.UUID; + +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.messaging.MessageHeaders; +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.StateMachineContextRepository; +import org.springframework.statemachine.kryo.MessageHeadersSerializer; +import org.springframework.statemachine.kryo.StateMachineContextSerializer; +import org.springframework.statemachine.kryo.UUIDSerializer; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * A {@link StateMachineContextRepository} backed by a redis and kryo serialization. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class RedisStateMachineContextRepository implements StateMachineContextRepository> { + + private static final ThreadLocal kryoThreadLocal = new ThreadLocal() { + + @SuppressWarnings("rawtypes") + @Override + protected Kryo initialValue() { + Kryo kryo = new Kryo(); + kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer()); + kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer()); + kryo.addDefaultSerializer(UUID.class, new UUIDSerializer()); + return kryo; + } + }; + + private final RedisOperations redisOperations; + + /** + * Instantiates a new redis state machine context repository. + * + * @param redisConnectionFactory the redis connection factory + */ + public RedisStateMachineContextRepository(RedisConnectionFactory redisConnectionFactory) { + redisOperations = createDefaultTemplate(redisConnectionFactory); + } + + @Override + public void save(StateMachineContext context, String id) { + redisOperations.opsForValue().set(id, serialize(context)); + } + + @Override + public StateMachineContext getContext(String id) { + return deserialize(redisOperations.opsForValue().get(id)); + } + + private static RedisTemplate createDefaultTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate(); + template.setKeySerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setConnectionFactory(connectionFactory); + template.afterPropertiesSet(); + return template; + } + + private byte[] serialize(StateMachineContext context) { + Kryo kryo = kryoThreadLocal.get(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Output output = new Output(out); + kryo.writeObject(output, context); + output.close(); + return out.toByteArray(); + } + + @SuppressWarnings("unchecked") + private StateMachineContext deserialize(byte[] data) { + if (data == null || data.length == 0) { + return null; + } + Kryo kryo = kryoThreadLocal.get(); + ByteArrayInputStream in = new ByteArrayInputStream(data); + Input input = new Input(in); + return kryo.readObject(input, StateMachineContext.class); + } + +} diff --git a/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java b/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java index f5b3e4e6..bc9e6f9e 100644 --- a/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java +++ b/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java @@ -18,10 +18,6 @@ package org.springframework.statemachine.zookeeper; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.UUID; import org.apache.curator.framework.CuratorFramework; @@ -33,12 +29,12 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.StateMachineException; import org.springframework.statemachine.StateMachinePersist; -import org.springframework.statemachine.support.DefaultExtendedState; -import org.springframework.statemachine.support.DefaultStateMachineContext; +import org.springframework.statemachine.kryo.MessageHeadersSerializer; +import org.springframework.statemachine.kryo.StateMachineContextSerializer; +import org.springframework.statemachine.kryo.UUIDSerializer; import org.springframework.util.Assert; import com.esotericsoftware.kryo.Kryo; -import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; @@ -148,66 +144,4 @@ public class ZookeeperStateMachinePersist implements StateMachinePersist extends Serializer> { - - @Override - public void write(Kryo kryo, Output output, StateMachineContext context) { - kryo.writeClassAndObject(output, context.getEvent()); - kryo.writeClassAndObject(output, context.getState()); - kryo.writeClassAndObject(output, context.getEventHeaders()); - kryo.writeClassAndObject(output, context.getExtendedState().getVariables()); - kryo.writeClassAndObject(output, context.getChilds()); - } - - @SuppressWarnings("unchecked") - @Override - public StateMachineContext read(Kryo kryo, Input input, Class> clazz) { - E event = (E) kryo.readClassAndObject(input); - S state = (S) kryo.readClassAndObject(input); - Map eventHeaders = (Map) kryo.readClassAndObject(input); - Map variables = (Map) kryo.readClassAndObject(input); - List> childs = (List>) kryo.readClassAndObject(input); - return new DefaultStateMachineContext(childs, state, event, eventHeaders, new DefaultExtendedState(variables)); - } - - } - - private static class MessageHeadersSerializer extends Serializer { - - @Override - public void write(Kryo kryo, Output output, MessageHeaders object) { - HashMap map = new HashMap(); - for (Entry entry : object.entrySet()) { - map.put(entry.getKey(), entry.getValue()); - } - kryo.writeClassAndObject(output, map); - } - - @SuppressWarnings("unchecked") - @Override - public MessageHeaders read(Kryo kryo, Input input, Class type) { - Map eventHeaders = (Map) kryo.readClassAndObject(input); - return new MessageHeaders(eventHeaders); - } - - } - - private static class UUIDSerializer extends Serializer { - - public UUIDSerializer() { - setImmutable(true); - } - - @Override - public void write(final Kryo kryo, final Output output, final UUID uuid) { - output.writeLong(uuid.getMostSignificantBits()); - output.writeLong(uuid.getLeastSignificantBits()); - } - - @Override - public UUID read(final Kryo kryo, final Input input, final Class uuidClass) { - return new UUID(input.readLong(), input.readLong()); - } - } - }