├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── images ├── AggregateFlow.png ├── Example.png └── FlowContext.png ├── pom.xml ├── spring-event-sourcing-example ├── README.md ├── lombok.config ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── romeh │ │ └── ordermanager │ │ ├── OrderManagerApplication.java │ │ ├── SwaggerConfiguration.java │ │ ├── config │ │ └── OrderEntityProperties.java │ │ ├── domain │ │ └── OrderManager.java │ │ ├── entities │ │ ├── OrderState.java │ │ ├── Response.java │ │ ├── commands │ │ │ └── OrderCmd.java │ │ ├── enums │ │ │ └── OrderStatus.java │ │ └── events │ │ │ ├── CreatedEvent.java │ │ │ ├── FinishedEvent.java │ │ │ ├── OrderEvent.java │ │ │ ├── SignedEvent.java │ │ │ └── ValidatedEvent.java │ │ ├── reader │ │ ├── entities │ │ │ └── JournalReadItem.java │ │ ├── services │ │ │ ├── OrderNotFoundException.java │ │ │ ├── ReadStoreExtractor.java │ │ │ └── ReadStoreStreamerService.java │ │ └── streamer │ │ │ ├── IgniteSink.java │ │ │ ├── IgniteSinkConstants.java │ │ │ ├── IgniteSource.java │ │ │ ├── IgniteSourceConstants.java │ │ │ ├── StreamReport.java │ │ │ └── grid │ │ │ ├── cq │ │ │ ├── IgniteCacheSinkTaskCQ.java │ │ │ └── IgniteCacheSourceTaskCQ.java │ │ │ ├── events │ │ │ ├── IgniteCacheSinkTask.java │ │ │ └── IgniteCacheSourceTask.java │ │ │ └── streamer │ │ │ └── IgniteCacheEventStreamerRx.java │ │ ├── rest │ │ ├── OrderRestController.java │ │ ├── dto │ │ │ └── OrderRequest.java │ │ └── errors │ │ │ ├── ApiError.java │ │ │ └── RestExceptionHandler.java │ │ ├── serializer │ │ └── OrderManagerSerializer.java │ │ └── services │ │ └── OrdersBroker.java │ ├── proto │ └── EventsAndCommands.proto │ └── resources │ ├── application.yml │ ├── bootstrap.yml │ ├── eventSourcing.conf │ └── logback.xml └── springboot-akka-event-sourcing-starter ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── spring │ │ └── akka │ │ └── eventsourcing │ │ ├── SpringActorProducer.java │ │ ├── SpringExtension.java │ │ ├── config │ │ ├── AkkaAutoConfiguration.java │ │ ├── AkkaProperties.java │ │ └── PersistentEntityProperties.java │ │ └── persistance │ │ ├── AsyncResult.java │ │ ├── ErrorResponse.java │ │ ├── ResumeProcessing.java │ │ └── eventsourcing │ │ ├── ExecutionFlow.java │ │ ├── FlowContext.java │ │ ├── PersistentEntity.java │ │ ├── PersistentEntityBroker.java │ │ ├── PersistentEntitySharding.java │ │ ├── ReadOnlyFlowContext.java │ │ ├── TriFunction.java │ │ ├── actions │ │ ├── Persist.java │ │ ├── PersistAll.java │ │ ├── PersistNone.java │ │ └── PersistOne.java │ │ └── annotations │ │ └── PersistentActor.java └── resources │ ├── META-INF │ └── spring.factories │ └── reference.conf └── test ├── java └── com │ └── spring │ └── akka │ └── eventsourcing │ ├── EntityActorTest.java │ ├── EntityActorTestAsync.java │ ├── TestConfig.java │ ├── example │ ├── OrderEntity.java │ ├── OrderEntityProperties.java │ ├── OrderState.java │ ├── OrderStatus.java │ ├── commands │ │ └── OrderCmd.java │ ├── events │ │ ├── CreatedEvent.java │ │ ├── FinishedEvent.java │ │ ├── OrderEvent.java │ │ ├── SignedEvent.java │ │ └── ValidatedEvent.java │ └── response │ │ └── Response.java │ └── support │ ├── SpringContextSupport.java │ ├── TestCluster.java │ └── UnitTestPortManager.java └── resources └── akka.initializer.conf /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mahmoud.romeh@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Spring boot akka event sourcing starter ![Twitter Follow](https://img.shields.io/twitter/follow/mromeh.svg?style=social) 3 | 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/e1167d854e8b40928e4fef24788d31c2)](https://app.codacy.com/app/Romeh/spring-boot-akka-event-sourcing-starter?utm_source=github.com&utm_medium=referral&utm_content=Romeh/spring-boot-akka-event-sourcing-starter&utm_campaign=Badge_Grade_Dashboard) 5 | 6 | ![Maven Central](https://img.shields.io/nexus/r/https/oss.sonatype.org/io.github.romeh/springboot-akka-event-sourcing-starter.svg?style=flat) 7 | 8 | ![alt text](images/FlowContext.png) 9 | 10 | - Spring boot akka persistence event sourcing starter that cover the following :Smooth integration between Akka persistence and Spring Boot 2 11 | - Generic DSL for the aggregate flow definition for commands and events 12 | ![alt text](images/AggregateFlow.png) 13 | - Abstract Aggregate persistent entity actor with all common logic in place and which can be used with the concrete managed spring beans implementation of different aggregate entities 14 | - Abstract cluster sharding run-time configuration and access via spring boot custom configuration and a generic entity broker that abstract the cluster shading implementation for you 15 | - Abstracted mixed configuration for your actor system and the entity configuration via spring configuration and akka configuration 16 | 17 | 18 | For how to use the event sourcing starter toolkit , you need to add the following maven dependency: 19 | Maven dependency 20 | ```` 21 | 22 | io.github.romeh 23 | springboot-akka-event-sourcing-starter 24 | 1.0.2 25 | 26 | ```` 27 | 28 | 29 | For detailed technical details and explanation , check my 4 parts blog posts: 30 | 31 | - Part 1:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-1/ 32 | - Part 2:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-2/ 33 | - Part 3:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-3-the-working-example/ 34 | - Part 4:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-4-final/ 35 | 36 | 37 | Spring boot , Akka and Ignite used versions: 38 | -------------- 39 | 40 | Spring boot 2.1.0.RELEASE+, Akka version :2.5.18+ , Ignite Version :2.6.0+ 41 | -------------------------------------------------------------------------------- /images/AggregateFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-akka-event-sourcing-starter/806344c92f304f1130ae7a08820121e043b9b59b/images/AggregateFlow.png -------------------------------------------------------------------------------- /images/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-akka-event-sourcing-starter/806344c92f304f1130ae7a08820121e043b9b59b/images/Example.png -------------------------------------------------------------------------------- /images/FlowContext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Romeh/spring-boot-akka-event-sourcing-starter/806344c92f304f1130ae7a08820121e043b9b59b/images/FlowContext.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | io.github.romeh 6 | springboot-event-sourcing-initializer 7 | 1.0.2 8 | pom 9 | 10 | 4.0.0 11 | akka event-sourcing initializer master 12 | AKKA event-sourcing Spring boot master project. 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.2.2.RELEASE 18 | 19 | 20 | 21 | 22 | 1.0 23 | 2.2.2.RELEASE 24 | 2.5.27 25 | 1.3.2 26 | 1.1.1 27 | UTF-8 28 | 3.3 29 | 2.2 30 | 1.8 31 | 4.13.1 32 | 3.3.0 33 | 1.10.19 34 | 1.7.25 35 | 1.2.3 36 | 1.7.25 37 | 2.9.3 38 | 2.9.7 39 | 3.0.2 40 | 0.9.2 41 | 1.18.10 42 | 43 | 44 | 45 | springboot-akka-event-sourcing-starter 46 | spring-event-sourcing-example 47 | 48 | 49 | 50 | 51 | 52 | 53 | com.typesafe.akka 54 | akka-actor_2.12 55 | ${akka.framework.version} 56 | 57 | 58 | 59 | com.typesafe.akka 60 | akka-cluster_2.12 61 | ${akka.framework.version} 62 | 63 | 64 | 65 | com.typesafe.akka 66 | akka-cluster-metrics_2.12 67 | ${akka.framework.version} 68 | 69 | 70 | 71 | com.typesafe.akka 72 | akka-remote_2.12 73 | ${akka.framework.version} 74 | 75 | 76 | 77 | com.typesafe.akka 78 | akka-cluster-tools_2.12 79 | ${akka.framework.version} 80 | 81 | 82 | 83 | com.typesafe.akka 84 | akka-multi-node-testkit_2.12 85 | ${akka.framework.version} 86 | 87 | 88 | 89 | com.typesafe.akka 90 | akka-testkit_2.12 91 | ${akka.framework.version} 92 | test 93 | 94 | 95 | 96 | com.typesafe.akka 97 | akka-slf4j_2.12 98 | ${akka.framework.version} 99 | 100 | 101 | 102 | com.typesafe.akka 103 | akka-persistence_2.12 104 | ${akka.framework.version} 105 | 106 | 107 | 108 | com.typesafe.akka 109 | akka-cluster-sharding_2.12 110 | ${akka.framework.version} 111 | 112 | 113 | 114 | com.typesafe 115 | config 116 | ${typesafe.config.version} 117 | 118 | 119 | 120 | org.springframework.boot 121 | spring-boot-starter 122 | ${spring.boot.version} 123 | 124 | 125 | org.springframework.boot 126 | spring-boot-starter-test 127 | ${spring.boot.version} 128 | test 129 | 130 | 131 | 132 | 133 | junit 134 | junit 135 | ${junit.version} 136 | test 137 | 138 | 139 | 140 | org.mockito 141 | mockito-all 142 | ${mockito.version} 143 | test 144 | 145 | 146 | 147 | com.codahale.metrics 148 | metrics-core 149 | ${codahale.metrics.version} 150 | 151 | 152 | 153 | 154 | 155 | org.slf4j 156 | slf4j-api 157 | ${slf4j.api.version} 158 | 159 | 160 | 161 | ch.qos.logback 162 | logback-classic 163 | ${logback.version} 164 | 165 | 166 | 167 | org.slf4j 168 | jcl-over-slf4j 169 | ${jcl.over.slf4j.version} 170 | 171 | 172 | 173 | 174 | io.vavr 175 | vavr 176 | ${vavr.version} 177 | 178 | 179 | 180 | org.apache.maven.plugins 181 | maven-jar-plugin 182 | ${maven.jar.version} 183 | 184 | 185 | org.projectlombok 186 | lombok 187 | ${lombok.version} 188 | provided 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | org.apache.maven.plugins 200 | maven-release-plugin 201 | 2.5.1 202 | 203 | 204 | org.apache.maven.scm 205 | maven-scm-provider-gitexe 206 | 1.9.2 207 | 208 | 209 | 210 | 211 | pom.xml 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Spring boot akka event sourcing starter working example 3 | 4 | The order manager aggregate flow will be as the following : 5 | ![alt text](../images/Example.png) 6 | 7 | For more detailed explanation of the how the example application implemented and how it work , 8 | Please check my blog post : 9 | 10 | - Part 3:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-3-the-working-example/ 11 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/lombok.config: -------------------------------------------------------------------------------- 1 | lombok.anyConstructor.addConstructorProperties=true -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/OrderManagerApplication.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | 7 | /** 8 | * the main app runner 9 | * 10 | * @author romeh 11 | */ 12 | 13 | @SpringBootApplication 14 | public class OrderManagerApplication { 15 | 16 | public static void main(String[] args) { 17 | 18 | SpringApplication.run(OrderManagerApplication.class, args); 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/SwaggerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager; 2 | 3 | import java.util.Collections; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import springfox.documentation.builders.PathSelectors; 9 | import springfox.documentation.builders.RequestHandlerSelectors; 10 | import springfox.documentation.service.ApiInfo; 11 | import springfox.documentation.service.Contact; 12 | import springfox.documentation.spi.DocumentationType; 13 | import springfox.documentation.spring.web.plugins.Docket; 14 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 15 | 16 | /** 17 | * Configuration class which enables Swagger 18 | * 19 | * @author romih 20 | */ 21 | @Configuration 22 | @EnableSwagger2 23 | public class SwaggerConfiguration { 24 | 25 | @Bean 26 | public Docket api() { 27 | return new Docket(DocumentationType.SWAGGER_2) 28 | .select() 29 | .apis(RequestHandlerSelectors.any()) 30 | .paths(PathSelectors.any()) 31 | .build() 32 | .apiInfo(apiInfo()); 33 | } 34 | 35 | private ApiInfo apiInfo() { 36 | ApiInfo apiInfo = new ApiInfo( 37 | "Order manager REST API", 38 | "Order manager REST API documentation.", 39 | "API 1.0", 40 | "Terms of service based into company terms of use", 41 | new Contact("Romeh", null, "romeh@test.com"), 42 | "License of API for YourCompany use only", null, Collections.emptyList()); 43 | return apiInfo; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/config/OrderEntityProperties.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.config; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.function.Function; 7 | 8 | import javax.annotation.PostConstruct; 9 | 10 | import org.springframework.stereotype.Component; 11 | 12 | import com.romeh.ordermanager.domain.OrderManager; 13 | import com.romeh.ordermanager.entities.commands.OrderCmd; 14 | import com.romeh.ordermanager.entities.events.CreatedEvent; 15 | import com.romeh.ordermanager.entities.events.FinishedEvent; 16 | import com.romeh.ordermanager.entities.events.OrderEvent; 17 | import com.romeh.ordermanager.entities.events.SignedEvent; 18 | import com.romeh.ordermanager.entities.events.ValidatedEvent; 19 | import com.spring.akka.eventsourcing.config.PersistentEntityProperties; 20 | 21 | /** 22 | * the main order entity required configuration for event souring toolkit 23 | */ 24 | @Component 25 | public class OrderEntityProperties implements PersistentEntityProperties { 26 | 27 | private Map, String> tags; 28 | 29 | /** 30 | * init the event tags map 31 | */ 32 | @PostConstruct 33 | public void init() { 34 | Map, String> init = new HashMap<>(); 35 | init.put(CreatedEvent.class, "CreatedOrders"); 36 | init.put(FinishedEvent.class, "FinishedOrders"); 37 | init.put(SignedEvent.class, "SignedOrders"); 38 | init.put(ValidatedEvent.class, "ValidatedOrders"); 39 | tags = Collections.unmodifiableMap(init); 40 | 41 | } 42 | 43 | /** 44 | * @return the entity should save snapshot of its state after how many persisted events 45 | */ 46 | @Override 47 | public int snapshotStateAfter() { 48 | return 5; 49 | } 50 | 51 | /** 52 | * @return the entity should passivate after how long of being non actively serving requests 53 | */ 54 | @Override 55 | public long entityPassivateAfter() { 56 | return 60; 57 | } 58 | 59 | /** 60 | * @return map of event class to tag value , used into event tagging before persisting the events into the event store by akka persistence 61 | */ 62 | @Override 63 | public Map, String> tags() { 64 | 65 | return tags; 66 | } 67 | 68 | /** 69 | * @return number of cluster sharding for the entity 70 | */ 71 | @Override 72 | public int numberOfShards() { 73 | return 20; 74 | } 75 | 76 | /** 77 | * @return persistenceIdPostfix function , used in cluster sharding entity routing 78 | */ 79 | @Override 80 | public Function persistenceIdPostfix() { 81 | return OrderCmd::getOrderId; 82 | } 83 | 84 | /** 85 | * @return entity persistenceIdPrefix , used in cluster sharding routing 86 | */ 87 | @Override 88 | public String persistenceIdPrefix() { 89 | return OrderManager.class.getSimpleName(); 90 | } 91 | 92 | /** 93 | * @return entity class 94 | */ 95 | @Override 96 | public Class getEntityClass() { 97 | return OrderManager.class; 98 | } 99 | 100 | /** 101 | * @return main super root command type 102 | */ 103 | @Override 104 | public Class getRootCommandType() { 105 | return OrderCmd.class; 106 | } 107 | 108 | /** 109 | * @return main super root event type 110 | */ 111 | @Override 112 | public Class getRootEventType() { 113 | return OrderEvent.class; 114 | } 115 | 116 | 117 | @Override 118 | public String asyncPersistentEntityDispatcherName() { 119 | return null; 120 | } 121 | 122 | /** 123 | * @return the custom configurable dispatcher name used for async blocking IO operations in the persistent entity, 124 | * to be used instead of the default akka actors dispatchers to not starve the actors thread dispatcher with blocking IO 125 | */ 126 | @Override 127 | public String pipeDispatcherName() { 128 | return null; 129 | } 130 | 131 | /** 132 | * @return the timeout for any async command handler actions , used to monitor pipeTo actions 133 | */ 134 | @Override 135 | public long scheduledAsyncEntityActionTimeout() { 136 | return 3; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/domain/OrderManager.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.domain; 2 | 3 | import static akka.actor.SupervisorStrategy.escalate; 4 | import static akka.actor.SupervisorStrategy.restart; 5 | import static akka.actor.SupervisorStrategy.resume; 6 | import static akka.actor.SupervisorStrategy.stop; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | 16 | import com.romeh.ordermanager.entities.OrderState; 17 | import com.romeh.ordermanager.entities.Response; 18 | import com.romeh.ordermanager.entities.commands.OrderCmd; 19 | import com.romeh.ordermanager.entities.enums.OrderStatus; 20 | import com.romeh.ordermanager.entities.events.CreatedEvent; 21 | import com.romeh.ordermanager.entities.events.FinishedEvent; 22 | import com.romeh.ordermanager.entities.events.OrderEvent; 23 | import com.romeh.ordermanager.entities.events.SignedEvent; 24 | import com.romeh.ordermanager.entities.events.ValidatedEvent; 25 | import com.spring.akka.eventsourcing.config.PersistentEntityProperties; 26 | import com.spring.akka.eventsourcing.persistance.AsyncResult; 27 | import com.spring.akka.eventsourcing.persistance.eventsourcing.ExecutionFlow; 28 | import com.spring.akka.eventsourcing.persistance.eventsourcing.FlowContext; 29 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntity; 30 | import com.spring.akka.eventsourcing.persistance.eventsourcing.ReadOnlyFlowContext; 31 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist; 32 | import com.spring.akka.eventsourcing.persistance.eventsourcing.annotations.PersistentActor; 33 | 34 | import akka.actor.OneForOneStrategy; 35 | import akka.actor.SupervisorStrategy; 36 | import akka.japi.pf.DeciderBuilder; 37 | import scala.concurrent.duration.Duration; 38 | 39 | /** 40 | * Tha main Event sourcing DDD aggregate class for order domain which handle the order commands within it is boundary context 41 | * 42 | * @author romeh 43 | */ 44 | @PersistentActor 45 | public class OrderManager extends PersistentEntity { 46 | 47 | /** 48 | * how to handle supervisor strategy definition for the parent actor of the entity 49 | */ 50 | private static SupervisorStrategy strategy = 51 | new OneForOneStrategy(10, Duration.create(1, TimeUnit.MINUTES), DeciderBuilder. 52 | match(ArithmeticException.class, e -> resume()). 53 | match(NullPointerException.class, e -> restart()). 54 | match(IllegalArgumentException.class, e -> stop()). 55 | matchAny(o -> escalate()).build()); 56 | 57 | /** 58 | * @param persistentEntityConfig the akka persistent entity configuration 59 | */ 60 | @Autowired 61 | public OrderManager(PersistentEntityProperties persistentEntityConfig) { 62 | super(persistentEntityConfig); 63 | } 64 | 65 | /** 66 | * @param state the current State 67 | * @return the initialized behavior for the entity 68 | */ 69 | @Override 70 | protected ExecutionFlow executionFlow(OrderState state) { 71 | switch (state.getOrderStatus()) { 72 | case NotStarted: 73 | return notStarted(state); 74 | case Created: 75 | return waitingForValidation(state); 76 | case Validated: 77 | return waitingForSigning(state); 78 | case Signed: 79 | return complected(state); 80 | case COMPLETED: 81 | return complected(state); 82 | default: 83 | throw new IllegalStateException(); 84 | 85 | } 86 | } 87 | 88 | @Override 89 | protected OrderState initialState() { 90 | return new OrderState(Collections.emptyList(), OrderStatus.NotStarted); 91 | } 92 | 93 | /** 94 | * ExecutionFlow for the not started state. 95 | */ 96 | private ExecutionFlow notStarted(OrderState state) { 97 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 98 | 99 | // Command handlers 100 | executionFlowBuilder.onCommand(OrderCmd.CreateCmd.class, (start, ctx, currentState) -> 101 | persistAndReply(ctx, new CreatedEvent(start.getOrderId(), OrderStatus.Created)) 102 | ); 103 | 104 | // Event handlers 105 | executionFlowBuilder.onEvent(CreatedEvent.class, (started, currentState) -> 106 | createImmutableState(state, started, OrderStatus.Created) 107 | ); 108 | 109 | return executionFlowBuilder.build(); 110 | } 111 | 112 | /** 113 | * ExecutionFlow for the not created and not yet validated. 114 | */ 115 | 116 | private ExecutionFlow waitingForValidation(OrderState state) { 117 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 118 | // Command handlers 119 | executionFlowBuilder.onCommand(OrderCmd.ValidateCmd.class, (start, ctx, currentState) -> 120 | persistAndReply(ctx, new ValidatedEvent(start.getOrderId(), OrderStatus.Validated)) 121 | ); 122 | // Read only command handlers 123 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.CreateCmd.class, this::alreadyDone); 124 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.SignCmd.class, this::notAllowed); 125 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.GetOrderStatusCmd.class, (cmd, ctx) -> ctx.reply(getState())); 126 | 127 | // Event handlers 128 | executionFlowBuilder.onEvent(ValidatedEvent.class, (validated, currentState) -> 129 | createImmutableState(state, validated, validated.getOrderStatus()) 130 | ); 131 | 132 | return executionFlowBuilder.build(); 133 | } 134 | 135 | /** 136 | * ExecutionFlow for the not validated and not yet signed. 137 | */ 138 | private ExecutionFlow waitingForSigning(OrderState state) { 139 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 140 | // Command handlers 141 | executionFlowBuilder.onCommand(OrderCmd.SignCmd.class, (start, ctx, currentState) -> 142 | persistAndReply(ctx, new SignedEvent(start.getOrderId(), OrderStatus.Signed)) 143 | ); 144 | // Async Command handler 145 | executionFlowBuilder.asyncOnCommand(OrderCmd.AsyncSignCmd.class, (signed, ctx, currentState) -> CompletableFuture 146 | .supplyAsync(() -> AsyncResult.builder() 147 | .persist(persistAndReply(ctx, new SignedEvent(signed.getOrderId(), OrderStatus.Signed))) 148 | .build()) 149 | ); 150 | // Read only command handlers 151 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.GetOrderStatusCmd.class, (cmd, ctx) -> ctx.reply(getState())); 152 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.ValidateCmd.class, this::alreadyDone); 153 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.CreateCmd.class, this::alreadyDone); 154 | // Event handlers 155 | executionFlowBuilder.onEvent(SignedEvent.class, (signed, currentState) -> 156 | createImmutableState(state, signed, signed.getOrderStatus()) 157 | ); 158 | 159 | return executionFlowBuilder.build(); 160 | } 161 | 162 | /** 163 | * ExecutionFlow for signed and final state 164 | */ 165 | private ExecutionFlow complected(OrderState state) { 166 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 167 | // just read only command handlers as it is final state 168 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.GetOrderStatusCmd.class, (cmd, ctx) -> ctx.reply(getState())); 169 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.CreateCmd.class, this::alreadyDone); 170 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.ValidateCmd.class, this::alreadyDone); 171 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.SignCmd.class, this::alreadyDone); 172 | // Event handlers 173 | executionFlowBuilder.onEvent(FinishedEvent.class, (finished, currentState) -> 174 | createImmutableState(state, finished, finished.getOrderStatus()) 175 | ); 176 | 177 | return executionFlowBuilder.build(); 178 | } 179 | 180 | /** 181 | * @param testState current state 182 | * @param testEvent new event 183 | * @param orderStatus new order status 184 | * @return immutable state 185 | */ 186 | private OrderState createImmutableState(OrderState testState, OrderEvent testEvent, OrderStatus orderStatus) { 187 | final List eventsHistory = new ArrayList<>(testState.getEventsHistory()); 188 | eventsHistory.add(testEvent); 189 | return new OrderState(eventsHistory, orderStatus); 190 | 191 | } 192 | 193 | /** 194 | * Persist a single event then respond with done. 195 | */ 196 | private Persist persistAndDone(FlowContext ctx, OrderEvent event) { 197 | return ctx.thenPersist(event, (e) -> ctx.reply(Response.builder().orderId(event.getOrderId()).responseMsg("successfully executed").orderStatus(event.getOrderStatus().name()).build())); 198 | } 199 | 200 | /** 201 | * Persist a single event then respond with done. 202 | */ 203 | private Persist persistAndReply(FlowContext ctx, OrderEvent event) { 204 | return ctx.thenPersist(event, (e) -> ctx.reply(Response.builder().orderStatus(event.getOrderStatus().name()).orderId(event.getOrderId()).build())); 205 | } 206 | 207 | /** 208 | * Convenience method to handle when a command has already been processed (idempotent processing). 209 | */ 210 | private void alreadyDone(OrderCmd cmd, ReadOnlyFlowContext ctx) { 211 | ctx.reply(Response.builder().orderId(cmd.getOrderId()).responseMsg("the command is already done and applied before").build()); 212 | } 213 | 214 | /** 215 | * Convenience method to handle when a command has is not allowed based into order state. 216 | */ 217 | private void notAllowed(OrderCmd cmd, ReadOnlyFlowContext ctx) { 218 | ctx.reply(Response.builder().orderId(cmd.getOrderId()).errorMessage("the request action is not allowed for the current order statue").errorCode("1111").build()); 219 | } 220 | 221 | /** 222 | * @return supervisorStrategy the actor supervisor strategy 223 | */ 224 | @Override 225 | public SupervisorStrategy supervisorStrategy() { 226 | return strategy; 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/OrderState.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | import com.romeh.ordermanager.entities.enums.OrderStatus; 7 | import com.romeh.ordermanager.entities.events.OrderEvent; 8 | 9 | import lombok.Value; 10 | 11 | /** 12 | * the main immutable order state object 13 | */ 14 | @Value 15 | public class OrderState implements Serializable { 16 | 17 | private final List eventsHistory; 18 | private final OrderStatus orderStatus; 19 | 20 | public OrderState(List eventsHistory, OrderStatus orderStatus) { 21 | this.eventsHistory = eventsHistory; 22 | 23 | this.orderStatus = orderStatus; 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/Response.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities; 2 | 3 | 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | /** 8 | * generic service response object 9 | */ 10 | @Data 11 | @Builder 12 | public class Response { 13 | 14 | private String orderId; 15 | private String orderStatus; 16 | private String errorCode; 17 | private String responseMsg; 18 | private String errorMessage; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/commands/OrderCmd.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities.commands; 2 | 3 | 4 | import java.io.Serializable; 5 | import java.util.Map; 6 | 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | 9 | import lombok.Value; 10 | 11 | 12 | public interface OrderCmd extends Serializable { 13 | 14 | String getOrderId(); 15 | 16 | Map getOrderDetails(); 17 | 18 | @Value 19 | @JsonDeserialize(as = CreateCmd.class) 20 | final class CreateCmd implements OrderCmd { 21 | private String orderId; 22 | private Map orderDetails; 23 | } 24 | 25 | @Value 26 | @JsonDeserialize(as = ValidateCmd.class) 27 | final class ValidateCmd implements OrderCmd { 28 | private String orderId; 29 | private Map orderDetails; 30 | } 31 | 32 | @Value 33 | @JsonDeserialize(as = SignCmd.class) 34 | final class SignCmd implements OrderCmd { 35 | private String orderId; 36 | private Map orderDetails; 37 | } 38 | 39 | @Value 40 | @JsonDeserialize(as = GetOrderStatusCmd.class) 41 | final class GetOrderStatusCmd implements OrderCmd { 42 | private String orderId; 43 | private Map orderDetails; 44 | } 45 | 46 | @Value 47 | @JsonDeserialize(as = AsyncSignCmd.class) 48 | final class AsyncSignCmd implements OrderCmd { 49 | private String orderId; 50 | private Map orderDetails; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/enums/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities.enums; 2 | 3 | public enum OrderStatus { 4 | 5 | NotStarted, Created, Validated, Signed, COMPLETED; 6 | } 7 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/events/CreatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities.events; 2 | 3 | 4 | import com.romeh.ordermanager.entities.enums.OrderStatus; 5 | 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Value; 8 | 9 | @Value 10 | @EqualsAndHashCode(callSuper = true) 11 | public class CreatedEvent extends OrderEvent { 12 | 13 | public CreatedEvent(String orderId, OrderStatus orderStatus) { 14 | super(orderId, orderStatus); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/events/FinishedEvent.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities.events; 2 | 3 | 4 | import com.romeh.ordermanager.entities.enums.OrderStatus; 5 | 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Value; 8 | 9 | @Value 10 | @EqualsAndHashCode(callSuper = true) 11 | public class FinishedEvent extends OrderEvent { 12 | 13 | public FinishedEvent(String orderId, OrderStatus orderStatus) { 14 | super(orderId, orderStatus); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/events/OrderEvent.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities.events; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.romeh.ordermanager.entities.enums.OrderStatus; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Getter 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public abstract class OrderEvent implements Serializable { 15 | private String orderId; 16 | private OrderStatus orderStatus; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/events/SignedEvent.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities.events; 2 | 3 | import com.romeh.ordermanager.entities.enums.OrderStatus; 4 | 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Value; 7 | 8 | @EqualsAndHashCode(callSuper = true) 9 | @Value 10 | public class SignedEvent extends OrderEvent { 11 | 12 | public SignedEvent(String orderId, OrderStatus orderStatus) { 13 | super(orderId, orderStatus); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/entities/events/ValidatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.entities.events; 2 | 3 | 4 | import com.romeh.ordermanager.entities.enums.OrderStatus; 5 | 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Value; 8 | 9 | @EqualsAndHashCode(callSuper = true) 10 | @Value 11 | public class ValidatedEvent extends OrderEvent { 12 | 13 | public ValidatedEvent(String orderId, OrderStatus orderStatus) { 14 | super(orderId, orderStatus); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/entities/JournalReadItem.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.entities; 2 | 3 | import org.apache.ignite.binary.BinaryObjectException; 4 | import org.apache.ignite.binary.BinaryReader; 5 | import org.apache.ignite.binary.BinaryWriter; 6 | import org.apache.ignite.binary.Binarylizable; 7 | import org.apache.ignite.cache.query.annotations.QuerySqlField; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | 13 | /** 14 | * Created by MRomeh 15 | * the journal read side cache value object 16 | */ 17 | @Data 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class JournalReadItem implements Binarylizable { 21 | 22 | public static final String ORDER_ID = "orderId"; 23 | public static final String STATUS_FIELD = "status"; 24 | 25 | 26 | @QuerySqlField(index = true) 27 | private String orderId; 28 | @QuerySqlField(index = true) 29 | private String status; 30 | 31 | 32 | @Override 33 | public void writeBinary(BinaryWriter out) throws BinaryObjectException { 34 | out.writeString(ORDER_ID, orderId); 35 | out.writeString(STATUS_FIELD, status); 36 | } 37 | 38 | @Override 39 | public void readBinary(BinaryReader in) throws BinaryObjectException { 40 | orderId = in.readString(ORDER_ID); 41 | status = in.readString(STATUS_FIELD); 42 | } 43 | } -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/services/OrderNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.services; 2 | 3 | /** 4 | * order not found exception 5 | * 6 | * @author romeh 7 | */ 8 | public class OrderNotFoundException extends Exception { 9 | 10 | public OrderNotFoundException(String errorMsg) { 11 | super(errorMsg); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/services/ReadStoreExtractor.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.services; 2 | 3 | import java.io.NotSerializableException; 4 | import java.util.Map; 5 | 6 | import javax.cache.event.CacheEntryEvent; 7 | 8 | import org.apache.ignite.binary.BinaryObject; 9 | import org.apache.ignite.lang.IgniteBiTuple; 10 | import org.apache.ignite.stream.StreamSingleTupleExtractor; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import com.romeh.ordermanager.entities.events.OrderEvent; 15 | import com.romeh.ordermanager.reader.entities.JournalReadItem; 16 | import com.romeh.ordermanager.serializer.OrderManagerSerializer; 17 | 18 | import akka.persistence.ignite.common.enums.FieldNames; 19 | import akka.persistence.serialization.MessageFormats; 20 | import akka.protobuf.InvalidProtocolBufferException; 21 | 22 | /** 23 | * @author romeh 24 | */ 25 | public class ReadStoreExtractor implements StreamSingleTupleExtractor, String, JournalReadItem> { 26 | private static final Logger log = LoggerFactory.getLogger(ReadStoreExtractor.class); 27 | private static final OrderManagerSerializer orderOrderManagerSerializer = new OrderManagerSerializer(); 28 | 29 | /** 30 | * @param msg the cache entry event which contain the added/update value object 31 | * @return the mapped read cache entry which will be streamed to the target cache 32 | */ 33 | @Override 34 | public Map.Entry extract(CacheEntryEvent msg) { 35 | 36 | IgniteBiTuple readRecord = null; 37 | final BinaryObject value = msg.getValue(); 38 | final String orderId = value.field(FieldNames.persistenceId.name()); 39 | final byte[] payload = value.field(FieldNames.payload.name()); 40 | try { 41 | final MessageFormats.PersistentMessage persistentMessage = MessageFormats.PersistentMessage.parseFrom(payload); 42 | if (persistentMessage.hasPayload()) { 43 | final OrderEvent event = (OrderEvent) orderOrderManagerSerializer.fromBinary(persistentMessage.getPayload().getPayload().toByteArray(), persistentMessage.getPayload().getPayloadManifest().toStringUtf8()); 44 | readRecord = new IgniteBiTuple<>(event.getOrderId(), new JournalReadItem(event.getOrderId(), event.getOrderStatus().name())); 45 | } 46 | } catch (InvalidProtocolBufferException | NotSerializableException e) { 47 | log.error("error in parsing the msg for id {} with error {}", orderId, e); 48 | } 49 | 50 | return readRecord; 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/services/ReadStoreStreamerService.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.services; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.Optional; 9 | 10 | import org.apache.ignite.IgniteCache; 11 | import org.apache.ignite.cache.CacheAtomicityMode; 12 | import org.apache.ignite.cache.CacheMode; 13 | import org.apache.ignite.cache.QueryEntity; 14 | import org.apache.ignite.cache.QueryIndex; 15 | import org.apache.ignite.configuration.CacheConfiguration; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.context.ApplicationListener; 18 | import org.springframework.context.event.ContextRefreshedEvent; 19 | import org.springframework.stereotype.Component; 20 | 21 | import com.romeh.ordermanager.reader.entities.JournalReadItem; 22 | import com.romeh.ordermanager.reader.streamer.IgniteSinkConstants; 23 | import com.romeh.ordermanager.reader.streamer.IgniteSourceConstants; 24 | import com.romeh.ordermanager.reader.streamer.grid.streamer.IgniteCacheEventStreamerRx; 25 | 26 | import akka.actor.AbstractActor; 27 | import akka.actor.ActorSystem; 28 | import akka.actor.Props; 29 | import akka.japi.pf.ReceiveBuilder; 30 | import akka.persistence.ignite.common.entities.JournalStarted; 31 | import akka.persistence.ignite.extension.IgniteExtension; 32 | import akka.persistence.ignite.extension.IgniteExtensionProvider; 33 | 34 | /** 35 | * the read side service to show the case how you can stream stored write event store to a read side store using Rx java and ignite streamer APIs 36 | * 37 | * @author romeh 38 | */ 39 | @Component 40 | public class ReadStoreStreamerService implements ApplicationListener { 41 | 42 | private static final String JOURNAL_CACHE = "akka-journal"; 43 | private static final String READ_CACHE = "Read-Store"; 44 | private final IgniteExtension igniteExtension; 45 | private final ActorSystem actorSystem; 46 | private final IgniteCache readStore; 47 | 48 | @Autowired 49 | public ReadStoreStreamerService(ActorSystem actorSystem) { 50 | this.actorSystem = actorSystem; 51 | this.igniteExtension = IgniteExtensionProvider.EXTENSION.get(actorSystem); 52 | // make sure the read store cache is created 53 | //usually you should not need that as the writer and reader should be different nodes 54 | this.readStore = getOrCreateReadStoreCache(); 55 | } 56 | 57 | 58 | /** 59 | * @param orderId order id to get the status for 60 | * @return the order status if any 61 | */ 62 | public JournalReadItem getOrderStatus(String orderId) throws OrderNotFoundException { 63 | return Optional.ofNullable(readStore.get(orderId)) 64 | .orElseThrow(() -> new OrderNotFoundException("order is not found in the read store")); 65 | } 66 | 67 | /** 68 | * @param contextStartedEvent the spring context started event 69 | * start the events streamer once the spring context init is finished 70 | */ 71 | @Override 72 | public void onApplicationEvent(ContextRefreshedEvent contextStartedEvent) { 73 | actorSystem.actorOf(Props.create(IgniteStreamerStarter.class, IgniteStreamerStarter::new), "IgniteStreamerActor"); 74 | 75 | } 76 | 77 | /** 78 | * start the event streamer from the journal write event store to the read side cache store which store only query intersted data 79 | */ 80 | private void startIgniteStreamer() { 81 | // streamer parameters 82 | Map sourceMap = new HashMap<>(); 83 | sourceMap.put(IgniteSourceConstants.CACHE_NAME, JOURNAL_CACHE); 84 | Map sinkMap = new HashMap<>(); 85 | sinkMap.put(IgniteSinkConstants.CACHE_NAME, READ_CACHE); 86 | sinkMap.put(IgniteSinkConstants.CACHE_ALLOW_OVERWRITE, "true"); 87 | sinkMap.put(IgniteSinkConstants.SINGLE_TUPLE_EXTRACTOR_CLASS, ReadStoreExtractor.class.getName()); 88 | // start the streamer 89 | final IgniteCacheEventStreamerRx journalStreamer = IgniteCacheEventStreamerRx.builderWithContinuousQuery() 90 | .pollingInterval(500) 91 | .flushBufferSize(5) 92 | .retryTimesOnError(2) 93 | .sourceCache(sourceMap, igniteExtension.getIgnite()) 94 | .sinkCache(sinkMap, igniteExtension.getIgnite()) 95 | .build(); 96 | 97 | journalStreamer.execute(); 98 | } 99 | 100 | /** 101 | * check if the read side cache is already created or create it if missing 102 | * usually you should not need that as the writer and reader should be different nodes 103 | */ 104 | private IgniteCache getOrCreateReadStoreCache() { 105 | IgniteCache cache = igniteExtension.getIgnite().cache(READ_CACHE); 106 | 107 | if (null == cache) { 108 | CacheConfiguration readItemCacheConfiguration = new CacheConfiguration<>(); 109 | readItemCacheConfiguration.setBackups(1); 110 | readItemCacheConfiguration.setName(READ_CACHE); 111 | readItemCacheConfiguration.setAtomicityMode(CacheAtomicityMode.ATOMIC); 112 | readItemCacheConfiguration.setCacheMode(CacheMode.PARTITIONED); 113 | readItemCacheConfiguration.setQueryEntities(Collections.singletonList(createJournalBinaryQueryEntity())); 114 | cache = igniteExtension.getIgnite().getOrCreateCache(readItemCacheConfiguration); 115 | } 116 | return cache; 117 | } 118 | 119 | /** 120 | * @return QueryEntity which the binary query definition of the binary object stored into the read side cache 121 | */ 122 | private QueryEntity createJournalBinaryQueryEntity() { 123 | QueryEntity queryEntity = new QueryEntity(); 124 | queryEntity.setValueType(JournalReadItem.class.getName()); 125 | queryEntity.setKeyType(String.class.getName()); 126 | LinkedHashMap fields = new LinkedHashMap<>(); 127 | fields.put(JournalReadItem.ORDER_ID, String.class.getName()); 128 | fields.put(JournalReadItem.STATUS_FIELD, String.class.getName()); 129 | queryEntity.setFields(fields); 130 | queryEntity.setIndexes(Arrays.asList(new QueryIndex(JournalReadItem.ORDER_ID), 131 | new QueryIndex(JournalReadItem.STATUS_FIELD))); 132 | return queryEntity; 133 | 134 | } 135 | 136 | /** 137 | * simple akka actor to listen to the journal started event so it can start the event store streamer once the persistence store is running 138 | * usually you should not need that as the writer and reader should be different nodes but as here for the sake of the example we have reader and writer 139 | * running in the same server ignite node 140 | */ 141 | private final class IgniteStreamerStarter extends AbstractActor { 142 | 143 | @Override 144 | public void preStart() { 145 | getContext().getSystem().eventStream().subscribe(getSelf(), JournalStarted.class); 146 | } 147 | 148 | @Override 149 | public Receive createReceive() { 150 | // start the streamer once it receive the journal started event 151 | return ReceiveBuilder.create().match(JournalStarted.class, journalStarted -> startIgniteStreamer()).build(); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/IgniteSink.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.streamer; 2 | 3 | import java.util.Collection; 4 | 5 | /** 6 | * the generic Ignite sink task interface to be used into the streamer flow 7 | * 8 | * @author romeh 9 | */ 10 | public interface IgniteSink { 11 | 12 | /** 13 | * @param records the records to stream to the sink cache 14 | */ 15 | void put(Collection records); 16 | 17 | /** 18 | * stop the sink streamer 19 | */ 20 | void stop(); 21 | 22 | /** 23 | * flush the buffered records in the sink streamer 24 | */ 25 | void flush(); 26 | 27 | /** 28 | * @return boolean of if the sink streamer is already stopped 29 | */ 30 | boolean isStopped(); 31 | } 32 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/IgniteSinkConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.romeh.ordermanager.reader.streamer; 19 | 20 | /** 21 | * Sink configuration strings. 22 | */ 23 | public class IgniteSinkConstants { 24 | 25 | private IgniteSinkConstants() { 26 | } 27 | 28 | /** 29 | * Cache name. 30 | */ 31 | public static final String CACHE_NAME = "cacheName"; 32 | 33 | /** 34 | * Flag to enable overwriting existing values in cache. 35 | */ 36 | public static final String CACHE_ALLOW_OVERWRITE = "cacheAllowOverwrite"; 37 | 38 | /** 39 | * Size of per-node buffer before data is sent to remote node. 40 | */ 41 | public static final String CACHE_PER_NODE_DATA_SIZE = "cachePerNodeDataSize"; 42 | 43 | /** 44 | * Maximum number of parallel stream operations per node. 45 | */ 46 | public static final String CACHE_PER_NODE_PAR_OPS = "cachePerNodeParOps"; 47 | 48 | /** 49 | * Class to transform the entry before feeding into cache. 50 | */ 51 | public static final String SINGLE_TUPLE_EXTRACTOR_CLASS = "singleTupleExtractorCls"; 52 | } 53 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/IgniteSource.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.streamer; 2 | 3 | import java.util.Collection; 4 | 5 | 6 | /** 7 | * the generic interface for ignite source definition which can be used into the streamer API 8 | * 9 | * @author romeh 10 | */ 11 | public interface IgniteSource { 12 | 13 | /** 14 | * @return the records which have been polled from the source cache 15 | */ 16 | Collection poll(); 17 | 18 | /** 19 | * stop the source cache task 20 | */ 21 | void stop(); 22 | 23 | /** 24 | * @return if the source cache task is stopped 25 | */ 26 | boolean isStopped(); 27 | } 28 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/IgniteSourceConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.romeh.ordermanager.reader.streamer; 19 | 20 | /** 21 | * Sink configuration strings. 22 | */ 23 | public class IgniteSourceConstants { 24 | 25 | private IgniteSourceConstants() { 26 | } 27 | 28 | /** 29 | * Cache name. 30 | */ 31 | public static final String CACHE_NAME = "cacheName"; 32 | /** 33 | * Internal buffer size. 34 | */ 35 | public static final String INTL_BUF_SIZE = "evtBufferSize"; 36 | 37 | /** 38 | * Size of one chunk drained from the internal buffer. 39 | */ 40 | public static final String INTL_BATCH_SIZE = "evtBatchSize"; 41 | 42 | /** 43 | * User-defined filter class. 44 | */ 45 | public static final String CACHE_FILTER_CLASS = "cacheFilterCls"; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/StreamReport.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.streamer; 2 | 3 | import java.util.HashSet; 4 | import java.util.Objects; 5 | import java.util.Set; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | /** 9 | * the streamer API report for how many records has been streamed and any error reporting as well 10 | * 11 | * @author romeh 12 | */ 13 | public class StreamReport { 14 | 15 | private final AtomicInteger addedRecord = new AtomicInteger(0); 16 | private final Set errorMsgs = new HashSet<>(); 17 | 18 | 19 | public void addErrorMsg(String errorMsg) { 20 | errorMsgs.add(errorMsg); 21 | } 22 | 23 | public Set getErrorMsgs() { 24 | return errorMsgs; 25 | } 26 | 27 | public int getAddedRecords() { 28 | return addedRecord.get(); 29 | } 30 | 31 | 32 | public void incrementAddedRecords(int count) { 33 | addedRecord.getAndAdd(count); 34 | } 35 | 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | StreamReport that = (StreamReport) o; 42 | return Objects.equals(addedRecord, that.addedRecord) && 43 | Objects.equals(errorMsgs, that.errorMsgs); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(addedRecord, errorMsgs); 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "StreamReport{" + 54 | "addedRecord=" + addedRecord + 55 | ", errorMsgs=" + errorMsgs + 56 | '}'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/grid/cq/IgniteCacheSinkTaskCQ.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.romeh.ordermanager.reader.streamer.grid.cq; 19 | 20 | import java.util.Collection; 21 | import java.util.Map; 22 | import java.util.Objects; 23 | import java.util.Optional; 24 | 25 | import javax.cache.event.CacheEntryEvent; 26 | 27 | import org.apache.ignite.Ignite; 28 | import org.apache.ignite.IgniteDataStreamer; 29 | import org.apache.ignite.stream.StreamSingleTupleExtractor; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import com.romeh.ordermanager.reader.streamer.IgniteSink; 34 | import com.romeh.ordermanager.reader.streamer.IgniteSinkConstants; 35 | 36 | /** 37 | * Task to consume sequences of SinkRecords generated from the ignite cache continuous query and write data to grid. 38 | */ 39 | public class IgniteCacheSinkTaskCQ implements IgniteSink { 40 | /** 41 | * Logger. 42 | */ 43 | private static final Logger log = LoggerFactory.getLogger(IgniteCacheSinkTaskCQ.class); 44 | /** 45 | * Flag for stopped state. 46 | */ 47 | private volatile boolean stopped = true; 48 | 49 | /** 50 | * the sink cache ignite sink data streamer context 51 | */ 52 | private final transient StreamerContext streamerContext; 53 | 54 | 55 | public IgniteCacheSinkTaskCQ(Map props, Ignite sinkNode) { 56 | Objects.requireNonNull(sinkNode); 57 | String cacheName = Optional.ofNullable(props.get(IgniteSinkConstants.CACHE_NAME)) 58 | .orElseThrow(() -> new IllegalArgumentException("Cache name in sink task can not be NULL !")); 59 | 60 | streamerContext = new StreamerContext(sinkNode, cacheName, initTransformer(props)); 61 | 62 | if (props.containsKey(IgniteSinkConstants.CACHE_ALLOW_OVERWRITE)) 63 | streamerContext.getStreamer().allowOverwrite( 64 | Boolean.parseBoolean(props.get(IgniteSinkConstants.CACHE_ALLOW_OVERWRITE))); 65 | 66 | if (props.containsKey(IgniteSinkConstants.CACHE_PER_NODE_DATA_SIZE)) 67 | streamerContext.getStreamer().perNodeBufferSize( 68 | Integer.parseInt(props.get(IgniteSinkConstants.CACHE_PER_NODE_DATA_SIZE))); 69 | 70 | if (props.containsKey(IgniteSinkConstants.CACHE_PER_NODE_PAR_OPS)) 71 | streamerContext.getStreamer().perNodeParallelOperations( 72 | Integer.parseInt(props.get(IgniteSinkConstants.CACHE_PER_NODE_PAR_OPS))); 73 | 74 | stopped = false; 75 | } 76 | 77 | @SuppressWarnings("unchecked") 78 | private StreamSingleTupleExtractor initTransformer(Map props) { 79 | StreamSingleTupleExtractor instance = null; 80 | if (props.containsKey(IgniteSinkConstants.SINGLE_TUPLE_EXTRACTOR_CLASS)) { 81 | String transformerCls = props.get(IgniteSinkConstants.SINGLE_TUPLE_EXTRACTOR_CLASS); 82 | if (transformerCls != null && !transformerCls.isEmpty()) { 83 | try { 84 | Class> clazz = 85 | (Class>) 86 | Class.forName(transformerCls); 87 | 88 | instance = clazz.newInstance(); 89 | } catch (Exception e) { 90 | throw new IllegalStateException("Failed to instantiate the provided transformer!", e); 91 | } 92 | } 93 | } 94 | return instance; 95 | 96 | } 97 | 98 | 99 | public boolean isStopped() { 100 | return stopped; 101 | } 102 | 103 | /** 104 | * Buffers records. 105 | * 106 | * @param records Records to inject into grid. 107 | */ 108 | @SuppressWarnings("unchecked") 109 | @Override 110 | public void put(Collection records) { 111 | if (log.isDebugEnabled()) { 112 | log.debug("Sink cache put : {}", records); 113 | } 114 | if (null != records && !records.isEmpty()) { 115 | for (CacheEntryEvent record : records) { 116 | // Data is flushed asynchronously when CACHE_PER_NODE_DATA_SIZE is reached. 117 | if (streamerContext.getExtractor() != null) { 118 | Map.Entry entry = streamerContext.getExtractor().extract(record); 119 | if (null != entry) { 120 | streamerContext.getStreamer().addData(entry.getKey(), entry.getValue()); 121 | } 122 | } else { 123 | if (record.getKey() != null) { 124 | streamerContext.getStreamer().addData(record.getKey(), record.getValue()); 125 | } else { 126 | log.error("Failed to stream a record with null key!"); 127 | } 128 | } 129 | } 130 | } 131 | 132 | } 133 | 134 | /** 135 | * Pushes buffered data to grid. Flush interval is configured by worker configurations. 136 | */ 137 | @Override 138 | public void flush() { 139 | if (log.isDebugEnabled()) { 140 | log.debug("Sink Cache flush is called"); 141 | } 142 | if (stopped) 143 | return; 144 | 145 | streamerContext.getStreamer().flush(); 146 | } 147 | 148 | /** 149 | * Stops the grid client. 150 | */ 151 | @Override 152 | public void stop() { 153 | if (stopped) 154 | return; 155 | 156 | stopped = true; 157 | streamerContext.getStreamer().close(); 158 | } 159 | 160 | 161 | /** 162 | * Streamer context initializing grid and data streamer instances on demand. 163 | */ 164 | private static class StreamerContext { 165 | private final transient Ignite IGNITE; 166 | private final transient IgniteDataStreamer STREAMER; 167 | /** 168 | * Sink Cache name. 169 | */ 170 | private final String cacheName; 171 | 172 | /** 173 | * Entry transformer. 174 | */ 175 | private final StreamSingleTupleExtractor extractor; 176 | 177 | /** 178 | * Constructor. 179 | * @param ignite 180 | * @param cacheName 181 | * @param extractor 182 | */ 183 | StreamerContext(Ignite ignite, String cacheName, StreamSingleTupleExtractor extractor) { 184 | IGNITE = ignite; 185 | this.cacheName = cacheName; 186 | this.extractor = extractor; 187 | STREAMER = IGNITE.dataStreamer(this.cacheName); 188 | } 189 | 190 | 191 | /** 192 | * Obtains grid instance. 193 | * 194 | * @return Grid instance. 195 | */ 196 | public Ignite getIgnite() { 197 | return IGNITE; 198 | } 199 | 200 | /** 201 | * Obtains data streamer instance. 202 | * 203 | * @return Data streamer instance. 204 | */ 205 | IgniteDataStreamer getStreamer() { 206 | return STREAMER; 207 | } 208 | 209 | public String getCacheName() { 210 | return cacheName; 211 | } 212 | 213 | public StreamSingleTupleExtractor getExtractor() { 214 | return extractor; 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/grid/cq/IgniteCacheSourceTaskCQ.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.romeh.ordermanager.reader.streamer.grid.cq; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Objects; 25 | import java.util.concurrent.BlockingQueue; 26 | import java.util.concurrent.LinkedBlockingQueue; 27 | import java.util.concurrent.TimeUnit; 28 | import java.util.concurrent.atomic.AtomicBoolean; 29 | import java.util.function.Consumer; 30 | 31 | import javax.cache.Cache; 32 | import javax.cache.event.CacheEntryEvent; 33 | import javax.cache.event.CacheEntryListenerException; 34 | import javax.cache.event.EventType; 35 | 36 | import org.apache.ignite.Ignite; 37 | import org.apache.ignite.IgniteException; 38 | import org.apache.ignite.binary.BinaryObject; 39 | import org.apache.ignite.cache.CacheEntryEventSerializableFilter; 40 | import org.apache.ignite.cache.affinity.Affinity; 41 | import org.apache.ignite.cache.query.ContinuousQuery; 42 | import org.apache.ignite.cache.query.QueryCursor; 43 | import org.apache.ignite.lang.IgniteAsyncCallback; 44 | import org.apache.ignite.lang.IgnitePredicate; 45 | import org.apache.ignite.resources.IgniteInstanceResource; 46 | import org.slf4j.Logger; 47 | import org.slf4j.LoggerFactory; 48 | 49 | import com.romeh.ordermanager.reader.streamer.IgniteSource; 50 | import com.romeh.ordermanager.reader.streamer.IgniteSourceConstants; 51 | 52 | /** 53 | * Task to consume remote cluster cache events from the grid and inject them into Kafka. 54 | *

55 | * Note that a task will create a bounded queue in the grid for more reliable data transfer. 56 | * Queue size can be changed by {@link IgniteSourceConstants#INTL_BUF_SIZE}. 57 | */ 58 | public class IgniteCacheSourceTaskCQ implements IgniteSource { 59 | /** 60 | * Logger. 61 | */ 62 | private static final Logger log = LoggerFactory.getLogger(IgniteCacheSourceTaskCQ.class); 63 | 64 | /** 65 | * Event buffer size. 66 | */ 67 | private int evtBufSize = 1000; 68 | /** 69 | * Event buffer. 70 | */ 71 | private BlockingQueue evtBuf = new LinkedBlockingQueue<>(evtBufSize); 72 | /** 73 | * Flag for stopped state. 74 | */ 75 | private final AtomicBoolean stopped = new AtomicBoolean(true); 76 | /** 77 | * Max number of events taken from the buffer at once. 78 | */ 79 | private int evtBatchSize = 10; 80 | /** 81 | * Local listener. 82 | */ 83 | private final TaskLocalListener locLsnr; 84 | 85 | 86 | private final QueryCursor> cursor; 87 | 88 | 89 | public IgniteCacheSourceTaskCQ(Map props, Ignite igniteNode) { 90 | Objects.requireNonNull(igniteNode); 91 | /** 92 | * Cache name. 93 | */ 94 | String cacheName = props.get(IgniteSourceConstants.CACHE_NAME); 95 | 96 | if (props.containsKey(IgniteSourceConstants.INTL_BUF_SIZE)) { 97 | evtBufSize = Integer.parseInt(props.get(IgniteSourceConstants.INTL_BUF_SIZE)); 98 | evtBuf = new LinkedBlockingQueue<>(evtBufSize); 99 | } 100 | 101 | if (props.containsKey(IgniteSourceConstants.INTL_BATCH_SIZE)) 102 | evtBatchSize = Integer.parseInt(props.get(IgniteSourceConstants.INTL_BATCH_SIZE)); 103 | 104 | try { 105 | locLsnr = new TaskLocalListener(this::handleCacheEvent); 106 | CacheEntryFilter rmtLsnr = new CacheEntryFilter(startSourceCacheListeners(props)); 107 | // Creating a continuous query. 108 | ContinuousQuery qry = new ContinuousQuery<>(); 109 | qry.setLocalListener(locLsnr::apply); 110 | qry.setRemoteFilterFactory(() -> rmtLsnr); 111 | cursor = igniteNode.cache(cacheName).withKeepBinary().query(qry); 112 | 113 | } catch (Exception e) { 114 | log.error("Failed to register event listener!", e); 115 | throw new IllegalStateException(e); 116 | } finally { 117 | stopped.set(false); 118 | } 119 | } 120 | 121 | @SuppressWarnings("unchecked") 122 | private static IgnitePredicate startSourceCacheListeners(Map props) { 123 | 124 | IgnitePredicate ignitePredicate = null; 125 | if (props.containsKey(IgniteSourceConstants.CACHE_FILTER_CLASS)) { 126 | String filterCls = props.get(IgniteSourceConstants.CACHE_FILTER_CLASS); 127 | if (filterCls != null && !filterCls.isEmpty()) { 128 | try { 129 | Class> clazz = 130 | (Class>) Class.forName(filterCls); 131 | ignitePredicate = clazz.newInstance(); 132 | } catch (Exception e) { 133 | log.error("Failed to instantiate the provided filter! " + 134 | "User-enabled filtering is ignored!", e); 135 | } 136 | } 137 | } 138 | return ignitePredicate; 139 | } 140 | 141 | public boolean isStopped() { 142 | return stopped.get(); 143 | } 144 | 145 | @Override 146 | public List poll() { 147 | if (log.isDebugEnabled()) { 148 | log.debug("Cache source polling has been started !"); 149 | } 150 | ArrayList evts = new ArrayList<>(evtBatchSize); 151 | 152 | if (stopped.get()) 153 | return evts; 154 | 155 | try { 156 | if (evtBuf.drainTo(evts, evtBatchSize) > 0) { 157 | if (log.isDebugEnabled()) { 158 | log.debug("Polled events {}", evts); 159 | } 160 | return evts; 161 | } 162 | } catch (IgniteException e) { 163 | log.error("Error when polling event queue!", e); 164 | } 165 | 166 | // for shutdown. 167 | return Collections.emptyList(); 168 | } 169 | 170 | 171 | /** 172 | * Stops the grid client. 173 | */ 174 | @Override 175 | public void stop() { 176 | if (stopped.get()) 177 | return; 178 | 179 | if (stopped.compareAndSet(false, true)) { 180 | stopRemoteListen(); 181 | } 182 | 183 | } 184 | 185 | /** 186 | * Stops the remote listener. 187 | */ 188 | private void stopRemoteListen() { 189 | if (cursor != null) 190 | cursor.close(); 191 | } 192 | 193 | /** 194 | * Local listener buffering cache events to be further sent to Kafka. 195 | */ 196 | private static class TaskLocalListener implements IgnitePredicate>> { 197 | /** 198 | * {@inheritDoc} 199 | */ 200 | 201 | private final transient Consumer> evntBufferConsumer; 202 | 203 | public TaskLocalListener(Consumer> evntBufferConsumer) { 204 | this.evntBufferConsumer = evntBufferConsumer; 205 | } 206 | 207 | @Override 208 | public boolean apply(Iterable> cacheEntryEvents) { 209 | cacheEntryEvents.forEach(evt -> { 210 | if (evt.getEventType().equals(EventType.CREATED) || evt.getEventType().equals(EventType.UPDATED)) 211 | evntBufferConsumer.accept(evt); 212 | }); 213 | return true; 214 | } 215 | } 216 | 217 | 218 | public void handleCacheEvent(CacheEntryEvent evt) { 219 | try { 220 | if (!evtBuf.offer(evt, 10, TimeUnit.MILLISECONDS)) 221 | log.error("Failed to buffer event {}", evt.getEventType()); 222 | } catch (InterruptedException e) { 223 | log.error("error has been thrown in TaskLocalListener {} ", e); 224 | Thread.currentThread().interrupt(); 225 | } 226 | } 227 | 228 | /** 229 | * remote filer 230 | */ 231 | @IgniteAsyncCallback 232 | private static class CacheEntryFilter implements CacheEntryEventSerializableFilter { 233 | /** 234 | * Ignite instance. 235 | */ 236 | @IgniteInstanceResource 237 | private transient Ignite ignite; 238 | 239 | private final IgnitePredicate filter; 240 | 241 | private CacheEntryFilter(IgnitePredicate filter) { 242 | this.filter = filter; 243 | } 244 | 245 | 246 | @Override 247 | public boolean evaluate(CacheEntryEvent evt) throws CacheEntryListenerException { 248 | Affinity affinity = ignite.affinity(evt.getSource().getName()); 249 | 250 | if (evt.getEventType().equals(EventType.CREATED) || evt.getEventType().equals(EventType.UPDATED) && affinity.isPrimary(ignite.cluster().localNode(), evt.getKey())) { 251 | // Process this event. Ignored on backups. 252 | return filter == null || !filter.apply(evt); 253 | } 254 | return false; 255 | } 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/grid/events/IgniteCacheSinkTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.romeh.ordermanager.reader.streamer.grid.events; 19 | 20 | import java.util.Collection; 21 | import java.util.Map; 22 | import java.util.Objects; 23 | import java.util.Optional; 24 | 25 | import org.apache.ignite.Ignite; 26 | import org.apache.ignite.IgniteDataStreamer; 27 | import org.apache.ignite.events.CacheEvent; 28 | import org.apache.ignite.stream.StreamSingleTupleExtractor; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | import com.romeh.ordermanager.reader.streamer.IgniteSink; 33 | import com.romeh.ordermanager.reader.streamer.IgniteSinkConstants; 34 | 35 | /** 36 | * Task to consume sequences of SinkRecords generated from ignite events stream and write data to grid. 37 | */ 38 | public class IgniteCacheSinkTask implements IgniteSink { 39 | /** 40 | * Logger. 41 | */ 42 | private static final Logger log = LoggerFactory.getLogger(IgniteCacheSinkTask.class); 43 | /** 44 | * Flag for stopped state. 45 | */ 46 | private static volatile boolean stopped = true; 47 | /** 48 | * Cache name. 49 | */ 50 | private static String cacheName; 51 | private static StreamerContext streamerContext; 52 | /** 53 | * Entry transformer. 54 | */ 55 | private final StreamSingleTupleExtractor extractor; 56 | 57 | 58 | public IgniteCacheSinkTask(Map props, Ignite sinkNode) { 59 | Objects.requireNonNull(sinkNode); 60 | cacheName = Optional.ofNullable(props.get(IgniteSinkConstants.CACHE_NAME)) 61 | .orElseThrow(() -> new IllegalArgumentException("Cache name in sink task can not be NULL !")); 62 | 63 | streamerContext = new StreamerContext(sinkNode); 64 | 65 | if (props.containsKey(IgniteSinkConstants.CACHE_ALLOW_OVERWRITE)) 66 | streamerContext.getStreamer().allowOverwrite( 67 | Boolean.parseBoolean(props.get(IgniteSinkConstants.CACHE_ALLOW_OVERWRITE))); 68 | 69 | if (props.containsKey(IgniteSinkConstants.CACHE_PER_NODE_DATA_SIZE)) 70 | streamerContext.getStreamer().perNodeBufferSize( 71 | Integer.parseInt(props.get(IgniteSinkConstants.CACHE_PER_NODE_DATA_SIZE))); 72 | 73 | if (props.containsKey(IgniteSinkConstants.CACHE_PER_NODE_PAR_OPS)) 74 | streamerContext.getStreamer().perNodeParallelOperations( 75 | Integer.parseInt(props.get(IgniteSinkConstants.CACHE_PER_NODE_PAR_OPS))); 76 | 77 | 78 | extractor = initTransformer(props); 79 | 80 | stopped = false; 81 | 82 | } 83 | 84 | @SuppressWarnings("unchecked") 85 | private StreamSingleTupleExtractor initTransformer(Map props) { 86 | StreamSingleTupleExtractor extractor = null; 87 | if (props.containsKey(IgniteSinkConstants.SINGLE_TUPLE_EXTRACTOR_CLASS)) { 88 | String transformerCls = props.get(IgniteSinkConstants.SINGLE_TUPLE_EXTRACTOR_CLASS); 89 | if (transformerCls != null && !transformerCls.isEmpty()) { 90 | try { 91 | Class> clazz = 92 | (Class>) 93 | Class.forName(transformerCls); 94 | 95 | extractor = clazz.newInstance(); 96 | } catch (Exception e) { 97 | throw new IllegalStateException("Failed to instantiate the provided transformer!", e); 98 | } 99 | } 100 | } 101 | return extractor; 102 | 103 | } 104 | 105 | 106 | public boolean isStopped() { 107 | return stopped; 108 | } 109 | 110 | /** 111 | * Buffers records. 112 | * 113 | * @param records Records to inject into grid. 114 | */ 115 | @SuppressWarnings("unchecked") 116 | @Override 117 | public void put(Collection records) { 118 | if (log.isDebugEnabled()) { 119 | log.debug("Sink cache put : {}", records); 120 | } 121 | if (null != records && !records.isEmpty()) { 122 | for (CacheEvent record : records) { 123 | // Data is flushed asynchronously when CACHE_PER_NODE_DATA_SIZE is reached. 124 | if (extractor != null) { 125 | Map.Entry entry = extractor.extract(record); 126 | if (null != entry) { 127 | streamerContext.getStreamer().addData(entry.getKey(), entry.getValue()); 128 | } 129 | } else { 130 | if (record.key() != null) { 131 | streamerContext.getStreamer().addData(record.key(), record.hasNewValue() ? record.newValue() : record.oldValue()); 132 | } else { 133 | log.error("Failed to stream a record with null key!"); 134 | } 135 | } 136 | } 137 | } 138 | 139 | } 140 | 141 | /** 142 | * Pushes buffered data to grid. Flush interval is configured by worker configurations. 143 | */ 144 | @Override 145 | public void flush() { 146 | if (log.isDebugEnabled()) { 147 | log.debug("Sink Cache flush is called"); 148 | } 149 | if (stopped) 150 | return; 151 | 152 | streamerContext.getStreamer().flush(); 153 | } 154 | 155 | /** 156 | * Stops the grid client. 157 | */ 158 | @Override 159 | public void stop() { 160 | if (stopped) 161 | return; 162 | 163 | stopped = true; 164 | streamerContext.getStreamer().close(); 165 | } 166 | 167 | 168 | /** 169 | * Streamer context initializing grid and data streamer instances on demand. 170 | */ 171 | private static class StreamerContext { 172 | private final Ignite ignite; 173 | private final IgniteDataStreamer streamer; 174 | 175 | /** 176 | * Constructor. 177 | * 178 | * @param ignite 179 | */ 180 | StreamerContext(Ignite ignite) { 181 | this.ignite = ignite; 182 | streamer = this.ignite.dataStreamer(cacheName); 183 | } 184 | 185 | 186 | /** 187 | * Obtains grid instance. 188 | * 189 | * @return Grid instance. 190 | */ 191 | public Ignite getIgnite() { 192 | return ignite; 193 | } 194 | 195 | /** 196 | * Obtains data streamer instance. 197 | * 198 | * @return Data streamer instance. 199 | */ 200 | IgniteDataStreamer getStreamer() { 201 | return streamer; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/grid/events/IgniteCacheSourceTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.romeh.ordermanager.reader.streamer.grid.events; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Objects; 25 | import java.util.UUID; 26 | import java.util.concurrent.BlockingQueue; 27 | import java.util.concurrent.LinkedBlockingQueue; 28 | import java.util.concurrent.TimeUnit; 29 | import java.util.concurrent.atomic.AtomicBoolean; 30 | 31 | import org.apache.ignite.Ignite; 32 | import org.apache.ignite.IgniteException; 33 | import org.apache.ignite.cache.affinity.Affinity; 34 | import org.apache.ignite.events.CacheEvent; 35 | import org.apache.ignite.events.EventType; 36 | import org.apache.ignite.lang.IgniteBiPredicate; 37 | import org.apache.ignite.lang.IgnitePredicate; 38 | import org.apache.ignite.resources.IgniteInstanceResource; 39 | import org.slf4j.Logger; 40 | import org.slf4j.LoggerFactory; 41 | 42 | import com.romeh.ordermanager.reader.streamer.IgniteSource; 43 | import com.romeh.ordermanager.reader.streamer.IgniteSourceConstants; 44 | 45 | /** 46 | * Task to consume remote cluster cache events from the grid and inject them into Kafka. 47 | *

48 | * Note that a task will create a bounded queue in the grid for more reliable data transfer. 49 | * Queue size can be changed by {@link IgniteSourceConstants#INTL_BUF_SIZE}. 50 | */ 51 | public class IgniteCacheSourceTask implements IgniteSource { 52 | /** 53 | * Logger. 54 | */ 55 | private static final Logger log = LoggerFactory.getLogger(IgniteCacheSourceTask.class); 56 | 57 | /** 58 | * Event buffer size. 59 | */ 60 | private static int evtBufSize = 1000; 61 | /** 62 | * Cache name. 63 | */ 64 | private static String cacheName; 65 | /** 66 | * Local listener. 67 | */ 68 | private static final TaskLocalListener locLsnr = new TaskLocalListener(); 69 | /** 70 | * User-defined filter. 71 | */ 72 | private static IgnitePredicate filter; 73 | /** 74 | * Event buffer. 75 | */ 76 | private static BlockingQueue evtBuf = new LinkedBlockingQueue<>(evtBufSize); 77 | /** 78 | * Flag for stopped state. 79 | */ 80 | private final transient AtomicBoolean stopped = new AtomicBoolean(true); 81 | private final transient Ignite sourceNode; 82 | /** 83 | * Remote Listener id. 84 | */ 85 | private final UUID rmtLsnrId; 86 | /** 87 | * Max number of events taken from the buffer at once. 88 | */ 89 | private int evtBatchSize = 10; 90 | 91 | 92 | public IgniteCacheSourceTask(Map props, Ignite igniteNode) { 93 | Objects.requireNonNull(igniteNode); 94 | sourceNode = igniteNode; 95 | cacheName = props.get(IgniteSourceConstants.CACHE_NAME); 96 | 97 | if (props.containsKey(IgniteSourceConstants.INTL_BUF_SIZE)) { 98 | evtBufSize = Integer.parseInt(props.get(IgniteSourceConstants.INTL_BUF_SIZE)); 99 | evtBuf = new LinkedBlockingQueue<>(evtBufSize); 100 | } 101 | 102 | if (props.containsKey(IgniteSourceConstants.INTL_BATCH_SIZE)) 103 | evtBatchSize = Integer.parseInt(props.get(IgniteSourceConstants.INTL_BATCH_SIZE)); 104 | 105 | TaskRemoteFilter rmtLsnr = new TaskRemoteFilter(cacheName); 106 | filter = startSourceCacheListeners(props); 107 | try { 108 | rmtLsnrId = sourceNode.events(sourceNode.cluster().forCacheNodes(cacheName)) 109 | .remoteListen(locLsnr, rmtLsnr, EventType.EVT_CACHE_OBJECT_PUT); 110 | } catch (Exception e) { 111 | log.error("Failed to register event listener!", e); 112 | throw new IllegalStateException(e); 113 | } finally { 114 | stopped.set(false); 115 | } 116 | 117 | } 118 | 119 | @SuppressWarnings("unchecked") 120 | private IgnitePredicate startSourceCacheListeners(Map props) { 121 | 122 | IgnitePredicate ignitePredicate = null; 123 | if (props.containsKey(IgniteSourceConstants.CACHE_FILTER_CLASS)) { 124 | String filterCls = props.get(IgniteSourceConstants.CACHE_FILTER_CLASS); 125 | if (filterCls != null && !filterCls.isEmpty()) { 126 | try { 127 | Class> clazz = 128 | (Class>) Class.forName(filterCls); 129 | 130 | ignitePredicate = clazz.newInstance(); 131 | } catch (Exception e) { 132 | log.error("Failed to instantiate the provided filter! " + 133 | "User-enabled filtering is ignored!", e); 134 | } 135 | } 136 | } 137 | return ignitePredicate; 138 | } 139 | 140 | 141 | public boolean isStopped() { 142 | return stopped.get(); 143 | } 144 | 145 | @Override 146 | public List poll() { 147 | if (log.isDebugEnabled()) { 148 | log.debug("Cache source polling has been started !"); 149 | } 150 | ArrayList evts = new ArrayList<>(evtBatchSize); 151 | 152 | if (stopped.get()) 153 | return evts; 154 | 155 | try { 156 | if (evtBuf.drainTo(evts, evtBatchSize) > 0) { 157 | if (log.isDebugEnabled()) { 158 | log.debug("Polled events {}", evts); 159 | } 160 | return evts; 161 | } 162 | } catch (IgniteException e) { 163 | log.error("Error when polling event queue!", e); 164 | } 165 | 166 | // for shutdown. 167 | return Collections.emptyList(); 168 | } 169 | 170 | 171 | /** 172 | * Stops the grid client. 173 | */ 174 | @Override 175 | public void stop() { 176 | if (stopped.get()) 177 | return; 178 | 179 | if (stopped.compareAndSet(false, true)) { 180 | stopRemoteListen(); 181 | } 182 | 183 | } 184 | 185 | /** 186 | * Stops the remote listener. 187 | */ 188 | private void stopRemoteListen() { 189 | if (rmtLsnrId != null) 190 | sourceNode.events(sourceNode.cluster().forCacheNodes(cacheName)) 191 | .stopRemoteListen(rmtLsnrId); 192 | } 193 | 194 | /** 195 | * Local listener buffering cache events to be further sent to Kafka. 196 | */ 197 | private static class TaskLocalListener implements IgniteBiPredicate { 198 | /** 199 | * {@inheritDoc} 200 | */ 201 | @Override 202 | public boolean apply(UUID id, CacheEvent evt) { 203 | try { 204 | if (!evtBuf.offer(evt, 10, TimeUnit.MILLISECONDS)) 205 | log.error("Failed to buffer event {}", evt.name()); 206 | } catch (InterruptedException e) { 207 | log.error("Error has been thrown in TaskLocalListener {}", e); 208 | } 209 | 210 | return true; 211 | } 212 | } 213 | 214 | /** 215 | * Remote filter. 216 | */ 217 | private static class TaskRemoteFilter implements IgnitePredicate { 218 | /** 219 | * Cache name. 220 | */ 221 | private final String cacheName; 222 | /** */ 223 | @IgniteInstanceResource 224 | Ignite ignite; 225 | 226 | /** 227 | * @param cacheName Cache name. 228 | */ 229 | TaskRemoteFilter(String cacheName) { 230 | this.cacheName = cacheName; 231 | } 232 | 233 | /** 234 | * {@inheritDoc} 235 | */ 236 | @Override 237 | public boolean apply(CacheEvent evt) { 238 | Affinity affinity = ignite.affinity(cacheName); 239 | 240 | if (affinity.isPrimary(ignite.cluster().localNode(), evt.key()) && evt.cacheName().equals(cacheName)) { 241 | // Process this event. Ignored on backups. 242 | return filter == null || !filter.apply(evt); 243 | } 244 | 245 | return false; 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/reader/streamer/grid/streamer/IgniteCacheEventStreamerRx.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.reader.streamer.grid.streamer; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | import javax.cache.CacheException; 9 | 10 | import org.apache.ignite.Ignite; 11 | import org.apache.ignite.IgniteException; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.romeh.ordermanager.reader.streamer.IgniteSink; 16 | import com.romeh.ordermanager.reader.streamer.IgniteSource; 17 | import com.romeh.ordermanager.reader.streamer.StreamReport; 18 | import com.romeh.ordermanager.reader.streamer.grid.cq.IgniteCacheSinkTaskCQ; 19 | import com.romeh.ordermanager.reader.streamer.grid.cq.IgniteCacheSourceTaskCQ; 20 | import com.romeh.ordermanager.reader.streamer.grid.events.IgniteCacheSinkTask; 21 | import com.romeh.ordermanager.reader.streamer.grid.events.IgniteCacheSourceTask; 22 | 23 | import io.reactivex.Flowable; 24 | import io.reactivex.functions.Consumer; 25 | import io.reactivex.functions.Predicate; 26 | 27 | /** 28 | * the main cache data event streamer between source cache and sink cache 29 | * 30 | * @author romeh 31 | */ 32 | public final class IgniteCacheEventStreamerRx { 33 | /** 34 | * Logger. 35 | */ 36 | private static final Logger log = LoggerFactory.getLogger(IgniteCacheEventStreamerRx.class); 37 | private static final Predicate retryPredicate = throwable -> 38 | throwable instanceof CacheException || throwable instanceof IgniteException; 39 | private static final Consumer onErrorAction = throwable -> 40 | log.error("error has thrown in the final stage of the flow {}", throwable.getMessage()); 41 | private final IgniteSource igniteCacheSourceTask; 42 | private final IgniteSink igniteCacheSinkTask; 43 | private final long periodicInterval; 44 | private final long retryTimes; 45 | private final int flushBufferSize; 46 | private final AtomicInteger atomicInteger = new AtomicInteger(0); 47 | private final StreamReport streamReport = new StreamReport(); 48 | private final Consumer onNextAction; 49 | 50 | /** 51 | * @param igniteCacheSourceTask ignite source cache task 52 | * @param igniteCacheSinkTask ignite sink cache task 53 | * @param retryTimes how many times to retry in case of error 54 | * @param flushBufferSize flush buffer size in the streamer 55 | * @param periodicInterval the polling interval 56 | */ 57 | private IgniteCacheEventStreamerRx(IgniteSource igniteCacheSourceTask, IgniteSink igniteCacheSinkTask, long retryTimes, int flushBufferSize, long periodicInterval) { 58 | this.igniteCacheSourceTask = igniteCacheSourceTask; 59 | this.igniteCacheSinkTask = igniteCacheSinkTask; 60 | if (retryTimes != 0L) { 61 | this.retryTimes = retryTimes; 62 | } else { 63 | this.retryTimes = 2; 64 | } 65 | if (flushBufferSize != 0) { 66 | this.flushBufferSize = flushBufferSize; 67 | } else { 68 | this.flushBufferSize = 10; 69 | } 70 | if (periodicInterval != 0) { 71 | this.periodicInterval = periodicInterval; 72 | } else { 73 | this.periodicInterval = 500L; 74 | } 75 | this.onNextAction = cacheEvents -> { 76 | igniteCacheSinkTask.flush(); 77 | streamReport.incrementAddedRecords(cacheEvents.size()); 78 | }; 79 | } 80 | 81 | /** 82 | * @return streamer using Ignite cache continuous query 83 | */ 84 | public static BuilderCq builderWithContinuousQuery() { 85 | return new BuilderCq(); 86 | } 87 | 88 | /** 89 | * @return streamer using Ignite events streaming 90 | */ 91 | public static BuilderWithSystemEvents builderWithGridEvents() { 92 | return new BuilderWithSystemEvents(); 93 | } 94 | 95 | /** 96 | * @return the streamer execution report 97 | */ 98 | public final StreamReport getExecutionReport() { 99 | return streamReport; 100 | } 101 | 102 | /** 103 | * start and execute the streamer flow 104 | */ 105 | @SuppressWarnings("unchecked") 106 | public final void execute() { 107 | 108 | log.info("starting the stream for the ignite source/sink flow"); 109 | 110 | if (igniteCacheSourceTask.isStopped()) throw new IllegalStateException("Ignite source task is not yet started"); 111 | if (igniteCacheSinkTask.isStopped()) throw new IllegalStateException("Ignite sink task is not yet started"); 112 | 113 | //noinspection unchecked 114 | Flowable.fromCallable(igniteCacheSourceTask::poll) 115 | .repeatWhen(flowable -> flowable.delay(periodicInterval, TimeUnit.MILLISECONDS)) 116 | .retry(retryTimes, retryPredicate) 117 | .doOnError(throwable -> streamReport.addErrorMsg(throwable.getMessage())) 118 | .doOnNext(igniteCacheSinkTask::put) 119 | .doOnError(throwable -> streamReport.addErrorMsg(throwable.getMessage())) 120 | .doOnNext(data -> { 121 | if (null != data && !data.isEmpty() && atomicInteger.addAndGet(data.size()) % flushBufferSize == 0) { 122 | igniteCacheSinkTask.flush(); 123 | } 124 | }) 125 | .retry(retryTimes, retryPredicate) 126 | .doOnError(throwable -> streamReport.addErrorMsg(throwable.getMessage())) 127 | .doFinally(() -> { 128 | log.info("cleaning and stopping ignite tasks from the stream"); 129 | if (log.isDebugEnabled()) { 130 | log.debug("final execution report: error messages : {}, Total streamed record number : {}", streamReport.getErrorMsgs().toArray(), streamReport.getAddedRecords()); 131 | } 132 | igniteCacheSinkTask.stop(); 133 | igniteCacheSourceTask.stop(); 134 | }) 135 | .subscribe(onNextAction, onErrorAction); 136 | } 137 | 138 | /** 139 | * the ignite continuous query streamer builder 140 | */ 141 | public static final class BuilderCq { 142 | private IgniteCacheSourceTaskCQ igniteCacheSourceTask; 143 | private IgniteCacheSinkTaskCQ igniteCacheSinkTask; 144 | private long retryTimesOnError; 145 | private int flushBufferSize; 146 | private long pollingInterval; 147 | 148 | private BuilderCq() { 149 | } 150 | 151 | public BuilderCq sourceCache(Map props, Ignite sourceNode) { 152 | igniteCacheSourceTask = new IgniteCacheSourceTaskCQ(props, sourceNode); 153 | return this; 154 | } 155 | 156 | public BuilderCq sinkCache(Map props, Ignite sinkNode) { 157 | igniteCacheSinkTask = new IgniteCacheSinkTaskCQ(props, sinkNode); 158 | return this; 159 | } 160 | 161 | public BuilderCq retryTimesOnError(long retryTimesOnError) { 162 | this.retryTimesOnError = retryTimesOnError; 163 | return this; 164 | } 165 | 166 | public BuilderCq flushBufferSize(int flushBufferSize) { 167 | this.flushBufferSize = flushBufferSize; 168 | return this; 169 | } 170 | 171 | public BuilderCq pollingInterval(int pollingInterval) { 172 | this.pollingInterval = pollingInterval; 173 | return this; 174 | } 175 | 176 | @SuppressWarnings("unchecked") 177 | public IgniteCacheEventStreamerRx build() { 178 | return new IgniteCacheEventStreamerRx(igniteCacheSourceTask, igniteCacheSinkTask, retryTimesOnError, flushBufferSize, pollingInterval); 179 | } 180 | 181 | } 182 | 183 | /** 184 | * the ignite events streamer builder 185 | */ 186 | public static final class BuilderWithSystemEvents { 187 | private IgniteCacheSourceTask igniteCacheSourceTask; 188 | private IgniteCacheSinkTask igniteCacheSinkTask; 189 | private long retryTimesOnError; 190 | private int flushBufferSize; 191 | private long pollingInterval; 192 | 193 | private BuilderWithSystemEvents() { 194 | } 195 | 196 | public BuilderWithSystemEvents sourceCache(Map props, Ignite sourceNode) { 197 | igniteCacheSourceTask = new IgniteCacheSourceTask(props, sourceNode); 198 | return this; 199 | } 200 | 201 | public BuilderWithSystemEvents sinkCache(Map props, Ignite sinkNode) { 202 | igniteCacheSinkTask = new IgniteCacheSinkTask(props, sinkNode); 203 | return this; 204 | } 205 | 206 | public BuilderWithSystemEvents retryTimesOnError(long retryTimesOnError) { 207 | this.retryTimesOnError = retryTimesOnError; 208 | return this; 209 | } 210 | 211 | public BuilderWithSystemEvents flushBufferSize(int flushBufferSize) { 212 | this.flushBufferSize = flushBufferSize; 213 | return this; 214 | } 215 | 216 | public BuilderWithSystemEvents pollingInterval(int pollingInterval) { 217 | this.pollingInterval = pollingInterval; 218 | return this; 219 | } 220 | 221 | @SuppressWarnings("unchecked") 222 | public IgniteCacheEventStreamerRx build() { 223 | return new IgniteCacheEventStreamerRx(igniteCacheSourceTask, igniteCacheSinkTask, retryTimesOnError, flushBufferSize, pollingInterval); 224 | } 225 | 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/rest/OrderRestController.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.rest; 2 | 3 | import java.util.Collections; 4 | import java.util.UUID; 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | import javax.validation.Valid; 8 | import javax.validation.constraints.NotNull; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestMethod; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import com.romeh.ordermanager.entities.OrderState; 18 | import com.romeh.ordermanager.entities.Response; 19 | import com.romeh.ordermanager.entities.commands.OrderCmd; 20 | import com.romeh.ordermanager.reader.entities.JournalReadItem; 21 | import com.romeh.ordermanager.reader.services.OrderNotFoundException; 22 | import com.romeh.ordermanager.rest.dto.OrderRequest; 23 | import com.romeh.ordermanager.services.OrdersBroker; 24 | 25 | import io.swagger.annotations.Api; 26 | 27 | 28 | /** 29 | * The main order domain REST API 30 | * 31 | * @author romeh 32 | */ 33 | 34 | @RestController 35 | @RequestMapping("/orders") 36 | @Api(value = "Order Manager REST API demo") 37 | public class OrderRestController { 38 | 39 | @Autowired 40 | private OrdersBroker ordersBroker; 41 | 42 | /** 43 | * @param orderRequest json order request 44 | * @return ASYNC generic JSON response 45 | */ 46 | @RequestMapping(method = RequestMethod.POST) 47 | public CompletableFuture createOrder(@RequestBody @Valid OrderRequest orderRequest) { 48 | return ordersBroker.createOrder(new OrderCmd.CreateCmd(UUID.randomUUID().toString(), orderRequest.getOrderDetails())); 49 | 50 | } 51 | 52 | /** 53 | * @param validateCmd validate order command JSON 54 | * @return ASYNC generic JSON response 55 | */ 56 | @RequestMapping(value = "validate", method = RequestMethod.POST) 57 | public CompletableFuture validateOrder(@RequestBody @Valid OrderCmd.ValidateCmd validateCmd) { 58 | return ordersBroker.validateOrder(validateCmd); 59 | 60 | } 61 | 62 | /** 63 | * @param signCmd sign order command JSON 64 | * @return ASYNC generic JSON response 65 | */ 66 | @RequestMapping(value = "sign", method = RequestMethod.POST) 67 | public CompletableFuture signOrder(@RequestBody @Valid OrderCmd.SignCmd signCmd) { 68 | return ordersBroker.signeOrder(signCmd); 69 | 70 | } 71 | 72 | /** 73 | * @param orderId unique orderId string value 74 | * @return ASYNC OrderState Json response 75 | */ 76 | @RequestMapping(value = "/{orderId}", method = RequestMethod.GET) 77 | public CompletableFuture getOrderState(@PathVariable @NotNull String orderId) { 78 | return ordersBroker.getOrderStatus(new OrderCmd.GetOrderStatusCmd(orderId, Collections.emptyMap())); 79 | 80 | } 81 | 82 | 83 | /** 84 | * @param orderId unique orderId string value 85 | * @return JournalReadItem Json response if any 86 | */ 87 | @RequestMapping(value = "/readStore/{orderId}", method = RequestMethod.GET) 88 | public JournalReadItem getOrderLastStateFromReadStore(@PathVariable @NotNull String orderId) throws OrderNotFoundException { 89 | return ordersBroker.getOrderLastStatus(orderId); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/rest/dto/OrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.rest.dto; 2 | 3 | import java.util.Map; 4 | 5 | import lombok.Data; 6 | 7 | /** 8 | * order request json object for rest API 9 | * 10 | * @author romeh 11 | */ 12 | @Data 13 | public class OrderRequest { 14 | private Map orderDetails; 15 | } 16 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/rest/errors/ApiError.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.rest.errors; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import javax.validation.ConstraintViolation; 9 | 10 | import org.hibernate.validator.internal.engine.path.PathImpl; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.validation.FieldError; 13 | import org.springframework.validation.ObjectError; 14 | 15 | import com.fasterxml.jackson.annotation.JsonFormat; 16 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 17 | import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; 18 | import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; 19 | 20 | import lombok.AllArgsConstructor; 21 | import lombok.Data; 22 | import lombok.EqualsAndHashCode; 23 | 24 | /** 25 | * @author romeh 26 | */ 27 | @Data 28 | @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.CUSTOM, property = "error", visible = true) 29 | @JsonTypeIdResolver(LowerCaseClassNameResolver.class) 30 | class ApiError { 31 | private HttpStatus status; 32 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") 33 | private LocalDateTime timestamp; 34 | private String message; 35 | private String debugMessage; 36 | private List subErrors; 37 | 38 | private ApiError() { 39 | timestamp = LocalDateTime.now(); 40 | } 41 | 42 | ApiError(HttpStatus status) { 43 | this(); 44 | this.status = status; 45 | } 46 | 47 | ApiError(HttpStatus status, Throwable ex) { 48 | this(); 49 | this.status = status; 50 | this.message = "Unexpected error"; 51 | this.debugMessage = ex.getLocalizedMessage(); 52 | } 53 | 54 | ApiError(HttpStatus status, String message, Throwable ex) { 55 | this(); 56 | this.status = status; 57 | this.message = message; 58 | this.debugMessage = ex.getLocalizedMessage(); 59 | } 60 | 61 | private void addSubError(ApiSubError subError) { 62 | if (subErrors == null) { 63 | subErrors = new ArrayList<>(); 64 | } 65 | subErrors.add(subError); 66 | } 67 | 68 | private void addValidationError(String object, String field, Object rejectedValue, String message) { 69 | addSubError(new ApiValidationError(object, field, rejectedValue, message)); 70 | } 71 | 72 | private void addValidationError(String object, String message) { 73 | addSubError(new ApiValidationError(object, message)); 74 | } 75 | 76 | private void addValidationError(FieldError fieldError) { 77 | this.addValidationError( 78 | fieldError.getObjectName(), 79 | fieldError.getField(), 80 | fieldError.getRejectedValue(), 81 | fieldError.getDefaultMessage()); 82 | } 83 | 84 | void addValidationErrors(List fieldErrors) { 85 | fieldErrors.forEach(this::addValidationError); 86 | } 87 | 88 | private void addValidationError(ObjectError objectError) { 89 | this.addValidationError( 90 | objectError.getObjectName(), 91 | objectError.getDefaultMessage()); 92 | } 93 | 94 | void addValidationError(List globalErrors) { 95 | globalErrors.forEach(this::addValidationError); 96 | } 97 | 98 | /** 99 | * Utility method for adding error of ConstraintViolation. Usually when a @Validated validation fails. 100 | * 101 | * @param cv the ConstraintViolation 102 | */ 103 | private void addValidationError(ConstraintViolation cv) { 104 | this.addValidationError( 105 | cv.getRootBeanClass().getSimpleName(), 106 | ((PathImpl) cv.getPropertyPath()).getLeafNode().asString(), 107 | cv.getInvalidValue(), 108 | cv.getMessage()); 109 | } 110 | 111 | void addValidationErrors(Set> constraintViolations) { 112 | constraintViolations.forEach(this::addValidationError); 113 | } 114 | 115 | 116 | abstract class ApiSubError { 117 | 118 | } 119 | 120 | @Data 121 | @EqualsAndHashCode(callSuper = false) 122 | @AllArgsConstructor 123 | class ApiValidationError extends ApiSubError { 124 | private String object; 125 | private String field; 126 | private Object rejectedValue; 127 | private String message; 128 | 129 | ApiValidationError(String object, String message) { 130 | this.object = object; 131 | this.message = message; 132 | } 133 | } 134 | } 135 | 136 | class LowerCaseClassNameResolver extends TypeIdResolverBase { 137 | 138 | @Override 139 | public String idFromValue(Object value) { 140 | return value.getClass().getSimpleName().toLowerCase(); 141 | } 142 | 143 | @Override 144 | public String idFromValueAndType(Object value, Class suggestedType) { 145 | return idFromValue(value); 146 | } 147 | 148 | @Override 149 | public JsonTypeInfo.Id getMechanism() { 150 | return JsonTypeInfo.Id.CUSTOM; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/rest/errors/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.rest.errors; 2 | 3 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 4 | import static org.springframework.http.HttpStatus.NOT_FOUND; 5 | import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE; 6 | 7 | import org.springframework.core.Ordered; 8 | import org.springframework.core.annotation.Order; 9 | import org.springframework.http.HttpHeaders; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.http.converter.HttpMessageNotReadableException; 13 | import org.springframework.http.converter.HttpMessageNotWritableException; 14 | import org.springframework.web.HttpMediaTypeNotSupportedException; 15 | import org.springframework.web.bind.MethodArgumentNotValidException; 16 | import org.springframework.web.bind.MissingServletRequestParameterException; 17 | import org.springframework.web.bind.annotation.ControllerAdvice; 18 | import org.springframework.web.bind.annotation.ExceptionHandler; 19 | import org.springframework.web.context.request.ServletWebRequest; 20 | import org.springframework.web.context.request.WebRequest; 21 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 22 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 23 | 24 | import com.romeh.ordermanager.reader.services.OrderNotFoundException; 25 | 26 | import akka.pattern.AskTimeoutException; 27 | import lombok.extern.slf4j.Slf4j; 28 | 29 | /** 30 | * the generic error and execptional handler for the REST API 31 | * 32 | * @author romeh 33 | */ 34 | @Order(Ordered.HIGHEST_PRECEDENCE) 35 | @ControllerAdvice 36 | @Slf4j 37 | public class RestExceptionHandler extends ResponseEntityExceptionHandler { 38 | /** 39 | * Handle MissingServletRequestParameterException. Triggered when a 'required' request parameter is missing. 40 | * 41 | * @param ex MissingServletRequestParameterException 42 | * @param headers HttpHeaders 43 | * @param status HttpStatus 44 | * @param request WebRequest 45 | * @return the ApiError object 46 | */ 47 | @Override 48 | protected ResponseEntity handleMissingServletRequestParameter( 49 | MissingServletRequestParameterException ex, HttpHeaders headers, 50 | HttpStatus status, WebRequest request) { 51 | String error = ex.getParameterName() + " parameter is missing"; 52 | return buildResponseEntity(new ApiError(BAD_REQUEST, error, ex)); 53 | } 54 | 55 | 56 | /** 57 | * Handle HttpMediaTypeNotSupportedException. This one triggers when JSON is invalid as well. 58 | * 59 | * @param ex HttpMediaTypeNotSupportedException 60 | * @param headers HttpHeaders 61 | * @param status HttpStatus 62 | * @param request WebRequest 63 | * @return the ApiError object 64 | */ 65 | @Override 66 | protected ResponseEntity handleHttpMediaTypeNotSupported( 67 | HttpMediaTypeNotSupportedException ex, 68 | HttpHeaders headers, 69 | HttpStatus status, 70 | WebRequest request) { 71 | StringBuilder builder = new StringBuilder(); 72 | builder.append(ex.getContentType()); 73 | builder.append(" media type is not supported. Supported media types are "); 74 | ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", ")); 75 | return buildResponseEntity(new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex)); 76 | } 77 | 78 | /** 79 | * Handle MethodArgumentNotValidException. Triggered when an object fails @Valid validation. 80 | * 81 | * @param ex the MethodArgumentNotValidException that is thrown when @Valid validation fails 82 | * @param headers HttpHeaders 83 | * @param status HttpStatus 84 | * @param request WebRequest 85 | * @return the ApiError object 86 | */ 87 | @Override 88 | protected ResponseEntity handleMethodArgumentNotValid( 89 | MethodArgumentNotValidException ex, 90 | HttpHeaders headers, 91 | HttpStatus status, 92 | WebRequest request) { 93 | ApiError apiError = new ApiError(SERVICE_UNAVAILABLE); 94 | apiError.setMessage("Validation error"); 95 | apiError.addValidationErrors(ex.getBindingResult().getFieldErrors()); 96 | apiError.addValidationError(ex.getBindingResult().getGlobalErrors()); 97 | return buildResponseEntity(apiError); 98 | } 99 | 100 | /** 101 | * Handles javax.validation.ConstraintViolationException. Thrown when @Validated fails. 102 | * 103 | * @param ex the ConstraintViolationException 104 | * @return the ApiError object 105 | */ 106 | @ExceptionHandler(javax.validation.ConstraintViolationException.class) 107 | protected ResponseEntity handleConstraintViolation( 108 | javax.validation.ConstraintViolationException ex) { 109 | ApiError apiError = new ApiError(BAD_REQUEST); 110 | apiError.setMessage("Validation error"); 111 | apiError.addValidationErrors(ex.getConstraintViolations()); 112 | return buildResponseEntity(apiError); 113 | } 114 | 115 | /** 116 | * Handles AskTimeoutException. Created to encapsulate errors with more detail than akka.pattern.AskTimeoutException 117 | * 118 | * @param ex the AskTimeoutException 119 | * @return the ApiError object 120 | */ 121 | @ExceptionHandler(AskTimeoutException.class) 122 | protected ResponseEntity handleAkkaAskTimeout( 123 | AskTimeoutException ex) { 124 | ApiError apiError = new ApiError(NOT_FOUND); 125 | apiError.setMessage(ex.getMessage()); 126 | return buildResponseEntity(apiError); 127 | } 128 | 129 | 130 | /** 131 | * Handles OrderNotFoundException. Created to encapsulate errors when order is not found in the read store 132 | * 133 | * @param ex the OrderNotFoundException 134 | * @return the ApiError object 135 | */ 136 | @ExceptionHandler(OrderNotFoundException.class) 137 | protected ResponseEntity handleOrderNotFound( 138 | OrderNotFoundException ex) { 139 | ApiError apiError = new ApiError(NOT_FOUND); 140 | apiError.setMessage(ex.getMessage()); 141 | return buildResponseEntity(apiError); 142 | } 143 | 144 | /** 145 | * Handle HttpMessageNotReadableException. Happens when request JSON is malformed. 146 | * 147 | * @param ex HttpMessageNotReadableException 148 | * @param headers HttpHeaders 149 | * @param status HttpStatus 150 | * @param request WebRequest 151 | * @return the ApiError object 152 | */ 153 | @Override 154 | protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { 155 | ServletWebRequest servletWebRequest = (ServletWebRequest) request; 156 | log.info("{} to {}", servletWebRequest.getHttpMethod(), servletWebRequest.getRequest().getServletPath()); 157 | String error = "Malformed JSON request"; 158 | return buildResponseEntity(new ApiError(BAD_REQUEST, error, ex)); 159 | } 160 | 161 | /** 162 | * Handle HttpMessageNotWritableException. 163 | * 164 | * @param ex HttpMessageNotWritableException 165 | * @param headers HttpHeaders 166 | * @param status HttpStatus 167 | * @param request WebRequest 168 | * @return the ApiError object 169 | */ 170 | @Override 171 | protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { 172 | String error = "Error writing JSON output"; 173 | return buildResponseEntity(new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, error, ex)); 174 | } 175 | 176 | /** 177 | * Handle Exception, handle generic Exception.class 178 | * 179 | * @param ex the Exception 180 | * @return the ApiError object 181 | */ 182 | @ExceptionHandler(MethodArgumentTypeMismatchException.class) 183 | protected ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, 184 | WebRequest request) { 185 | ApiError apiError = new ApiError(BAD_REQUEST); 186 | apiError.setMessage(String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName())); 187 | apiError.setDebugMessage(ex.getMessage()); 188 | return buildResponseEntity(apiError); 189 | } 190 | 191 | 192 | private ResponseEntity buildResponseEntity(ApiError apiError) { 193 | return new ResponseEntity<>(apiError, apiError.getStatus()); 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/serializer/OrderManagerSerializer.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.serializer; 2 | 3 | 4 | import java.io.NotSerializableException; 5 | 6 | import com.google.protobuf.InvalidProtocolBufferException; 7 | import com.romeh.ordermanager.entities.commands.OrderCmd; 8 | import com.romeh.ordermanager.entities.enums.OrderStatus; 9 | import com.romeh.ordermanager.entities.events.CreatedEvent; 10 | import com.romeh.ordermanager.entities.events.FinishedEvent; 11 | import com.romeh.ordermanager.entities.events.OrderEvent; 12 | import com.romeh.ordermanager.entities.events.SignedEvent; 13 | import com.romeh.ordermanager.entities.events.ValidatedEvent; 14 | import com.romeh.ordermanager.protobuf.EventsAndCommands; 15 | 16 | import akka.serialization.SerializerWithStringManifest; 17 | 18 | /** 19 | * @author romeh 20 | */ 21 | public class OrderManagerSerializer extends SerializerWithStringManifest { 22 | 23 | 24 | private static final String CREATED_EVENT = "CreatedEvent"; 25 | private static final String VALIDATED_EVENT = "ValidatedEvent"; 26 | private static final String SIGNED_EVENT = "SignedEvent"; 27 | private static final String FINISHED_EVENT = "FinishedEvent"; 28 | private static final String CREATE_COMMAND = "CreateCommand"; 29 | private static final String VALIDATE_COMMAND = "ValdiateCommand"; 30 | private static final String SIGN_COMMAND = "SignCommand"; 31 | private static final String GET_COMMAND = "GetCommand"; 32 | private static final String ASYNC_COMMAND = "AsyncCommand"; 33 | 34 | 35 | @Override 36 | public int identifier() { 37 | return 100; 38 | } 39 | 40 | @Override 41 | public String manifest(Object o) { 42 | if (o instanceof OrderCmd.CreateCmd) 43 | return CREATE_COMMAND; 44 | if (o instanceof OrderCmd.ValidateCmd) 45 | return VALIDATE_COMMAND; 46 | if (o instanceof OrderCmd.SignCmd) 47 | return SIGN_COMMAND; 48 | if (o instanceof OrderCmd.GetOrderStatusCmd) 49 | return GET_COMMAND; 50 | if (o instanceof OrderCmd.AsyncSignCmd) 51 | return ASYNC_COMMAND; 52 | if (o instanceof CreatedEvent) 53 | return CREATED_EVENT; 54 | if (o instanceof SignedEvent) 55 | return SIGNED_EVENT; 56 | if (o instanceof ValidatedEvent) 57 | return VALIDATED_EVENT; 58 | if (o instanceof FinishedEvent) 59 | return FINISHED_EVENT; 60 | else 61 | throw new IllegalArgumentException("Unknown type: " + o); 62 | } 63 | 64 | @Override 65 | public byte[] toBinary(Object o) { 66 | 67 | if (o instanceof OrderEvent) { 68 | OrderEvent orderEvent = (OrderEvent) o; 69 | return EventsAndCommands.OrderEvent.newBuilder() 70 | .setOrderId(orderEvent.getOrderId()) 71 | .setOrderStatus(orderEvent.getOrderStatus().name()) 72 | .build().toByteArray(); 73 | 74 | } else if (o instanceof OrderCmd) { 75 | OrderCmd orderCmd = (OrderCmd) o; 76 | return EventsAndCommands.OrderCmd.newBuilder() 77 | .setOrderId(orderCmd.getOrderId()) 78 | .putAllOrderDetails(orderCmd.getOrderDetails()) 79 | .build().toByteArray(); 80 | } else { 81 | throw new IllegalArgumentException("Cannot serialize object of type " + o.getClass().getName()); 82 | } 83 | 84 | } 85 | 86 | @Override 87 | public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException { 88 | try { 89 | if (manifest.equals(CREATE_COMMAND)) { 90 | final EventsAndCommands.OrderCmd orderCmd = EventsAndCommands.OrderCmd.parseFrom(bytes); 91 | return new OrderCmd.CreateCmd(orderCmd.getOrderId(), orderCmd.getOrderDetailsMap()); 92 | } 93 | if (manifest.equals(SIGN_COMMAND)) { 94 | final EventsAndCommands.OrderCmd orderCmd = EventsAndCommands.OrderCmd.parseFrom(bytes); 95 | return new OrderCmd.SignCmd(orderCmd.getOrderId(), orderCmd.getOrderDetailsMap()); 96 | } 97 | if (manifest.equals(VALIDATE_COMMAND)) { 98 | final EventsAndCommands.OrderCmd orderCmd = EventsAndCommands.OrderCmd.parseFrom(bytes); 99 | return new OrderCmd.ValidateCmd(orderCmd.getOrderId(), orderCmd.getOrderDetailsMap()); 100 | } 101 | if (manifest.equals(GET_COMMAND)) { 102 | final EventsAndCommands.OrderCmd orderCmd = EventsAndCommands.OrderCmd.parseFrom(bytes); 103 | return new OrderCmd.GetOrderStatusCmd(orderCmd.getOrderId(), orderCmd.getOrderDetailsMap()); 104 | } 105 | if (manifest.equals(ASYNC_COMMAND)) { 106 | final EventsAndCommands.OrderCmd orderCmd = EventsAndCommands.OrderCmd.parseFrom(bytes); 107 | return new OrderCmd.AsyncSignCmd(orderCmd.getOrderId(), orderCmd.getOrderDetailsMap()); 108 | } 109 | if (manifest.equals(CREATED_EVENT)) { 110 | final EventsAndCommands.OrderEvent orderEvent = EventsAndCommands.OrderEvent.parseFrom(bytes); 111 | return new CreatedEvent(orderEvent.getOrderId(), OrderStatus.valueOf(orderEvent.getOrderStatus())); 112 | } 113 | if (manifest.equals(SIGNED_EVENT)) { 114 | final EventsAndCommands.OrderEvent orderEvent = EventsAndCommands.OrderEvent.parseFrom(bytes); 115 | return new SignedEvent(orderEvent.getOrderId(), OrderStatus.valueOf(orderEvent.getOrderStatus())); 116 | } 117 | if (manifest.equals(VALIDATED_EVENT)) { 118 | final EventsAndCommands.OrderEvent orderEvent = EventsAndCommands.OrderEvent.parseFrom(bytes); 119 | return new ValidatedEvent(orderEvent.getOrderId(), OrderStatus.valueOf(orderEvent.getOrderStatus())); 120 | } 121 | if (manifest.equals(FINISHED_EVENT)) { 122 | final EventsAndCommands.OrderEvent orderEvent = EventsAndCommands.OrderEvent.parseFrom(bytes); 123 | return new FinishedEvent(orderEvent.getOrderId(), OrderStatus.valueOf(orderEvent.getOrderStatus())); 124 | } else { 125 | throw new NotSerializableException( 126 | "Unimplemented deserialization of message with manifest [" + manifest + "] in " + getClass().getName()); 127 | } 128 | } catch (InvalidProtocolBufferException e) { 129 | throw new NotSerializableException(e.getMessage()); 130 | } 131 | 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/java/com/romeh/ordermanager/services/OrdersBroker.java: -------------------------------------------------------------------------------- 1 | package com.romeh.ordermanager.services; 2 | 3 | import java.time.Duration; 4 | import java.util.Optional; 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.function.Function; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import com.romeh.ordermanager.domain.OrderManager; 12 | import com.romeh.ordermanager.entities.OrderState; 13 | import com.romeh.ordermanager.entities.Response; 14 | import com.romeh.ordermanager.entities.commands.OrderCmd; 15 | import com.romeh.ordermanager.reader.entities.JournalReadItem; 16 | import com.romeh.ordermanager.reader.services.OrderNotFoundException; 17 | import com.romeh.ordermanager.reader.services.ReadStoreStreamerService; 18 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntityBroker; 19 | 20 | import akka.actor.ActorRef; 21 | import akka.pattern.PatternsCS; 22 | 23 | /** 24 | * the orders service that handle order commands and respond in async mode 25 | * 26 | * @author romeh 27 | */ 28 | @Service 29 | public class OrdersBroker { 30 | 31 | private final static Duration timeout = Duration.ofMillis(2000); 32 | /** 33 | * the AKKA sharding persistent entities general broker 34 | */ 35 | private final PersistentEntityBroker persistentEntityBroker; 36 | private final ReadStoreStreamerService readStoreStreamerService; 37 | /** 38 | * generic completable future handle response function 39 | */ 40 | private final Function handlerResponse = o -> { 41 | if (o instanceof Response) { 42 | return (Response) o; 43 | } else { 44 | return Response.builder().errorCode("1100").errorMessage("unexpected error has been found").build(); 45 | } 46 | }; 47 | 48 | /** 49 | * generic completable future get order state handle response function 50 | */ 51 | private final Function handleGetState = o -> Optional.ofNullable(o).map(getState -> (OrderState) getState) 52 | .orElseThrow(() -> new IllegalStateException("un-expected error has been thrown")); 53 | 54 | /** 55 | * generic completable future handle exception function 56 | */ 57 | private final Function handleException = throwable -> Response.builder().errorCode("1111").errorMessage(throwable.getLocalizedMessage()).build(); 58 | 59 | 60 | @Autowired 61 | public OrdersBroker(PersistentEntityBroker persistentEntityBroker, ReadStoreStreamerService readStoreStreamerService) { 62 | this.persistentEntityBroker = persistentEntityBroker; 63 | this.readStoreStreamerService = readStoreStreamerService; 64 | } 65 | 66 | /** 67 | * create order service API 68 | * 69 | * @param createCmd create order command 70 | * @return generic response object 71 | */ 72 | public CompletableFuture createOrder(OrderCmd.CreateCmd createCmd) { 73 | 74 | return PatternsCS.ask(getOrderEntity(), createCmd, timeout).toCompletableFuture() 75 | .thenApply(handlerResponse).exceptionally(handleException); 76 | } 77 | 78 | /** 79 | * validate order service API 80 | * 81 | * @param validateCmd validate order command 82 | * @return generic response object 83 | */ 84 | public CompletableFuture validateOrder(OrderCmd.ValidateCmd validateCmd) { 85 | return PatternsCS.ask(getOrderEntity(), validateCmd, timeout).toCompletableFuture() 86 | .thenApply(handlerResponse).exceptionally(handleException); 87 | } 88 | /** 89 | * Sign order service API 90 | * 91 | * @param signCmd sign order command 92 | * @return generic response object 93 | */ 94 | public CompletableFuture signeOrder(OrderCmd.SignCmd signCmd) { 95 | return PatternsCS.ask(getOrderEntity(), signCmd, timeout).toCompletableFuture() 96 | .thenApply(handlerResponse).exceptionally(handleException); 97 | } 98 | 99 | /** 100 | * get order state service API from write store 101 | * 102 | * @param getOrderStatusCmd get Order state command 103 | * @return order state 104 | */ 105 | public CompletableFuture getOrderStatus(OrderCmd.GetOrderStatusCmd getOrderStatusCmd) { 106 | return PatternsCS.ask(getOrderEntity(), getOrderStatusCmd, timeout).toCompletableFuture() 107 | .thenApply(handleGetState); 108 | } 109 | 110 | /** 111 | * get last order state service API from read store 112 | * 113 | * @param orderId the order id 114 | * @return order last status 115 | */ 116 | public JournalReadItem getOrderLastStatus(String orderId) throws OrderNotFoundException { 117 | return readStoreStreamerService.getOrderStatus(orderId); 118 | } 119 | 120 | /** 121 | * @return Persistent entity actor reference based into AKKA cluster sharding 122 | */ 123 | private ActorRef getOrderEntity() { 124 | return persistentEntityBroker.findPersistentEntity(OrderManager.class); 125 | 126 | } 127 | 128 | 129 | } 130 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/proto/EventsAndCommands.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package com.romeh.ordermanager.protobuf; 3 | option java_package = "com.romeh.ordermanager.protobuf"; 4 | option optimize_for = SPEED; 5 | 6 | 7 | /*enum OrderStatus{ 8 | NotStarted = 0; 9 | Created = 1; 10 | Validated = 2; 11 | Signed = 3; 12 | COMPLETED = 4; 13 | 14 | }*/ 15 | 16 | /*message CreatedEvent{ 17 | string orderId=1; 18 | string orderStatus= 2; 19 | } 20 | 21 | message FinishedEvent{ 22 | string orderId=1; 23 | string orderStatus= 2; 24 | } 25 | message SignedEvent{ 26 | string orderId=1; 27 | string orderStatus= 2; 28 | } 29 | 30 | message ValidatedEvent{ 31 | string orderId=1; 32 | string orderStatus= 2; 33 | }*/ 34 | 35 | message OrderEvent { 36 | string orderId = 1; 37 | string orderStatus = 2; 38 | } 39 | 40 | message OrderCmd { 41 | string orderId=1; 42 | map orderDetails = 2; 43 | } 44 | 45 | /*message CreateCmd{ 46 | string orderId=1; 47 | map orderDetails = 2; 48 | } 49 | 50 | message ValidateCmd{ 51 | string orderId=1; 52 | map orderDetails = 2; 53 | } 54 | 55 | message SignCmd{ 56 | string orderId=1; 57 | map orderDetails = 2; 58 | } 59 | 60 | message GetOrderStatusCmd{ 61 | string orderId=1; 62 | map orderDetails = 2; 63 | } 64 | 65 | message AsyncSignCmd{ 66 | string orderId=1; 67 | map orderDetails = 2; 68 | }*/ 69 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jackson: 3 | default-property-inclusion: non_null 4 | 5 | akka: 6 | config: eventSourcing.conf 7 | system-name: orderManagerSystem 8 | 9 | server: 10 | tomcat: 11 | accessLog.enabled: true 12 | basedir: "./Application" 13 | accessLogPattern: "%h %l %u %t \"%r\" %s %b %D" 14 | port: 9595 15 | 16 | 17 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | # change the application name for your project name , this name is reserved for the maven archetype code generation 3 | application: 4 | name: OrderManager 5 | 6 | -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/resources/eventSourcing.conf: -------------------------------------------------------------------------------- 1 | # This file provides a set of AKKA configuration for test scenario. 2 | 3 | akka { 4 | 5 | loggers = ["akka.event.slf4j.Slf4jLogger"] 6 | loglevel = "info" 7 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 8 | actor { 9 | provider = "akka.cluster.ClusterActorRefProvider" 10 | deployment { 11 | # Use a wild dispatcher to take care for any actor. 12 | "/*" { 13 | router = round-robin-pool 14 | # Don't change below to anything higher than 1. For such needs duplicate another one with actor name as shown below. 15 | nr-of-instances = 1 16 | } 17 | } 18 | allow-java-serialization = off 19 | serializers { 20 | myEventsAndCmdsSerializer = "com.romeh.ordermanager.serializer.OrderManagerSerializer" 21 | } 22 | serialization-bindings { 23 | "com.romeh.ordermanager.entities.commands.OrderCmd" = myEventsAndCmdsSerializer 24 | "com.romeh.ordermanager.entities.events.OrderEvent" = myEventsAndCmdsSerializer 25 | } 26 | } 27 | 28 | cluster.sharding { 29 | remember-entities = off 30 | state-store-mode = ddata 31 | } 32 | remote { 33 | log-remote-lifecycle-events = off 34 | netty.tcp { 35 | hostname = "127.0.0.1" 36 | port = 3001 37 | } 38 | } 39 | 40 | cluster { 41 | seed-nodes = ["akka.tcp://orderManagerSystem@127.0.0.1:3001"] 42 | // auto-down-unreachable-after = 10s 43 | metrics.enabled = off 44 | 45 | } 46 | extensions = ["akka.persistence.ignite.extension.IgniteExtensionProvider"] 47 | // # Level DB is goood for laptop based development. 48 | // persistence { 49 | // journal { 50 | // plugin = "akka.persistence.journal.leveldb" 51 | // leveldb { 52 | // dir = "target/example/journal" 53 | // native = false 54 | // } 55 | // } 56 | // snapshot-store { 57 | // plugin = "akka.persistence.snapshot-store.local" 58 | // local.dir = "target/example/snapshots" 59 | // } 60 | // } 61 | 62 | persistence.journal.plugin = "akka.persistence.journal.ignite" 63 | persistence.snapshot-store.plugin = "akka.persistence.snapshot.ignite" 64 | 65 | } 66 | ignite { 67 | isClientNode = false 68 | // for ONLY testing 69 | tcpDiscoveryAddresses = "localhost" 70 | metricsLogFrequency = 0 71 | // thread pools based into target machine specs 72 | queryThreadPoolSize = 4 73 | dataStreamerThreadPoolSize = 1 74 | managementThreadPoolSize = 2 75 | publicThreadPoolSize = 4 76 | systemThreadPoolSize = 2 77 | rebalanceThreadPoolSize = 1 78 | asyncCallbackPoolSize = 4 79 | peerClassLoadingEnabled = false 80 | enableFilePersistence = true 81 | igniteConnectorPort = 11211 82 | igniteServerPortRange = "47500..47509" 83 | ignitePersistenceFilePath = "WriteData" 84 | } 85 | 86 | akka.log-config-on-start = false 87 | 88 | akka.actor.default-dispatcher.default-executor.fallback = "thread-pool-executor" 89 | 90 | # This dispatcher is being refered by Service when it creates actors. 91 | task-runner-actor-dispatcher { 92 | 93 | # Dispatcher is the name of the event-based dispatcher 94 | type = Dispatcher 95 | 96 | # What kind of ExecutionService to use 97 | executor = "thread-pool-executor" 98 | 99 | # This will be used if you have set "executor = "thread-pool-executor"" 100 | thread-pool-executor { 101 | 102 | # Keep alive time for threads 103 | keep-alive-time = 60s 104 | 105 | # Min number of threads to cap factor-based core number to 106 | core-pool-size-min = 8 107 | 108 | # The core pool size factor is used to determine thread pool core size 109 | # using the following formula: ceil(available processors * factor). 110 | # Resulting size is then bounded by the core-pool-size-min and 111 | # core-pool-size-max values. 112 | core-pool-size-factor = 3.0 113 | 114 | # Max number of threads to cap factor-based number to 115 | core-pool-size-max = 64 116 | 117 | # Minimum number of threads to cap factor-based max number to 118 | # (if using a bounded task queue) 119 | max-pool-size-min = 8 120 | 121 | # Max no of threads (if using a bounded task queue) is determined by 122 | # calculating: ceil(available processors * factor) 123 | max-pool-size-factor = 3.0 124 | 125 | # Max number of threads to cap factor-based max number to 126 | # (if using a bounded task queue) 127 | max-pool-size-max = 64 128 | 129 | # Specifies the bounded capacity of the task queue (< 1 == unbounded) 130 | task-queue-size = -1 131 | 132 | # Specifies which type of task queue will be used, can be "array" or 133 | # "linked" (default) 134 | task-queue-type = "linked" 135 | 136 | # Allow core threads to time out 137 | allow-core-timeout = on 138 | } 139 | 140 | # Throughput defines the maximum number of messages to be 141 | # processed per actor before the thread jumps to the next actor. 142 | # Set to 1 for as fair as possible. 143 | throughput = 1 144 | } -------------------------------------------------------------------------------- /spring-event-sourcing-example/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ${FILE_LOG_PATTERN} 11 | utf8 12 | 13 | 14 | 15 | 16 | ${LOG_HOME}/Application.log 17 | true 18 | 19 | ${FILE_LOG_PATTERN} 20 | 21 | 22 | Application.log.%i 23 | 24 | 26 | 10MB 27 | 28 | 29 | 30 | 31 | 0 32 | 10000 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Spring boot akka event sourcing starter 3 | 4 | - Spring boot akka persistence event sourcing starter that cover the following :Smooth integration between Akka persistence and Spring Boot 5 | - Generic DSL for the aggregate flow definition for commands and events 6 | - Abstract Aggregate persistent entity actor with all common logic in place and which can be used with the concrete managed spring beans implementation of different aggregate entities 7 | - Abstract cluster sharding run-time configuration and access via spring boot custom configuration and a generic entity broker that abstract the cluster shading implementation for you 8 | - Abstracted mixed configuration for your actor system and the entity configuration via spring configuration and akka configuration 9 | 10 | For detailed technical details and explanation , check my 4 parts blog posts: 11 | - Part 1:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-1/ 12 | - Part 2:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-2/ 13 | - Part 3:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-3-the-working-example/ 14 | - Part 4:https://mromeh.com/2018/04/27/spring-boot-akka-event-sourcing-starter-part-4-final/ 15 | 16 | 17 | Spring boot , Akka and Ignite used versions: 18 | -------------- 19 | 20 | Spring boot 1.5.9.RELEASE, Akka version :2.5.9+ , Ignite Version :2.3.0+ 21 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | io.github.romeh 7 | springboot-event-sourcing-initializer 8 | 1.0.2 9 | 10 | 11 | 4.0.0 12 | springboot-akka-event-sourcing-starter 13 | jar 14 | 15 | springboot-akka-event-sourcing-starter 16 | spring boot akka persistence event sourcing starter 17 | https://github.com/Romeh/springboot-akka-event-sourcing-starter 18 | 19 | 20 | **/domain/*.java,**/config/*.java,**/exceptions/*.java 21 | 22 | 23 | 24 | 25 | MRomeh 26 | Mahmoud Romeh 27 | mahmoud.romeh@gmail.com 28 | 29 | 30 | 31 | 32 | https://github.com/Romeh/springboot-akka-event-sourcing-starter/issues 33 | GitHub Issues 34 | 35 | 36 | 37 | 38 | MIT License 39 | http://www.opensource.org/licenses/mit-license.php 40 | repo 41 | 42 | 43 | 44 | 45 | https://github.com/Romeh/springboot-akka-event-sourcing-starter 46 | scm:git:git://github.com/Romeh/springboot-akka-event-sourcing-starter.git 47 | scm:git:git@github.com:Romeh/springboot-akka-event-sourcing-starter.git 48 | 49 | 50 | 51 | 52 | 53 | ossrh 54 | https://oss.sonatype.org/content/repositories/snapshots 55 | 56 | 57 | ossrh 58 | https://oss.sonatype.org/service/local/staging/deploy/maven2 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-configuration-processor 70 | true 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-autoconfigure 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-test 79 | test 80 | 81 | 82 | 83 | redis.clients 84 | jedis 85 | 86 | 87 | 88 | org.slf4j 89 | slf4j-api 90 | 91 | 92 | 93 | com.typesafe.akka 94 | akka-actor_2.12 95 | 96 | 97 | 98 | com.typesafe.akka 99 | akka-persistence_2.12 100 | 101 | 102 | 103 | com.typesafe.akka 104 | akka-cluster-sharding_2.12 105 | 106 | 107 | 108 | com.typesafe.akka 109 | akka-slf4j_2.12 110 | 111 | 112 | com.typesafe.akka 113 | akka-testkit_2.12 114 | 115 | 116 | 117 | junit 118 | junit 119 | test 120 | 121 | 122 | 123 | io.vavr 124 | vavr 125 | 126 | 127 | 128 | org.mockito 129 | mockito-all 130 | test 131 | 132 | 133 | 134 | org.projectlombok 135 | lombok 136 | 137 | 138 | 139 | com.github.dnvriend 140 | akka-persistence-inmemory_2.12 141 | 2.4.20.0 142 | test 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-compiler-plugin 152 | 3.5.1 153 | 154 | 1.8 155 | 1.8 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | akk-persistence-test-repo 164 | your custom repo 165 | http://dl.bintray.com/dnvriend/maven 166 | 167 | 168 | 169 | 170 | 171 | release-sign-artifacts 172 | 173 | 174 | performRelease 175 | true 176 | 177 | 178 | 179 | 180 | 181 | org.apache.maven.plugins 182 | maven-source-plugin 183 | 3.0.1 184 | 185 | 186 | attach-sources 187 | verify 188 | 189 | jar-no-fork 190 | 191 | 192 | 193 | 194 | 195 | org.apache.maven.plugins 196 | maven-javadoc-plugin 197 | 2.9.1 198 | 199 | 200 | attach-javadocs 201 | 202 | jar 203 | 204 | 205 | -Xdoclint:none 206 | 207 | 208 | 209 | 210 | 211 | org.apache.maven.plugins 212 | maven-gpg-plugin 213 | 1.5 214 | 215 | ${gpg.passphrase} 216 | 217 | 218 | 219 | sign-artifacts 220 | verify 221 | 222 | sign 223 | 224 | 225 | 226 | 227 | 228 | org.sonatype.plugins 229 | nexus-staging-maven-plugin 230 | 1.6.7 231 | true 232 | 233 | ossrh 234 | https://oss.sonatype.org/ 235 | true 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/SpringActorProducer.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing; 2 | 3 | import org.springframework.context.ApplicationContext; 4 | 5 | import akka.actor.Actor; 6 | import akka.actor.IndirectActorProducer; 7 | 8 | /** 9 | * An actor producer that lets Spring create the Actor instances. 10 | */ 11 | public class SpringActorProducer implements IndirectActorProducer { 12 | 13 | private final ApplicationContext applicationContext; 14 | private final String actorBeanName; 15 | private final Class requiredType; 16 | private final Object[] args; 17 | 18 | public SpringActorProducer(ApplicationContext applicationContext, String actorBeanName) { 19 | this.applicationContext = applicationContext; 20 | this.actorBeanName = actorBeanName; 21 | this.requiredType = null; 22 | this.args = null; 23 | } 24 | 25 | public SpringActorProducer(ApplicationContext applicationContext, String actorBeanName, Object[] args) { 26 | this.applicationContext = applicationContext; 27 | this.actorBeanName = actorBeanName; 28 | this.requiredType = null; 29 | this.args = args; 30 | } 31 | 32 | public SpringActorProducer(ApplicationContext applicationContext, Class requiredType) { 33 | this.applicationContext = applicationContext; 34 | this.actorBeanName = null; 35 | this.requiredType = requiredType; 36 | this.args = null; 37 | } 38 | 39 | public SpringActorProducer(ApplicationContext applicationContext, Class requiredType, Object[] args) { 40 | this.applicationContext = applicationContext; 41 | this.actorBeanName = null; 42 | this.requiredType = requiredType; 43 | this.args = args; 44 | } 45 | 46 | public SpringActorProducer(ApplicationContext applicationContext, String actorBeanName, Class requiredType) { 47 | this.applicationContext = applicationContext; 48 | this.actorBeanName = actorBeanName; 49 | this.requiredType = requiredType; 50 | this.args = null; 51 | } 52 | 53 | @Override 54 | public Actor produce() { 55 | Actor result; 56 | if (actorBeanName != null && requiredType != null) { 57 | result = (Actor) applicationContext.getBean(actorBeanName, requiredType); 58 | } else if (requiredType != null) { 59 | if (args == null) { 60 | result = (Actor) applicationContext.getBean(requiredType); 61 | } else { 62 | result = (Actor) applicationContext.getBean(requiredType, args); 63 | } 64 | } else { 65 | if (args == null) { 66 | result = (Actor) applicationContext.getBean(actorBeanName); 67 | } else { 68 | result = (Actor) applicationContext.getBean(actorBeanName, args); 69 | } 70 | 71 | } 72 | return result; 73 | } 74 | 75 | @Override 76 | @SuppressWarnings("unchecked") 77 | public Class actorClass() { 78 | return (Class) (requiredType != null ? requiredType : applicationContext.getType(actorBeanName)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/SpringExtension.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing; 2 | 3 | import java.util.Collections; 4 | 5 | import javax.annotation.PostConstruct; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.stereotype.Service; 10 | 11 | import akka.actor.AbstractActor; 12 | import akka.actor.AbstractExtensionId; 13 | import akka.actor.ActorContext; 14 | import akka.actor.ActorRef; 15 | import akka.actor.ActorSystem; 16 | import akka.actor.ExtendedActorSystem; 17 | import akka.actor.Extension; 18 | import akka.actor.Props; 19 | import akka.routing.RouterConfig; 20 | 21 | /** 22 | * An Akka Extension to provide access to Spring managed Actor Beans. 23 | */ 24 | @Service 25 | public class SpringExtension extends AbstractExtensionId { 26 | 27 | @Autowired 28 | private ApplicationContext applicationContext; 29 | 30 | @Autowired 31 | private ActorSystem actorSystem; 32 | 33 | @PostConstruct 34 | public void postConstruct() { 35 | this.get(actorSystem).initialize(applicationContext); 36 | } 37 | 38 | /** 39 | * Is used by Akka to instantiate the Extension identified by this 40 | * ExtensionId, internal use only. 41 | */ 42 | @Override 43 | public SpringExt createExtension(ExtendedActorSystem system) { 44 | return new SpringExt(); 45 | } 46 | 47 | public ActorRef actorOf(ActorContext actorContext, String actorSpringBeanName, String actorLogicalName, Object... actorParameters) { 48 | return actorContext.actorOf(get(actorContext.system()).props(actorSpringBeanName, actorParameters), actorLogicalName); 49 | } 50 | 51 | public ActorRef actorOf(ActorContext actorContext, Class requiredType, String actorLogicalName, Object... actorParameters) { 52 | return actorContext.actorOf(get(actorContext.system()).props(requiredType, actorParameters), actorLogicalName); 53 | } 54 | 55 | public ActorRef actorOf(ActorContext actorContext, String actorSpringBeanName, String actorLogicalName, Class requiredType) { 56 | return actorContext.actorOf(get(actorContext.system()).props(actorSpringBeanName, requiredType), actorLogicalName); 57 | } 58 | 59 | public ActorRef actorOf(ActorContext actorContext, String actorSpringBeanName, Object... actorParameters) { 60 | return actorContext.actorOf(get(actorContext.system()).props(actorSpringBeanName, actorParameters)); 61 | } 62 | 63 | public ActorRef actorOf(ActorSystem actorSystem, String actorSpringBeanName, Object... actorParameters) { 64 | return actorSystem.actorOf(get(actorSystem).props(actorSpringBeanName, actorParameters)); 65 | } 66 | 67 | public ActorRef actorOf(ActorSystem actorSystem, String actorSpringBeanName) { 68 | return actorSystem.actorOf(get(actorSystem).props(actorSpringBeanName)); 69 | } 70 | 71 | public ActorRef actorOf(ActorSystem actorSystem, String actorSpringBeanName, RouterConfig routerConfig, String dispatcher, String actorLogicalName) { 72 | return actorSystem.actorOf(get(actorSystem).props(actorSpringBeanName).withRouter(routerConfig).withDispatcher(dispatcher), actorLogicalName); 73 | 74 | } 75 | 76 | public ActorRef actorOf(ActorSystem actorSystem, String actorSpringBeanName, RouterConfig routerConfig, String dispatcher) { 77 | return actorSystem.actorOf(get(actorSystem).props(actorSpringBeanName).withRouter(routerConfig).withDispatcher(dispatcher)); 78 | 79 | } 80 | 81 | public ActorRef actorOf(ActorSystem actorSystem, String actorSpringBeanName, RouterConfig routerConfig, String dispatcher, Object... actorParameters) { 82 | return actorSystem.actorOf(get(actorSystem).props(actorSpringBeanName, actorParameters).withRouter(routerConfig).withDispatcher(dispatcher)); 83 | 84 | } 85 | 86 | public ActorRef actorOf(ActorSystem actorSystem, String actorSpringBeanName, RouterConfig routerConfig, String dispatcher, String actorLogicalName, Object... actorParameters) { 87 | return actorSystem.actorOf(get(actorSystem).props(actorSpringBeanName, actorParameters).withRouter(routerConfig).withDispatcher(dispatcher), actorLogicalName); 88 | 89 | } 90 | 91 | public ActorRef actorOf(ActorSystem actorSystem, String actorSpringBeanName, String actorLogicalName, Object... actorParameters) { 92 | return actorSystem.actorOf(get(actorSystem).props(actorSpringBeanName, actorParameters), actorLogicalName); 93 | 94 | } 95 | 96 | 97 | /** 98 | * The Extension implementation. 99 | */ 100 | public static class SpringExt implements Extension { 101 | private volatile ApplicationContext applicationContext; 102 | 103 | /** 104 | * Used to initialize the Spring application context for the extension. 105 | * 106 | * @param applicationContext - spring application context. 107 | */ 108 | public void initialize(ApplicationContext applicationContext) { 109 | this.applicationContext = applicationContext; 110 | } 111 | 112 | 113 | /** 114 | * Create a Props for the specified actorBeanName using the 115 | * SpringActorProducer class. 116 | * 117 | * @param actorBeanName The name of the actor bean to create Props for 118 | * @return a Props that will create the named actor bean using Spring 119 | */ 120 | public Props props(String actorBeanName) { 121 | return props(actorBeanName, Collections.emptyList()); 122 | } 123 | 124 | /** 125 | * Create a Props for the specified actorBeanName using the 126 | * SpringActorProducer class. 127 | * 128 | * @param actorBeanName The name of the actor bean to create Props for 129 | * @param parameters If any parameters this Actor needs, pass null if no parameters. 130 | * @return a Props that will create the named actor bean using Spring 131 | */ 132 | public Props props(String actorBeanName, Object... parameters) { 133 | return (parameters != null && parameters.length > 0) ? Props.create(SpringActorProducer.class, applicationContext, 134 | actorBeanName, 135 | parameters) : Props.create(SpringActorProducer.class, 136 | applicationContext, 137 | actorBeanName); 138 | 139 | 140 | } 141 | 142 | /** 143 | * Create a Props for the specified actorBeanName using the SpringActorProducer class. 144 | * 145 | * @param requiredType Type of the actor bean must match. Can be an interface or superclass of the actual class, 146 | * or {@code null} for any match. For example, if the value is {@code Object.class}, this method will succeed 147 | * whatever the class of the returned instance. 148 | * @return a Props that will create the actor bean using Spring 149 | */ 150 | public Props props(Class requiredType, Object... args) { 151 | return (args != null && args.length > 0) ? Props.create(SpringActorProducer.class, applicationContext, 152 | requiredType, 153 | args) : Props.create(SpringActorProducer.class, 154 | applicationContext, 155 | requiredType); 156 | } 157 | 158 | /** 159 | * Create a Props for the specified actorBeanName using the SpringActorProducer class. 160 | * 161 | * @param actorBeanName The name of the actor bean to create Props for 162 | * @param requiredType Type of the actor bean must match. Can be an interface or superclass of the actual class, 163 | * or {@code null} for any match. For example, if the value is {@code Object.class}, this method will succeed 164 | * whatever the class of the returned instance. 165 | * @return a Props that will create the actor bean using Spring 166 | */ 167 | public Props props(String actorBeanName, Class requiredType) { 168 | return Props.create(SpringActorProducer.class, applicationContext, actorBeanName, requiredType); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/config/AkkaAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.config; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntity; 12 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntityBroker; 13 | 14 | import akka.actor.ActorSystem; 15 | 16 | @Configuration 17 | @ConditionalOnClass(PersistentEntity.class) 18 | @EnableConfigurationProperties(AkkaProperties.class) 19 | public class AkkaAutoConfiguration { 20 | 21 | @Bean(destroyMethod = "terminate") 22 | @ConditionalOnMissingBean 23 | public ActorSystem getActorSystem(AkkaProperties akkaProperties) { 24 | ActorSystem system; 25 | if (akkaProperties.getSystemName() != null && akkaProperties.getConfig() != null) { 26 | system = ActorSystem.create(akkaProperties.getSystemName(), akkaProperties.getConfig()); 27 | } else if (akkaProperties.getSystemName() != null) { 28 | system = ActorSystem.create(akkaProperties.getSystemName()); 29 | } else { 30 | system = ActorSystem.create(); 31 | } 32 | return system; 33 | } 34 | 35 | @Bean 36 | @ConditionalOnMissingBean 37 | public PersistentEntityBroker persistentEntityBroker(ActorSystem actorSystem, List persistentEntityProperties) { 38 | return new PersistentEntityBroker(actorSystem, persistentEntityProperties); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/config/AkkaProperties.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import com.typesafe.config.Config; 6 | import com.typesafe.config.ConfigFactory; 7 | 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | @ConfigurationProperties(prefix = "spring.akka") 12 | @Getter 13 | @Setter 14 | public class AkkaProperties { 15 | 16 | private String systemName; 17 | private Config config; 18 | 19 | public void setConfig(String config) { 20 | Config defaultConfig = ConfigFactory.empty(); 21 | this.config = defaultConfig.withFallback(ConfigFactory.load((config))).resolve(); 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/config/PersistentEntityProperties.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.config; 2 | 3 | 4 | import java.util.Map; 5 | import java.util.function.Function; 6 | 7 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntity; 8 | 9 | public interface PersistentEntityProperties { 10 | int snapshotStateAfter(); 11 | 12 | long entityPassivateAfter(); 13 | 14 | Map, String> tags(); 15 | 16 | int numberOfShards(); 17 | 18 | Function persistenceIdPostfix(); 19 | 20 | String persistenceIdPrefix(); 21 | 22 | Class getEntityClass(); 23 | 24 | Class getRootCommandType(); 25 | 26 | Class getRootEventType(); 27 | 28 | String asyncPersistentEntityDispatcherName(); 29 | 30 | String pipeDispatcherName(); 31 | 32 | long scheduledAsyncEntityActionTimeout(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/AsyncResult.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist; 6 | 7 | import lombok.Builder; 8 | import lombok.Value; 9 | 10 | /** 11 | * @param the event type 12 | *

13 | * the async result class that contain the async persist action and if exist the error response 14 | */ 15 | @Builder 16 | @Value 17 | public class AsyncResult implements Serializable { 18 | private Persist persist; 19 | private ErrorResponse errorResponse; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance; 2 | 3 | import java.io.Serializable; 4 | 5 | import lombok.Builder; 6 | import lombok.Value; 7 | 8 | /** 9 | * Generic Error response class 10 | */ 11 | @Builder 12 | @Value 13 | public class ErrorResponse implements Serializable { 14 | private String errorMsg; 15 | private String errorCode; 16 | private String exceptionMsg; 17 | } 18 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/ResumeProcessing.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * the message trigger type to inform the persistent actor to resume processing and stop stashing messages 7 | */ 8 | public class ResumeProcessing implements Serializable { 9 | } 10 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/ExecutionFlow.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.CompletionStage; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.BiFunction; 7 | 8 | import com.spring.akka.eventsourcing.persistance.AsyncResult; 9 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist; 10 | 11 | import lombok.Builder; 12 | import lombok.Data; 13 | import lombok.Singular; 14 | 15 | /** 16 | * @param root command class type 17 | * @param root event class 18 | * @param root state class 19 | *

20 | * builder class for the execution flow of the aggregate entity 21 | */ 22 | @Builder 23 | @Data 24 | public class ExecutionFlow { 25 | 26 | private S state; 27 | @Singular("onCommand") 28 | private Map, TriFunction>> onCommand; 29 | @Singular("asyncOnCommand") 30 | private Map, TriFunction>>> asyncOnCommand; 31 | @Singular("onEvent") 32 | private Map, BiFunction> onEvent; 33 | 34 | 35 | public static class ExecutionFlowBuilder { 36 | 37 | /* 38 | * Register a read-only command handler for a given command class. A read-only command 39 | * handler does not persist events (i.e. it does not change state) but it may perform side 40 | * effects, such as replying to the request. Replies are sent with the `reply` method of the 41 | * context that is passed to the command handler function. 42 | */ 43 | public ExecutionFlowBuilder onReadOnlyCommand(Class command, BiConsumer handler) { 44 | 45 | onCommand(command, (cmd, flowContext, state) -> { 46 | handler.accept(cmd, flowContext); 47 | return flowContext.done(); 48 | }); 49 | return this; 50 | } 51 | 52 | 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/FlowContext.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing; 2 | 3 | import java.util.List; 4 | import java.util.function.Consumer; 5 | 6 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist; 7 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.PersistAll; 8 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.PersistNone; 9 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.PersistOne; 10 | 11 | public abstract class FlowContext extends ReadOnlyFlowContext { 12 | /** 13 | * A command handler may return this `Persist` type to define 14 | * that one event is to be persisted. 15 | */ 16 | public Persist thenPersist(E event) { 17 | return PersistOne.builder().event(event).afterPersist(null).build(); 18 | } 19 | 20 | /** 21 | * A command handler may return this `Persist` type to define 22 | * that one event is to be persisted. External side effects can be 23 | * performed after successful persist in the `afterPersist` function. 24 | */ 25 | public Persist thenPersist(E event, Consumer afterPersist) { 26 | 27 | return PersistOne.builder().event(event).afterPersist(afterPersist).build(); 28 | } 29 | 30 | /** 31 | * A command handler may return this `Persist`type to define 32 | * that several events are to be persisted. 33 | */ 34 | public Persist thenPersistAll(List events) { 35 | return PersistAll.builder().afterPersist(null).events(events).build(); 36 | } 37 | 38 | /** 39 | * A command handler may return this `Persist` type to define 40 | * that several events are to be persisted. External side effects can be 41 | * performed after successful persist in the `afterPersist` function. 42 | * `afterPersist` is invoked once when all events have been persisted 43 | * successfully. 44 | */ 45 | public Persist thenPersistAll(List events, Consumer> afterPersist) { 46 | return PersistAll.builder().afterPersist(afterPersist).events(events).build(); 47 | } 48 | 49 | 50 | /** 51 | * A command handler may return this `Persist` type to define 52 | * that no events are to be persisted. 53 | */ 54 | public Persist done() { 55 | return PersistNone.builder().build(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/PersistentEntityBroker.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing; 2 | 3 | import static akka.actor.Props.create; 4 | 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import javax.annotation.PostConstruct; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | 15 | import com.spring.akka.eventsourcing.config.PersistentEntityProperties; 16 | 17 | import akka.actor.ActorRef; 18 | import akka.actor.ActorSystem; 19 | 20 | /** 21 | * generic spring managed bean persistent entity broker that abstract cluster sharding 22 | * for the enabled entity managed spring beans persistent actors 23 | */ 24 | @Service 25 | public class PersistentEntityBroker { 26 | 27 | private final ActorSystem actorSystem; 28 | 29 | private final List persistentEntityProperties; 30 | 31 | private Map, ActorRef> shardingRegistery; 32 | 33 | 34 | @Autowired 35 | public PersistentEntityBroker(ActorSystem actorSystem, List persistentEntityProperties) { 36 | this.actorSystem = actorSystem; 37 | this.persistentEntityProperties = persistentEntityProperties; 38 | } 39 | 40 | /** 41 | * auto discover the different configuration of the different persistent entity actors and create a lookup of them for the broker 42 | */ 43 | @PostConstruct 44 | public void init() { 45 | 46 | Map, ActorRef> initMap = new HashMap<>(); 47 | 48 | persistentEntityProperties.forEach(persistentEntityConfig -> initMap.put(persistentEntityConfig.getEntityClass(), 49 | PersistentEntitySharding.of(create(persistentEntityConfig.getEntityClass(), persistentEntityConfig), 50 | persistentEntityConfig.persistenceIdPrefix(), persistentEntityConfig.persistenceIdPostfix(), 51 | persistentEntityConfig.numberOfShards()).shardRegion(actorSystem))); 52 | 53 | shardingRegistery = Collections.unmodifiableMap(initMap); 54 | 55 | } 56 | 57 | /** 58 | * @param entityClass the persistent entity class to get the cluster sharding for 59 | * @return the found actor ref based into the sharding registery lookup 60 | */ 61 | public ActorRef findPersistentEntity(Class entityClass) { 62 | 63 | final ActorRef sharedRegionPersistentActor = shardingRegistery.get(entityClass); 64 | if (sharedRegionPersistentActor == null) { 65 | throw new IllegalStateException("no persistent entity configuration(PersistentEntityProperties) has been define for that entity class " + entityClass.getSimpleName()); 66 | } 67 | return sharedRegionPersistentActor; 68 | 69 | 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/PersistentEntitySharding.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing; 2 | 3 | import java.util.function.Function; 4 | 5 | import akka.actor.ActorRef; 6 | import akka.actor.ActorSystem; 7 | import akka.actor.Props; 8 | import akka.cluster.sharding.ClusterSharding; 9 | import akka.cluster.sharding.ClusterShardingSettings; 10 | import akka.cluster.sharding.ShardRegion; 11 | import akka.cluster.sharding.ShardRegion.MessageExtractor; 12 | import akka.persistence.AbstractPersistentActor; 13 | 14 | /** 15 | * Base class for setting up sharding of persistent actors that: 16 | *

17 | * - Have their persistenceId conform to [prefix] + "_" + [id], e.g. "doc_249e1098-1cd9-4ffe-a494-73a430983590" 18 | * - Respond to a specific base class of commands 19 | * - Send the commands unchanged from the SharedRegion router onto the persistent actors themselves 20 | * 21 | * @param Base class of commands that the actor will respond to 22 | */ 23 | 24 | public class PersistentEntitySharding { 25 | private final Props props; 26 | private final String persistenceIdPrefix; 27 | private final Function persistenceIdPostfix; 28 | private final int numberOfShards; 29 | private final MessageExtractor messageExtractor = new MessageExtractor() { 30 | @Override 31 | public String entityId(Object command) { 32 | return getEntityId(command); 33 | } 34 | 35 | @Override 36 | public String shardId(Object command) { 37 | if (command instanceof ShardRegion.StartEntity) { 38 | ShardRegion.StartEntity startEntity = (ShardRegion.StartEntity) command; 39 | return getShardId(startEntity.entityId()); 40 | } else { 41 | return getShardId(getEntityId(command)); 42 | } 43 | 44 | } 45 | 46 | @Override 47 | public Object entityMessage(Object command) { 48 | // we don't need need to unwrap messages sent to the router, they can just be 49 | // forwarded to the target persistent actor directly. 50 | return command; 51 | } 52 | }; 53 | 54 | protected PersistentEntitySharding(Props props, String persistenceIdPrefix, Function entityIdForCommand, int numberOfShards) { 55 | this.props = props; 56 | this.persistenceIdPrefix = persistenceIdPrefix; 57 | this.persistenceIdPostfix = entityIdForCommand; 58 | this.numberOfShards = numberOfShards; 59 | } 60 | 61 | /** 62 | * Creates a PersistentEntitySharding for an actor that is created according to [props]. The actor must be a subclass of {@link AbstractPersistentActor}. 63 | * Entities will be sharded onto 256 shards. 64 | * 65 | * @param persistenceIdPrefix Fixed prefix for each persistence id. This is typically the name of your aggregate root, e.g. "document" or "user". 66 | * @param persistenceIdPostfix Function that returns the last part of the persistence id that a command is routed to. This typically is the real ID of your entity, or UUID. 67 | */ 68 | public static PersistentEntitySharding of(Props props, String persistenceIdPrefix, Function persistenceIdPostfix) { 69 | return new PersistentEntitySharding<>(props, persistenceIdPrefix, persistenceIdPostfix, 256); 70 | } 71 | 72 | /** 73 | * Creates a PersistentEntitySharding for an actor that is created according to [props]. The actor must be a subclass of {@link AbstractPersistentActor}. 74 | * 75 | * @param numberOfShards Number of shards to divide all entity/persistence ids into. This can not be changed after the first run. 76 | * @param persistenceIdPrefix Fixed prefix for each persistence id. This is typically the name of your aggregate root, e.g. "document" or "user". 77 | * @param persistenceIdPostfix Function that returns the last part of the persistence id that a command is routed to. This typically is the real ID of your entity, or UUID. 78 | */ 79 | public static PersistentEntitySharding of(Props props, String persistenceIdPrefix, Function persistenceIdPostfix, int numberOfShards) { 80 | return new PersistentEntitySharding<>(props, persistenceIdPrefix, persistenceIdPostfix, numberOfShards); 81 | } 82 | 83 | /** 84 | * Starts the cluster router (ShardRegion) for this persistent actor type on the given actor system, 85 | * and returns its ActorRef. If it's already running, just returns the ActorRef. 86 | */ 87 | public ActorRef shardRegion(ActorSystem system) { 88 | return ClusterSharding.get(system).start( 89 | persistenceIdPrefix, 90 | props, 91 | ClusterShardingSettings.create(system), 92 | messageExtractor); 93 | } 94 | 95 | /** 96 | * Returns the postfix part of a generated persistence ID. 97 | * 98 | * @param persistenceId A persistenceId of an actor that was spawned by sending a command to it through the 99 | * ActorRef returned by {@link #shardRegion}. 100 | */ 101 | public String getPersistenceIdPostfix(String persistenceId) { 102 | return persistenceId.substring(persistenceIdPrefix.length() + 1); 103 | } 104 | 105 | /** 106 | * Returns the entityId (=persistenceId) to which the given command should be routed 107 | */ 108 | @SuppressWarnings("unchecked") 109 | public String getEntityId(Object command) { 110 | return persistenceIdPrefix + "_" + persistenceIdPostfix.apply((C) command); 111 | 112 | } 113 | 114 | /** 115 | * Returns the shard on which the given entityId should be placed 116 | */ 117 | public String getShardId(String entityId) { 118 | return String.valueOf(entityId.hashCode() % numberOfShards); 119 | } 120 | 121 | 122 | } 123 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/ReadOnlyFlowContext.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing; 2 | 3 | public abstract class ReadOnlyFlowContext { 4 | 5 | /** 6 | * Send reply to a command. The type `R` must be the type defined by 7 | * the command. 8 | */ 9 | public abstract void reply(R msg); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/TriFunction.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * @author romeh 8 | * Tri function support 9 | */ 10 | @FunctionalInterface 11 | public interface TriFunction { 12 | 13 | R apply(A a, B b, C c); 14 | 15 | default TriFunction andThen( 16 | Function after) { 17 | Objects.requireNonNull(after); 18 | return (A a, B b, C c) -> after.apply(apply(a, b, c)); 19 | } 20 | } -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/Persist.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions; 2 | 3 | import java.io.Serializable; 4 | import java.util.function.Consumer; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | 11 | 12 | /** 13 | * main class for the action result that need to be done from the command context after executing the command handler 14 | * 15 | * @param the event type 16 | */ 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | @Getter 20 | @Setter 21 | public class Persist implements Serializable { 22 | 23 | private E event; 24 | private Consumer afterPersist; 25 | } 26 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/PersistAll.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions; 2 | 3 | 4 | import java.util.List; 5 | import java.util.function.Consumer; 6 | 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | /** 11 | * main class for the action result that need to be done from the command context after executing the command handler in case of many events to persist 12 | * 13 | * @param the event type 14 | */ 15 | public class PersistAll extends Persist { 16 | @Getter 17 | private List events; 18 | @Getter 19 | private Consumer> afterPersistAll; 20 | 21 | @Builder 22 | private PersistAll(Consumer> afterPersist, List events) { 23 | super(null, null); 24 | this.events = events; 25 | this.afterPersistAll = afterPersist; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/PersistNone.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions; 2 | 3 | import lombok.Builder; 4 | 5 | /** 6 | * simply it mean nothing to be persisted as an event which is the case of readOnly command handlers 7 | */ 8 | @Builder 9 | public class PersistNone extends Persist { 10 | 11 | public PersistNone() { 12 | super(null, null); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/actions/PersistOne.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.actions; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import lombok.Builder; 6 | 7 | /** 8 | * persist one event as a result of command handler execution and check if there is any after persist logic is needed. 9 | * 10 | * @param the event type 11 | */ 12 | 13 | public class PersistOne extends Persist { 14 | @Builder 15 | private PersistOne(E event, Consumer afterPersist) { 16 | super(event, afterPersist); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/java/com/spring/akka/eventsourcing/persistance/eventsourcing/annotations/PersistentActor.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.persistance.eventsourcing.annotations; 2 | 3 | 4 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 5 | import org.springframework.context.annotation.Scope; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * a generic annotation to mark custom actor class as a managed spring bean 10 | */ 11 | @Component 12 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 13 | public @interface PersistentActor { 14 | } 15 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.spring.akka.eventsourcing.config.AkkaAutoConfiguration -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | internal.blocking-io-dispatcher { 2 | type = Dispatcher 3 | executor = "thread-pool-executor" 4 | thread-pool-executor { 5 | fixed-pool-size = 8 6 | } 7 | throughput = 1 8 | } 9 | internal-thread-pool-dispatcher { 10 | # Dispatcher is the name of the event-based dispatcher 11 | type = Dispatcher 12 | # What kind of ExecutionService to use 13 | executor = "thread-pool-executor" 14 | # Configuration for the thread pool 15 | thread-pool-executor { 16 | # minimum number of threads to cap factor-based core number to 17 | core-pool-size-min = 4 18 | # No of core threads ... ceil(available processors * factor) 19 | core-pool-size-factor = 2.0 20 | # maximum number of threads to cap factor-based number to 21 | core-pool-size-max = 16 22 | 23 | # Minimum number of threads to cap factor-based max number to 24 | # (if using a bounded task queue) 25 | max-pool-size-min = 8 26 | 27 | # Max no of threads (if using a bounded task queue) is determined by 28 | # calculating: ceil(available processors * factor) 29 | max-pool-size-factor = 2.0 30 | 31 | # Specifies the bounded capacity of the task queue (< 1 == unbounded) 32 | task-queue-size = -1 33 | 34 | # Specifies which type of task queue will be used, can be "array" or 35 | # "linked" (default) 36 | task-queue-type = "linked" 37 | 38 | # Allow core threads to time out 39 | allow-core-timeout = on 40 | 41 | } 42 | # Throughput defines the maximum number of messages to be 43 | # processed per actor before the thread jumps to the next actor. 44 | # Set to 1 for as fair as possible. 45 | throughput = 1 46 | } -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/EntityActorTest.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import com.spring.akka.eventsourcing.example.OrderEntity; 14 | import com.spring.akka.eventsourcing.example.OrderEntityProperties; 15 | import com.spring.akka.eventsourcing.example.OrderState; 16 | import com.spring.akka.eventsourcing.example.OrderStatus; 17 | import com.spring.akka.eventsourcing.example.commands.OrderCmd; 18 | import com.spring.akka.eventsourcing.example.response.Response; 19 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntityBroker; 20 | 21 | import akka.Done; 22 | import akka.actor.ActorRef; 23 | import akka.pattern.PatternsCS; 24 | import akka.util.Timeout; 25 | 26 | /** 27 | * These test cases are time sensitive and may (extremely rarely) fail if run with test suite because of GC pause etc. 28 | * Run them as part of separate suite or increase the expiry delay to higher number and adjust test cases delays accordingly. 29 | */ 30 | @SpringBootTest(classes = TestConfig.class) 31 | @RunWith(SpringRunner.class) 32 | public class EntityActorTest { 33 | 34 | @Autowired 35 | PersistentEntityBroker persistentEntityBroker; 36 | 37 | @Autowired 38 | OrderEntityProperties actorProperties; 39 | 40 | @Test 41 | public void actorShouldGoToDifferentStagesProperly() { 42 | 43 | ActorRef testActorEntity = persistentEntityBroker.findPersistentEntity(OrderEntity.class); 44 | 45 | // check the response with order status is created 46 | final CompletableFuture result = PatternsCS.ask(testActorEntity, new OrderCmd.CreateCmd("123456"), Timeout.apply( 47 | 5, TimeUnit.SECONDS)).toCompletableFuture(); 48 | 49 | 50 | result.whenComplete((o, throwable) -> { 51 | Response response = (Response) o; 52 | System.out.println("step 1: " + response.toString()); 53 | Assert.assertEquals("123456", response.getOrderId()); 54 | Assert.assertEquals(response.getOrderStatus(), OrderStatus.Created.name()); 55 | 56 | }); 57 | 58 | pauseSeconds(5); 59 | 60 | if (result.isDone()) { 61 | final CompletableFuture result2 = PatternsCS.ask(testActorEntity, new OrderCmd.ValidateCmd("123456"), 5000).toCompletableFuture(); 62 | result2.whenComplete((o, throwable) -> { 63 | Response response = (Response) o; 64 | System.out.println("step 2: " + response.toString()); 65 | Assert.assertEquals("123456", response.getOrderId()); 66 | Assert.assertEquals(response.getOrderStatus(), OrderStatus.Validated.name()); 67 | 68 | }); 69 | 70 | pauseSeconds(5); 71 | 72 | if (result2.isDone()) { 73 | final CompletableFuture result3 = PatternsCS.ask(testActorEntity, new OrderCmd.SignCmd("123456"), 5000).toCompletableFuture(); 74 | result3.whenComplete((o, throwable) -> { 75 | Response response = (Response) o; 76 | System.out.println("step 3: " + response.toString()); 77 | Assert.assertEquals("123456", response.getOrderId()); 78 | Assert.assertEquals(response.getOrderStatus(), OrderStatus.Signed.name()); 79 | }); 80 | 81 | pauseSeconds(5); 82 | 83 | if (result3.isDone()) { 84 | final CompletableFuture result4 = PatternsCS.ask(testActorEntity, new OrderCmd.GetOrderStatusCmd("123456"), 5000).toCompletableFuture(); 85 | result4.whenComplete((o, throwable) -> { 86 | OrderState response = (OrderState) o; 87 | System.out.println("step 4: " + response.toString()); 88 | Assert.assertEquals(3, response.getEventsHistory().size()); 89 | Assert.assertEquals(OrderStatus.Signed.name(), response.getOrderStatus()); 90 | }); 91 | 92 | pauseSeconds(5); 93 | 94 | if (result4.isDone()) { 95 | 96 | PatternsCS.ask(testActorEntity, new OrderCmd.SignCmd("123456"), 5000).whenComplete((o, throwable) -> { 97 | 98 | Assert.assertTrue(o instanceof Done); 99 | System.out.println("step 5: " + o.toString()); 100 | }); 101 | } 102 | 103 | 104 | } 105 | } 106 | } 107 | 108 | } 109 | 110 | private void pauseSeconds(int seconds) { 111 | try { 112 | Thread.sleep(seconds * 1000); 113 | } catch (InterruptedException e) { 114 | e.printStackTrace(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/EntityActorTestAsync.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import com.spring.akka.eventsourcing.example.OrderEntity; 14 | import com.spring.akka.eventsourcing.example.OrderEntityProperties; 15 | import com.spring.akka.eventsourcing.example.OrderStatus; 16 | import com.spring.akka.eventsourcing.example.commands.OrderCmd; 17 | import com.spring.akka.eventsourcing.example.response.Response; 18 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntityBroker; 19 | 20 | import akka.actor.ActorRef; 21 | import akka.pattern.PatternsCS; 22 | import akka.util.Timeout; 23 | 24 | /** 25 | * These test cases are time sensitive and may (extremely rarely) fail if run with test suite because of GC pause etc. 26 | * Run them as part of separate suite or increase the expiry delay to higher number and adjust test cases delays accordingly. 27 | */ 28 | @SpringBootTest(classes = TestConfig.class) 29 | @RunWith(SpringRunner.class) 30 | public class EntityActorTestAsync { 31 | 32 | @Autowired 33 | PersistentEntityBroker persistentEntityBroker; 34 | 35 | @Autowired 36 | OrderEntityProperties actorProperties; 37 | 38 | 39 | @Test 40 | public void actorShouldGoToDifferentStagesProperly() throws Exception { 41 | 42 | ActorRef testActorEntity = persistentEntityBroker.findPersistentEntity(OrderEntity.class); 43 | 44 | // check the response with order status is created 45 | final CompletableFuture result = PatternsCS.ask(testActorEntity, new OrderCmd.CreateCmd("123456"), Timeout.apply( 46 | 5, TimeUnit.SECONDS)).toCompletableFuture(); 47 | 48 | result.whenComplete((o, throwable) -> { 49 | Response response = (Response) o; 50 | System.out.println("Step Async 1: " + response.toString()); 51 | Assert.assertEquals("123456", response.getOrderId()); 52 | Assert.assertEquals(response.getOrderStatus(), OrderStatus.Created.name()); 53 | 54 | }); 55 | 56 | pauseSeconds(5); 57 | 58 | if (result.isDone()) { 59 | final CompletableFuture result2 = PatternsCS.ask(testActorEntity, new OrderCmd.ValidateCmd("123456"), 5000).toCompletableFuture(); 60 | result2.whenComplete((o, throwable) -> { 61 | Response response = (Response) o; 62 | System.out.println("Step Async 2: " + response.toString()); 63 | Assert.assertEquals("123456", response.getOrderId()); 64 | Assert.assertEquals(response.getOrderStatus(), OrderStatus.Validated.name()); 65 | 66 | }); 67 | 68 | pauseSeconds(5); 69 | 70 | if (result2.isDone()) { 71 | // send command which will trigger async action in the order entity to test the async handling and stashing Msgs till the processing is dine 72 | final CompletableFuture result3 = PatternsCS.ask(testActorEntity, new OrderCmd.AsyncSignCmd("123456"), 5000).toCompletableFuture(); 73 | result3.whenComplete((o, throwable) -> { 74 | Response response = (Response) o; 75 | System.out.println("Step Async 3: " + response.toString()); 76 | Assert.assertEquals("123456", response.getOrderId()); 77 | Assert.assertEquals(response.getOrderStatus(), OrderStatus.Signed.name()); 78 | }); 79 | // send without waiting the next command which should be stashed if the processing of the async action is still on in place 80 | final CompletableFuture result4 = PatternsCS.ask(testActorEntity, new OrderCmd.SignCmd("123456"), Timeout.apply( 81 | 8, TimeUnit.SECONDS)).toCompletableFuture(); 82 | result4.whenComplete((o, throwable) -> { 83 | Response response = (Response) o; 84 | System.out.println("Step Async 4: " + response.toString()); 85 | Assert.assertEquals("123456", response.getOrderId()); 86 | Assert.assertEquals(response.getOrderStatus(), OrderStatus.Signed.name()); 87 | }); 88 | 89 | 90 | } 91 | } 92 | } 93 | 94 | private void pauseSeconds(int seconds) { 95 | try { 96 | Thread.sleep(seconds * 1000); 97 | } catch (InterruptedException e) { 98 | e.printStackTrace(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/TestConfig.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import com.spring.akka.eventsourcing.config.AkkaProperties; 9 | 10 | /** 11 | * @author romeh 12 | */ 13 | @Configuration 14 | @EnableAutoConfiguration 15 | @ComponentScan("com.spring.akka.eventsourcing") 16 | public class TestConfig { 17 | @Bean 18 | public AkkaProperties akkaProperties() { 19 | AkkaProperties akkaProperties = new AkkaProperties(); 20 | akkaProperties.setConfig("akka.initializer.conf"); 21 | akkaProperties.setSystemName("testActorSystem"); 22 | return akkaProperties; 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/OrderEntity.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example; 2 | 3 | import static akka.actor.SupervisorStrategy.escalate; 4 | import static akka.actor.SupervisorStrategy.restart; 5 | import static akka.actor.SupervisorStrategy.resume; 6 | import static akka.actor.SupervisorStrategy.stop; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | 16 | import com.spring.akka.eventsourcing.config.PersistentEntityProperties; 17 | import com.spring.akka.eventsourcing.example.commands.OrderCmd; 18 | import com.spring.akka.eventsourcing.example.events.CreatedEvent; 19 | import com.spring.akka.eventsourcing.example.events.FinishedEvent; 20 | import com.spring.akka.eventsourcing.example.events.OrderEvent; 21 | import com.spring.akka.eventsourcing.example.events.SignedEvent; 22 | import com.spring.akka.eventsourcing.example.events.ValidatedEvent; 23 | import com.spring.akka.eventsourcing.example.response.Response; 24 | import com.spring.akka.eventsourcing.persistance.AsyncResult; 25 | import com.spring.akka.eventsourcing.persistance.eventsourcing.ExecutionFlow; 26 | import com.spring.akka.eventsourcing.persistance.eventsourcing.FlowContext; 27 | import com.spring.akka.eventsourcing.persistance.eventsourcing.PersistentEntity; 28 | import com.spring.akka.eventsourcing.persistance.eventsourcing.ReadOnlyFlowContext; 29 | import com.spring.akka.eventsourcing.persistance.eventsourcing.actions.Persist; 30 | import com.spring.akka.eventsourcing.persistance.eventsourcing.annotations.PersistentActor; 31 | 32 | import akka.Done; 33 | import akka.actor.OneForOneStrategy; 34 | import akka.actor.SupervisorStrategy; 35 | import akka.japi.pf.DeciderBuilder; 36 | import scala.concurrent.duration.Duration; 37 | 38 | @PersistentActor 39 | public class OrderEntity extends PersistentEntity { 40 | 41 | // how to handle supervisor strategy definition for the parent actor 42 | private static SupervisorStrategy strategy = 43 | new OneForOneStrategy(10, Duration.create(1, TimeUnit.MINUTES), DeciderBuilder. 44 | match(ArithmeticException.class, e -> resume()). 45 | match(NullPointerException.class, e -> restart()). 46 | match(IllegalArgumentException.class, e -> stop()). 47 | matchAny(o -> escalate()).build()); 48 | 49 | /** 50 | * @param persistentEntityConfig the akka persistent entity configuration 51 | */ 52 | @Autowired 53 | public OrderEntity(PersistentEntityProperties persistentEntityConfig) { 54 | super(persistentEntityConfig); 55 | } 56 | 57 | /** 58 | * @param state the current State 59 | * @return the initialized behavior for the entity 60 | */ 61 | @Override 62 | protected ExecutionFlow executionFlow(OrderState state) { 63 | switch (state.getOrderStatus()) { 64 | case NotStarted: 65 | return notStarted(state); 66 | case Created: 67 | return waitingForValidation(state); 68 | case Validated: 69 | return waitingForSigning(state); 70 | case Signed: 71 | return complected(state); 72 | default: 73 | throw new IllegalStateException(); 74 | 75 | } 76 | } 77 | 78 | @Override 79 | protected OrderState initialState() { 80 | return new OrderState(Collections.emptyList(), OrderStatus.NotStarted); 81 | } 82 | 83 | /** 84 | * ExecutionFlow for the not started state. 85 | */ 86 | private ExecutionFlow notStarted(OrderState state) { 87 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 88 | 89 | // Command handlers 90 | executionFlowBuilder.onCommand(OrderCmd.CreateCmd.class, (start, ctx, currentState) -> 91 | persistAndReply(ctx, new CreatedEvent(start.getOrderId(), OrderStatus.Created)) 92 | ); 93 | 94 | // Event handlers 95 | executionFlowBuilder.onEvent(CreatedEvent.class, (started, currentState) -> createImmutableState(state, started, OrderStatus.Created)); 96 | 97 | return executionFlowBuilder.build(); 98 | } 99 | 100 | /** 101 | * ExecutionFlow for the not created and not yet validated. 102 | */ 103 | 104 | private ExecutionFlow waitingForValidation(OrderState state) { 105 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 106 | // Command handlers 107 | executionFlowBuilder.onCommand(OrderCmd.ValidateCmd.class, (start, ctx, currentState) -> 108 | persistAndReply(ctx, new ValidatedEvent(start.getOrderId(), OrderStatus.Validated)) 109 | ); 110 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.CreateCmd.class, this::alreadyDone); 111 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.GetOrderStatusCmd.class, (cmd, ctx) -> ctx.reply(getState())); 112 | 113 | // Event handlers 114 | executionFlowBuilder.onEvent(ValidatedEvent.class, (validated, currentState) -> 115 | createImmutableState(state, validated, validated.getOrderStatus()) 116 | ); 117 | 118 | return executionFlowBuilder.build(); 119 | } 120 | 121 | /** 122 | * ExecutionFlow for the not validated and not yet signed. 123 | */ 124 | private ExecutionFlow waitingForSigning(OrderState state) { 125 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 126 | // Command handlers 127 | executionFlowBuilder.onCommand(OrderCmd.SignCmd.class, (start, ctx, currentState) -> 128 | persistAndReply(ctx, new SignedEvent(start.getOrderId(), OrderStatus.Signed)) 129 | ); 130 | executionFlowBuilder.asyncOnCommand(OrderCmd.AsyncSignCmd.class, (signed, ctx, currentState) -> CompletableFuture 131 | .supplyAsync(() -> AsyncResult.builder() 132 | .persist(persistAndReply(ctx, new SignedEvent(signed.getOrderId(), OrderStatus.Signed))) 133 | .build()) 134 | ); 135 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.GetOrderStatusCmd.class, (cmd, ctx) -> ctx.reply(getState())); 136 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.ValidateCmd.class, this::alreadyDone); 137 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.CreateCmd.class, this::alreadyDone); 138 | // Event handlers 139 | executionFlowBuilder.onEvent(SignedEvent.class, (signed, currentState) -> 140 | createImmutableState(state, signed, signed.getOrderStatus()) 141 | ); 142 | 143 | return executionFlowBuilder.build(); 144 | } 145 | 146 | /** 147 | * ExecutionFlow for signed and final state 148 | */ 149 | private ExecutionFlow complected(OrderState state) { 150 | final ExecutionFlow.ExecutionFlowBuilder executionFlowBuilder = newFlowBuilder(state); 151 | // just read only command handlers as it is final state 152 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.GetOrderStatusCmd.class, (cmd, ctx) -> ctx.reply(getState())); 153 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.CreateCmd.class, this::alreadyDone); 154 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.ValidateCmd.class, this::alreadyDone); 155 | executionFlowBuilder.onReadOnlyCommand(OrderCmd.SignCmd.class, this::alreadyDone); 156 | // Event handlers 157 | executionFlowBuilder.onEvent(FinishedEvent.class, (finished, currentState) -> 158 | createImmutableState(state, finished, finished.getOrderStatus()) 159 | ); 160 | 161 | return executionFlowBuilder.build(); 162 | } 163 | 164 | /** 165 | * @param testState current state 166 | * @param testEvent new event 167 | * @param orderStatus new order status 168 | * @return immutable state 169 | */ 170 | private OrderState createImmutableState(OrderState testState, OrderEvent testEvent, OrderStatus orderStatus) { 171 | final List eventsHistory = new ArrayList<>(testState.getEventsHistory()); 172 | eventsHistory.add(testEvent); 173 | return new OrderState(eventsHistory, orderStatus); 174 | 175 | } 176 | 177 | /** 178 | * Persist a single event then respond with done. 179 | */ 180 | private Persist persistAndDone(FlowContext ctx, OrderEvent event) { 181 | return ctx.thenPersist(event, (e) -> ctx.reply(Done.getInstance())); 182 | } 183 | 184 | /** 185 | * Persist a single event then respond with done. 186 | */ 187 | private Persist persistAndReply(FlowContext ctx, OrderEvent event) { 188 | return ctx.thenPersist(event, (e) -> ctx.reply(Response.builder().orderStatus(event.getOrderStatus().name()).orderId(event.getOrderId()).build())); 189 | } 190 | 191 | /** 192 | * Convenience method to handle when a command has already been processed (idempotent processing). 193 | */ 194 | private void alreadyDone(OrderCmd cmd, ReadOnlyFlowContext ctx) { 195 | ctx.reply(Done.getInstance()); 196 | } 197 | 198 | @Override 199 | public SupervisorStrategy supervisorStrategy() { 200 | return strategy; 201 | } 202 | 203 | 204 | } 205 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/OrderEntityProperties.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.function.Function; 7 | 8 | import javax.annotation.PostConstruct; 9 | 10 | import org.springframework.stereotype.Component; 11 | 12 | import com.spring.akka.eventsourcing.config.PersistentEntityProperties; 13 | import com.spring.akka.eventsourcing.example.commands.OrderCmd; 14 | import com.spring.akka.eventsourcing.example.events.CreatedEvent; 15 | import com.spring.akka.eventsourcing.example.events.FinishedEvent; 16 | import com.spring.akka.eventsourcing.example.events.OrderEvent; 17 | import com.spring.akka.eventsourcing.example.events.SignedEvent; 18 | import com.spring.akka.eventsourcing.example.events.ValidatedEvent; 19 | 20 | @Component 21 | public class OrderEntityProperties implements PersistentEntityProperties { 22 | 23 | private Map, String> tags; 24 | 25 | @PostConstruct 26 | public void init() { 27 | Map, String> init = new HashMap<>(); 28 | init.put(CreatedEvent.class, "CreatedOrders"); 29 | init.put(FinishedEvent.class, "FinishedOrders"); 30 | init.put(SignedEvent.class, "SignedOrders"); 31 | init.put(ValidatedEvent.class, "ValidatedOrders"); 32 | tags = Collections.unmodifiableMap(init); 33 | 34 | } 35 | 36 | @Override 37 | public int snapshotStateAfter() { 38 | return 5; 39 | } 40 | 41 | @Override 42 | public long entityPassivateAfter() { 43 | return 60; 44 | } 45 | 46 | @Override 47 | public Map, String> tags() { 48 | 49 | return tags; 50 | } 51 | 52 | @Override 53 | public int numberOfShards() { 54 | return 20; 55 | } 56 | 57 | @Override 58 | public Function persistenceIdPostfix() { 59 | return OrderCmd::getOrderId; 60 | } 61 | 62 | @Override 63 | public String persistenceIdPrefix() { 64 | return OrderEntity.class.getSimpleName(); 65 | } 66 | 67 | @Override 68 | public Class getEntityClass() { 69 | return OrderEntity.class; 70 | } 71 | 72 | @Override 73 | public Class getRootCommandType() { 74 | return OrderCmd.class; 75 | } 76 | 77 | @Override 78 | public Class getRootEventType() { 79 | return OrderEvent.class; 80 | } 81 | 82 | @Override 83 | public String asyncPersistentEntityDispatcherName() { 84 | return null; 85 | } 86 | 87 | @Override 88 | public String pipeDispatcherName() { 89 | return null; 90 | } 91 | 92 | @Override 93 | public long scheduledAsyncEntityActionTimeout() { 94 | return 3; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/OrderState.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | import com.spring.akka.eventsourcing.example.events.OrderEvent; 7 | 8 | import lombok.Value; 9 | 10 | @Value 11 | public class OrderState implements Serializable { 12 | 13 | private final List eventsHistory; 14 | private final OrderStatus orderStatus; 15 | 16 | public OrderState(List eventsHistory, OrderStatus orderStatus) { 17 | this.eventsHistory = eventsHistory; 18 | 19 | this.orderStatus = orderStatus; 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example; 2 | 3 | public enum OrderStatus { 4 | 5 | NotStarted, Created, Validated, Signed, COMPLETED; 6 | } 7 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/commands/OrderCmd.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example.commands; 2 | 3 | import java.io.Serializable; 4 | 5 | import lombok.Value; 6 | 7 | 8 | public interface OrderCmd extends Serializable { 9 | 10 | String getOrderId(); 11 | 12 | @Value 13 | final class CreateCmd implements OrderCmd { 14 | private String orderId; 15 | } 16 | 17 | @Value 18 | final class ValidateCmd implements OrderCmd { 19 | private String orderId; 20 | } 21 | 22 | @Value 23 | final class SignCmd implements OrderCmd { 24 | private String orderId; 25 | } 26 | 27 | @Value 28 | final class GetOrderStatusCmd implements OrderCmd { 29 | private String orderId; 30 | } 31 | 32 | @Value 33 | final class AsyncSignCmd implements OrderCmd { 34 | private String orderId; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/events/CreatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example.events; 2 | 3 | 4 | import com.spring.akka.eventsourcing.example.OrderStatus; 5 | 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Value; 8 | 9 | @Value 10 | @EqualsAndHashCode(callSuper = true) 11 | public class CreatedEvent extends OrderEvent { 12 | 13 | public CreatedEvent(String orderId, OrderStatus orderStatus) { 14 | super(orderId, orderStatus); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/events/FinishedEvent.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example.events; 2 | 3 | 4 | import com.spring.akka.eventsourcing.example.OrderStatus; 5 | 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Value; 8 | 9 | @Value 10 | @EqualsAndHashCode(callSuper = true) 11 | public class FinishedEvent extends OrderEvent { 12 | 13 | public FinishedEvent(String orderId, OrderStatus orderStatus) { 14 | super(orderId, orderStatus); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/events/OrderEvent.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example.events; 2 | 3 | import java.io.Serializable; 4 | 5 | import com.spring.akka.eventsourcing.example.OrderStatus; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Getter 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public abstract class OrderEvent implements Serializable { 15 | private String orderId; 16 | private OrderStatus orderStatus; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/events/SignedEvent.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example.events; 2 | 3 | import com.spring.akka.eventsourcing.example.OrderStatus; 4 | 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Value; 7 | 8 | @EqualsAndHashCode(callSuper = true) 9 | @Value 10 | public class SignedEvent extends OrderEvent { 11 | 12 | public SignedEvent(String orderId, OrderStatus orderStatus) { 13 | super(orderId, orderStatus); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/events/ValidatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example.events; 2 | 3 | import com.spring.akka.eventsourcing.example.OrderStatus; 4 | 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Value; 7 | 8 | @EqualsAndHashCode(callSuper = true) 9 | @Value 10 | public class ValidatedEvent extends OrderEvent { 11 | 12 | public ValidatedEvent(String orderId, OrderStatus orderStatus) { 13 | super(orderId, orderStatus); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/example/response/Response.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.example.response; 2 | 3 | 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | @Data 8 | @Builder 9 | public class Response { 10 | 11 | private String orderId; 12 | private String orderStatus; 13 | private int errorCode; 14 | private String errorMessage; 15 | } 16 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/support/SpringContextSupport.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.support; 2 | 3 | 4 | import java.util.Enumeration; 5 | import java.util.Properties; 6 | 7 | import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; 8 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 9 | 10 | /** 11 | * A convenient class to start up spring application context and stellar system and provides methods 12 | * to shut down system gracefully. 13 | *

14 | * This class help to run test cases in parallel by having many stellar system including clustered one 15 | * running at the same time , each of these system will have its own spring context,configuration & 16 | * properties values. 17 | */ 18 | public class SpringContextSupport { 19 | 20 | private Properties providedProperties = new Properties(); 21 | private AnnotationConfigApplicationContext applicationContext; 22 | 23 | public SpringContextSupport() { 24 | 25 | } 26 | 27 | public SpringContextSupport(Properties providedProperties) { 28 | this.providedProperties = providedProperties; 29 | } 30 | 31 | public static SpringContextSupport instance(Properties properties) { 32 | return new SpringContextSupport(properties); 33 | } 34 | 35 | /** 36 | * This is not a singleton, a convenient method to create a fresh instance. 37 | * 38 | * @return 39 | */ 40 | public static SpringContextSupport instance() { 41 | return new SpringContextSupport(); 42 | } 43 | 44 | public AnnotationConfigApplicationContext getApplicationContext() { 45 | 46 | if (applicationContext == null) { 47 | throw new IllegalStateException("Call build() method first."); 48 | } 49 | 50 | return applicationContext; 51 | } 52 | 53 | public void close() { 54 | 55 | 56 | applicationContext.close(); 57 | 58 | } 59 | 60 | public T getBean(Class clazz) { 61 | return getApplicationContext().getBean(clazz); 62 | } 63 | 64 | public SpringContextSupport add(String propertyKey, String propertyValue) { 65 | providedProperties.setProperty(propertyKey, propertyValue); 66 | return this; 67 | } 68 | 69 | public SpringContextSupport build(String[] packages) { 70 | 71 | applicationContext = new AnnotationConfigApplicationContext(); 72 | 73 | applicationContext.scan(packages); 74 | 75 | applicationContext.addBeanFactoryPostProcessor(propertyPlaceholderConfigurerWith(providedProperties)); 76 | applicationContext.refresh(); 77 | 78 | 79 | return this; 80 | } 81 | 82 | private PropertyPlaceholderConfigurer propertyPlaceholderConfigurerWith(Properties userProvidedProperties) { 83 | 84 | PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer(); 85 | configurer.setProperties(mergeWithDefault(userProvidedProperties)); 86 | return configurer; 87 | 88 | } 89 | 90 | private Properties mergeWithDefault(Properties userProvidedProperties) { 91 | 92 | Properties defaultProperties = new Properties(); 93 | 94 | Enumeration keys = userProvidedProperties.propertyNames(); 95 | while (keys.hasMoreElements()) { 96 | String key = (String) keys.nextElement(); 97 | defaultProperties.put(key, userProvidedProperties.getProperty(key)); 98 | } 99 | 100 | 101 | return defaultProperties; 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/support/TestCluster.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.support; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | 10 | public class TestCluster { 11 | 12 | protected static final Logger log = LoggerFactory.getLogger(TestCluster.class); 13 | 14 | private List springContextSupportForClusterNodes = new ArrayList<>(); 15 | 16 | private String[] configFile; 17 | private String[] ruleConfig; 18 | private int numberOfWorkerNodes = 0; 19 | 20 | 21 | public TestCluster(String[] configFileArg, String[] ruleConfigArg, int numberOfWorkerNodes) { 22 | 23 | this.configFile = configFileArg; 24 | this.ruleConfig = ruleConfigArg; 25 | this.numberOfWorkerNodes = numberOfWorkerNodes; 26 | } 27 | 28 | public static TestCluster createTwoSeedCluster(String configFileArg, String ruleConfigArg, int numberOfWorkerNodes) { 29 | 30 | String[] configFile = {"stellar.configuration", configFileArg}; 31 | String[] ruleConfig = {"stellar.rule.configuration", ""}; 32 | if (ruleConfigArg != null) { 33 | ruleConfig[1] = ruleConfigArg; 34 | } 35 | 36 | return new TestCluster(configFile, ruleConfig, numberOfWorkerNodes); 37 | 38 | } 39 | 40 | public void startCluster() throws Exception { 41 | 42 | // Start seed nodes 43 | 44 | int seed1Port = UnitTestPortManager.instance().getNextPort(); 45 | int seed2Port = UnitTestPortManager.instance().getNextPort(); 46 | 47 | startNode(seed1Port, seed1Port, seed2Port); 48 | startNode(seed2Port, seed1Port, seed2Port); 49 | 50 | // Start worker nodes. 51 | 52 | for (int i = 0; i < numberOfWorkerNodes; i++) { 53 | startNode(UnitTestPortManager.instance().getNextPort(), seed1Port, seed2Port); 54 | } 55 | 56 | // ClusterFormationDetectorActor.waitForAllNodesToJoinCluster(getStellarFromAnyClusterNode().getClusteredActorSystem(), 2 + numberOfWorkerNodes); 57 | 58 | 59 | } 60 | 61 | private void startNode(int clusterInstancePort, int seed1Port, int seed2Port) { 62 | 63 | String[] packages = { 64 | "akka.initializer" 65 | }; 66 | 67 | SpringContextSupport springContextSupport = SpringContextSupport.instance().build(packages); 68 | 69 | springContextSupportForClusterNodes.add(springContextSupport); 70 | 71 | } 72 | 73 | public void shutDownCluster() { 74 | springContextSupportForClusterNodes.stream().forEach(e -> e.close()); 75 | } 76 | 77 | 78 | } 79 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/java/com/spring/akka/eventsourcing/support/UnitTestPortManager.java: -------------------------------------------------------------------------------- 1 | package com.spring.akka.eventsourcing.support; 2 | 3 | import java.util.concurrent.ThreadLocalRandom; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | /** 7 | * Helps to create unique akka port for each unit test cases and helps avoid conflicts while 8 | * testing multiple unit test cases that starts akka (clusters or non-clusters) concurrently 9 | * in the same JVM. 10 | */ 11 | public class UnitTestPortManager { 12 | 13 | private static UnitTestPortManager portSingleton = new UnitTestPortManager(); 14 | private AtomicInteger port = new AtomicInteger(ThreadLocalRandom.current().nextInt(2000, 65000)); 15 | 16 | public static UnitTestPortManager instance() { 17 | return portSingleton; 18 | } 19 | 20 | public int getNextPort() { 21 | return port.incrementAndGet(); 22 | } 23 | 24 | public String getNextPortAsString() { 25 | return getNextPort() + ""; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /springboot-akka-event-sourcing-starter/src/test/resources/akka.initializer.conf: -------------------------------------------------------------------------------- 1 | # This file provides a set of AKKA configuration for test scenario. 2 | 3 | akka { 4 | 5 | loggers = ["akka.event.slf4j.Slf4jLogger"] 6 | loglevel = "info" 7 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 8 | 9 | actor { 10 | provider = "akka.cluster.ClusterActorRefProvider" 11 | deployment { 12 | # Use a wild dispatcher to take care for any actor. 13 | "/*" { 14 | router = round-robin-pool 15 | # Don't change below to anything higher than 1. For such needs duplicate another one with actor name as shown below. 16 | nr-of-instances = 1 17 | } 18 | /responseHandlerActor { 19 | router = round-robin-pool 20 | nr-of-instances = 10 21 | } 22 | } 23 | } 24 | 25 | remote { 26 | log-remote-lifecycle-events = off 27 | netty.tcp { 28 | hostname = "127.0.0.1" 29 | port = 3001 30 | } 31 | } 32 | 33 | cluster { 34 | seed-nodes = ["akka.tcp://testActorSystem@127.0.0.1:3001"] 35 | auto-down-unreachable-after = 10s 36 | metrics.enabled = off 37 | 38 | } 39 | 40 | persistence { 41 | journal { 42 | plugin = "dummy-journal" 43 | } 44 | snapshot-store { 45 | plugin = "dummy-snapshot-store" 46 | } 47 | } 48 | } 49 | 50 | akka { 51 | persistence { 52 | journal.plugin = "inmemory-journal" 53 | snapshot-store.plugin = "inmemory-snapshot-store" 54 | } 55 | } 56 | 57 | akka.log-config-on-start = false 58 | 59 | # Define the dispatcher for whole actor system, which points to default thread-pool-exector. 60 | # This definition does not refer to dispatcher that is defined for task runner dispatcher just below 61 | # rather it points to the default thread-pool-dispachter available within AKKA default configuration. 62 | # https://github.com/akka/akka/blob/v2.4.1/akka-actor/src/main/resources/reference.conf#L332 63 | akka.actor.default-dispatcher.default-executor.fallback = "thread-pool-executor" 64 | 65 | # This dispatcher is being refered by Service when it creates actors. 66 | task-runner-actor-dispatcher { 67 | 68 | # Dispatcher is the name of the event-based dispatcher 69 | type = Dispatcher 70 | 71 | # What kind of ExecutionService to use 72 | executor = "thread-pool-executor" 73 | 74 | # This will be used if you have set "executor = "thread-pool-executor"" 75 | thread-pool-executor { 76 | 77 | # Keep alive time for threads 78 | keep-alive-time = 60s 79 | 80 | # Min number of threads to cap factor-based core number to 81 | core-pool-size-min = 8 82 | 83 | # The core pool size factor is used to determine thread pool core size 84 | # using the following formula: ceil(available processors * factor). 85 | # Resulting size is then bounded by the core-pool-size-min and 86 | # core-pool-size-max values. 87 | core-pool-size-factor = 3.0 88 | 89 | # Max number of threads to cap factor-based number to 90 | core-pool-size-max = 64 91 | 92 | # Minimum number of threads to cap factor-based max number to 93 | # (if using a bounded task queue) 94 | max-pool-size-min = 8 95 | 96 | # Max no of threads (if using a bounded task queue) is determined by 97 | # calculating: ceil(available processors * factor) 98 | max-pool-size-factor = 3.0 99 | 100 | # Max number of threads to cap factor-based max number to 101 | # (if using a bounded task queue) 102 | max-pool-size-max = 64 103 | 104 | # Specifies the bounded capacity of the task queue (< 1 == unbounded) 105 | task-queue-size = -1 106 | 107 | # Specifies which type of task queue will be used, can be "array" or 108 | # "linked" (default) 109 | task-queue-type = "linked" 110 | 111 | # Allow core threads to time out 112 | allow-core-timeout = on 113 | } 114 | 115 | # Throughput defines the maximum number of messages to be 116 | # processed per actor before the thread jumps to the next actor. 117 | # Set to 1 for as fair as possible. 118 | throughput = 1 119 | } 120 | --------------------------------------------------------------------------------