├── .gitignore ├── README.md ├── build.sbt ├── docker-compose.yml ├── executor ├── drf-executor.sh ├── invocation-examples.sh └── throttle-executor.sh ├── images └── microservice │ └── Dockerfile ├── project ├── build.properties └── plugins.sbt └── src └── main ├── resources └── logback.xml └── scala └── io └── datastrophic ├── common └── CassandraUtil.scala ├── mesos ├── BaseMesosExecutor.scala ├── BaseMesosScheduler.scala ├── BinarySerDe.scala ├── TaskBuilder.scala ├── drf │ ├── DRFDemoExecutor.scala │ ├── DRFDemoFramework.scala │ └── DRFDemoScheduler.scala └── throttler │ ├── CassandraTaskBuilder.scala │ ├── ThrottleExecutor.scala │ ├── ThrottleScheduler.scala │ └── Throttler.scala └── microservice ├── AkkaMicroservice.scala └── Model.scala /.gitignore: -------------------------------------------------------------------------------- 1 | /RUNNING_PID 2 | /logs/ 3 | /project/*-shim.sbt 4 | /project/project/ 5 | /project/target/ 6 | /target/ 7 | 8 | *.log 9 | .idea 10 | *.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This project is dedicated to provide a set of examples to illustrate main Mesos use cases applied to architectures based on SMACK stack. 4 | What is covered: 5 | 6 | * How to run dockerized Chronos via Marathon 7 | * How to build and submit microservices in Docker containers via Marathon 8 | * Different modes of running Spark jobs on cluster (fine-grained and coarse-crained) 9 | * Running scheduled Spark jobs in Chronos 10 | * Example implementation of Mesos Framework aimed to show how to build basic schedulers 11 | 12 | ####Environment description: 13 | The whole environment is represented by set of Docker containers orchestrated via `docker-compose`: 14 | 15 | - ZooKeeper 16 | - Mesos Master 17 | - Mesos Agent 18 | - Marathon 19 | - Cassandra 20 | 21 | To attach to running container (consider as ssh-ing to the host) container name or id is needed and could be found via 22 | 23 | docker-compose ps 24 | or 25 | 26 | docker ps 27 | and then 28 | 29 | docker exec -ti bash 30 | For example to connect to Mesos Slave: 31 | 32 | docker exec -ti mesosworkshop_mesos-slave_1 bash 33 | 34 | ####Prerequisites 35 | 36 | Docker and docker-compose are used for running code samples: 37 | 38 | docker version 1.10 39 | docker-compose 1.6.0 40 | 41 | For building the app, SBT is used 42 | 43 | SBT 0.13 44 | 45 | The application was created with Typesafe Activator 46 | 47 | ####Environment setup 48 | It's possible to create a full Mesos environment on the local machine using docker-compose. 49 | 50 | Depending on one's needs virtual machine memory could be adjusted to different value, but memory should be gte 4GB. Steps to create new 51 | docker-machine and launch docker images: 52 | 53 | docker-machine create -d virtualbox --virtualbox-memory "8000" --virtualbox-cpu-count "6" mesos 54 | eval "$(docker-machine env mesos)" 55 | 56 | (!) Add address of `docker-machine ip mesos` to /etc/hosts with next hostnames: 57 | mesos mesos-master mesos-slave zookeeper marathon chronos 58 | 59 | docker-compose up 60 | 61 | After that Mesos Master, Mesos Slave and Marathon should be available: 62 | 63 | * Mesos Master [http://mesos-master:5050](http://mesos-master:5050) 64 | * Marathon [http://marathon:8080](http://marathon:8080) 65 | 66 | ## Submitting docker images to Marathon 67 | To submit a container to Mesos via Marathon send POST request to `http://marathon:8080/v2/apps` 68 | ([swagger ui available](http://marathon:8080/help)). Let's launch dockerized Chronos via Marathon. Because of local environment 69 | specifics hostname resolution and network discovery suffers when running docker-in-docker, so ip address in `CHRONOS_MASTER` and `CHRONOS_ZK_HOSTS` should point 70 | to `docker-machine ip mesos`. 71 | 72 | curl -XPOST 'http://marathon:8080/v2/apps' -H 'Content-Type: application/json' -d '{ 73 | "id": "chronos", 74 | "container": { 75 | "type": "DOCKER", 76 | "docker": { 77 | "network": "HOST", 78 | "image": "datastrophic/chronos:mesos-0.28.1-chronos-2.5", 79 | "parameters": [ 80 | { "key": "env", "value": "CHRONOS_HTTP_PORT=4400" }, 81 | { "key": "env", "value": "LIBPROCESS_IP='"$(docker-machine ip mesos)"'" }, 82 | { "key": "env", "value": "CHRONOS_MASTER=zk://'"$(docker-machine ip mesos)"':2181/mesos" }, 83 | { "key": "env", "value": "CHRONOS_ZK_HOSTS='"$(docker-machine ip mesos)"':2181"} 84 | ] 85 | } 86 | }, 87 | "ports": [ 88 | 4400 89 | ], 90 | "cpus": 1, 91 | "mem": 512, 92 | "instances": 1 93 | }' 94 | 95 | After that Chronos application should appear in Marathon UI and [Chronos UI](http://mesos:4400/) should be available on it's own and visible in 96 | Mesos Master's UI. 97 | 98 | ## Running dockerized services 99 | This project comes with simple [Akka microservice](src/main/scala/akka/microservice/AkkaMicroservice) that accepts events, stores them in Cassandra and 100 | allows to count totals based on campaign id and event type. Event model: 101 | 102 | case class Event(id: String, campaignId: String, eventType: String, value: Long, timestamp: String) 103 | 104 | We're going to launch this microservice on Mesos in Docker container and scale number of its instances up and down with Marathon. 105 | First the Docker image with the latest service binary should be built and deployed to Mesos. From the root of the project: 106 | 107 | sbt clean assembly distribute 108 | 109 | docker build -t datastrophic/akka-microservice:latest images/microservice 110 | 111 | curl -XPOST 'http://marathon:8080/v2/apps' -H 'Content-Type: application/json' -d '{ 112 | "id": "akka-microservice", 113 | "container": { 114 | "type": "DOCKER", 115 | "docker": { 116 | "image": "datastrophic/akka-microservice:latest", 117 | "network": "BRIDGE", 118 | "portMappings": [{"containerPort": 31337, "servicePort": 31337}], 119 | "parameters": [ 120 | { "key": "env", "value": "CASSANDRA_HOST='"$(docker-machine ip mesos)"'" }, 121 | { "key": "env", "value": "CASSANDRA_KEYSPACE=demo" }, 122 | { "key": "env", "value": "CASSANDRA_TABLE=event"} 123 | ] 124 | } 125 | }, 126 | "cpus": 1, 127 | "mem": 512, 128 | "instances": 1 129 | }' 130 | 131 | Via Marathon UI identify the host and port of the service (e.g. mesos-slave:31158) and perform some queries to post and read the data: 132 | 133 | curl -XPOST 'http://mesos-slave:31711/event' -H 'Content-Type: application/json' -d '{ 134 | "id": "2e272715-c267-4c6b-8ab7-c9f96c5ab15a", 135 | "campaignId": "275ef4a2-513e-43e2-b85a-e656737c1147", 136 | "eventType": "impression", 137 | "value": 42, 138 | "timestamp": "2016-03-15 12:15:39" 139 | }' 140 | 141 | curl -XPOST 'http://mesos-slave:31711/event' -H 'Content-Type: application/json' -d '{ 142 | "id": "2e272715-c267-4c6b-8ab7-c9f96c5ab15a", 143 | "campaignId": "275ef4a2-513e-43e2-b85a-e656737c1147", 144 | "eventType": "click", 145 | "value": 13, 146 | "timestamp": "2016-03-15 12:15:39" 147 | }' 148 | 149 | 150 | curl -XGET http://mesos-slave:31711/campaign/275ef4a2-513e-43e2-b85a-e656737c1147/totals/impression 151 | 152 | curl -XGET http://mesos-slave:31711/campaign/275ef4a2-513e-43e2-b85a-e656737c1147/totals/click 153 | 154 | You can try to scale up and down the number of instances of the service and query different ones to verify that everything is working. 155 | 156 | ## Running Spark applications 157 | 158 | ###Running from cluster nodes 159 | For running Spark jobs attaching one of the slave nodes is needed to run `spark-shell` and `spark-submit`: 160 | 161 | docker exec -ti mesosworkshop_mesos-slave_1 bash 162 | 163 | Coarse-grained mode (default) (one executor per host, amount of Mesos tasks = amount of Spark executors = number of physical nodes, 164 | spark-submit registered as a framework) 165 | 166 | In addition, for coarse-grained mode, you can control the maximum number of resources Spark will acquire. 167 | By default, it will acquire all cores in the cluster (that get offered by Mesos), which only makes sense if you 168 | run just one application at a time. You can cap the maximum number of cores using conf.set("spark.cores.max", "10") (for example). 169 | 170 | /opt/spark/bin/spark-submit \ 171 | --class org.apache.spark.examples.SparkPi \ 172 | --master mesos://zk://zookeeper:2181/mesos \ 173 | --deploy-mode client \ 174 | --total-executor-cores 2 \ 175 | /opt/spark/lib/spark-examples-1.6.0-hadoop2.6.0.jar \ 176 | 250 177 | 178 | 179 | In “fine-grained” mode, each Spark task runs as a separate Mesos task. This allows multiple instances of Spark (and other frameworks) 180 | to share machines at a very fine granularity, where each application gets more or fewer machines as it ramps up and down, but it 181 | comes with an additional overhead in launching each task. This mode may be inappropriate for low-latency requirements like 182 | interactive queries or serving web requests. 183 | 184 | /opt/spark/bin/spark-submit \ 185 | --class org.apache.spark.examples.SparkPi \ 186 | --master mesos://zk://zookeeper:2181/mesos \ 187 | --deploy-mode client \ 188 | --conf "spark.mesos.coarse=false"\ 189 | --total-executor-cores 2 \ 190 | /opt/spark/lib/spark-examples-1.6.0-hadoop2.6.0.jar \ 191 | 250 192 | 193 | 194 | ### Submitting Spark jobs via Marathon and Chronos 195 | 196 | ####Marathon 197 | Marathon is designed for keeping long-running apps alive, so in context of Spark execution long-running Spark Streaming jobs 198 | are the best candidates to run via Marathon. 199 | 200 | curl -XPOST 'http://marathon:8080/v2/apps' -H 'Content-Type: application/json' -d '{ 201 | "cmd": "/opt/spark/bin/spark-submit --class org.apache.spark.examples.SparkPi --master mesos://zk://zookeeper:2181/mesos --deploy-mode client --total-executor-cores 2 /opt/spark/lib/spark-examples-1.6.0-hadoop2.6.0.jar 250", 202 | "id": "spark-pi", 203 | "cpus": 1, 204 | "mem": 1024, 205 | "instances": 1 206 | }' 207 | 208 | ####Chronos 209 | Spark jobs for running in Chronos are basically all the computational jobs needed to run on schedule, another option is one-shot 210 | applications needed to be run only once. 211 | 212 | curl -L -H 'Content-Type: application/json' -X POST http://mesos:4400/scheduler/iso8601 -d '{ 213 | "name": "Scheduled Spark Submit Job", 214 | "command": "/opt/spark/bin/spark-submit --class org.apache.spark.examples.SparkPi --master mesos://zk://zookeeper:2181/mesos --deploy-mode client --total-executor-cores 2 /opt/spark/lib/spark-examples-1.6.0-hadoop2.6.0.jar 250", 215 | "shell": true, 216 | "async": false, 217 | "cpus": 0.1, 218 | "disk": 256, 219 | "mem": 1024, 220 | "owner": "anton@datastrophic.io", 221 | "description": "SparkPi job executed every 3 minutes", 222 | "schedule": "R/2016-03-14T12:35:00.000Z/PT3M" 223 | }' 224 | 225 | ##Mesos Framework Examples 226 | ###Cassandra Load Testing Framework 227 | __Throttler__ framework is designed for load testing Cassandra in distributed manner. The Scheduler performance is controlled 228 | by the next properties: 229 | 230 | --total-queries - total amount of queries to execute during the test 231 | --queries-per-task - how many queries are executed within single Mesos Task 232 | --parallelism - how many Tasks are executed simultaneously 233 | 234 | Scheduler implementation: [ThrottleScheduler](src/main/scala/io/datastrophic/mesos/throttler/ThrottleScheduler.scala). 235 | 236 | To run the framework build the project and distribute across containers: 237 | 238 | sbt clean assembly distribute 239 | 240 | This build a fatjar and distribute across containers via linked volume directories, so the resulting jar will be 241 | immediately available in containers. To run Throttler user should be logged in (attached to) docker container and from there execute: 242 | 243 | java -cp /throttle/throttle-framework.jar -Dexecutor.path=/throttle/throttle-executor.sh io.datastrophic.mesos.throttler.Throttler \ 244 | --mesos-master zk://zookeeper:2181/mesos \ 245 | --cassandra-host cassandra \ 246 | --keyspace demo_framework \ 247 | --total-queries 100 \ 248 | --queries-per-task 5 \ 249 | --parallelism 5 250 | 251 | One can play with load test parameters, but remember that everything is executed inside a virtual machine (in case of Mac) and memory 252 | starvation could start pretty quickly when the load becomes high. Which in turn can lead to containers failures. 253 | 254 | ###Dominant Resource Fairness Demo Framework 255 | Mesos uses Dominant Resource Fairness algorithm to achieve fair resource allocation across frameworks. DRF framework is used to 256 | demonstrate different cases and how DRF handles them. 257 | 258 | Scheduler implementation: [DRFDemoScheduler](src/main/scala/io/datastrophic/mesos/drf/DRFDemoScheduler.scala). 259 | 260 | This framework has been used to provide some experimental results for 261 | [Datastrophic blog post about DRF](http://datastrophic.io/resource-allocation-in-mesos-dominant-resource-fairness-explained/). 262 | It is supposed that multiple instances of this framework should be run in parallel on the same cluster to observe DRF behavior 263 | when frameworks compete for resources. So main parameters for the framework are name (to distinguish it among others) and 264 | resources needed to run one task (cpu and memory). Invocation example: 265 | 266 | java -cp /throttle/throttle-framework.jar -Dexecutor.path=/throttle/drf-executor.sh io.datastrophic.mesos.drf.DRFDemoFramework \ 267 | --mesos-master zk://zookeeper:2181/mesos \ 268 | --framework-name 'Framework A' \ 269 | --task-cpus 0.5 \ 270 | --task-memory 512 -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := """mesos-workshop""" 2 | 3 | version := "1.0" 4 | 5 | scalaVersion := "2.11.7" 6 | 7 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8") 8 | 9 | javacOptions ++= Seq("-source", "1.8", "-target", "1.8") 10 | 11 | libraryDependencies ++= { 12 | val akkaVersion = "2.4.2" 13 | val scalaTestVersion = "2.2.5" 14 | val mesosVersion = "0.28.1" 15 | 16 | Seq( 17 | "org.apache.mesos" % "mesos" % mesosVersion, 18 | "com.datastax.cassandra" % "cassandra-driver-core" % "3.0.0", 19 | "com.github.scopt" %% "scopt" % "3.3.0", 20 | "com.typesafe.akka" %% "akka-actor" % akkaVersion, 21 | "com.typesafe.akka" %% "akka-stream" % akkaVersion, 22 | "com.typesafe.akka" %% "akka-http-experimental" % akkaVersion, 23 | "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaVersion, 24 | "ch.qos.logback" % "logback-classic" % "1.1.2", 25 | "com.typesafe.akka" %% "akka-http-testkit" % akkaVersion, 26 | "org.scalatest" %% "scalatest" % scalaTestVersion % "test", 27 | "org.scalatest" %% "scalatest" % scalaTestVersion % "test" 28 | ) 29 | } 30 | 31 | assemblyJarName in assembly := "mesos-workshop.jar" 32 | 33 | assemblyOption in assembly := (assemblyOption in assembly).value.copy(includeScala = true) 34 | 35 | assemblyMergeStrategy in assembly := { 36 | case m if m.toLowerCase.matches("meta-inf.*\\.sf$") => MergeStrategy.discard 37 | case PathList("META-INF", xs @ _*) => MergeStrategy.discard 38 | case PathList("reference.conf") => MergeStrategy.concat 39 | case x => MergeStrategy.defaultMergeStrategy(x) 40 | } 41 | 42 | 43 | val copyTask = TaskKey[Unit]("distribute", "Copying fatjar to proper directories for being picked up by Docker") 44 | 45 | copyTask <<= assembly map { (asm) => 46 | val local = asm.getPath 47 | val microserviceContext = "images/microservice/akka-microservice.jar" 48 | val executorContext = "executor/throttle-framework.jar" 49 | 50 | println(s"Copying: $local -> $microserviceContext") 51 | Seq("cp", local, microserviceContext) !! 52 | 53 | println(s"Copying: $local -> $executorContext") 54 | Seq("cp", local, executorContext) !! 55 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | zookeeper: 5 | image: mesoscloud/zookeeper:3.4.6-ubuntu-14.04 6 | hostname: "zookeeper" 7 | ports: 8 | - "2181:2181" 9 | - "2888:2888" 10 | - "3888:3888" 11 | 12 | mesos-master: 13 | image: datastrophic/mesos-master:0.28.1 14 | hostname: "mesos-master" 15 | privileged: true 16 | environment: 17 | - MESOS_HOSTNAME=mesos-master 18 | - MESOS_CLUSTER=SMACK 19 | - MESOS_QUORUM=1 20 | - MESOS_ZK=zk://zookeeper:2181/mesos 21 | - MESOS_LOG_DIR=/tmp/mesos/logs 22 | links: 23 | - zookeeper 24 | ports: 25 | - "5050:5050" 26 | 27 | mesos-slave: 28 | image: datastrophic/mesos-slave-spark:mesos-0.28.1-spark-1.6 29 | hostname: "mesos-slave" 30 | privileged: true 31 | environment: 32 | - MESOS_HOSTNAME=mesos-slave 33 | - MESOS_PORT=5151 34 | - MESOS_MASTER=zk://zookeeper:2181/mesos 35 | - SPARK_PUBLIC_DNS=mesos-slave 36 | - CUSTOM_EXECUTOR_HOME=/throttle 37 | links: 38 | - zookeeper 39 | - mesos-master 40 | - cassandra 41 | ports: 42 | - "5151:5151" 43 | - "4040:4040" 44 | volumes: 45 | - /sys/fs/cgroup:/sys/fs/cgroup 46 | - /var/run/docker.sock:/var/run/docker.sock 47 | - ./executor:/throttle 48 | 49 | marathon: 50 | image: datastrophic/marathon:0.15.3 51 | hostname: "marathon" 52 | environment: 53 | - MARATHON_HOSTNAME=marathon 54 | - MARATHON_MASTER=zk://zookeeper:2181/mesos 55 | - MARATHON_ZK=zk://zookeeper:2181/marathon 56 | links: 57 | - zookeeper 58 | - mesos-master 59 | - mesos-slave 60 | ports: 61 | - "8080:8080" 62 | 63 | cassandra: 64 | image: cassandra:3.3 65 | hostname: "cassandra" 66 | ports: 67 | - "9160:9160" 68 | - "9042:9042" -------------------------------------------------------------------------------- /executor/drf-executor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | java -cp $CUSTOM_EXECUTOR_HOME/throttle-framework.jar io.datastrophic.mesos.drf.DRFDemoExecutor -------------------------------------------------------------------------------- /executor/invocation-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | java -cp /throttle/throttle-framework.jar -Dexecutor.path=/throttle/throttle-executor.sh io.datastrophic.mesos.Throttler \ 4 | --mesos-master zk://zookeeper:2181/mesos \ 5 | --cassandra-host cassandra \ 6 | --keyspace demo_framework \ 7 | --total-queries 100 \ 8 | --queries-per-task 5 \ 9 | --parallelism 5 10 | 11 | java -cp /throttle/throttle-framework.jar -Dexecutor.path=/throttle/drf-executor.sh io.datastrophic.mesos.drf.DRFDemoFramework \ 12 | --mesos-master zk://zookeeper:2181/mesos \ 13 | --framework-name 'Framework A' \ 14 | --task-cpus 2 \ 15 | --task-memory 1000 16 | 17 | java -cp /throttle/throttle-framework.jar -Dexecutor.path=/throttle/drf-executor.sh io.datastrophic.mesos.drf.DRFDemoFramework \ 18 | --mesos-master zk://zookeeper:2181/mesos \ 19 | --framework-name 'Framework B' \ 20 | --task-cpus 1 \ 21 | --task-memory 2500 22 | 23 | java -cp /throttle/throttle-framework.jar -Dexecutor.path=/throttle/drf-executor.sh io.datastrophic.mesos.drf.DRFDemoFramework \ 24 | --mesos-master zk://zookeeper:2181/mesos \ 25 | --framework-name 'Framework C' \ 26 | --task-cpus 0.5 \ 27 | --task-memory 512 -------------------------------------------------------------------------------- /executor/throttle-executor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | java -cp $CUSTOM_EXECUTOR_HOME/throttle-framework.jar io.datastrophic.mesos.throttler.ThrottleExecutor -------------------------------------------------------------------------------- /images/microservice/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:openjdk-8u72-jre 2 | 3 | COPY akka-microservice.jar / 4 | 5 | EXPOSE 31337 6 | 7 | CMD java -cp /akka-microservice.jar io.datastrophic.microservice.AkkaMicroservice --service-port 31337 --cassandra-host $CASSANDRA_HOST --keyspace $CASSANDRA_KEYSPACE --table $CASSANDRA_TABLE -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Mon Mar 07 13:23:29 CET 2016 3 | template.uuid=e17acfbb-1ff5-41f5-b8cf-2c40be6a8340 4 | sbt.version=0.13.8 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "sonatype-releases" at "https://oss.sonatype.org/content/repositories/releases/" 2 | 3 | // fatjar assembly 4 | addSbtPlugin("com.eed3si9n"%"sbt-assembly"%"0.14.0") -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %date{ISO8601} %-5level [%X{akkaSource}] - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/common/CassandraUtil.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.common 2 | 3 | import com.datastax.driver.core.{Cluster, Session} 4 | 5 | object CassandraUtil { 6 | 7 | def buildSession(host: String): Session = { 8 | Cluster.builder() 9 | .addContactPoint(host) 10 | .build() 11 | .connect() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/BaseMesosExecutor.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos 2 | 3 | import org.apache.mesos.Protos.{SlaveInfo, TaskID} 4 | import org.apache.mesos.{Executor, ExecutorDriver} 5 | 6 | trait BaseMesosExecutor extends Executor { 7 | 8 | override def frameworkMessage(driver: ExecutorDriver, data: Array[Byte]): Unit = {} 9 | 10 | override def error(driver: ExecutorDriver, message: String): Unit = {} 11 | 12 | override def reregistered(driver: ExecutorDriver, slaveInfo: SlaveInfo): Unit = {} 13 | 14 | override def killTask(driver: ExecutorDriver, taskId: TaskID): Unit = {} 15 | 16 | override def disconnected(driver: ExecutorDriver): Unit = {} 17 | 18 | override def shutdown(driver: ExecutorDriver): Unit = {} 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/BaseMesosScheduler.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos 2 | 3 | import org.apache.mesos.Protos._ 4 | import org.apache.mesos.{Scheduler, SchedulerDriver} 5 | import org.slf4j.LoggerFactory 6 | 7 | trait BaseMesosScheduler extends Scheduler { 8 | 9 | private val logger = LoggerFactory.getLogger(getClass.getName) 10 | 11 | override def offerRescinded(driver: SchedulerDriver, offerId: OfferID): Unit = logger.info(s"Offer rescinded: $offerId") 12 | 13 | override def disconnected(driver: SchedulerDriver): Unit = logger.info(s"Disconnected") 14 | 15 | override def reregistered(driver: SchedulerDriver, masterInfo: MasterInfo): Unit = logger.info(s"Reregistered: $masterInfo") 16 | 17 | override def slaveLost(driver: SchedulerDriver, slaveId: SlaveID): Unit = logger.info(s"Slave Lost: $slaveId") 18 | 19 | override def error(driver: SchedulerDriver, message: String): Unit = logger.info(s"Error: $message") 20 | 21 | override def frameworkMessage(driver: SchedulerDriver, executorId: ExecutorID, slaveId: SlaveID, data: Array[Byte]): Unit = { 22 | logger.info(s"Framework message: ${String.copyValueOf(data.map(_.toChar))}") 23 | } 24 | 25 | override def registered(driver: SchedulerDriver, frameworkId: FrameworkID, masterInfo: MasterInfo): Unit = {} 26 | 27 | override def executorLost(driver: SchedulerDriver, executorId: ExecutorID, slaveId: SlaveID, status: Int): Unit = {} 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/BinarySerDe.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos 2 | 3 | import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream} 4 | 5 | trait BinarySerDe { 6 | def serialize[T](o: T): Array[Byte] = { 7 | val bos = new ByteArrayOutputStream 8 | val oos = new ObjectOutputStream(bos) 9 | oos.writeObject(o) 10 | oos.close 11 | return bos.toByteArray 12 | } 13 | 14 | def deserialize[T](bytes: Array[Byte]): T = { 15 | val bis = new ByteArrayInputStream(bytes) 16 | val ois = new ObjectInputStream(bis) 17 | return ois.readObject.asInstanceOf[T] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/TaskBuilder.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos 2 | 3 | import java.util.UUID 4 | 5 | import org.apache.mesos.Protos.Value.Scalar 6 | import org.apache.mesos.Protos.Value.Type.SCALAR 7 | import org.apache.mesos.Protos._ 8 | import org.apache.mesos.SchedulerDriver 9 | 10 | trait TaskBuilder extends BinarySerDe { 11 | def buildDummyTask(offer: Offer, cpus: Double, memory: Int, executorInfo: ExecutorInfo, prefix: String) = { 12 | val cpuResource = Resource.newBuilder 13 | .setType(SCALAR) 14 | .setName("cpus") 15 | .setScalar(Scalar.newBuilder.setValue(cpus)) 16 | .setRole("*") 17 | .build 18 | 19 | val memResource = Resource.newBuilder 20 | .setType(SCALAR) 21 | .setName("mem") 22 | .setScalar(Scalar.newBuilder.setValue(memory)) 23 | .setRole("*") 24 | .build 25 | 26 | TaskInfo.newBuilder() 27 | .setSlaveId(SlaveID.newBuilder().setValue(offer.getSlaveId.getValue).build()) 28 | .setTaskId(TaskID.newBuilder().setValue(s"${prefix}_DRFTask_$uuid")) 29 | .setExecutor(executorInfo) 30 | .setName(UUID.randomUUID().toString) 31 | .addResources(cpuResource) 32 | .addResources(memResource) 33 | .build() 34 | } 35 | 36 | def buildExecutorInfo(d: SchedulerDriver, prefix: String): ExecutorInfo = { 37 | val scriptPath = System.getProperty("executor.path","/throttle/throttle-executor.sh") 38 | ExecutorInfo.newBuilder() 39 | .setCommand(CommandInfo.newBuilder().setValue("/bin/sh "+scriptPath)) 40 | .setExecutorId(ExecutorID.newBuilder().setValue(s"${prefix}_$uuid")) 41 | .build() 42 | } 43 | 44 | def uuid = UUID.randomUUID() 45 | } -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/drf/DRFDemoExecutor.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos.drf 2 | 3 | import java.util.concurrent.{ExecutorService, Executors} 4 | 5 | import com.google.protobuf.ByteString 6 | import io.datastrophic.mesos.{BaseMesosExecutor, BinarySerDe} 7 | import org.apache.mesos.Protos._ 8 | import org.apache.mesos.{ExecutorDriver, MesosExecutorDriver} 9 | import org.slf4j.LoggerFactory 10 | 11 | import scala.util.Random 12 | 13 | object DRFDemoExecutor extends BinarySerDe { 14 | 15 | def main(args: Array[String]) { 16 | val logger = LoggerFactory.getLogger(getClass.getName) 17 | System.loadLibrary("mesos") 18 | var threadPool: ExecutorService = null 19 | 20 | val exec = new BaseMesosExecutor { 21 | override def launchTask(driver: ExecutorDriver, task: TaskInfo): Unit = { 22 | threadPool.execute(new Runnable() { 23 | override def run(): Unit = { 24 | val taskStatus = TaskStatus.newBuilder().setTaskId(task.getTaskId) 25 | val taskId = task.getTaskId.getValue 26 | 27 | logger.info(s"Task $taskId received by executor: ${task.getExecutor.getExecutorId.getValue}") 28 | 29 | driver.sendStatusUpdate( 30 | taskStatus 31 | .setState(TaskState.TASK_RUNNING) 32 | .build() 33 | ) 34 | 35 | val delay = 20000 + Random.nextInt(20000) 36 | logger.info(s"Running dummy task for ${delay/1000f} sec.") 37 | 38 | Thread.sleep(delay) 39 | 40 | val msg = s"Task $taskId finished" 41 | logger.info(msg) 42 | 43 | driver.sendStatusUpdate( 44 | taskStatus 45 | .setState(TaskState.TASK_FINISHED) 46 | .setData(ByteString.copyFrom(serialize(msg))) 47 | .build() 48 | ) 49 | } 50 | }) 51 | } 52 | 53 | override def registered(driver: ExecutorDriver, executorInfo: ExecutorInfo, frameworkInfo: FrameworkInfo, slaveInfo: SlaveInfo): Unit = { 54 | threadPool = Executors.newCachedThreadPool() 55 | } 56 | } 57 | 58 | new MesosExecutorDriver(exec).run() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/drf/DRFDemoFramework.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos.drf 2 | 3 | import org.apache.mesos.MesosSchedulerDriver 4 | import org.apache.mesos.Protos.FrameworkInfo 5 | import org.slf4j.LoggerFactory 6 | 7 | object DRFDemoFramework { 8 | 9 | val logger = LoggerFactory.getLogger(getClass.getName) 10 | 11 | def run(config: Config): Unit ={ 12 | val framework = FrameworkInfo.newBuilder 13 | .setName(config.name) 14 | .setUser("") 15 | .setRole("*") 16 | .setCheckpoint(false) 17 | .setFailoverTimeout(0.0d) 18 | .build() 19 | 20 | val driver = new MesosSchedulerDriver(new DRFDemoScheduler(config), framework, config.mesosURL) 21 | driver.run() 22 | } 23 | 24 | def main(args: Array[String]): Unit = { 25 | val parser = new scopt.OptionParser[Config]("scopt") { 26 | head("scopt", "3.x") 27 | opt[String]('m', "mesos-master") required() action { (x, c) => c.copy(mesosURL = x) } text ("mesos master url") 28 | opt[String]('n', "framework-name") required() action { (x, c) => c.copy(name = x) } text ("name of the framework to register") 29 | opt[Double]('c', "task-cpus") required() action { (x, c) => c.copy(cpus = x) } text ("cpu share per task") 30 | opt[Int]('t', "task-memory") required() action { (x, c) => c.copy(mem = x) } text ("amount of memory per task") 31 | help("help") text("prints this usage text") 32 | } 33 | 34 | parser.parse(args, Config()) map { config => 35 | run(config) 36 | } getOrElse { 37 | println("Not all program arguments provided, can't continue") 38 | } 39 | } 40 | } 41 | 42 | protected case class Config( 43 | mesosURL: String = "", 44 | name: String = "", 45 | cpus: Double = 0.1, 46 | mem: Int = 512 47 | ) -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/drf/DRFDemoScheduler.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos.drf 2 | 3 | import java.util 4 | import java.util.concurrent.ConcurrentHashMap 5 | import java.util.concurrent.locks.ReentrantLock 6 | 7 | import io.datastrophic.mesos.{BaseMesosScheduler, TaskBuilder} 8 | import org.apache.mesos.Protos._ 9 | import org.apache.mesos.SchedulerDriver 10 | import org.slf4j.LoggerFactory 11 | 12 | import scala.collection.JavaConversions._ 13 | 14 | class DRFDemoScheduler(val config: Config) extends BaseMesosScheduler with TaskBuilder{ 15 | 16 | private val logger = LoggerFactory.getLogger(classOf[DRFDemoScheduler]) 17 | private val stateLock = new ReentrantLock() 18 | val executors = new ConcurrentHashMap[String, ExecutorInfo]() 19 | 20 | override def statusUpdate(driver: SchedulerDriver, status: TaskStatus): Unit = { 21 | status.getState match { 22 | case TaskState.TASK_FINISHED => 23 | logger.info(s"Task finished on slave ${status.getSlaveId.getValue}. Message: ${deserialize[String](status.getData.toByteArray)}") 24 | case TaskState.TASK_RUNNING => () 25 | case _ => 26 | logger.info(s"${status.toString}") 27 | } 28 | } 29 | 30 | override def resourceOffers(driver: SchedulerDriver, offers: util.List[Offer]): Unit = { 31 | for(offer <- offers){ 32 | stateLock.synchronized { 33 | logger.info(s"Received resource offer: cpus:${getCpus(offer)} mem: ${getMemory(offer)}") 34 | 35 | 36 | if(isOfferValid(offer)){ 37 | val executorInfo = executors.getOrElseUpdate(offer.getSlaveId.getValue, buildExecutorInfo(driver, "DRFDemoExecutor")) 38 | 39 | //amount of tasks is calculated to fully use resources from the offer 40 | val tasks = buildTasks(offer, config, executorInfo) 41 | logger.info(s"Launching ${tasks.size} tasks on slave ${offer.getSlaveId.getValue}") 42 | driver.launchTasks(List(offer.getId), tasks) 43 | } else { 44 | logger.info(s"Offer provides insufficient resources. Declining.") 45 | driver.declineOffer(offer.getId) 46 | } 47 | } 48 | } 49 | } 50 | 51 | def buildTasks(offer: Offer, config: Config, executorInfo: ExecutorInfo): List[TaskInfo] = { 52 | val amount = Math.min(getCpus(offer)/config.cpus, getMemory(offer)/config.mem).toInt 53 | (1 to amount).map(_ => 54 | buildDummyTask(offer, config.cpus, config.mem, executorInfo, config.name.replace(" ","_")) 55 | ).toList 56 | } 57 | 58 | private def isOfferValid(offer: Offer): Boolean = getCpus(offer) >= config.cpus && getMemory(offer) >= config.mem 59 | 60 | private def getCpus(offer: Offer) = offer.getResourcesList.find(resource => resource.getName == "cpus").map(_.getScalar.getValue).getOrElse(0.0) 61 | 62 | private def getMemory(offer: Offer) = offer.getResourcesList.find(resource => resource.getName == "mem").map(_.getScalar.getValue).getOrElse(0.0) 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/throttler/CassandraTaskBuilder.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos.throttler 2 | 3 | import java.util.UUID 4 | 5 | import com.google.protobuf.ByteString 6 | import io.datastrophic.mesos.TaskBuilder 7 | import org.apache.mesos.Protos.Value.Scalar 8 | import org.apache.mesos.Protos.Value.Type._ 9 | import org.apache.mesos.Protos._ 10 | 11 | trait CassandraTaskBuilder extends TaskBuilder { 12 | def config: Config 13 | 14 | def buildCassandraTask(offer: Offer, executorInfo: ExecutorInfo, totalQueries: Int) = { 15 | val task = new Task( 16 | config.cassandraHost, 17 | (1 to totalQueries).map(pk => s"INSERT INTO ${config.keyspace}.test (pk, ck, rand) VALUES($pk, $uuid, $uuid);").toList 18 | ) 19 | 20 | val cpus = Resource.newBuilder 21 | .setType(SCALAR) 22 | .setName("cpus") 23 | .setScalar(Scalar.newBuilder.setValue(1.0)) 24 | .setRole("*") 25 | .build 26 | 27 | TaskInfo.newBuilder() 28 | .setSlaveId(SlaveID.newBuilder().setValue(offer.getSlaveId.getValue).build()) 29 | .setTaskId(TaskID.newBuilder().setValue(s"ThrottleTask_$uuid")) 30 | .setExecutor(executorInfo) 31 | .setName(UUID.randomUUID().toString) 32 | .addResources(cpus) 33 | .setData(ByteString.copyFrom(serialize(task))) 34 | .build() 35 | } 36 | } 37 | 38 | @SerialVersionUID(1458551286) 39 | case class Task[T]( val host: String, queries: List[String]) extends Serializable 40 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/throttler/ThrottleExecutor.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos.throttler 2 | 3 | import java.util.concurrent.{ExecutorService, Executors} 4 | 5 | import com.google.protobuf.ByteString 6 | import io.datastrophic.common.CassandraUtil 7 | import io.datastrophic.mesos.{BaseMesosExecutor, BinarySerDe} 8 | import org.apache.mesos.Protos._ 9 | import org.apache.mesos.{ExecutorDriver, MesosExecutorDriver} 10 | import org.slf4j.LoggerFactory 11 | 12 | import scala.util.{Failure, Success, Try} 13 | 14 | object ThrottleExecutor extends BinarySerDe { 15 | 16 | def main(args: Array[String]) { 17 | val logger = LoggerFactory.getLogger(getClass.getName) 18 | System.loadLibrary("mesos") 19 | var classLoader: ClassLoader = null 20 | var threadPool: ExecutorService = null 21 | 22 | val exec = new BaseMesosExecutor { 23 | override def launchTask(driver: ExecutorDriver, task: TaskInfo): Unit = { 24 | val arg = task.getData.toByteArray 25 | 26 | threadPool.execute(new Runnable() { 27 | override def run(): Unit = { 28 | val deserializedTask = deserialize[Task[Any]](task.getData.toByteArray) 29 | 30 | val taskStatus = TaskStatus.newBuilder().setTaskId(task.getTaskId) 31 | 32 | logger.info(s"Task ${task.getTaskId.getValue} received by executor: ${task.getExecutor.getExecutorId.getValue}") 33 | 34 | driver.sendStatusUpdate( 35 | taskStatus 36 | .setState(TaskState.TASK_RUNNING) 37 | .build() 38 | ) 39 | 40 | val start = System.currentTimeMillis() 41 | Try{ 42 | val session = CassandraUtil.buildSession(deserializedTask.host) 43 | deserializedTask.queries.foreach(session.execute) 44 | } match { 45 | case Success(_) => 46 | logger.info(s"Task ${task.getTaskId.getValue} finished") 47 | val msg = s"Executed ${deserializedTask.queries.size} queries in ${(System.currentTimeMillis()- start)/1000f} sec." 48 | 49 | Thread.sleep(5000) 50 | 51 | driver.sendStatusUpdate( 52 | taskStatus 53 | .setState(TaskState.TASK_FINISHED) 54 | .setData(ByteString.copyFrom( 55 | serialize(msg) 56 | )) 57 | .build() 58 | ) 59 | 60 | case Failure(ex) => 61 | logger.error(s"Exception in Task ${task.getTaskId.getValue}", ex) 62 | driver.sendStatusUpdate( 63 | taskStatus 64 | .setState(TaskState.TASK_ERROR) 65 | .setData(ByteString.copyFrom(serialize(ex.getMessage))) 66 | .build()) 67 | } 68 | } 69 | 70 | }) 71 | } 72 | 73 | override def registered(driver: ExecutorDriver, executorInfo: ExecutorInfo, frameworkInfo: FrameworkInfo, slaveInfo: SlaveInfo): Unit = { 74 | classLoader = this.getClass.getClassLoader 75 | Thread.currentThread.setContextClassLoader(classLoader) 76 | threadPool = Executors.newCachedThreadPool() 77 | } 78 | } 79 | 80 | new MesosExecutorDriver(exec).run() 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/throttler/ThrottleScheduler.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos.throttler 2 | 3 | import java.util 4 | import java.util.concurrent.ConcurrentHashMap 5 | import java.util.concurrent.atomic.AtomicInteger 6 | import java.util.concurrent.locks.ReentrantLock 7 | 8 | import io.datastrophic.mesos.BaseMesosScheduler 9 | import org.apache.mesos.Protos._ 10 | import org.apache.mesos.SchedulerDriver 11 | import org.slf4j.LoggerFactory 12 | 13 | import scala.collection.JavaConversions._ 14 | 15 | class ThrottleScheduler(val config: Config) extends BaseMesosScheduler with CassandraTaskBuilder { 16 | 17 | private val logger = LoggerFactory.getLogger(classOf[ThrottleScheduler]) 18 | private val stateLock = new ReentrantLock() 19 | 20 | val queriesToRun = new AtomicInteger(config.totalQueries) 21 | val currentTasks = new AtomicInteger(0) 22 | val errors = new AtomicInteger(0) 23 | val executors = new ConcurrentHashMap[String, ExecutorInfo]() 24 | 25 | override def statusUpdate(driver: SchedulerDriver, status: TaskStatus): Unit = { 26 | status.getState match { 27 | case TaskState.TASK_FINISHED => 28 | logger.info(s"Task finished on slave ${status.getSlaveId.getValue}. Message: ${deserialize[String](status.getData.toByteArray)}") 29 | currentTasks.decrementAndGet() 30 | 31 | if(queriesToRun.get() == 0){ 32 | logger.info(s"All queries launched, exit now.") 33 | System.exit(0) 34 | } 35 | case TaskState.TASK_ERROR => 36 | logger.error(s"Task error on slave ${status.getSlaveId.getValue}. Exception message: ${deserialize[String](status.getData.toByteArray)}. Total " + 37 | s"errors: ${errors.get()}") 38 | currentTasks.decrementAndGet() 39 | 40 | if(errors.incrementAndGet() > 5){ 41 | logger.info("Too many errors in tasks, shutting down.") 42 | System.exit(1) 43 | } 44 | 45 | case TaskState.TASK_RUNNING => () 46 | 47 | case _ => 48 | logger.info(s"${status.toString}") 49 | } 50 | } 51 | 52 | override def resourceOffers(driver: SchedulerDriver, offers: util.List[Offer]): Unit = { 53 | for(offer <- offers){ 54 | stateLock.synchronized { 55 | logger.debug(s"Received resource offer: ${offer.toString}") 56 | if(queriesToRun.get() > 0) { 57 | if(currentTasks.get() <= config.parallelism){ 58 | logger.info(s"Launching task on slave ${offer.getSlaveId.getValue}") 59 | 60 | val numberOfQueries = if(queriesToRun.get() < config.queriesPerTask) queriesToRun.get() else config.queriesPerTask 61 | val executorInfo = executors.getOrElseUpdate(offer.getSlaveId.getValue, buildExecutorInfo(driver, "ThrottlerFG")) 62 | 63 | val taskInfo = buildCassandraTask(offer, executorInfo, numberOfQueries) 64 | driver.launchTasks(List(offer.getId), List(taskInfo)) 65 | 66 | currentTasks.incrementAndGet() 67 | queriesToRun.getAndSet(queriesToRun.get() - numberOfQueries) 68 | } else { 69 | logger.info(s"Already running ${config.parallelism} tasks on cluster. Declining the offer") 70 | driver.declineOffer(offer.getId) 71 | } 72 | } else { 73 | logger.info(s"All queries launched, waiting for tasks to complete. Declining offer.") 74 | driver.declineOffer(offer.getId) 75 | } 76 | } 77 | } 78 | } 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/mesos/throttler/Throttler.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.mesos.throttler 2 | 3 | import io.datastrophic.common.CassandraUtil 4 | import org.apache.mesos.MesosSchedulerDriver 5 | import org.apache.mesos.Protos.FrameworkInfo 6 | import org.slf4j.LoggerFactory 7 | 8 | object Throttler { 9 | import SchemaBuilder._ 10 | 11 | val logger = LoggerFactory.getLogger(getClass.getName) 12 | 13 | def run(config: Config): Unit ={ 14 | val framework = FrameworkInfo.newBuilder 15 | .setName("Throttler") 16 | .setUser("") 17 | .setRole("*") 18 | .setCheckpoint(false) 19 | .setFailoverTimeout(0.0d) 20 | .build() 21 | 22 | ensureSchema(config) 23 | 24 | val driver = new MesosSchedulerDriver(new ThrottleScheduler(config), framework, config.mesosURL) 25 | driver.run() 26 | } 27 | 28 | def main(args: Array[String]): Unit = { 29 | val parser = new scopt.OptionParser[Config]("scopt") { 30 | head("scopt", "3.x") 31 | opt[String]('m', "mesos-master") required() action { (x, c) => c.copy(mesosURL = x) } text ("mesos master") 32 | opt[String]('h', "cassandra-host") required() action { (x, c) => c.copy(cassandraHost = x) } text ("cassandra hostname") 33 | opt[String]('k', "keyspace") required() action { (x, c) => c.copy(keyspace = x) } text ("keyspace name") 34 | opt[Int]('t', "total-queries") required() action { (x, c) => c.copy(totalQueries = x) } text ("total amount of queries to execute") 35 | opt[Int]('f', "queries-per-task") required() action { (x, c) => c.copy(queriesPerTask = x) } text ("amount of queries to execute within single task") 36 | opt[Int]('p', "parallelism") required() action { (x, c) => c.copy(parallelism = x) } text ("number of tasks run in parallel") 37 | help("help") text("prints this usage text") 38 | } 39 | 40 | parser.parse(args, Config()) map { config => 41 | run(config) 42 | } getOrElse { 43 | println("Not all program arguments provided, can't continue") 44 | } 45 | } 46 | } 47 | 48 | protected case class Config( 49 | mesosURL: String = "", 50 | cassandraHost: String = "", 51 | keyspace: String = "", 52 | totalQueries: Int = 0, 53 | queriesPerTask: Int = 0, 54 | parallelism: Int = 1 55 | ) 56 | 57 | object SchemaBuilder { 58 | def ensureSchema(config: Config): Unit = { 59 | val session = CassandraUtil.buildSession(config.cassandraHost) 60 | 61 | session.execute(s"CREATE KEYSPACE IF NOT EXISTS ${config.keyspace} WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1 };") 62 | 63 | session.execute(s""" 64 | CREATE TABLE IF NOT EXISTS ${config.keyspace}.test ( 65 | pk int, 66 | ck uuid, 67 | rand uuid, 68 | PRIMARY KEY(pk, ck) 69 | ); 70 | """.stripMargin) 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/microservice/AkkaMicroservice.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.microservice 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.UUID 5 | 6 | import akka.actor.ActorSystem 7 | import akka.event.Logging 8 | import akka.http.scaladsl.Http 9 | import akka.http.scaladsl.model.StatusCodes._ 10 | import akka.http.scaladsl.server.Directives 11 | import akka.stream.ActorMaterializer 12 | import io.datastrophic.common.CassandraUtil 13 | 14 | import scala.collection.JavaConversions._ 15 | import scala.util.{Failure, Success, Try} 16 | 17 | class Service(val clusterContext: ClusterContext) extends Directives with Protocols { 18 | implicit val system = ActorSystem() 19 | implicit def executor = system.dispatcher 20 | implicit val materializer = ActorMaterializer() 21 | val logger = Logging(system, getClass) 22 | 23 | val format = new SimpleDateFormat("yyyy-dd-MM HH:mm:ss") 24 | 25 | def save(event: Event) = { 26 | Try { 27 | clusterContext.session.execute(s"""INSERT INTO ${clusterContext.keyspace}.${clusterContext.table} (id, campaign_id, event_type, value, time, internal_id) 28 | VALUES (${event.id.toString}, ${event.campaignId}, '${event.eventType}', ${event.value}, 29 | ${format.parse(event.timestamp).getTime}, ${UUID.randomUUID().toString});""".stripMargin) 30 | } 31 | } 32 | 33 | def getTotals(campaignId: UUID, eventType: String) = { 34 | Try { 35 | val rows = clusterContext.session.execute(s""" 36 | SELECT value from ${clusterContext.keyspace}.${clusterContext.table} 37 | WHERE campaign_id = ${campaignId.toString} AND event_type = '$eventType';""".stripMargin) 38 | 39 | rows.all().foldLeft(0L){(acc, row) => acc + row.getLong(0)} 40 | } 41 | } 42 | 43 | val routes = { 44 | logRequestResult("akka-http-microservice") { 45 | pathPrefix("event") { 46 | (post & entity(as[Event])) { event => 47 | logger.info(s"Event received: $event") 48 | complete { 49 | save(event) match { 50 | case Success(_) => 51 | logger.info(s"Event stored: $event") 52 | OK 53 | case Failure(ex) => 54 | logger.error(s"Error during writing to storage", ex) 55 | BadRequest -> ex.getMessage 56 | } 57 | } 58 | } 59 | } ~ 60 | pathPrefix("campaign" / JavaUUID / "totals"){ uuid => 61 | (get & path(Segment)) { eventType => 62 | logger.info(s"Counting total of '$eventType' events for campaign: $uuid") 63 | complete { 64 | getTotals(uuid, eventType) match { 65 | case Success(value) => TotalsResponse(uuid.toString, value) 66 | case Failure(ex) => 67 | logger.error(s"Error during read from storage", ex) 68 | BadRequest -> ex.getMessage 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | def start(port: Int) = { 77 | Http().bindAndHandle(routes, interface = "0.0.0.0", port) 78 | } 79 | } 80 | 81 | object AkkaMicroservice { 82 | import SchemaHelper._ 83 | 84 | def run(config: Config) = { 85 | val session = CassandraUtil.buildSession(config.cassandraHost) 86 | 87 | val clusterContext = ClusterContext(session, config.keyspace, config.table) 88 | 89 | createSchema(clusterContext) 90 | 91 | val service = new Service(clusterContext) 92 | service.start(config.port) 93 | } 94 | 95 | def main(args: Array[String]): Unit = { 96 | val parser = new scopt.OptionParser[Config]("scopt") { 97 | head("scopt", "3.x") 98 | opt[Int]('p', "service-port") required() action { (x, c) => c.copy(port = x) } text ("service port to listen on") 99 | opt[String]('h', "cassandra-host") required() action { (x, c) => c.copy(cassandraHost = x) } text ("cassandra hostname") 100 | opt[String]('k', "keyspace") required() action { (x, c) => c.copy(keyspace = x) } text ("keyspace name") 101 | opt[String]('t', "table") required() action { (x, c) => c.copy(table = x) } text ("table name") 102 | help("help") text("prints this usage text") 103 | } 104 | 105 | parser.parse(args, Config()) map { config => 106 | run(config) 107 | } getOrElse { 108 | println("Not all program arguments provided, can't continue") 109 | } 110 | } 111 | } 112 | 113 | object SchemaHelper { 114 | def createSchema(context: ClusterContext): Unit = { 115 | context.session.execute(s"CREATE KEYSPACE IF NOT EXISTS ${context.keyspace} WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1 };") 116 | 117 | context.session.execute(s""" 118 | CREATE TABLE IF NOT EXISTS ${context.keyspace}.event ( 119 | id uuid, 120 | campaign_id uuid, 121 | event_type text, 122 | value bigint, 123 | time timestamp, 124 | internal_id uuid, 125 | PRIMARY KEY((campaign_id, event_type), id, time, internal_id) 126 | ); 127 | """.stripMargin) 128 | } 129 | } 130 | 131 | protected case class Config(port: Int = 0, cassandraHost: String = "", keyspace: String = "", table: String = "") 132 | -------------------------------------------------------------------------------- /src/main/scala/io/datastrophic/microservice/Model.scala: -------------------------------------------------------------------------------- 1 | package io.datastrophic.microservice 2 | 3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 4 | import com.datastax.driver.core.Session 5 | import spray.json.DefaultJsonProtocol 6 | 7 | case class Event(id: String, campaignId: String, eventType: String, value: Long, timestamp: String) 8 | 9 | case class TotalsResponse(campaignId: String, total: Long) 10 | 11 | case class ClusterContext(session: Session, keyspace: String, table: String) 12 | 13 | trait Protocols extends SprayJsonSupport with DefaultJsonProtocol { 14 | implicit val eventFormat = jsonFormat5(Event.apply) 15 | implicit val campaignResponseFormat = jsonFormat2(TotalsResponse.apply) 16 | } 17 | --------------------------------------------------------------------------------