├── .gitignore ├── README.md ├── application ├── Dockerfile ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── sqshq │ │ │ └── robotsystem │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── Actor.java │ │ │ ├── Counters.java │ │ │ └── spring │ │ │ │ ├── SpringActorProducer.java │ │ │ │ ├── SpringExtension.java │ │ │ │ └── SpringProps.java │ │ │ ├── processor │ │ │ ├── ProcessorActor.java │ │ │ └── service │ │ │ │ ├── ProcessorService.java │ │ │ │ └── ProcessorServiceImpl.java │ │ │ ├── receiver │ │ │ ├── ReceiverActor.java │ │ │ └── ReceiverController.java │ │ │ └── transmitter │ │ │ ├── RobotActor.java │ │ │ └── WebsocketHandler.java │ └── resources │ │ ├── application.conf │ │ └── application.yml │ └── test │ ├── java │ └── .gitkeep │ └── resources │ └── .gitkeep ├── build.gradle ├── docker-compose.yml ├── load-test ├── build.gradle └── src │ └── gatling │ ├── resources │ ├── bodies │ │ └── .gitkeep │ ├── conf │ │ ├── gatling-akka.conf │ │ ├── gatling.conf │ │ ├── logback.xml │ │ └── recorder.conf │ └── data │ │ └── .gitkeep │ └── scala │ └── com │ └── sqshq │ └── robotsystem │ └── RobotSystemSimulation.scala └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle working directory 2 | .gradle 3 | 4 | # Idea module files, project folder 5 | *.iml 6 | .idea/ 7 | 8 | # Eclipse 9 | .classpath 10 | .project 11 | .settings/ 12 | 13 | # Build output 14 | build 15 | target 16 | logs 17 | ======= 18 | 19 | # Emacs files 20 | *~ 21 | \#* 22 | .\#* 23 | 24 | # Log files 25 | *.log 26 | *.log.gz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Robot Control System 2 | Akka Cluster using Java, Spring Boot and Docker. Demo application for [the conference talk](https://www.slideshare.net/AlexanderLukyanchiko/actorbased-concurrency-in-a-modern-java-enterprise). 3 | 4 | screen shot 2017-05-09 at 16 25 00 5 | -------------------------------------------------------------------------------- /application/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./build/libs/application.jar /app/ 5 | CMD ["java", "-Xmx128m", "-jar", "/app/application.jar"] 6 | 7 | EXPOSE 7000 -------------------------------------------------------------------------------- /application/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'org.springframework.boot' 3 | 4 | sourceCompatibility = 1.8 5 | 6 | buildscript { 7 | ext { 8 | versions = [boot: '1.5.1.RELEASE'] 9 | } 10 | repositories { 11 | mavenCentral() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${versions.boot}") 16 | } 17 | } 18 | 19 | repositories { 20 | mavenCentral() 21 | jcenter() 22 | } 23 | 24 | ext { 25 | libs = [ 26 | spring : ['org.springframework.boot:spring-boot-starter-web', 27 | 'org.springframework.boot:spring-boot-starter-websocket'], 28 | akka : ['com.typesafe.akka:akka-cluster_2.11:2.5.1', 29 | 'com.typesafe.akka:akka-cluster-tools_2.11:2.5.1', 30 | 'com.typesafe.akka:akka-distributed-data_2.11:2.5.1', 31 | 'com.typesafe.akka:akka-slf4j_2.11:2.5.1', 32 | 'com.typesafe.akka:akka-cluster-metrics_2.11:2.5.1'], 33 | constructr: ['de.heikoseeberger:constructr_2.11:0.16.1', 34 | 'de.heikoseeberger:constructr-coordination-etcd_2.11:0.16.1'], 35 | test : ['com.typesafe.akka:akka-testkit_2.11:2.5.1', 36 | 'junit:junit:4.12'] 37 | ] 38 | } 39 | 40 | dependencies { 41 | compile libs.spring 42 | compile libs.akka 43 | compile libs.constructr 44 | testCompile libs.test 45 | } 46 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/Application.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem; 2 | 3 | import akka.actor.ActorRef; 4 | import akka.actor.ActorSystem; 5 | import akka.cluster.Cluster; 6 | import akka.cluster.metrics.AdaptiveLoadBalancingGroup; 7 | import akka.cluster.metrics.CpuMetricsSelector; 8 | import akka.cluster.routing.ClusterRouterGroup; 9 | import akka.cluster.routing.ClusterRouterGroupSettings; 10 | import akka.routing.RoundRobinPool; 11 | import com.sqshq.robotsystem.config.spring.SpringExtension; 12 | import com.sqshq.robotsystem.config.spring.SpringProps; 13 | import com.sqshq.robotsystem.processor.ProcessorActor; 14 | import com.sqshq.robotsystem.transmitter.WebsocketHandler; 15 | import com.typesafe.config.ConfigFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.SpringApplication; 18 | import org.springframework.boot.autoconfigure.SpringBootApplication; 19 | import org.springframework.context.ApplicationContext; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.Profile; 22 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 23 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 24 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 25 | 26 | import java.util.List; 27 | 28 | import static java.util.Collections.singletonList; 29 | 30 | @SpringBootApplication 31 | public class Application { 32 | 33 | @Autowired 34 | private ActorSystem system; 35 | 36 | public static void main(String[] args) { 37 | SpringApplication.run(Application.class, args); 38 | } 39 | 40 | @Bean 41 | public ActorSystem actorSystem(ApplicationContext context) { 42 | 43 | ActorSystem system = ActorSystem.create("robotsystem", ConfigFactory.load()); 44 | SpringExtension.getInstance().get(system).initialize(context); 45 | 46 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 47 | Cluster cluster = Cluster.get(system); 48 | cluster.leave(cluster.selfAddress()); 49 | }) 50 | ); 51 | 52 | return system; 53 | } 54 | 55 | @Bean("clusterProcessorRouter") 56 | @Profile("receiver") 57 | public ActorRef clusterProcessorRouter() { 58 | List path = singletonList("/user/localProcessorRouter"); 59 | return system.actorOf(new ClusterRouterGroup(new AdaptiveLoadBalancingGroup(CpuMetricsSelector.getInstance(), path), 60 | new ClusterRouterGroupSettings(100, path, false, "processor")).props(), "clusterProcessorRouter"); 61 | } 62 | 63 | @Bean("localProcessorRouter") 64 | @Profile("processor") 65 | public ActorRef localProcessorRouter() { 66 | return system.actorOf(SpringProps.create(system, ProcessorActor.class) 67 | .withDispatcher("processor-dispatcher") 68 | .withRouter(new RoundRobinPool(10)), "localProcessorRouter"); 69 | } 70 | 71 | @EnableWebSocket 72 | @Profile("transmitter") 73 | public class WebSocketConfiguration implements WebSocketConfigurer { 74 | 75 | @Autowired 76 | private WebsocketHandler handler; 77 | 78 | @Override 79 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 80 | registry.addHandler(handler, "/robots/socket").setAllowedOrigins("*"); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/config/Actor.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.config; 2 | 3 | import org.springframework.context.annotation.Scope; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | @Target({ ElementType.TYPE }) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Documented 15 | @Component 16 | @Scope("prototype") 17 | public @interface Actor { 18 | } 19 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/config/Counters.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.config; 2 | 3 | public enum Counters { 4 | SUBSCRIBED_ROBOTS 5 | } 6 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/config/spring/SpringActorProducer.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.config.spring; 2 | 3 | import akka.actor.Actor; 4 | import akka.actor.IndirectActorProducer; 5 | import org.springframework.context.ApplicationContext; 6 | 7 | public class SpringActorProducer implements IndirectActorProducer { 8 | 9 | private final ApplicationContext applicationContext; 10 | private final Class actorBeanClass; 11 | private final Object[] parameters; 12 | 13 | public SpringActorProducer(ApplicationContext applicationContext, Class actorBeanClass, Object[] parameters) { 14 | this.applicationContext = applicationContext; 15 | this.actorBeanClass = actorBeanClass; 16 | this.parameters = parameters; 17 | } 18 | 19 | public SpringActorProducer(ApplicationContext applicationContext, Class actorBeanClass) { 20 | this.applicationContext = applicationContext; 21 | this.actorBeanClass = actorBeanClass; 22 | this.parameters = null; 23 | } 24 | 25 | @Override 26 | public Actor produce() { 27 | return applicationContext.getBean(actorBeanClass, parameters); 28 | } 29 | 30 | @Override 31 | public Class actorClass() { 32 | return actorBeanClass; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/config/spring/SpringExtension.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.config.spring; 2 | 3 | import akka.actor.AbstractExtensionId; 4 | import akka.actor.Actor; 5 | import akka.actor.ExtendedActorSystem; 6 | import akka.actor.Extension; 7 | import akka.actor.Props; 8 | import org.springframework.context.ApplicationContext; 9 | 10 | public class SpringExtension extends AbstractExtensionId { 11 | 12 | private static SpringExtension instance = new SpringExtension(); 13 | 14 | @Override 15 | public SpringExt createExtension(ExtendedActorSystem system) { 16 | return new SpringExt(); 17 | } 18 | 19 | public static SpringExtension getInstance() { 20 | return instance; 21 | } 22 | 23 | public static class SpringExt implements Extension { 24 | 25 | private static ApplicationContext applicationContext; 26 | 27 | public void initialize(ApplicationContext applicationContext) { 28 | SpringExt.applicationContext = applicationContext; 29 | } 30 | 31 | Props props(Class actorBeanClass) { 32 | return Props.create(SpringActorProducer.class, applicationContext, actorBeanClass); 33 | } 34 | 35 | Props props(Class actorBeanClass, Object... parameters) { 36 | return Props.create(SpringActorProducer.class, applicationContext, actorBeanClass, parameters); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/config/spring/SpringProps.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.config.spring; 2 | 3 | import akka.actor.Actor; 4 | import akka.actor.ActorSystem; 5 | import akka.actor.Props; 6 | 7 | public class SpringProps { 8 | 9 | public static Props create(ActorSystem system, Class actorBeanClass) { 10 | return SpringExtension.getInstance().get(system).props(actorBeanClass); 11 | } 12 | 13 | public static Props create(ActorSystem system, Class actorBeanClass, Object... parameters) { 14 | return SpringExtension.getInstance().get(system).props(actorBeanClass, parameters); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/processor/ProcessorActor.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.processor; 2 | 3 | import akka.actor.AbstractActor; 4 | import akka.actor.ActorRef; 5 | import akka.cluster.ddata.DistributedData; 6 | import akka.cluster.ddata.PNCounter; 7 | import akka.cluster.ddata.PNCounterKey; 8 | import akka.cluster.ddata.Replicator; 9 | import akka.cluster.pubsub.DistributedPubSub; 10 | import akka.cluster.pubsub.DistributedPubSubMediator; 11 | import com.sqshq.robotsystem.config.Actor; 12 | import com.sqshq.robotsystem.config.Counters; 13 | import com.sqshq.robotsystem.processor.service.ProcessorService; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | 18 | import java.math.BigInteger; 19 | import java.util.Random; 20 | 21 | @Actor 22 | public class ProcessorActor extends AbstractActor { 23 | 24 | private final Logger log = LoggerFactory.getLogger(getClass()); 25 | private final ActorRef mediator = DistributedPubSub.get(getContext().getSystem()).mediator(); 26 | private final Random random = new Random(); 27 | private BigInteger robotsCounter = BigInteger.ZERO; 28 | 29 | @Autowired 30 | private ProcessorService processorService; 31 | 32 | public ProcessorActor() { 33 | DistributedData.get(getContext().getSystem()).replicator() 34 | .tell(new Replicator.Subscribe<>(PNCounterKey.create(Counters.SUBSCRIBED_ROBOTS.name()), getSelf()), self()); 35 | } 36 | 37 | @Override 38 | @SuppressWarnings("unchecked") 39 | public Receive createReceive() { 40 | return receiveBuilder() 41 | .match(Integer.class, this::process) 42 | .match(Replicator.Changed.class, a -> a.key().id().equals(Counters.SUBSCRIBED_ROBOTS.name()), 43 | change -> robotsCounter = ((Replicator.Changed) change).dataValue().getValue()) 44 | .matchAny(this::unhandled) 45 | .build(); 46 | } 47 | 48 | private void process(Integer sensorData) { 49 | log.info("processor working on data: {}", sensorData); 50 | 51 | int computedValue = processorService.compute(sensorData); 52 | int targetRobot = random.nextInt(robotsCounter.intValue()) + 1; 53 | 54 | mediator.tell(new DistributedPubSubMediator.Publish(String.valueOf(targetRobot), computedValue), self()); 55 | sender().tell("The task sent to robot #" + targetRobot, self()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/processor/service/ProcessorService.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.processor.service; 2 | 3 | public interface ProcessorService { 4 | 5 | int compute(int n); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/processor/service/ProcessorServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.processor.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | /** 6 | * Stateless service which calculates dummy CPU-intensive task 7 | */ 8 | @Service 9 | public class ProcessorServiceImpl implements ProcessorService { 10 | 11 | @Override 12 | public int compute(int n) { 13 | return nthPrime(n); 14 | } 15 | 16 | private int nthPrime(int n) { 17 | int candidate, count; 18 | for (candidate = 2, count = 0; count < n; ++candidate) { 19 | if (isPrime(candidate)) { 20 | ++count; 21 | } 22 | } 23 | return candidate - 1; 24 | } 25 | 26 | private boolean isPrime(int n) { 27 | for (int i = 2; i < n; ++i) { 28 | if (n % i == 0) { 29 | return false; 30 | } 31 | } 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/receiver/ReceiverActor.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.receiver; 2 | 3 | import akka.actor.AbstractActor; 4 | import akka.actor.ActorRef; 5 | import com.sqshq.robotsystem.config.Actor; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.web.context.request.async.DeferredResult; 11 | 12 | @Actor 13 | public class ReceiverActor extends AbstractActor { 14 | 15 | private final Logger log = LoggerFactory.getLogger(getClass()); 16 | private final DeferredResult deferredResult; 17 | 18 | @Autowired 19 | @Qualifier("clusterProcessorRouter") 20 | private ActorRef router; 21 | 22 | public ReceiverActor(DeferredResult deferredResult) { 23 | this.deferredResult = deferredResult; 24 | } 25 | 26 | @Override 27 | public Receive createReceive() { 28 | return receiveBuilder() 29 | .match(Integer.class, this::dispatch) 30 | .match(String.class, this::complete) 31 | .matchAny(this::unhandled) 32 | .build(); 33 | } 34 | 35 | private void dispatch(Integer data) { 36 | log.info("receiver dispatching the data: {}", data); 37 | router.tell(data, self()); 38 | } 39 | 40 | private void complete(String result) { 41 | log.info("receiver responding to sender: {}", result); 42 | deferredResult.setResult(result); 43 | getContext().stop(self()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/receiver/ReceiverController.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.receiver; 2 | 3 | import akka.actor.ActorRef; 4 | import akka.actor.ActorSystem; 5 | import com.sqshq.robotsystem.config.spring.SpringProps; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.context.request.async.DeferredResult; 13 | 14 | @RestController 15 | @Profile("receiver") 16 | public class ReceiverController { 17 | 18 | @Autowired 19 | private ActorSystem system; 20 | 21 | @RequestMapping(value = "/sensors/data", method = RequestMethod.POST) 22 | private DeferredResult receiveSensorsData(@RequestBody String data) { 23 | DeferredResult result = new DeferredResult<>(); 24 | system.actorOf(SpringProps.create(system, ReceiverActor.class, result)) 25 | .tell(Integer.valueOf(data), ActorRef.noSender()); 26 | return result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/transmitter/RobotActor.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.transmitter; 2 | 3 | import akka.actor.AbstractActor; 4 | import akka.cluster.pubsub.DistributedPubSub; 5 | import akka.cluster.pubsub.DistributedPubSubMediator; 6 | import com.sqshq.robotsystem.config.Actor; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.web.socket.TextMessage; 10 | import org.springframework.web.socket.WebSocketSession; 11 | 12 | import java.io.IOException; 13 | 14 | @Actor 15 | public class RobotActor extends AbstractActor { 16 | 17 | private final Logger log = LoggerFactory.getLogger(getClass()); 18 | private final WebSocketSession session; 19 | private final Integer robotId; 20 | 21 | public RobotActor(WebSocketSession session, Integer robotId) { 22 | this.session = session; 23 | this.robotId = robotId; 24 | DistributedPubSub.get(getContext().system()) 25 | .mediator() 26 | .tell(new DistributedPubSubMediator.Subscribe( 27 | robotId.toString(), getSelf()), getSelf()); 28 | } 29 | 30 | @Override 31 | public Receive createReceive() { 32 | return receiveBuilder() 33 | .match(TextMessage.class, this::processMessageFromRobot) 34 | .match(Integer.class, this::sendDataToRobot) 35 | .match(DistributedPubSubMediator.SubscribeAck.class, this::processSubscription) 36 | .matchAny(this::unhandled) 37 | .build(); 38 | } 39 | 40 | private void processMessageFromRobot(TextMessage message) { 41 | log.info("received message from robot {}: {}", robotId, message); 42 | } 43 | 44 | private void sendDataToRobot(Integer data) throws IOException { 45 | log.info("sending message to robot {}: {}", robotId, data); 46 | session.sendMessage(new TextMessage(data.toString())); 47 | } 48 | 49 | private void processSubscription(DistributedPubSubMediator.SubscribeAck ack) { 50 | log.info("robot #{} sucessfully subscribed", robotId); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /application/src/main/java/com/sqshq/robotsystem/transmitter/WebsocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem.transmitter; 2 | 3 | import akka.actor.ActorRef; 4 | import akka.actor.ActorSystem; 5 | import akka.actor.PoisonPill; 6 | import akka.cluster.Cluster; 7 | import akka.cluster.ddata.DistributedData; 8 | import akka.cluster.ddata.Key; 9 | import akka.cluster.ddata.PNCounter; 10 | import akka.cluster.ddata.PNCounterKey; 11 | import akka.cluster.ddata.Replicator; 12 | import com.sqshq.robotsystem.config.Counters; 13 | import com.sqshq.robotsystem.config.spring.SpringProps; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.web.socket.CloseStatus; 18 | import org.springframework.web.socket.TextMessage; 19 | import org.springframework.web.socket.WebSocketSession; 20 | import org.springframework.web.socket.handler.TextWebSocketHandler; 21 | 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | 24 | @Component 25 | public class WebsocketHandler extends TextWebSocketHandler { 26 | 27 | private final Logger log = LoggerFactory.getLogger(getClass()); 28 | private final Key clusterRobotsCounter = PNCounterKey.create(Counters.SUBSCRIBED_ROBOTS.name()); 29 | private final AtomicInteger localRobotsCounter = new AtomicInteger(); 30 | private final ActorRef replicator; 31 | private final ActorSystem system; 32 | 33 | public WebsocketHandler(ActorSystem system) { 34 | this.system = system; 35 | this.replicator = DistributedData.get(system).replicator(); 36 | } 37 | 38 | @Override 39 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 40 | log.debug("new robot connected via websocket: {}", session.getId()); 41 | replicator.tell(new Replicator.Update<>(clusterRobotsCounter, PNCounter.create(), 42 | Replicator.writeLocal(), counter -> counter.increment(Cluster.get(system), 1)), ActorRef.noSender()); 43 | session.getAttributes().put("actor", system.actorOf( 44 | SpringProps.create(system, RobotActor.class, session, localRobotsCounter.incrementAndGet()))); 45 | } 46 | 47 | @Override 48 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 49 | ActorRef actor = (ActorRef) session.getAttributes().get("actor"); 50 | actor.tell(PoisonPill.getInstance(), ActorRef.noSender()); 51 | replicator.tell(new Replicator.Update<>(clusterRobotsCounter, PNCounter.create(), 52 | Replicator.writeLocal(), counter -> counter.decrement(Cluster.get(system), 1)), ActorRef.noSender()); 53 | localRobotsCounter.decrementAndGet(); 54 | } 55 | 56 | @Override 57 | protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 58 | log.debug("robot {} has been disconnected from websocket", session.getId()); 59 | ActorRef actor = (ActorRef) session.getAttributes().get("actor"); 60 | actor.tell(message, ActorRef.noSender()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /application/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | 3 | loggers = ["akka.event.slf4j.Slf4jLogger"] 4 | log-dead-letters-during-shutdown = off 5 | loglevel = "DEBUG" 6 | 7 | extensions = ["de.heikoseeberger.constructr.ConstructrExtension", "akka.cluster.metrics.ClusterMetricsExtension"] 8 | 9 | actor { 10 | provider = "cluster" 11 | } 12 | 13 | remote { 14 | netty.tcp { 15 | bind-hostname = 0.0.0.0 16 | } 17 | } 18 | 19 | cluster { 20 | metrics.enabled = off 21 | roles = [${?SPRING_PROFILES_ACTIVE}] 22 | } 23 | } 24 | 25 | processor-dispatcher { 26 | type = Dispatcher 27 | executor = "fork-join-executor" 28 | fork-join-executor { 29 | parallelism-max = 2 30 | } 31 | } 32 | 33 | constructr { 34 | coordination { 35 | host = etcd 36 | } 37 | } -------------------------------------------------------------------------------- /application/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | output: 3 | ansi: 4 | enabled: always 5 | main: 6 | web-environment: ${WEB_ENVIRONMENT} 7 | logging: 8 | level: 9 | ROOT: warn 10 | akka.cluster: info 11 | com.sqshq: info -------------------------------------------------------------------------------- /application/src/test/java/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/robot-control-system/4e9281f17c95db78b35d9285f7439ac40465c110/application/src/test/java/.gitkeep -------------------------------------------------------------------------------- /application/src/test/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/robot-control-system/4e9281f17c95db78b35d9285f7439ac40465c110/application/src/test/resources/.gitkeep -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.sqshq' 2 | version = '1.0.0' 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | etcd: 5 | image: quay.io/coreos/etcd 6 | ports: 7 | - 2379:2379 8 | environment: 9 | ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379 10 | ETCD_ADVERTISE_CLIENT_URLS: http://localhost:2379 11 | 12 | receiver: 13 | build: application 14 | depends_on: 15 | - etcd 16 | ports: 17 | - 10000:8080 18 | environment: 19 | SPRING_PROFILES_ACTIVE: receiver 20 | WEB_ENVIRONMENT: 'true' 21 | 22 | processor: 23 | build: application 24 | depends_on: 25 | - etcd 26 | environment: 27 | SPRING_PROFILES_ACTIVE: processor 28 | WEB_ENVIRONMENT: 'false' 29 | 30 | transmitter: 31 | build: application 32 | depends_on: 33 | - etcd 34 | environment: 35 | SPRING_PROFILES_ACTIVE: transmitter 36 | WEB_ENVIRONMENT: 'true' 37 | ports: 38 | - 20000:8080 -------------------------------------------------------------------------------- /load-test/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.github.lkishalmi.gatling" version "0.4.1" 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | compile 'org.scala-lang:scala-library:2.12.1' 11 | } 12 | 13 | gatling { 14 | simulations = { 15 | include "**/*Simulation*" 16 | } 17 | toolVersion = "2.2.4" 18 | logLevel = "INFO" 19 | } -------------------------------------------------------------------------------- /load-test/src/gatling/resources/bodies/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/robot-control-system/4e9281f17c95db78b35d9285f7439ac40465c110/load-test/src/gatling/resources/bodies/.gitkeep -------------------------------------------------------------------------------- /load-test/src/gatling/resources/conf/gatling-akka.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | #loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | #logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 4 | #log-dead-letters = off 5 | actor { 6 | default-dispatcher { 7 | #throughput = 20 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /load-test/src/gatling/resources/conf/gatling.conf: -------------------------------------------------------------------------------- 1 | ######################### 2 | # Gatling Configuration # 3 | ######################### 4 | 5 | # This file contains all the settings configurable for Gatling with their default values 6 | 7 | gatling { 8 | core { 9 | #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp) 10 | #runDescription = "" # The description for this simulation run, displayed in each report 11 | #encoding = "utf-8" # Encoding to use throughout Gatling for file and string manipulation 12 | #simulationClass = "" # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated) 13 | #mute = false # When set to true, don't ask for simulation name nor run description (currently only used by Gatling SBT plugin) 14 | #elFileBodiesCacheMaxCapacity = 200 # Cache size for request body EL templates, set to 0 to disable 15 | #rawFileBodiesCacheMaxCapacity = 200 # Cache size for request body Raw templates, set to 0 to disable 16 | #rawFileBodiesInMemoryMaxSize = 1000 # Below this limit, raw file bodies will be cached in memory 17 | 18 | extract { 19 | regex { 20 | #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching 21 | } 22 | xpath { 23 | #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries, set to 0 to disable caching 24 | } 25 | jsonPath { 26 | #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching 27 | #preferJackson = false # When set to true, prefer Jackson over Boon for JSON-related operations 28 | } 29 | css { 30 | #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries, set to 0 to disable caching 31 | } 32 | } 33 | 34 | directory { 35 | #data = user-files/data # Folder where user's data (e.g. files used by Feeders) is located 36 | #bodies = user-files/bodies # Folder where bodies are located 37 | #simulations = user-files/simulations # Folder where the bundle's simulations are located 38 | #reportsOnly = "" # If set, name of report folder to look for in order to generate its report 39 | #binaries = "" # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target. 40 | #results = results # Name of the folder where all reports folder are located 41 | } 42 | } 43 | charting { 44 | #noReports = false # When set to true, don't generate HTML reports 45 | #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports 46 | #useGroupDurationMetric = false # Switch group timings from cumulated response time to group duration. 47 | indicators { 48 | #lowerBound = 800 # Lower bound for the requests' response time to track in the reports and the console summary 49 | #higherBound = 1200 # Higher bound for the requests' response time to track in the reports and the console summary 50 | #percentile1 = 50 # Value for the 1st percentile to track in the reports, the console summary and Graphite 51 | #percentile2 = 75 # Value for the 2nd percentile to track in the reports, the console summary and Graphite 52 | #percentile3 = 95 # Value for the 3rd percentile to track in the reports, the console summary and Graphite 53 | #percentile4 = 99 # Value for the 4th percentile to track in the reports, the console summary and Graphite 54 | } 55 | } 56 | http { 57 | #fetchedCssCacheMaxCapacity = 200 # Cache size for CSS parsed content, set to 0 to disable 58 | #fetchedHtmlCacheMaxCapacity = 200 # Cache size for HTML parsed content, set to 0 to disable 59 | #perUserCacheMaxCapacity = 200 # Per virtual user cache size, set to 0 to disable 60 | #warmUpUrl = "http://gatling.io" # The URL to use to warm-up the HTTP stack (blank means disabled) 61 | #enableGA = true # Very light Google Analytics, please support 62 | ssl { 63 | keyStore { 64 | #type = "" # Type of SSLContext's KeyManagers store 65 | #file = "" # Location of SSLContext's KeyManagers store 66 | #password = "" # Password for SSLContext's KeyManagers store 67 | #algorithm = "" # Algorithm used SSLContext's KeyManagers store 68 | } 69 | trustStore { 70 | #type = "" # Type of SSLContext's TrustManagers store 71 | #file = "" # Location of SSLContext's TrustManagers store 72 | #password = "" # Password for SSLContext's TrustManagers store 73 | #algorithm = "" # Algorithm used by SSLContext's TrustManagers store 74 | } 75 | } 76 | ahc { 77 | #keepAlive = true # Allow pooling HTTP connections (keep-alive header automatically added) 78 | #connectTimeout = 10000 # Timeout when establishing a connection 79 | #handshakeTimeout = 10000 # Timeout when performing TLS hashshake 80 | #pooledConnectionIdleTimeout = 60000 # Timeout when a connection stays unused in the pool 81 | #readTimeout = 60000 # Timeout when a used connection stays idle 82 | #maxRetry = 2 # Number of times that a request should be tried again 83 | #requestTimeout = 60000 # Timeout of the requests 84 | #acceptAnyCertificate = true # When set to true, doesn't validate SSL certificates 85 | #httpClientCodecMaxInitialLineLength = 4096 # Maximum length of the initial line of the response (e.g. "HTTP/1.0 200 OK") 86 | #httpClientCodecMaxHeaderSize = 8192 # Maximum size, in bytes, of each request's headers 87 | #httpClientCodecMaxChunkSize = 8192 # Maximum length of the content or each chunk 88 | #webSocketMaxFrameSize = 10240000 # Maximum frame payload size 89 | #sslEnabledProtocols = [TLSv1.2, TLSv1.1, TLSv1] # Array of enabled protocols for HTTPS, if empty use the JDK defaults 90 | #sslEnabledCipherSuites = [] # Array of enabled cipher suites for HTTPS, if empty use the AHC defaults 91 | #sslSessionCacheSize = 0 # SSLSession cache size, set to 0 to use JDK's default 92 | #sslSessionTimeout = 0 # SSLSession timeout in seconds, set to 0 to use JDK's default (24h) 93 | #useOpenSsl = false # if OpenSSL should be used instead of JSSE (requires tcnative jar) 94 | #useNativeTransport = false # if native transport should be used instead of Java NIO (requires netty-transport-native-epoll, currently Linux only) 95 | #tcpNoDelay = true 96 | #soReuseAddress = false 97 | #soLinger = -1 98 | #soSndBuf = -1 99 | #soRcvBuf = -1 100 | #allocator = "pooled" # switch to unpooled for unpooled ByteBufAllocator 101 | #maxThreadLocalCharBufferSize = 200000 # Netty's default is 16k 102 | } 103 | dns { 104 | #queryTimeout = 5000 # Timeout of each DNS query in millis 105 | #maxQueriesPerResolve = 6 # Maximum allowed number of DNS queries for a given name resolution 106 | } 107 | } 108 | jms { 109 | #acknowledgedMessagesBufferSize = 5000 # size of the buffer used to tracked acknowledged messages and protect against duplicate receives 110 | } 111 | data { 112 | #writers = [console, file] # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite, jdbc) 113 | console { 114 | #light = false # When set to true, displays a light version without detailed request stats 115 | } 116 | file { 117 | #bufferSize = 8192 # FileDataWriter's internal data buffer size, in bytes 118 | } 119 | leak { 120 | #noActivityTimeout = 30 # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening 121 | } 122 | graphite { 123 | #light = false # only send the all* stats 124 | #host = "localhost" # The host where the Carbon server is located 125 | #port = 2003 # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle) 126 | #protocol = "tcp" # The protocol used to send data to Carbon (currently supported : "tcp", "udp") 127 | #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite 128 | #bufferSize = 8192 # GraphiteDataWriter's internal data buffer size, in bytes 129 | #writeInterval = 1 # GraphiteDataWriter's write interval, in seconds 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /load-test/src/gatling/resources/conf/logback.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n%rEx 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /load-test/src/gatling/resources/conf/recorder.conf: -------------------------------------------------------------------------------- 1 | recorder { 2 | core { 3 | #mode = "Proxy" 4 | #encoding = "utf-8" # The encoding used for reading/writing request bodies and the generated simulation 5 | #outputFolder = "" # The folder where generated simulation will we written 6 | #package = "" # The package's name of the generated simulation 7 | #className = "RecordedSimulation" # The name of the generated Simulation class 8 | #thresholdForPauseCreation = 100 # The minimum time, in milliseconds, that must pass between requests to trigger a pause creation 9 | #saveConfig = false # When set to true, the configuration from the Recorder GUI overwrites this configuration 10 | #headless = false # When set to true, run the Recorder in headless mode instead of the GUI 11 | #harFilePath = "" # The path of the HAR file to convert 12 | } 13 | filters { 14 | #filterStrategy = "Disabled" # The selected filter resources filter strategy (currently supported : "Disabled", "BlackList", "WhiteList") 15 | #whitelist = [] # The list of ressources patterns that are part of the Recorder's whitelist 16 | #blacklist = [] # The list of ressources patterns that are part of the Recorder's blacklist 17 | } 18 | http { 19 | #automaticReferer = true # When set to false, write the referer + enable 'disableAutoReferer' in the generated simulation 20 | #followRedirect = true # When set to false, write redirect requests + enable 'disableFollowRedirect' in the generated simulation 21 | #removeCacheHeaders = true # When set to true, removes from the generated requests headers leading to request caching 22 | #inferHtmlResources = true # When set to true, add inferred resources + set 'inferHtmlResources' with the configured blacklist/whitelist in the generated simulation 23 | #checkResponseBodies = false # When set to true, save response bodies as files and add raw checks in the generated simulation 24 | } 25 | proxy { 26 | #port = 8000 # Local port used by Gatling's Proxy for HTTP/HTTPS 27 | https { 28 | #mode = "SelfSignedCertificate" # The selected "HTTPS mode" (currently supported : "SelfSignedCertificate", "ProvidedKeyStore", "GatlingCertificateAuthority", "CustomCertificateAuthority") 29 | keyStore { 30 | #path = "" # The path of the custom key store 31 | #password = "" # The password for this key store 32 | #type = "JKS" # The type of the key store (currently supported: "JKS") 33 | } 34 | certificateAuthority { 35 | #certificatePath = "" # The path of the custom certificate 36 | #privateKeyPath = "" # The certificate's private key path 37 | } 38 | } 39 | outgoing { 40 | #host = "" # The outgoing proxy's hostname 41 | #username = "" # The username to use to connect to the outgoing proxy 42 | #password = "" # The password corresponding to the user to use to connect to the outgoing proxy 43 | #port = 0 # The HTTP port to use to connect to the outgoing proxy 44 | #sslPort = 0 # If set, The HTTPS port to use to connect to the outgoing proxy 45 | } 46 | } 47 | netty { 48 | #maxInitialLineLength = 10000 # Maximum length of the initial line of the response (e.g. "HTTP/1.0 200 OK") 49 | #maxHeaderSize = 20000 # Maximum size, in bytes, of each request's headers 50 | #maxChunkSize = 8192 # Maximum length of the content or each chunk 51 | #maxContentLength = 100000000 # Maximum length of the aggregated content of each response 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /load-test/src/gatling/resources/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/robot-control-system/4e9281f17c95db78b35d9285f7439ac40465c110/load-test/src/gatling/resources/data/.gitkeep -------------------------------------------------------------------------------- /load-test/src/gatling/scala/com/sqshq/robotsystem/RobotSystemSimulation.scala: -------------------------------------------------------------------------------- 1 | package com.sqshq.robotsystem 2 | 3 | import io.gatling.core.Predef._ 4 | import io.gatling.core.scenario.Simulation 5 | import io.gatling.core.structure.ScenarioBuilder 6 | import io.gatling.http.Predef.{http, status, ws} 7 | 8 | import scala.concurrent.duration._ 9 | import scala.util.Random 10 | 11 | class RobotSystemSimulation extends Simulation { 12 | 13 | val robotScenario: ScenarioBuilder = scenario("Robot behavior scenario") 14 | .exec(ws("Open WS").open("ws://localhost:20000/robots/socket")) 15 | .pause(30 seconds) 16 | .exec(ws("Close WS").close) 17 | 18 | val sensorDataScenario: ScenarioBuilder = scenario("Offer message scenario") 19 | .during(30 seconds) { 20 | feed(Feeder.sensorData) 21 | .exec(http("Sensor data request") 22 | .post("http://localhost:10000/sensors/data") 23 | .body(StringBody("${sensorData}")) 24 | .check(status.is(200))) 25 | } 26 | 27 | setUp( 28 | robotScenario 29 | .inject(rampUsers(100).over(5 seconds)) 30 | .protocols(http), 31 | sensorDataScenario 32 | .inject(atOnceUsers(300)) 33 | .throttle(reachRps(300).in(5 seconds), holdFor(30 seconds)) 34 | .protocols(http) 35 | ) 36 | } 37 | 38 | object Feeder { 39 | val sensorData = new Feeder[Int] { 40 | override def hasNext = true 41 | override def next: Map[String, Int] = { 42 | Map("sensorData" -> (Random.nextInt(1000) + 1000)) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'application' 2 | include 'load-test' --------------------------------------------------------------------------------