├── .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 |
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 extends Actor> actorBeanClass;
11 | private final Object[] parameters;
12 |
13 | public SpringActorProducer(ApplicationContext applicationContext, Class extends Actor> actorBeanClass, Object[] parameters) {
14 | this.applicationContext = applicationContext;
15 | this.actorBeanClass = actorBeanClass;
16 | this.parameters = parameters;
17 | }
18 |
19 | public SpringActorProducer(ApplicationContext applicationContext, Class extends Actor> 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 extends Actor> 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 extends Actor> actorBeanClass) {
32 | return Props.create(SpringActorProducer.class, applicationContext, actorBeanClass);
33 | }
34 |
35 | Props props(Class extends Actor> 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 extends akka.actor.Actor> actorBeanClass) {
10 | return SpringExtension.getInstance().get(system).props(actorBeanClass);
11 | }
12 |
13 | public static Props create(ActorSystem system, Class extends Actor> 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'
--------------------------------------------------------------------------------