├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── akkeeper-api └── src │ ├── main │ └── scala │ │ └── akkeeper │ │ └── api │ │ ├── ApiJsonProtocol.scala │ │ ├── CommonApi.scala │ │ ├── ContainerApi.scala │ │ ├── ContainerDefinition.scala │ │ ├── DeploymentApi.scala │ │ ├── InstanceId.scala │ │ ├── InstanceInfo.scala │ │ ├── MasterApi.scala │ │ ├── MonitoringApi.scala │ │ └── RequestId.scala │ └── test │ └── scala │ └── akkeeper │ └── api │ └── JsonApiSpec.scala ├── akkeeper-common └── src │ └── main │ └── scala │ └── akkeeper │ └── common │ ├── AkkeeperException.scala │ ├── CliArguments.scala │ ├── LaunchMode.scala │ ├── StopInstance.scala │ └── config │ ├── AkkeeperResource.scala │ ├── Configs.scala │ ├── RichConfig.scala │ └── package.scala ├── akkeeper-examples ├── config │ └── ping.conf └── src │ └── main │ ├── resources │ ├── application.conf │ └── log4j.properties │ └── scala │ └── akkeeper │ └── examples │ └── PingActor.scala ├── akkeeper-launcher └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── akkeeper │ │ └── launcher │ │ ├── LaunchArguments.scala │ │ ├── Launcher.scala │ │ ├── LauncherMain.scala │ │ └── yarn │ │ ├── YarnLauncher.scala │ │ └── YarnLauncherException.scala │ └── test │ └── scala │ └── akkeeper │ └── launcher │ ├── LauncherMainSpec.scala │ └── yarn │ └── YarnLauncherSpec.scala ├── akkeeper-yarn └── src │ ├── main │ └── scala │ │ └── akkeeper │ │ └── yarn │ │ ├── KerberosTicketRenewer.scala │ │ ├── LocalResourceNames.scala │ │ ├── YarnLocalResourceManager.scala │ │ ├── YarnUtils.scala │ │ └── client │ │ ├── YarnLauncherClient.scala │ │ └── YarnMasterClient.scala │ └── test │ ├── resources │ └── application-container-test.conf │ └── scala │ └── akkeeper │ └── yarn │ ├── YarnLocalResourceManagerSpec.scala │ └── YarnUtilsSpec.scala ├── akkeeper └── src │ ├── main │ ├── resources │ │ ├── log4j.properties │ │ └── reference.conf │ └── scala │ │ └── akkeeper │ │ ├── address │ │ └── package.scala │ │ ├── container │ │ ├── ContainerInstanceArguments.scala │ │ ├── ContainerInstanceMain.scala │ │ └── service │ │ │ └── ContainerInstanceService.scala │ │ ├── deploy │ │ ├── DeployClient.scala │ │ └── yarn │ │ │ ├── YarnApplicationMaster.scala │ │ │ ├── YarnApplicationMasterConfig.scala │ │ │ └── YarnMasterException.scala │ │ ├── master │ │ ├── MasterArguments.scala │ │ ├── MasterMain.scala │ │ ├── MasterRunner.scala │ │ ├── route │ │ │ ├── BaseController.scala │ │ │ ├── ContainerController.scala │ │ │ ├── ControllerComposite.scala │ │ │ ├── DeployController.scala │ │ │ ├── MasterController.scala │ │ │ └── MonitoringController.scala │ │ └── service │ │ │ ├── ContainerService.scala │ │ │ ├── DeployService.scala │ │ │ ├── HeartbeatService.scala │ │ │ ├── InternalMessages.scala │ │ │ ├── MasterService.scala │ │ │ ├── MasterServiceException.scala │ │ │ ├── MemberAutoDownService.scala │ │ │ ├── MonitoringService.scala │ │ │ ├── RemoteServiceFactory.scala │ │ │ └── RequestTrackingService.scala │ │ └── storage │ │ ├── InstanceStorage.scala │ │ ├── RecordAlreadyExistsException.scala │ │ ├── RecordNotFoundException.scala │ │ ├── Storage.scala │ │ └── zookeeper │ │ ├── ZookeeperClient.scala │ │ ├── ZookeeperClientConfig.scala │ │ ├── ZookeeperException.scala │ │ └── async │ │ ├── AsyncZookeeperClient.scala │ │ ├── BaseZookeeperStorage.scala │ │ └── ZookeeperInstanceStorage.scala │ └── test │ ├── resources │ └── application-container-test.conf │ └── scala │ └── akkeeper │ ├── ActorTestUtils.scala │ ├── AwaitMixin.scala │ ├── container │ └── service │ │ ├── ContainerInstanceServiceSpec.scala │ │ └── TestUserActor.scala │ ├── deploy │ └── yarn │ │ └── YarnApplicationMasterSpec.scala │ ├── master │ ├── route │ │ ├── ContainerControllerSpec.scala │ │ ├── ControllerCompositeSpec.scala │ │ ├── DeployControllerSpec.scala │ │ ├── MasterControllerSpec.scala │ │ ├── MonitoringControllerSpec.scala │ │ └── RestTestUtils.scala │ └── service │ │ ├── ContainerServiceSpec.scala │ │ ├── DeployServiceSpec.scala │ │ ├── HeartbeatServiceSpec.scala │ │ ├── MasterServiceSpec.scala │ │ ├── MemberAutoDownServiceSpec.scala │ │ └── MonitoringServiceSpec.scala │ └── storage │ └── zookeeper │ ├── ZookeeperClientConfigSpec.scala │ └── async │ ├── AsyncZookeeperStorageSpec.scala │ ├── ZookeeperBaseSpec.scala │ └── ZookeeperInstanceStorageSpec.scala ├── bin └── akkeeper-submit ├── build.sbt ├── docs └── rest.md ├── project ├── ReferenceMergeStrategy.scala ├── build.properties └── plugins.sbt └── scalastyle-config.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | project/project 3 | project/target 4 | **/target 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | script: 4 | - sbt scalastyle && sbt test:scalastyle 5 | - sbt ++$TRAVIS_SCALA_VERSION clean coverage test coverageReport && sbt coverageAggregate 6 | 7 | matrix: 8 | include: 9 | - jdk: openjdk8 10 | scala: 2.11.11 11 | - jdk: openjdk8 12 | scala: 2.12.6 13 | 14 | before_cache: 15 | - find "$HOME/.sbt/" -name '*.lock' -print0 | xargs -0 rm 16 | - find "$HOME/.ivy2/" -name 'ivydata-*.properties' -print0 | xargs -0 rm 17 | 18 | cache: 19 | directories: 20 | - $HOME/.ivy2/cache 21 | - $HOME/.sbt 22 | 23 | after_success: 24 | - sbt coveralls 25 | 26 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/ApiJsonProtocol.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | trait ApiJsonProtocol extends CommonApiJsonProtocol 19 | with ContainerApiJsonProtocol 20 | with DeployApiJsonProtocol 21 | with MonitoringApiJsonProtocol 22 | 23 | object ApiJsonProtocol extends ApiJsonProtocol 24 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/CommonApi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import spray.json._ 19 | 20 | /** The base interface for all messages that include unique request ID. */ 21 | trait WithRequestId { 22 | def requestId: RequestId 23 | } 24 | 25 | /** A response message that represents an arbitrary general error. 26 | * It's usually used when some unexpected or unhandled error occurs. 27 | * 28 | * @param requestId the ID of the original request that caused an operation 29 | * that failed. 30 | * @param cause the reason of the failure. 31 | */ 32 | case class OperationFailed(requestId: RequestId, cause: Throwable) extends WithRequestId 33 | 34 | /** JSON (de)serialization for the Common API requests and responses. */ 35 | trait CommonApiJsonProtocol extends DefaultJsonProtocol with RequestIdJsonProtocol { 36 | 37 | implicit val operationFailedWriter = new RootJsonWriter[OperationFailed] { 38 | override def write(obj: OperationFailed): JsValue = 39 | JsObject( 40 | "requestId" -> obj.requestId.toJson, 41 | "message" -> JsString(obj.cause.getMessage), 42 | "stackTrace" -> JsArray( 43 | obj.cause.getStackTrace.map(s => JsString(s.toString)): _* 44 | ) 45 | ) 46 | } 47 | } 48 | 49 | object CommonApiJsonProtocol extends CommonApiJsonProtocol 50 | 51 | class AutoRequestIdFormat[T <: WithRequestId](original: JsonFormat[T]) 52 | extends RootJsonFormat[T] with RequestIdJsonProtocol { 53 | 54 | override def read(json: JsValue): T = { 55 | val jsObject = json.asJsObject 56 | val fields = jsObject.fields 57 | val updatedJsObject = 58 | if (!fields.contains("requestId")) { 59 | jsObject.copy(fields + ("requestId" -> RequestId().toJson)) 60 | } else { 61 | jsObject 62 | } 63 | original.read(updatedJsObject) 64 | } 65 | 66 | override def write(obj: T): JsValue = original.write(obj) 67 | } 68 | 69 | object AutoRequestIdFormat { 70 | def apply[T <: WithRequestId](original: JsonFormat[T]): RootJsonFormat[T] = { 71 | new AutoRequestIdFormat(original) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/ContainerDefinition.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import spray.json._ 19 | 20 | /** A launch context that represents an actor that has to be launched 21 | * in container. 22 | * 23 | * @param name the name of the actor. 24 | * @param fqn the full qualified name of the actor, i.e. "com.myproject.MyActor". 25 | */ 26 | case class ActorLaunchContext(name: String, fqn: String) 27 | 28 | /** Contains all the necessary information to launch a new instance in container. 29 | * 30 | * @param name the unique name of the container. 31 | * @param cpus the number of CPUs that will be allocated for each 32 | * instance of this container. 33 | * @param memory the amount of RAM in MB that will be allocated for 34 | * each instance of this container. 35 | * @param actors the list of actors that will be deployed. See [[ActorLaunchContext]]. 36 | * @param jvmArgs the list of JVM arguments that will be passed to each instance 37 | * of this container. I.e. "-Xmx1g" 38 | * @param jvmProperties the map of JVM properties that will be passed to each 39 | * instance of this container. This map reflects the 40 | * behaviour of the "-Dproperty=value" JVM argument. 41 | * @param environment the map of environment variables that will passed to each 42 | * instance of this container. The key of the map is an environment 43 | * variable name and the value is a variable's value. 44 | */ 45 | case class ContainerDefinition(name: String, 46 | cpus: Int, 47 | memory: Int, 48 | actors: Seq[ActorLaunchContext], 49 | jvmArgs: Seq[String] = Seq.empty, 50 | jvmProperties: Map[String, String] = Map.empty, 51 | environment: Map[String, String] = Map.empty) 52 | 53 | trait ContainerDefinitionJsonProtocol extends DefaultJsonProtocol { 54 | implicit val actorLaunchContextFormat = jsonFormat2(ActorLaunchContext.apply) 55 | implicit val containerDefinitionFormat = jsonFormat7(ContainerDefinition.apply) 56 | } 57 | 58 | object ContainerDefinitionJsonProtocol extends ContainerDefinitionJsonProtocol 59 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/DeploymentApi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import spray.json.DefaultJsonProtocol 19 | 20 | /** A request to deploy (launch) new instance(s) of the given container. 21 | * The possible responses are: 22 | * 23 | * - [[SubmittedInstances]] - if the deployment attempt was successful. 24 | * - [[ContainerNotFound]] - if the container with requested name was not found. 25 | * - [[OperationFailed]] - if error occurred. 26 | * 27 | * @param name the name of the container that will be deployed. 28 | * @param quantity the number of instances that will be deployed. 29 | * @param jvmArgs the list of JVM arguments that override/extend JVM arguments 30 | * from the container definition. 31 | * @param properties the map of properties, where the key - is a property name, 32 | * and the value is a property value. Overrides/extends properties 33 | * from the container definition. 34 | * @param requestId the optional request ID. If not specified a random 35 | * ID will be generated. 36 | */ 37 | case class DeployContainer(name: String, quantity: Int, 38 | jvmArgs: Option[Seq[String]] = None, 39 | properties: Option[Map[String, String]] = None, 40 | requestId: RequestId = RequestId()) extends WithRequestId 41 | 42 | /** A response that indicates a successful deployment of new instances. 43 | * Note: this response only indicates the successful allocation of containers and doesn't 44 | * imply that actors are launched successfully too. 45 | * 46 | * @param requestId the ID of the original request. 47 | * @param containerName the name of the container that has been deployed. 48 | * @param instanceIds the list of IDs of newly launched instances. 49 | */ 50 | case class SubmittedInstances(requestId: RequestId, containerName: String, 51 | instanceIds: Seq[InstanceId]) extends WithRequestId 52 | 53 | /** JSON (de)serialization for the Deploy API requests and responses. */ 54 | trait DeployApiJsonProtocol extends DefaultJsonProtocol 55 | with RequestIdJsonProtocol with InstanceIdJsonProtocol { 56 | 57 | implicit val deployContainerFormat = AutoRequestIdFormat(jsonFormat5(DeployContainer)) 58 | implicit val deployedInstancesFormat = jsonFormat3(SubmittedInstances) 59 | } 60 | 61 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/InstanceId.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import java.util.UUID 19 | 20 | import spray.json._ 21 | 22 | /** Represents a unique ID of the instance. 23 | * 24 | * @param containerName the name of the container this instance 25 | * belongs to. 26 | * @param uuid the unique ID of the instance. 27 | */ 28 | case class InstanceId(containerName: String, uuid: UUID) { 29 | override def toString: String = containerName + "-" + uuid.toString 30 | } 31 | 32 | object InstanceId { 33 | def apply(containerName: String): InstanceId = InstanceId(containerName, UUID.randomUUID()) 34 | def fromString(str: String): InstanceId = { 35 | val split = str.split("-", 2) 36 | val (containerName, uuid) = (split(0), split(1)) 37 | InstanceId(containerName, UUID.fromString(uuid)) 38 | } 39 | } 40 | 41 | trait InstanceIdJsonProtocol extends DefaultJsonProtocol { 42 | implicit val instanceIdFormat = new JsonFormat[InstanceId] { 43 | override def write(obj: InstanceId): JsValue = JsString(obj.toString) 44 | override def read(json: JsValue): InstanceId = InstanceId.fromString(json.convertTo[String]) 45 | } 46 | } 47 | 48 | object InstanceIdJsonProtocol extends InstanceIdJsonProtocol 49 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/InstanceInfo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import spray.json._ 19 | 20 | /** Represents a status of the instance. */ 21 | sealed trait InstanceStatus { 22 | /** String representation of the status. */ 23 | def value: String 24 | } 25 | case object InstanceDeploying extends InstanceStatus { val value = "DEPLOYING" } 26 | case object InstanceDeployFailed extends InstanceStatus { val value = "DEPLOY_FAILED" } 27 | case object InstanceLaunching extends InstanceStatus { val value = "LAUNCHING" } 28 | case object InstanceUp extends InstanceStatus { val value = "UP" } 29 | case object InstanceUnreachable extends InstanceStatus { val value = "UNREACHABLE" } 30 | object InstanceStatus { 31 | def fromStringOption(status: String): Option[InstanceStatus] = { 32 | status.toUpperCase match { 33 | case InstanceDeploying.value => Some(InstanceDeploying) 34 | case InstanceDeployFailed.value => Some(InstanceDeployFailed) 35 | case InstanceLaunching.value => Some(InstanceLaunching) 36 | case InstanceUp.value => Some(InstanceUp) 37 | case InstanceUnreachable.value => Some(InstanceUnreachable) 38 | case other => None 39 | } 40 | } 41 | 42 | def fromString(status: String): InstanceStatus = { 43 | fromStringOption(status).getOrElse( 44 | throw new IllegalArgumentException(s"Unexpected instance status $status")) 45 | } 46 | } 47 | 48 | final case class InstanceAddress(protocol: String, system: String, host: Option[String], port: Option[Int]) 49 | 50 | final case class InstanceUniqueAddress(address: InstanceAddress, longUid: Long) 51 | 52 | /** A complete information about the instance. 53 | * 54 | * @param instanceId the unique ID of the instance. 55 | * @param status the instance's status. 56 | * @param containerName the name of the container this instance belongs to. 57 | * @param roles the list of the instance's Akka roles. 58 | * @param address the address of the instance. 59 | * @param actors the list of user actors that are available on this instance. 60 | * @param extra the key value properties with additional instance specific data. 61 | */ 62 | case class InstanceInfo private[akkeeper] (instanceId: InstanceId, status: InstanceStatus, 63 | containerName: String, roles: Set[String], 64 | address: Option[InstanceUniqueAddress], actors: Set[String], 65 | extra: Map[String, String] = Map.empty) 66 | 67 | object InstanceInfo { 68 | private def createWithStatus(instanceId: InstanceId, status: InstanceStatus): InstanceInfo = { 69 | InstanceInfo(instanceId, status, instanceId.containerName, Set.empty, None, Set.empty) 70 | } 71 | 72 | private[akkeeper] def deploying(instanceId: InstanceId): InstanceInfo = { 73 | createWithStatus(instanceId, InstanceDeploying) 74 | } 75 | 76 | private[akkeeper] def deployFailed(instanceId: InstanceId): InstanceInfo = { 77 | createWithStatus(instanceId, InstanceDeployFailed) 78 | } 79 | 80 | private[akkeeper] def launching(instanceId: InstanceId): InstanceInfo = { 81 | createWithStatus(instanceId, InstanceLaunching) 82 | } 83 | } 84 | 85 | trait InstanceInfoJsonProtocol extends DefaultJsonProtocol with InstanceIdJsonProtocol { 86 | implicit val instanceAddressFormat = jsonFormat4(InstanceAddress) 87 | implicit val instanceUniqueAddressFormat = jsonFormat2(InstanceUniqueAddress) 88 | 89 | implicit val instanceStatusFormat = new JsonFormat[InstanceStatus] { 90 | override def read(json: JsValue): InstanceStatus = { 91 | InstanceStatus.fromString(json.convertTo[String]) 92 | } 93 | override def write(obj: InstanceStatus): JsValue = { 94 | JsString(obj.value) 95 | } 96 | } 97 | 98 | implicit val instanceInfoFormat = jsonFormat7(InstanceInfo.apply) 99 | } 100 | 101 | object InstanceInfoJsonProtocol extends InstanceInfoJsonProtocol 102 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/MasterApi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | /** A command that terminates the Akkeeper Master instance. */ 19 | case object TerminateMaster 20 | 21 | /** A heartbeat message to prevent Akkeeper Master from termination. */ 22 | case object Heartbeat 23 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/MonitoringApi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import spray.json.DefaultJsonProtocol 19 | 20 | /** The base interface for all requests related to Monitoring API. */ 21 | sealed trait InstanceRequest extends WithRequestId 22 | 23 | /** A request to retrieve the information about the instance using its ID. 24 | * The possible responses are: 25 | * 26 | * - [[InstanceInfoResponse]] - contains information about the requested instance. 27 | * - [[InstanceNotFound]] - if the request instance was not found. 28 | * - [[OperationFailed]] - if other error occurred. 29 | * 30 | * @param instanceId the ID of the instance. 31 | * @param requestId the optional request ID. If not specified a random 32 | * ID will be generated. 33 | */ 34 | case class GetInstance(instanceId: InstanceId, 35 | requestId: RequestId = RequestId()) extends InstanceRequest 36 | 37 | /** A request to retrieve the IDs of all existing instances. 38 | * The possible responses are: 39 | * 40 | * - [[InstancesList]] - contains the list of existing instances. 41 | * - [[OperationFailed]] - if error occurred. 42 | * 43 | * @param requestId the optional request ID. If not specified a random 44 | * ID will be generated. 45 | */ 46 | case class GetInstances(requestId: RequestId = RequestId()) extends InstanceRequest 47 | 48 | /** A request to find the instance IDs that match specific requirements. 49 | * The possible responses are: 50 | * 51 | * - [[InstancesList]] - contains the list of requested instances. 52 | * - [[OperationFailed]] - if error occurred. 53 | * 54 | * @param roles the list of roles the requested instance must have. 55 | * An empty list means any roles. Note: an instance must 56 | * include all roles enumerated in this list in order to 57 | * meet the search requirements. 58 | * @param containerName the name of the container the requested instance 59 | * must belong to. If not specified instance with any 60 | * container will match. 61 | * @param statuses the list of desired statuses. 62 | * @param requestId the optional request ID. If not specified a random 63 | * ID will be generated. 64 | */ 65 | case class GetInstancesBy(roles: Set[String], 66 | containerName: Option[String], 67 | statuses: Set[InstanceStatus], 68 | requestId: RequestId = RequestId()) extends InstanceRequest 69 | 70 | /** A request to terminate a running instance. 71 | * The possible responses are: 72 | * 73 | * - [[InstanceTerminated]] - of the instance has been terminated successfully. 74 | * - [[OperationFailed]] - if error occurred. 75 | * 76 | * @param instanceId the ID of the instance that has to be terminated. 77 | * @param requestId the optional request ID. If not specified a random 78 | * ID will be generated. 79 | */ 80 | case class TerminateInstance(instanceId: InstanceId, 81 | requestId: RequestId = RequestId()) extends InstanceRequest 82 | 83 | /** The base interface for all responses related to Monitoring API. */ 84 | sealed trait InstanceResponse extends WithRequestId 85 | 86 | /** A response that contains an information about the requested instance. 87 | * This is a result of the [[GetInstance]] operation. 88 | * 89 | * @param requestId the ID of the original request. 90 | * @param info the information about the requested instance. See [[InstanceInfo]]. 91 | */ 92 | case class InstanceInfoResponse(requestId: RequestId, 93 | info: InstanceInfo) extends InstanceResponse 94 | 95 | /** A response that contains the list of IDs of existing instances. 96 | * This is a result of the [[GetInstances]] operation. 97 | * 98 | * @param requestId the ID of the original request. 99 | * @param instanceIds the list of instance IDs. 100 | */ 101 | case class InstancesList(requestId: RequestId, 102 | instanceIds: Seq[InstanceId]) extends InstanceResponse 103 | 104 | /** A response that indicates that the request instance was not found. 105 | * 106 | * @param requestId the ID of the original request. 107 | * @param instanceId the requested instance ID. 108 | */ 109 | case class InstanceNotFound(requestId: RequestId, 110 | instanceId: InstanceId) extends InstanceResponse 111 | 112 | /** A response that indicates the successful termination of the instance. 113 | * This is a result of the [[TerminateInstance]] operation. 114 | * 115 | * @param requestId the ID of the original request. 116 | * @param instanceId the ID of the terminated intance. 117 | */ 118 | case class InstanceTerminated(requestId: RequestId, 119 | instanceId: InstanceId) extends InstanceResponse 120 | 121 | /** JSON (de)serialization for the Monitoring API requests and responses. */ 122 | trait MonitoringApiJsonProtocol extends DefaultJsonProtocol 123 | with InstanceIdJsonProtocol with RequestIdJsonProtocol 124 | with InstanceInfoJsonProtocol { 125 | 126 | implicit val getInstanceFormat = AutoRequestIdFormat(jsonFormat2(GetInstance)) 127 | implicit val getInstancesFormat = AutoRequestIdFormat(jsonFormat1(GetInstances)) 128 | implicit val getInstancesByFormat = AutoRequestIdFormat(jsonFormat4(GetInstancesBy)) 129 | implicit val terminateInstancesFormat = AutoRequestIdFormat(jsonFormat2(TerminateInstance)) 130 | 131 | implicit val instanceInfoResponseFormat = jsonFormat2(InstanceInfoResponse) 132 | implicit val instanceListFormat = jsonFormat2(InstancesList) 133 | implicit val instanceNotFoundFormat = jsonFormat2(InstanceNotFound) 134 | implicit val instanceTerminatedFormat = jsonFormat2(InstanceTerminated) 135 | } 136 | -------------------------------------------------------------------------------- /akkeeper-api/src/main/scala/akkeeper/api/RequestId.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import java.util.UUID 19 | 20 | import spray.json._ 21 | 22 | /** The unique ID of the API request. 23 | * 24 | * @param uuid the unique ID. 25 | */ 26 | case class RequestId(uuid: UUID) { 27 | override def toString: String = uuid.toString 28 | } 29 | 30 | object RequestId { 31 | def apply(): RequestId = RequestId(UUID.randomUUID()) 32 | } 33 | 34 | trait RequestIdJsonProtocol extends DefaultJsonProtocol { 35 | implicit val requestIdFormat = new JsonFormat[RequestId] { 36 | 37 | override def write(obj: RequestId): JsValue = JsString(obj.toString) 38 | 39 | override def read(json: JsValue): RequestId = RequestId(UUID.fromString(json.convertTo[String])) 40 | } 41 | } 42 | 43 | object RequestIdJsonProtocol extends RequestIdJsonProtocol 44 | -------------------------------------------------------------------------------- /akkeeper-api/src/test/scala/akkeeper/api/JsonApiSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.api 17 | 18 | import org.scalatest.{FlatSpec, Matchers} 19 | import spray.json._ 20 | 21 | class JsonApiSpec extends FlatSpec with Matchers with ApiJsonProtocol { 22 | 23 | def testJson[T: JsonFormat](expected: T): Unit = { 24 | val jsonString = expected.toJson.compactPrint 25 | val actual = jsonString.parseJson.convertTo[T] 26 | actual shouldBe expected 27 | } 28 | 29 | "JSON API" should "(de)serialize Monitoring API" in { 30 | // scalastyle:off line.size.limit 31 | testJson(GetInstance(InstanceId("container"))) 32 | testJson(GetInstances()) 33 | testJson(GetInstancesBy(roles = Set("role"), containerName = Some("container"), statuses = Set(InstanceUp))) 34 | testJson(GetInstancesBy(roles = Set.empty, containerName = None, statuses = Set.empty)) 35 | testJson(TerminateInstance(InstanceId("container"))) 36 | testJson(InstanceInfoResponse(RequestId(), InstanceInfo.deploying(InstanceId("container")))) 37 | testJson(InstancesList(RequestId(), Seq(InstanceId("container")))) 38 | testJson(InstanceNotFound(RequestId(), InstanceId("container"))) 39 | testJson(InstanceTerminated(RequestId(), InstanceId("container"))) 40 | // scalastyle:on line.size.limit 41 | } 42 | 43 | it should "(de)serialize Deploy API" in { 44 | testJson(DeployContainer("container", 1, Some(Seq("arg")), Some(Map("prop" -> "value")))) 45 | testJson(SubmittedInstances(RequestId(), "container", Seq(InstanceId("container")))) 46 | } 47 | 48 | it should "deserialize Deploy Container and generate request ID" in { 49 | val deployJson = 50 | """ 51 | |{ "name": "container", "quantity": 1 } 52 | """.stripMargin 53 | val deployContainer = deployJson.parseJson.convertTo[DeployContainer] 54 | deployContainer.name shouldBe "container" 55 | deployContainer.quantity shouldBe 1 56 | } 57 | 58 | it should "(de)serialize Container API" in { 59 | val memory = 1024 60 | val actorLaunchContext = ActorLaunchContext("name", "fqn") 61 | val definition = ContainerDefinition("container", 1, memory, 62 | Seq(actorLaunchContext), Seq("arg"), Map("prop" -> "value"), 63 | Map("envprop" -> "another_value")) 64 | 65 | testJson(CreateContainer(definition)) 66 | testJson(UpdateContainer(definition)) 67 | testJson(GetContainer("container")) 68 | testJson(GetContainers()) 69 | testJson(DeleteContainer("container")) 70 | testJson(ContainersList(RequestId(), Seq("container"))) 71 | testJson(ContainerGetResult(RequestId(), definition)) 72 | testJson(ContainerNotFound(RequestId(), "container")) 73 | testJson(ContainerAlreadyExists(RequestId(), "container")) 74 | testJson(ContainerCreated(RequestId(), "container")) 75 | testJson(ContainerUpdated(RequestId(), "container")) 76 | testJson(ContainerDeleted(RequestId(), "container")) 77 | } 78 | 79 | it should "(de)serialize Common API" in { 80 | val operationFailed = OperationFailed(RequestId(), new Exception("fail")) 81 | val jsonPayload = operationFailed.toJson 82 | val actualMessage = jsonPayload.asJsObject.fields("message").convertTo[String] 83 | val actualRequestId = jsonPayload.asJsObject.fields("requestId").convertTo[String] 84 | actualMessage shouldBe "fail" 85 | actualRequestId shouldBe operationFailed.requestId.toString 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/AkkeeperException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.common 17 | 18 | class AkkeeperException(msg: String) extends Exception(msg) 19 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/CliArguments.scala: -------------------------------------------------------------------------------- 1 | package akkeeper.common 2 | 3 | private[akkeeper] object CliArguments { 4 | val AkkeeperJarArg = "akkeeperJar" 5 | val InstanceIdArg = "instanceId" 6 | val AppIdArg = "appId" 7 | val ConfigArg = "config" 8 | val MasterAddressArg = "masterAddress" 9 | val ActorLaunchContextsArg = "actorLaunchContexts" 10 | val PrincipalArg = "principal" 11 | } 12 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/LaunchMode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.common 17 | 18 | private[akkeeper] sealed abstract class LaunchMode(val value: String) 19 | private[akkeeper] case object YarnLaunchMode extends LaunchMode("yarn") 20 | 21 | private[akkeeper] object LaunchMode { 22 | def fromString(str: String): LaunchMode = str match { 23 | case YarnLaunchMode.value => YarnLaunchMode 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/StopInstance.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.common 17 | 18 | case object StopInstance 19 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/config/AkkeeperResource.scala: -------------------------------------------------------------------------------- 1 | package akkeeper.common.config 2 | 3 | import java.net.URI 4 | 5 | final case class AkkeeperResource(uri: URI, localPath: String, archive: Boolean = false) 6 | 7 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/config/Configs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.common.config 17 | 18 | import java.net.URI 19 | import java.time.{Duration => JavaDuration} 20 | import java.util.concurrent.TimeUnit 21 | 22 | import akkeeper.api.{ActorLaunchContext, ContainerDefinition} 23 | import akkeeper.common.config.ConfigUtils._ 24 | import com.typesafe.config.Config 25 | 26 | import scala.collection.JavaConverters._ 27 | import scala.concurrent.duration._ 28 | 29 | private[akkeeper] final class AkkeeperConfig(akkeeperConfig: Config) { 30 | lazy val containers: Seq[ContainerDefinition] = { 31 | if (akkeeperConfig.hasPath("containers")) { 32 | val configContainers = akkeeperConfig.getConfigList("containers").asScala 33 | configContainers.map(ConfigUtils.containerDefinitionFromConfig) 34 | } else { 35 | Seq.empty 36 | } 37 | } 38 | 39 | lazy val globalResources: Seq[AkkeeperResource] = { 40 | if (akkeeperConfig.hasPath("global-resources")) { 41 | akkeeperConfig.getConfigList("global-resources").asScala.map(akkeeperResourceFromConfig) 42 | } else { 43 | Seq.empty 44 | } 45 | } 46 | } 47 | 48 | private[akkeeper] final class AkkeeperAkkaConfig(akkeeperAkkaConfig: Config) { 49 | lazy val actorSystemName: String = akkeeperAkkaConfig.getString("system-name") 50 | lazy val port: Int = akkeeperAkkaConfig.getInt("port") 51 | lazy val seedNodesNum: Int = akkeeperAkkaConfig.getInt("seed-nodes-num") 52 | lazy val joinClusterTimeout: FiniteDuration = akkeeperAkkaConfig.getDuration("join-cluster-timeout") 53 | lazy val useAkkaCluster: Boolean = akkeeperAkkaConfig.getBoolean("use-akka-cluster") 54 | } 55 | 56 | private[akkeeper] final class MasterConfig(masterConfig: Config) { 57 | lazy val heartbeat: HeartbeatConfig = new HeartbeatConfig(masterConfig.getConfig("heartbeat")) 58 | lazy val instanceListTimeout: FiniteDuration = 59 | masterConfig.getDuration("instance-list-timeout") 60 | } 61 | 62 | private[akkeeper] final class HeartbeatConfig(heartbeatConfig: Config) { 63 | lazy val enabled: Boolean = heartbeatConfig.getBoolean("enabled") 64 | lazy val timeout: FiniteDuration = heartbeatConfig.getDuration("timeout") 65 | lazy val missedLimit: Int = heartbeatConfig.getInt("missed-limit") 66 | } 67 | 68 | private[akkeeper] final class MonitoringConfig(monitoringConfig: Config) { 69 | lazy val launchTimeout: FiniteDuration = monitoringConfig.getDuration("launch-timeout") 70 | lazy val instanceListRefreshInterval: FiniteDuration = 71 | monitoringConfig.getDuration("instance-list-refresh-interval") 72 | } 73 | 74 | private[akkeeper] final class LauncherConfig(launcherConfig: Config) { 75 | lazy val timeout: Option[Duration] = { 76 | if (launcherConfig.hasPath("timeout")) { 77 | Some(launcherConfig.getDuration("timeout", TimeUnit.SECONDS).seconds) 78 | } else { 79 | None 80 | } 81 | } 82 | } 83 | 84 | private[akkeeper] final class YarnConfig(yarnConfig: Config) { 85 | lazy val applicationName: String = yarnConfig.getString("application-name") 86 | lazy val maxAttempts: Int = yarnConfig.getInt("max-attempts") 87 | lazy val clientThreads: Int = yarnConfig.getInt("client-threads") 88 | lazy val masterCpus: Int = yarnConfig.getInt("master.cpus") 89 | lazy val masterMemory: Int = yarnConfig.getInt("master.memory") 90 | lazy val masterJvmArgs: Seq[String] = yarnConfig.getStringList("master.jvm.args").asScala 91 | lazy val stagingDirectory: Option[String] = 92 | if (yarnConfig.hasPath("staging-directory")) { 93 | Some(yarnConfig.getString("staging-directory")) 94 | } else { 95 | None 96 | } 97 | } 98 | 99 | private[akkeeper] final class RestConfig(restConfig: Config) { 100 | lazy val port: Int = restConfig.getInt("port") 101 | lazy val portMaxAttempts: Int = restConfig.getInt("port-max-attempts") 102 | lazy val requestTimeout: FiniteDuration = restConfig.getDuration("request-timeout") 103 | } 104 | 105 | private[akkeeper] final class KerberosConfig(kerberosConfig: Config) { 106 | lazy val ticketCheckInterval: Long = 107 | kerberosConfig.getDuration("ticket-check-interval", TimeUnit.MILLISECONDS) 108 | } 109 | 110 | object ConfigUtils { 111 | implicit def javaDuration2ScalaDuration(value: JavaDuration): FiniteDuration = { 112 | Duration.fromNanos(value.toNanos) 113 | } 114 | 115 | private[config] def actorLaunchContextFromConfig(config: Config): ActorLaunchContext = { 116 | ActorLaunchContext(name = config.getString("name"), fqn = config.getString("fqn")) 117 | } 118 | 119 | private[config] def containerDefinitionFromConfig(config: Config): ContainerDefinition = { 120 | val actorsConfig = config.getConfigList("actors").asScala 121 | val actors = actorsConfig.map(actorLaunchContextFromConfig) 122 | ContainerDefinition( 123 | name = config.getString("name"), 124 | cpus = config.getInt("cpus"), 125 | memory = config.getInt("memory"), 126 | actors = actors, 127 | jvmArgs = config.getListOfStrings("jvm-args"), 128 | jvmProperties = config.getMapOfStrings("properties"), 129 | environment = config.getMapOfStrings("environment") 130 | ) 131 | } 132 | 133 | private[config] def akkeeperResourceFromConfig(config: Config): AkkeeperResource = { 134 | AkkeeperResource( 135 | new URI(config.getString("uri")), 136 | config.getString("local-path"), 137 | config.getBoolean("archive") 138 | ) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/config/RichConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.common.config 17 | 18 | import com.typesafe.config.{Config, ConfigValueFactory} 19 | 20 | import scala.collection.JavaConverters._ 21 | 22 | 23 | private[akkeeper] final class RichConfig(config: Config) { 24 | 25 | // Configs 26 | def akkeeper: AkkeeperConfig = new AkkeeperConfig(config.getConfig("akkeeper")) 27 | def akkeeperAkka: AkkeeperAkkaConfig = new AkkeeperAkkaConfig(config.getConfig("akkeeper.akka")) 28 | def master: MasterConfig = new MasterConfig(config.getConfig("akkeeper.master")) 29 | def monitoring: MonitoringConfig = new MonitoringConfig(config.getConfig("akkeeper.monitoring")) 30 | def launcher: LauncherConfig = new LauncherConfig(config.getConfig("akkeeper.launcher")) 31 | def yarn: YarnConfig = new YarnConfig(config.getConfig("akkeeper.yarn")) 32 | def zookeeper: Config = config.getConfig("akkeeper.zookeeper") 33 | def rest: RestConfig = new RestConfig(config.getConfig("akkeeper.api.rest")) 34 | def kerberos: KerberosConfig = new KerberosConfig(config.getConfig("akkeeper.kerberos")) 35 | 36 | // Other 37 | def withMasterRole: Config = { 38 | config.withValue("akka.cluster.roles", 39 | ConfigValueFactory.fromIterable(Seq("akkeeperMaster").asJava)) 40 | } 41 | 42 | def withMasterPort: Config = { 43 | config.withValue("akka.remote.netty.tcp.port", ConfigValueFactory.fromAnyRef(akkeeperAkka.port)) 44 | } 45 | 46 | def withPrincipalAndKeytab(principal: String, keytab: String): Config = { 47 | config 48 | .withValue("akka.kerberos.principal", ConfigValueFactory.fromAnyRef(principal)) 49 | .withValue("akka.kerberos.keytab", ConfigValueFactory.fromAnyRef(keytab)) 50 | } 51 | 52 | // Utils 53 | def getMapOfStrings(path: String): Map[String, String] = { 54 | if (config.hasPath(path)) { 55 | config.getConfig(path).entrySet().asScala.map(entry => { 56 | entry.getKey -> entry.getValue.unwrapped().asInstanceOf[String] 57 | }).toMap 58 | } else { 59 | Map.empty 60 | } 61 | } 62 | 63 | def getListOfStrings(path: String): Seq[String] = { 64 | if (config.hasPath(path)) { 65 | config.getStringList(path).asScala 66 | } else { 67 | Seq.empty 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /akkeeper-common/src/main/scala/akkeeper/common/config/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.common 17 | 18 | import com.typesafe.config.Config 19 | 20 | package object config { 21 | implicit private[akkeeper] def enrichConfig(config: Config): RichConfig = new RichConfig(config) 22 | } 23 | -------------------------------------------------------------------------------- /akkeeper-examples/config/ping.conf: -------------------------------------------------------------------------------- 1 | akkeeper { 2 | containers = [ 3 | { 4 | name = "pingContainer" 5 | actors = [ 6 | { 7 | name = "pingService" 8 | fqn = "akkeeper.examples.PingActor" 9 | } 10 | ] 11 | cpus = 1 12 | memory = 1024 13 | jvm-args = [ "-Xmx1G" ] 14 | properties { 15 | ping-app.response-value = "Akkeeper" 16 | akka.cluster.roles.0 = "ping" 17 | } 18 | } 19 | ] 20 | instances = [ 21 | { 22 | name = "pingContainer" 23 | quantity = 1 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /akkeeper-examples/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | remote { 3 | netty { 4 | tcp.port = 0 5 | } 6 | } 7 | } 8 | 9 | ping-app { 10 | response-value = "default" 11 | } 12 | -------------------------------------------------------------------------------- /akkeeper-examples/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG, console 2 | 3 | log4j.appender.console=org.apache.log4j.ConsoleAppender 4 | log4j.appender.console.Target=System.out 5 | log4j.appender.console.threshold=DEBUG 6 | log4j.appender.console.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{3} [%t] [%X{akkaSource}]: %m%n 8 | 9 | log4j.logger.org.apache=ERROR 10 | log4j.logger.akka.cluster=INFO 11 | log4j.logger.akka.serialization=ERROR 12 | -------------------------------------------------------------------------------- /akkeeper-examples/src/main/scala/akkeeper/examples/PingActor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.examples 17 | 18 | import akka.actor.Actor 19 | 20 | case object Ping 21 | case class Pong(value: String) 22 | 23 | class PingActor extends Actor { 24 | 25 | private val responseValue = context.system.settings.config.getString("ping-app.response-value") 26 | 27 | override def receive: Receive = { 28 | case Ping => sender() ! Pong(s"Hello $responseValue") 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /akkeeper-launcher/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | akkeeper { 2 | 3 | yarn { 4 | 5 | # The YARN application name. 6 | application-name = "AkkeeperApplication" 7 | 8 | master { 9 | # The number of CPU cores that will be allocated for the 10 | # Akkeeper Application Master container. 11 | cpus = 1 12 | 13 | # The amount of RAM in MB that will be allocated for the 14 | # Akkeeper Application Master cotnainer. 15 | memory = 2048 16 | 17 | # The list of JVM arguments and flags that will be used 18 | # to launch the Akkeeper Application Master instance. 19 | jvm.args = ["-Xmx2g"] 20 | } 21 | 22 | # The maximum number of attempts for the Akkeeper applciation on YARN. 23 | max-attempts = 2 24 | 25 | # The number of threads that are used for communication with YARN RM and NM. 26 | client-threads = 5 27 | } 28 | 29 | launcher { 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /akkeeper-launcher/src/main/scala/akkeeper/launcher/LaunchArguments.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.launcher 17 | 18 | import java.net.URI 19 | 20 | import akkeeper.common.config.AkkeeperResource 21 | import akkeeper.launcher.LaunchArguments._ 22 | import com.typesafe.config.Config 23 | 24 | final case class LaunchArguments(akkeeperJarPath: URI = new URI("."), 25 | userJar: URI = new URI("."), 26 | otherJars: Seq[URI] = Seq.empty, 27 | resources: Seq[URI] = Seq.empty, 28 | globalResources: Seq[AkkeeperResource] = Seq.empty, 29 | masterJvmArgs: Seq[String] = Seq.empty, 30 | userConfig: Option[Config] = None, 31 | pollInterval: Long = DefaultPollInterval, 32 | yarnQueue: Option[String] = None, 33 | principal: Option[String] = None, 34 | keytab: Option[URI] = None) 35 | 36 | object LaunchArguments { 37 | val DefaultPollInterval = 1000 38 | } 39 | -------------------------------------------------------------------------------- /akkeeper-launcher/src/main/scala/akkeeper/launcher/Launcher.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.launcher 17 | 18 | import akkeeper.launcher.yarn.YarnLauncher 19 | import akkeeper.yarn.client.YarnLauncherClient 20 | import com.typesafe.config.Config 21 | import org.apache.hadoop.conf.Configuration 22 | import org.apache.hadoop.yarn.conf.YarnConfiguration 23 | 24 | import scala.concurrent.{ExecutionContext, Future} 25 | 26 | final case class LaunchResult(appId: String, masterHost: String) 27 | 28 | /** Launcher for the Akkeeper application. */ 29 | trait Launcher { 30 | 31 | /** Launches the Akkeeper application and returns a launch result. 32 | * 33 | * @param config the app configuration. 34 | * @param args the launch arguments. 35 | * @return the wrapped result that contains an ID of the 36 | * submitted application and the address of the node where 37 | * Akkeeper Master is running. 38 | */ 39 | def launch(config: Config, args: LaunchArguments): Future[LaunchResult] 40 | } 41 | 42 | object Launcher { 43 | def createYarnLauncher(yarnConfig: YarnConfiguration) 44 | (implicit context: ExecutionContext): Launcher = { 45 | new YarnLauncher(yarnConfig, () => new YarnLauncherClient) 46 | } 47 | 48 | def createYarnLauncher(yarnConfig: Configuration) 49 | (implicit context: ExecutionContext): Launcher = { 50 | createYarnLauncher(new YarnConfiguration(yarnConfig)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /akkeeper-launcher/src/main/scala/akkeeper/launcher/LauncherMain.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.launcher 17 | 18 | import java.io.File 19 | import java.net.URI 20 | 21 | import akkeeper.BuildInfo 22 | import akkeeper.common.config._ 23 | import akkeeper.yarn.YarnUtils 24 | import com.typesafe.config.ConfigFactory 25 | import scopt.OptionParser 26 | 27 | import scala.concurrent.Await 28 | import scala.concurrent.ExecutionContext.Implicits.global 29 | import scala.concurrent.duration._ 30 | import scala.util.control.NonFatal 31 | 32 | object LauncherMain extends App { 33 | 34 | private[akkeeper] def transformUri(uri: URI): URI = { 35 | val uriStr = uri.toString 36 | if (Option(uri.getScheme).isEmpty) { 37 | // If the scheme is not specified, use the "file" scheme as a default choice. 38 | if (uriStr.startsWith("/")) { 39 | // Absolute path. 40 | new URI("file://" + uriStr) 41 | } else { 42 | // Relative path. 43 | new URI("file://" + new File(uriStr).getAbsolutePath) 44 | } 45 | } else { 46 | uri 47 | } 48 | } 49 | 50 | private val optParser = new OptionParser[LaunchArguments](BuildInfo.name) { 51 | head(BuildInfo.name, BuildInfo.version) 52 | 53 | opt[URI]("akkeeperJar").required().action((v, c) => { 54 | c.copy(akkeeperJarPath = transformUri(v)) 55 | }).text("Path to the Akkeeper fat Jar.") 56 | 57 | opt[Seq[URI]]("jars").valueName(",,...").action((v, c) => { 58 | c.copy(otherJars = v.map(transformUri)) 59 | }).text("A comma-separated list of additional Jar files that have to be included into " + 60 | "the container's classpath.") 61 | 62 | opt[Seq[URI]]("resources").valueName(",,...").action((v, c) => { 63 | c.copy(resources = v.map(transformUri)) 64 | }).text("A comma-separated list of resource files that have to be distributed within a cluster.") 65 | 66 | opt[Seq[String]]("masterJvmArgs").valueName(",,...").action((v, c) => { 67 | c.copy(masterJvmArgs = v) 68 | }).text("Extra JVM arguments for the Akeeper master.") 69 | 70 | opt[String]("queue").valueName("").action((v, c) => { 71 | c.copy(yarnQueue = Some(v)) 72 | }).text("The YARN queue (default: 'default')") 73 | 74 | opt[String]("principal").valueName("principal").action((v, c) => { 75 | c.copy(principal = Some(v)) 76 | }).text("Principal to be used to login to KDC.") 77 | 78 | opt[URI]("keytab").valueName("").action((v, c) => { 79 | c.copy(keytab = Some(transformUri(v))) 80 | }).text("The full path to the file that contains the keytab for the principal specified above.") 81 | 82 | opt[File]("config").valueName("").action((v, c) => { 83 | c.copy(userConfig = Some(ConfigFactory.parseFile(v))) 84 | }).text("The path to the custom configuration file.") 85 | 86 | arg[URI]("").required().action((x, c) => { 87 | c.copy(userJar = transformUri(x)) 88 | }).text("The path to the user Jar file.") 89 | } 90 | 91 | private val DefaultLauncherTimeout = 90 seconds 92 | 93 | private def runYarn(launcherArgs: LaunchArguments): Unit = { 94 | val config = launcherArgs.userConfig 95 | .map(c => c.withFallback(ConfigFactory.load())) 96 | .getOrElse(ConfigFactory.load()) 97 | 98 | launcherArgs.principal.foreach( 99 | YarnUtils.loginFromKeytab(_, launcherArgs.keytab.get.toString) 100 | ) 101 | 102 | val launcherTimeout = config.launcher.timeout.getOrElse(DefaultLauncherTimeout) 103 | 104 | val launcher = Launcher.createYarnLauncher(YarnUtils.getYarnConfiguration) 105 | val launchResult = launcher.launch(config, launcherArgs) 106 | Await.result(launchResult, launcherTimeout) 107 | } 108 | 109 | def run(launcherArgs: LaunchArguments): Unit = { 110 | runYarn(launcherArgs) 111 | } 112 | 113 | try { 114 | optParser.parse(args, LaunchArguments()).foreach(run) 115 | } catch { 116 | case NonFatal(e) => 117 | e.printStackTrace() 118 | sys.exit(1) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /akkeeper-launcher/src/main/scala/akkeeper/launcher/yarn/YarnLauncherException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.launcher.yarn 17 | 18 | import akkeeper.common.AkkeeperException 19 | 20 | case class YarnLauncherException(msg: String) extends AkkeeperException(msg) 21 | -------------------------------------------------------------------------------- /akkeeper-launcher/src/test/scala/akkeeper/launcher/LauncherMainSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.launcher 17 | 18 | import java.net.URI 19 | 20 | import org.scalatest.{FlatSpec, Matchers} 21 | 22 | class LauncherMainSpec extends FlatSpec with Matchers { 23 | 24 | "Launcher Main" should "transform the input URI" in { 25 | val workingDir = System.getProperty("user.dir") + "/" 26 | LauncherMain.transformUri(new URI("./local/path/to.jar")) shouldBe 27 | new URI("file://" + workingDir + "./local/path/to.jar") 28 | LauncherMain.transformUri(new URI("local/path/to.jar")) shouldBe 29 | new URI("file://" + workingDir + "local/path/to.jar") 30 | 31 | LauncherMain.transformUri(new URI("/absolute/local/path/to.jar")) shouldBe 32 | new URI("file:///absolute/local/path/to.jar") 33 | 34 | LauncherMain.transformUri(new URI("file:///absolute/local/path/to.jar")) shouldBe 35 | new URI("file:///absolute/local/path/to.jar") 36 | 37 | LauncherMain.transformUri(new URI("hdfs:///absolute/local/path/to.jar")) shouldBe 38 | new URI("hdfs:///absolute/local/path/to.jar") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/main/scala/akkeeper/yarn/KerberosTicketRenewer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn 17 | 18 | import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit} 19 | 20 | import org.apache.hadoop.security.UserGroupInformation 21 | import org.slf4j.LoggerFactory 22 | 23 | /** Periodically checks whether the kerberos ticket has been expired, and renews 24 | * it if necessary. 25 | * 26 | * @param user the user whose credentials has to be checked. 27 | * @param checkInterval the ticket validation interval. 28 | */ 29 | final class KerberosTicketRenewer(user: UserGroupInformation, checkInterval: Long) { 30 | 31 | def this(user: UserGroupInformation) = this(user, KerberosTicketRenewer.DefaultInterval) 32 | 33 | private val logger = LoggerFactory.getLogger(classOf[KerberosTicketRenewer]) 34 | private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() 35 | 36 | def start(): Unit = { 37 | scheduler.scheduleAtFixedRate(new Runnable { 38 | override def run(): Unit = user.checkTGTAndReloginFromKeytab() 39 | }, checkInterval, checkInterval, TimeUnit.MILLISECONDS) 40 | logger.info("Kerberos Ticket Renewer started successfully") 41 | } 42 | 43 | def stop(): Unit = { 44 | scheduler.shutdown() 45 | logger.info("Kerberos Ticket Renewer stopped") 46 | } 47 | } 48 | 49 | object KerberosTicketRenewer { 50 | val DefaultInterval = 30000 // 30 seconds. 51 | } 52 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/main/scala/akkeeper/yarn/LocalResourceNames.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn 17 | 18 | private[akkeeper] object LocalResourceNames { 19 | val AkkeeperJarName = "akkeeper.jar" 20 | val UserJarName = "user.jar" 21 | val UserConfigName = "user_config.conf" 22 | val ApplicationConfigName = "application.conf" 23 | val ActorLaunchContextsName = "actors.json" 24 | val KeytabName = "akkeeper.keytab" 25 | 26 | val ExtraJarsDirName = "jars" 27 | val ResourcesDirName = "resources" 28 | } 29 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/main/scala/akkeeper/yarn/YarnLocalResourceManager.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn 17 | 18 | import java.io.{Closeable, InputStream} 19 | 20 | import org.apache.commons.io.IOUtils 21 | import org.apache.hadoop.conf.Configuration 22 | import org.apache.hadoop.fs.{FileStatus, FileSystem, Path} 23 | import org.apache.hadoop.yarn.api.records._ 24 | import org.apache.hadoop.yarn.util.ConverterUtils 25 | 26 | private[akkeeper] final class YarnLocalResourceManager(conf: Configuration, 27 | stagingDir: String) { 28 | 29 | private val stagingDirPath: Path = new Path(stagingDir) 30 | 31 | private def withStream[S <: Closeable, R](s: => S)(f: S => R): R = { 32 | val stream = s 33 | try { 34 | f(stream) 35 | } finally { 36 | stream.close() 37 | } 38 | } 39 | 40 | private def create(fs: FileSystem, 41 | status: FileStatus, 42 | localResourceType: LocalResourceType, 43 | localResourceVisibility: LocalResourceVisibility): LocalResource = { 44 | LocalResource.newInstance( 45 | ConverterUtils.getYarnUrlFromURI(fs.makeQualified(status.getPath).toUri), 46 | localResourceType, localResourceVisibility, 47 | status.getLen, status.getModificationTime 48 | ) 49 | } 50 | 51 | private def copyResourceToStagingDir(srcStream: InputStream, dstPath: String): Path = { 52 | val dstFs = stagingDirPath.getFileSystem(conf) 53 | val dst = new Path(stagingDirPath, dstPath) 54 | withStream(dstFs.create(dst)) { out => 55 | IOUtils.copy(srcStream, out) 56 | out.hsync() 57 | } 58 | dst 59 | } 60 | 61 | def getLocalResource( 62 | dstPath: Path, 63 | localResourceType: LocalResourceType = LocalResourceType.FILE, 64 | localResourceVisibility: LocalResourceVisibility = LocalResourceVisibility.APPLICATION 65 | ): LocalResource = { 66 | val dstFs = dstPath.getFileSystem(conf) 67 | val dstStatus = dstFs.getFileStatus(dstPath) 68 | create(dstFs, dstStatus, localResourceType, localResourceVisibility) 69 | } 70 | 71 | def uploadLocalResource(srcStream: InputStream, dstPath: String): LocalResource = { 72 | val dst = copyResourceToStagingDir(srcStream, dstPath) 73 | getLocalResource(dst) 74 | } 75 | 76 | def uploadLocalResource(srcPath: String, dstPath: String): LocalResource = { 77 | val path = new Path(srcPath) 78 | val srcFs = path.getFileSystem(conf) 79 | withStream(srcFs.open(path)) { srcStream => 80 | uploadLocalResource(srcStream, dstPath) 81 | } 82 | } 83 | 84 | def getLocalResourceFromStagingDir(dstPath: Path): LocalResource = { 85 | getLocalResource(new Path(stagingDirPath, dstPath)) 86 | } 87 | 88 | def getLocalResourceFromStagingDir(dstPath: String): LocalResource = { 89 | getLocalResourceFromStagingDir(new Path(dstPath)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/main/scala/akkeeper/yarn/YarnUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn 17 | 18 | import java.nio.ByteBuffer 19 | 20 | import org.apache.hadoop.conf.Configuration 21 | import org.apache.hadoop.fs.{FileSystem, Path} 22 | import org.apache.hadoop.io.DataOutputBuffer 23 | import org.apache.hadoop.mapred.Master 24 | import org.apache.hadoop.security.{Credentials, UserGroupInformation} 25 | import org.apache.hadoop.yarn.api.ApplicationConstants 26 | import org.apache.hadoop.yarn.api.ApplicationConstants.Environment 27 | import org.apache.hadoop.yarn.conf.YarnConfiguration 28 | import org.apache.hadoop.yarn.security.AMRMTokenIdentifier 29 | 30 | import scala.collection.JavaConverters._ 31 | 32 | 33 | private[akkeeper] object YarnUtils { 34 | 35 | private def buildClassPath(extraClassPath: Seq[String]): String = { 36 | val yarnBin = Environment.HADOOP_YARN_HOME.$$() + "/bin/yarn" 37 | val yarnClasspath = s"`$yarnBin classpath`" 38 | (LocalResourceNames.AkkeeperJarName +: yarnClasspath +: extraClassPath).mkString(":") 39 | } 40 | 41 | def buildCmd(mainClass: String, 42 | extraClassPath: Seq[String] = Seq.empty, 43 | jvmArgs: Seq[String] = Seq.empty, 44 | appArgs: Seq[String] = Seq.empty): List[String] = { 45 | val javaBin = List(Environment.JAVA_HOME.$$() + "/bin/java") 46 | val allJvmArgs = jvmArgs ++ List( 47 | "-cp", buildClassPath(extraClassPath) 48 | ) 49 | List("exec") ++ javaBin ++ allJvmArgs ++ List(mainClass) ++ appArgs ++ List( 50 | "1>", ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout", 51 | "2>", ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr") 52 | } 53 | 54 | def getHdfsConfiguration: Configuration = { 55 | val conf = new Configuration() 56 | sys.env.get("HADOOP_CONF_DIR").foreach(dir => { 57 | conf.addResource(new Path(dir, "core-site.xml")) 58 | conf.addResource(new Path(dir, "hdfs-site.xml")) 59 | }) 60 | conf 61 | } 62 | 63 | def getYarnConfiguration: YarnConfiguration = { 64 | val conf = new YarnConfiguration(getHdfsConfiguration) 65 | sys.env.get("YARN_CONF_DIR").foreach(dir => { 66 | conf.addResource(new Path(dir, "yarn-site.xml")) 67 | }) 68 | conf 69 | } 70 | 71 | def loginFromKeytab(principal: String, keytab: String): UserGroupInformation = { 72 | UserGroupInformation.setConfiguration(getYarnConfiguration) 73 | 74 | val initialUser = UserGroupInformation.getLoginUser 75 | val yarnTokens = initialUser.getTokens.asScala 76 | .filter(_.getKind == AMRMTokenIdentifier.KIND_NAME) 77 | 78 | UserGroupInformation.loginUserFromKeytab(principal, keytab) 79 | 80 | val loginUser = UserGroupInformation.getLoginUser 81 | yarnTokens.foreach(loginUser.addToken) 82 | loginUser 83 | } 84 | 85 | def obtainContainerTokens(stagingDir: String, 86 | hadoopConfig: Configuration): ByteBuffer = { 87 | val renewer = Master.getMasterPrincipal(hadoopConfig) 88 | 89 | val creds = new Credentials(UserGroupInformation.getCurrentUser.getCredentials) 90 | val hadoopFs = new Path(stagingDir) 91 | .getFileSystem(hadoopConfig) 92 | hadoopFs.addDelegationTokens(renewer, creds) 93 | 94 | val outstream = new DataOutputBuffer() 95 | creds.writeTokenStorageToStream(outstream) 96 | ByteBuffer.wrap(outstream.getData) 97 | } 98 | 99 | def defaultStagingDirectory(conf: Configuration): String = { 100 | new Path(FileSystem.get(conf).getHomeDirectory, ".akkeeper").toString 101 | } 102 | 103 | def appStagingDirectory(conf: Configuration, base: Option[String], appId: String): String = { 104 | new Path(base.getOrElse(defaultStagingDirectory(conf)), appId).toString 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/main/scala/akkeeper/yarn/client/YarnLauncherClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn.client 17 | 18 | import org.apache.hadoop.conf.Configuration 19 | import org.apache.hadoop.yarn.api.records._ 20 | import org.apache.hadoop.yarn.client.api.{YarnClient, YarnClientApplication} 21 | 22 | private[akkeeper] class YarnLauncherClient { 23 | private val yarnClient = YarnClient.createYarnClient() 24 | 25 | def init(configuration: Configuration): Unit = { 26 | yarnClient.init(configuration) 27 | } 28 | 29 | def start(): Unit = { 30 | yarnClient.start() 31 | } 32 | 33 | def stop(): Unit = { 34 | yarnClient.stop() 35 | } 36 | 37 | def getApplicationReport(appId: ApplicationId): ApplicationReport = { 38 | yarnClient.getApplicationReport(appId) 39 | } 40 | 41 | def createApplication(): YarnClientApplication = { 42 | yarnClient.createApplication() 43 | } 44 | 45 | def submitApplication(appContext: ApplicationSubmissionContext): ApplicationId = { 46 | yarnClient.submitApplication(appContext) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/main/scala/akkeeper/yarn/client/YarnMasterClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn.client 17 | 18 | import java.nio.ByteBuffer 19 | 20 | import org.apache.hadoop.yarn.api.protocolrecords.AllocateResponse 21 | import org.apache.hadoop.yarn.api.records._ 22 | import org.apache.hadoop.yarn.client.api.AMRMClient.ContainerRequest 23 | import org.apache.hadoop.yarn.client.api.{AMRMClient, NMClient} 24 | import org.apache.hadoop.yarn.conf.YarnConfiguration 25 | 26 | import scala.collection.JavaConverters._ 27 | 28 | private[akkeeper] class YarnMasterClient { 29 | private val amrmClient = AMRMClient.createAMRMClient[ContainerRequest]() 30 | private val nmClient = NMClient.createNMClient() 31 | 32 | def init(config: YarnConfiguration): Unit = { 33 | amrmClient.init(config) 34 | nmClient.init(config) 35 | } 36 | 37 | def start(): Unit = { 38 | amrmClient.start() 39 | nmClient.start() 40 | } 41 | 42 | def stop(): Unit = { 43 | nmClient.stop() 44 | amrmClient.stop() 45 | } 46 | 47 | def addContainerRequest(request: ContainerRequest): Unit = { 48 | amrmClient.addContainerRequest(request) 49 | } 50 | 51 | def allocate(progressIndicator: Float): AllocateResponse = { 52 | amrmClient.allocate(progressIndicator) 53 | } 54 | 55 | def releaseAssignedContainer(containerId: ContainerId): Unit = { 56 | amrmClient.releaseAssignedContainer(containerId) 57 | } 58 | 59 | def startContainer(container: Container, 60 | containerLaunchContext: ContainerLaunchContext): Map[String, ByteBuffer] = { 61 | nmClient.startContainer(container, containerLaunchContext).asScala.toMap 62 | } 63 | 64 | def registerApplicationMaster(appHostName: String, 65 | appHostPort: Int, 66 | appTrackingUrl: String): Unit = { 67 | amrmClient.registerApplicationMaster(appHostName, appHostPort, appTrackingUrl) 68 | } 69 | 70 | def unregisterApplicationMaster(appStatus: FinalApplicationStatus, 71 | appMessage: String, 72 | appTrackingUrl: String): Unit = { 73 | amrmClient.unregisterApplicationMaster(appStatus, appMessage, appTrackingUrl) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/test/resources/application-container-test.conf: -------------------------------------------------------------------------------- 1 | akkeeper { 2 | containers = [ 3 | { 4 | name = "container1" 5 | actors = [ 6 | { 7 | name = "actor1" 8 | fqn = "com.test.Actor1" 9 | }, 10 | { 11 | name = "actor2" 12 | fqn = "com.test.Actor2" 13 | } 14 | ], 15 | cpus = 1 16 | memory = 1024 17 | jvm-args = [ "-Xmx2G" ] 18 | properties { 19 | property = "value" 20 | } 21 | }, 22 | { 23 | name = "container2" 24 | actors = [ 25 | { 26 | name = "actor3" 27 | fqn = "com.test.Actor3" 28 | } 29 | ], 30 | cpus = 2 31 | memory = 2048 32 | environment { 33 | envProperty = "value" 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/test/scala/akkeeper/yarn/YarnLocalResourceManagerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn 17 | 18 | import java.io.File 19 | import java.util.UUID 20 | 21 | import org.apache.commons.io.IOUtils 22 | import org.apache.hadoop.conf.Configuration 23 | import org.apache.hadoop.fs.{FileSystem, Path} 24 | import org.apache.hadoop.yarn.api.records._ 25 | import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} 26 | 27 | class YarnLocalResourceManagerSpec extends FlatSpec with Matchers with BeforeAndAfterAll { 28 | 29 | private val hadoopConfig = new Configuration() 30 | private val stagingDir = s"/tmp/${UUID.randomUUID()}" 31 | private val hadoopFs = FileSystem.get(hadoopConfig) 32 | 33 | override protected def beforeAll(): Unit = { 34 | hadoopFs.mkdirs(new Path(stagingDir)) 35 | super.beforeAll() 36 | } 37 | 38 | override protected def afterAll(): Unit = { 39 | hadoopFs.delete(new Path(stagingDir), true) 40 | super.afterAll() 41 | } 42 | 43 | private def validateLocalResource(resource: LocalResource, expectedPath: String): Unit = { 44 | resource.getType shouldBe LocalResourceType.FILE 45 | resource.getVisibility shouldBe LocalResourceVisibility.APPLICATION 46 | resource.getResource.getFile shouldBe expectedPath 47 | resource.getResource.getScheme shouldBe "file" 48 | 49 | val file = new File(expectedPath) 50 | file.exists() shouldBe true 51 | } 52 | 53 | private def validateResourcePayload(expectedResource: String, 54 | actualResourcePath: String): Unit = { 55 | val expectedStream = getClass.getResourceAsStream(expectedResource) 56 | val actualStream = hadoopFs.open(new Path(actualResourcePath)) 57 | IOUtils.contentEquals(expectedStream, actualStream) shouldBe true 58 | } 59 | 60 | "YARN Local Resource Manager" should "create a local resource properly (using path)" in { 61 | val manager = new YarnLocalResourceManager(hadoopConfig, stagingDir) 62 | val resource = getClass.getResource("/application-container-test.conf").getPath 63 | val expectedFileName = UUID.randomUUID().toString 64 | val expectedPath = new Path(stagingDir, expectedFileName).toString 65 | 66 | val actualResult = manager.uploadLocalResource(resource, expectedFileName) 67 | validateLocalResource(actualResult, expectedPath) 68 | validateResourcePayload("/application-container-test.conf", expectedPath) 69 | } 70 | 71 | it should "create a local resource properly (using stream)" in { 72 | val manager = new YarnLocalResourceManager(hadoopConfig, stagingDir) 73 | val resource = getClass.getResourceAsStream("/application-container-test.conf") 74 | val expectedFileName = UUID.randomUUID().toString 75 | val expectedPath = new Path(stagingDir, expectedFileName).toString 76 | 77 | val actualResult = manager.uploadLocalResource(resource, expectedFileName) 78 | validateLocalResource(actualResult, expectedPath) 79 | validateResourcePayload("/application-container-test.conf", expectedPath) 80 | resource.close() 81 | } 82 | 83 | it should "create a local resource properly (from HDFS)" in { 84 | val manager = new YarnLocalResourceManager(hadoopConfig, stagingDir) 85 | val resource = getClass.getResource("/application-container-test.conf").getPath 86 | val expectedFileName = UUID.randomUUID().toString 87 | val expectedPath = new Path(stagingDir, expectedFileName).toString 88 | 89 | manager.uploadLocalResource(resource, expectedFileName) 90 | val newExpectedFileName = UUID.randomUUID().toString 91 | val newExpectedPath = new Path(stagingDir, newExpectedFileName).toString 92 | val actualResult = manager.uploadLocalResource(expectedPath, newExpectedFileName) 93 | validateLocalResource(actualResult, newExpectedPath) 94 | validateResourcePayload("/application-container-test.conf", newExpectedPath) 95 | } 96 | 97 | it should "return the existing local resource" in { 98 | val manager = new YarnLocalResourceManager(hadoopConfig, stagingDir) 99 | val resource = getClass.getResource("/application-container-test.conf").getPath 100 | val expectedFileName = UUID.randomUUID().toString 101 | val expectedPath = new Path(stagingDir, expectedFileName).toString 102 | 103 | manager.uploadLocalResource(resource, expectedFileName) 104 | val actualResult = manager.getLocalResourceFromStagingDir(expectedFileName) 105 | validateLocalResource(actualResult, expectedPath) 106 | validateResourcePayload("/application-container-test.conf", expectedPath) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /akkeeper-yarn/src/test/scala/akkeeper/yarn/YarnUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.yarn 17 | 18 | import org.scalatest.{FlatSpec, Matchers} 19 | 20 | class YarnUtilsSpec extends FlatSpec with Matchers { 21 | 22 | "YARN Utils" should "build a correct command" in { 23 | val expectedCmd = "exec {{JAVA_HOME}}/bin/java -Xmx1g " + 24 | "-cp akkeeper.jar:`{{HADOOP_YARN_HOME}}/bin/yarn classpath`:test1.jar:test2.jar " + 25 | "com.test.Main arg1 arg2 arg3 1> /stdout 2> /stderr" 26 | val mainClass = "com.test.Main" 27 | val classPath = Seq("test1.jar", "test2.jar") 28 | val jvmArgs = Seq("-Xmx1g") 29 | val appArgs = Seq("arg1", "arg2", "arg3") 30 | val cmd = YarnUtils.buildCmd(mainClass, classPath, jvmArgs, appArgs).mkString(" ") 31 | cmd shouldBe expectedCmd 32 | } 33 | 34 | it should "handle empty arguments correctly" in { 35 | val expectedCmd = "exec {{JAVA_HOME}}/bin/java " + 36 | "-cp akkeeper.jar:`{{HADOOP_YARN_HOME}}/bin/yarn classpath` " + 37 | " 1> /stdout 2> /stderr" 38 | val cmd = YarnUtils.buildCmd("", Seq.empty, Seq.empty, Seq.empty).mkString(" ") 39 | cmd shouldBe expectedCmd 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /akkeeper/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG, console 2 | 3 | log4j.appender.console=org.apache.log4j.ConsoleAppender 4 | log4j.appender.console.Target=System.out 5 | log4j.appender.console.threshold=DEBUG 6 | log4j.appender.console.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{3} [%t] [%X{akkaSource}]: %m%n 8 | 9 | log4j.logger.org.apache=ERROR 10 | log4j.logger.akka.cluster=INFO 11 | log4j.logger.akka.serialization=ERROR 12 | log4j.logger.akka.remote=ERROR 13 | -------------------------------------------------------------------------------- /akkeeper/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | loglevel = "DEBUG" 4 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 5 | 6 | actor { 7 | provider = "akka.cluster.ClusterActorRefProvider" 8 | } 9 | 10 | cluster { 11 | seed-node-timeout = 5s 12 | } 13 | } 14 | 15 | akkeeper { 16 | 17 | master { 18 | # The heartbeats are used when the Akkeeper application must be terminated 19 | # automatically in case when some external watchdog crashes or terminates. 20 | heartbeat { 21 | # Indicates whether to enable heartbeats. 22 | enabled = false 23 | 24 | # A heartbeat that hasn't been delivered within this timeout is considered "missed". 25 | timeout = 36s 26 | 27 | # The limit for a number of missed heartbeats after reaching which the Akkeeper application 28 | # automatically terminates. 29 | missed-limit = 3 30 | } 31 | 32 | # The timeout after which the Akkeeper Master gives up to wait for a list of active instances 33 | # and terminates with an error. This property is only used during the Akkeeper Master initialization 34 | # process. 35 | instance-list-timeout = 30s 36 | } 37 | 38 | monitoring { 39 | # The timeout after which an instance that hasn't been transitioned from the 40 | # launching state is considered dead. 41 | launch-timeout = 90s 42 | 43 | # The interval between instance list refreshment attempts. Used only when 44 | # the Akkeeper Master is rejoining the existing cluster. 45 | instance-list-refresh-interval = 45s 46 | } 47 | 48 | akka { 49 | 50 | # The Akka remote port for the Akkeeper Master instance. 51 | port = 0 52 | 53 | # The Akka System name. 54 | system-name = "AkkeeperSystem" 55 | 56 | # The number of seed nodes that should be used by the Akkeeper Master to 57 | # join the existing cluster. 58 | seed-nodes-num = 3 59 | 60 | # The timeout after which a node gives up its attempt to join a cluster. 61 | # On a master instance reaching this timeout means that the restarted master will start 62 | # a new cluster. 63 | join-cluster-timeout = 120s 64 | 65 | # Indicates whether Akkeeper Master and container instances should constitute an Akka Cluster. 66 | # Note: some features might not be supported when Akka Cluster is not enabled, while others 67 | # may perform less efficiently and/or create an extra load on third-party services like ZooKeeper. 68 | # Modify this value with caution. 69 | use-akka-cluster = true 70 | } 71 | 72 | api { 73 | rest { 74 | 75 | # The REST API port. 76 | port = 5050 77 | 78 | # The maximum number of attempts to bind to a next available port. 79 | port-max-attempts = 8 80 | 81 | # The client request timeout for the REST API. 82 | request-timeout = 30s 83 | } 84 | } 85 | 86 | yarn { 87 | 88 | # The YARN application name. 89 | application-name = "AkkeeperApplication" 90 | 91 | master { 92 | # The number of CPU cores that will be allocated for the 93 | # Akkeeper Application Master container. 94 | cpus = 1 95 | 96 | # The amount of RAM in MB that will be allocated for the 97 | # Akkeeper Application Master cotnainer. 98 | memory = 2048 99 | 100 | # The list of JVM arguments and flags that will be used 101 | # to launch the Akkeeper Application Master instance. 102 | jvm.args = ["-Xmx2g"] 103 | } 104 | 105 | # The maximum number of attempts for the Akkeeper applciation on YARN. 106 | max-attempts = 2 107 | 108 | # The number of threads that are used for communication with YARN RM and NM. 109 | client-threads = 5 110 | } 111 | 112 | kerberos { 113 | 114 | # Kerberos ticket check/update interval. 115 | ticket-check-interval = 60s 116 | } 117 | 118 | zookeeper { 119 | 120 | # The comma-separated list of ZK servers. 121 | servers = "" 122 | 123 | # The initial amount of time to wait between retries. 124 | connection-interval-ms = 3000 125 | 126 | # The maximum number of times to retry. 127 | max-retries = 8 128 | 129 | # The root ZK namespace for the Akkeeper application. 130 | namespace = "akkeeper" 131 | 132 | # If true, allow ZooKeeper client to enter read only mode in case 133 | # of a network partition. 134 | can-be-readonly = true 135 | 136 | # The number of ZK client threads. 137 | client-threads = 5 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/address/package.scala: -------------------------------------------------------------------------------- 1 | package akkeeper 2 | 3 | import akka.actor.Address 4 | import akka.cluster.UniqueAddress 5 | import akkeeper.api.{InstanceAddress, InstanceUniqueAddress} 6 | 7 | package object address { 8 | implicit def toAkkaAddress(addr: InstanceAddress): Address = { 9 | Address(addr.protocol, addr.system, addr.host, addr.port) 10 | } 11 | 12 | implicit def toInstanceAddress(addr: Address): InstanceAddress = { 13 | InstanceAddress(addr.protocol, addr.system, addr.host, addr.port) 14 | } 15 | 16 | implicit def toAkkaUniqueAddress(addr: InstanceUniqueAddress): UniqueAddress = { 17 | UniqueAddress(toAkkaAddress(addr.address), addr.longUid) 18 | } 19 | 20 | implicit def toInstanceUniqueAddress(addr: UniqueAddress): InstanceUniqueAddress = { 21 | InstanceUniqueAddress(toInstanceAddress(addr.address), addr.longUid) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/container/ContainerInstanceArguments.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.container 17 | 18 | import java.io.File 19 | 20 | import akka.actor.Address 21 | import akkeeper.api.InstanceId 22 | import com.typesafe.config.Config 23 | 24 | 25 | case class ContainerInstanceArguments(appId: String = "", 26 | instanceId: InstanceId = InstanceId("unknown"), 27 | masterAddress: Address = Address("none", "none"), 28 | actors: File = new File("."), 29 | userConfig: Option[Config] = None, 30 | principal: Option[String] = None) 31 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/container/ContainerInstanceMain.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.container 17 | 18 | import java.io.File 19 | 20 | import akka.actor.{ActorSystem, AddressFromURIString} 21 | import akkeeper.common._ 22 | import akkeeper.common.config._ 23 | import akkeeper.container.service.ContainerInstanceService 24 | import akkeeper.storage.InstanceStorageFactory 25 | import CliArguments._ 26 | import akkeeper.yarn.LocalResourceNames 27 | import com.typesafe.config.{Config, ConfigFactory} 28 | import scopt.OptionParser 29 | 30 | import scala.io.Source 31 | import scala.util.control.NonFatal 32 | import spray.json._ 33 | import akkeeper.BuildInfo 34 | import akkeeper.api.{ActorLaunchContext, ContainerDefinitionJsonProtocol, InstanceId} 35 | import akkeeper.storage.zookeeper.ZookeeperClientConfig 36 | 37 | import scala.concurrent.Await 38 | import scala.concurrent.duration.Duration 39 | 40 | object ContainerInstanceMain extends App with ContainerDefinitionJsonProtocol { 41 | 42 | private val appName: String = s"${BuildInfo.name}-instance" 43 | 44 | val optParser = new OptionParser[ContainerInstanceArguments](appName) { 45 | head(appName, BuildInfo.version) 46 | 47 | opt[String](AppIdArg).required().action((v, c) => { 48 | c.copy(appId = v) 49 | }).text("The ID of this application.") 50 | 51 | opt[String](InstanceIdArg).required().action((v, c) => { 52 | c.copy(instanceId = InstanceId.fromString(v)) 53 | }).text("The ID of this instance.") 54 | 55 | opt[String](MasterAddressArg).required().action((v, c) => { 56 | c.copy(masterAddress = AddressFromURIString.parse(v)) 57 | }).text("The address of the master instance.") 58 | 59 | opt[File](ActorLaunchContextsArg).required().action((v, c) => { 60 | c.copy(actors = v) 61 | }).text("The actor launch context in JSON format.") 62 | 63 | opt[File](ConfigArg).valueName("").optional().action((v, c) => { 64 | c.copy(userConfig = Some(ConfigFactory.parseFile(v))) 65 | }).text("The path to the configuration file.") 66 | 67 | opt[String](PrincipalArg).valueName("principal").optional().action((v, c) => { 68 | c.copy(principal = Some(v)) 69 | }).text("Principal to be used to login to KDC.") 70 | } 71 | 72 | def createInstanceConfig(instanceArgs: ContainerInstanceArguments): Config = { 73 | val baseConfig = instanceArgs.userConfig 74 | .map(_.withFallback(ConfigFactory.load())) 75 | .getOrElse(ConfigFactory.load()) 76 | instanceArgs.principal 77 | .map(p => baseConfig.withPrincipalAndKeytab(p, LocalResourceNames.KeytabName)) 78 | .getOrElse(baseConfig) 79 | } 80 | 81 | def run(instanceArgs: ContainerInstanceArguments): Unit = { 82 | val instanceConfig = createInstanceConfig(instanceArgs) 83 | val actorSystem = ActorSystem(instanceConfig.akkeeperAkka.actorSystemName, instanceConfig) 84 | 85 | val zkConfig = ZookeeperClientConfig.fromConfig(actorSystem.settings.config.zookeeper) 86 | val instanceStorage = InstanceStorageFactory(zkConfig.child(instanceArgs.appId)) 87 | 88 | val actorsJsonStr = Source.fromFile(instanceArgs.actors).getLines().mkString("\n") 89 | val actors = actorsJsonStr.parseJson.convertTo[Seq[ActorLaunchContext]] 90 | 91 | ContainerInstanceService.createLocal(actorSystem, actors, 92 | instanceStorage, instanceArgs.instanceId, instanceArgs.masterAddress, 93 | joinClusterTimeout = instanceConfig.akkeeperAkka.joinClusterTimeout, 94 | useAkkaCluster = instanceConfig.akkeeperAkka.useAkkaCluster) 95 | 96 | Await.result(actorSystem.whenTerminated, Duration.Inf) 97 | sys.exit(0) 98 | } 99 | 100 | try { 101 | optParser.parse(args, ContainerInstanceArguments()).foreach(run) 102 | } catch { 103 | case NonFatal(e) => 104 | e.printStackTrace() 105 | sys.exit(1) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/deploy/DeployClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.deploy 17 | 18 | import akkeeper.api.InstanceId 19 | import akkeeper.api.ContainerDefinition 20 | import akkeeper.deploy.yarn._ 21 | import akkeeper.yarn.client.YarnMasterClient 22 | 23 | import scala.concurrent.Future 24 | 25 | /** A client that is responsible for deploying new container instances. */ 26 | private[akkeeper] trait DeployClient { 27 | 28 | /** Starts the client. */ 29 | def start(): Unit 30 | 31 | /** Stops the client. */ 32 | def stop(): Unit 33 | 34 | /** Indicates that the client must be stopped because of the error. 35 | * 36 | * @param error the error. 37 | */ 38 | def stopWithError(error: Throwable): Unit 39 | 40 | /** Deploys new instances to a cluster. 41 | * 42 | * @param container the container definition that will be used to launch new instances. 43 | * See [[ContainerDefinition]]. 44 | * @param instances the list of instance IDs that will be deployed. The size of this list 45 | * determines the total number of instances that will be launched. 46 | * @return a collection of container objects that store the result of the deploy operation. 47 | * Each item in this list represents a result for a one particular instance. 48 | * See [[DeployResult]]. 49 | */ 50 | def deploy(container: ContainerDefinition, instances: Seq[InstanceId]): Seq[Future[DeployResult]] 51 | } 52 | 53 | /** A result of the deployment operation. Contains the ID of the instance to which this 54 | * result is related. 55 | */ 56 | private[akkeeper] sealed trait DeployResult { 57 | def instanceId: InstanceId 58 | } 59 | 60 | /** Indicates that the instance has been deployed successfully. */ 61 | private[akkeeper] case class DeploySuccessful(instanceId: InstanceId) extends DeployResult 62 | 63 | /** Indicates that the deployment process failed. */ 64 | private[akkeeper] case class DeployFailed(instanceId: InstanceId, 65 | e: Throwable) extends DeployResult 66 | 67 | private[akkeeper] trait DeployClientFactory[T] extends (T => DeployClient) 68 | 69 | private[akkeeper] object DeployClientFactory { 70 | 71 | implicit object YarnDeployClientFactory 72 | extends DeployClientFactory[YarnApplicationMasterConfig] { 73 | 74 | override def apply(config: YarnApplicationMasterConfig): DeployClient = { 75 | new YarnApplicationMaster(config, new YarnMasterClient) 76 | } 77 | } 78 | 79 | def apply[T: DeployClientFactory](config: T): DeployClient = { 80 | implicitly[T](config) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/deploy/yarn/YarnApplicationMasterConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.deploy.yarn 17 | 18 | import akka.actor.Address 19 | import com.typesafe.config.Config 20 | import org.apache.hadoop.yarn.conf.YarnConfiguration 21 | 22 | 23 | private[akkeeper] case class YarnApplicationMasterConfig(config: Config, 24 | yarnConf: YarnConfiguration, 25 | appId: String, 26 | selfAddress: Address, 27 | trackingUrl: String, 28 | principal: Option[String] = None) 29 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/deploy/yarn/YarnMasterException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.deploy.yarn 17 | 18 | import akkeeper.common.AkkeeperException 19 | 20 | case class YarnMasterException(msg: String) extends AkkeeperException(msg) 21 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/MasterArguments.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master 17 | 18 | import java.io.File 19 | 20 | private[akkeeper] case class MasterArguments(appId: String = "", 21 | config: Option[File] = None, 22 | principal: Option[String] = None) 23 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/MasterMain.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master 17 | 18 | import java.io.File 19 | 20 | import akkeeper.BuildInfo 21 | import akkeeper.common.CliArguments._ 22 | import scopt.OptionParser 23 | 24 | import scala.util.control.NonFatal 25 | 26 | object MasterMain extends App { 27 | 28 | private val appName: String = s"${BuildInfo.name}-master" 29 | 30 | val optParser = new OptionParser[MasterArguments](appName) { 31 | head(appName, BuildInfo.version) 32 | 33 | opt[String](AppIdArg).required().action((v, c) => { 34 | c.copy(appId = v) 35 | }).text("The ID of this application.") 36 | 37 | opt[File](ConfigArg).valueName("").optional().action((v, c) => { 38 | c.copy(config = Some(v)) 39 | }).text("The path to the custom configuration file.") 40 | 41 | opt[String](PrincipalArg).valueName("principal").action((v, c) => { 42 | c.copy(principal = Some(v)) 43 | }).text("Principal to be used to login to KDC.") 44 | } 45 | 46 | try { 47 | optParser.parse(args, MasterArguments()).foreach(args => { 48 | new YarnMasterRunner().run(args) 49 | sys.exit() 50 | }) 51 | } catch { 52 | case NonFatal(e) => 53 | e.printStackTrace() 54 | sys.exit(1) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/route/BaseController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.actor.ActorRef 19 | import akka.pattern.ask 20 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport 21 | import akka.http.scaladsl.model.StatusCode 22 | import akka.http.scaladsl.server.{Directives, Route} 23 | import akka.util.Timeout 24 | import akkeeper.api.CommonApiJsonProtocol 25 | import akkeeper.common.AkkeeperException 26 | import spray.json.RootJsonWriter 27 | 28 | import scala.collection.mutable 29 | import scala.reflect.ClassTag 30 | 31 | trait BaseController extends Directives with SprayJsonSupport with CommonApiJsonProtocol { 32 | 33 | def route: Route 34 | 35 | private val handlers: mutable.ArrayBuffer[PartialFunction[Any, Route]] = 36 | mutable.ArrayBuffer.empty 37 | 38 | final protected def registerHandler[T: RootJsonWriter: ClassTag](code: StatusCode): Unit = { 39 | handlers += { 40 | case v: T => 41 | complete(code -> v) 42 | } 43 | } 44 | 45 | final protected def applyHandlers: PartialFunction[Any, Route] = { 46 | handlers.reduce(_ orElse _) orElse { 47 | case other => 48 | val exception = new AkkeeperException(s"Unexpected response $other") 49 | failWith(exception) 50 | } 51 | } 52 | 53 | final protected def handleRequest(service: ActorRef, msg: Any) 54 | (implicit timeout: Timeout): Route = { 55 | val result = service ? msg 56 | onSuccess(result)(applyHandlers) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/route/ContainerController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.actor.ActorRef 19 | import akka.http.scaladsl.model.StatusCodes 20 | import akka.http.scaladsl.server.Route 21 | import akka.util.Timeout 22 | import akkeeper.api._ 23 | 24 | import scala.concurrent.ExecutionContext 25 | 26 | class ContainerController(service: ActorRef)(implicit dispatcher: ExecutionContext, 27 | timeout: Timeout) 28 | extends BaseController with ContainerApiJsonProtocol with ContainerDefinitionJsonProtocol { 29 | 30 | registerHandler[ContainerGetResult](StatusCodes.OK) 31 | registerHandler[ContainersList](StatusCodes.OK) 32 | registerHandler[ContainerNotFound](StatusCodes.NotFound) 33 | registerHandler[OperationFailed](StatusCodes.InternalServerError) 34 | 35 | override val route: Route = 36 | pathPrefix("containers") { 37 | path(Segment) { containerName => 38 | get { 39 | handleRequest(service, GetContainer(containerName)) 40 | } 41 | } ~ 42 | (pathEnd | pathSingleSlash) { 43 | get { 44 | handleRequest(service, GetContainers()) 45 | } 46 | } 47 | } 48 | } 49 | 50 | object ContainerController { 51 | def apply(service: ActorRef)(implicit dispatcher: ExecutionContext, 52 | timeout: Timeout): BaseController = { 53 | new ContainerController(service) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/route/ControllerComposite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.http.scaladsl.server.{PathMatchers, Route} 19 | import ControllerComposite._ 20 | 21 | case class ControllerComposite(basePath: String, 22 | controllers: Seq[BaseController]) extends BaseController { 23 | override val route: Route = pathPrefix(PathMatchers.separateOnSlashes(basePath)) { 24 | controllers.reduceLeft((a, b) => UnitController(a.route ~ b.route)).route 25 | } 26 | } 27 | 28 | object ControllerComposite { 29 | private case class UnitController(override val route: Route) extends BaseController 30 | } 31 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/route/DeployController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.actor.ActorRef 19 | import akka.http.scaladsl.model.StatusCodes 20 | import akka.http.scaladsl.server.Route 21 | import akka.util.Timeout 22 | import akkeeper.api._ 23 | import scala.concurrent.ExecutionContext 24 | 25 | class DeployController(service: ActorRef)(implicit dispatcher: ExecutionContext, 26 | timeout: Timeout) 27 | extends BaseController with DeployApiJsonProtocol with ContainerApiJsonProtocol { 28 | 29 | registerHandler[SubmittedInstances](StatusCodes.Accepted) 30 | registerHandler[ContainerNotFound](StatusCodes.NotFound) 31 | registerHandler[OperationFailed](StatusCodes.InternalServerError) 32 | 33 | override val route: Route = 34 | path("deploy") { 35 | post { 36 | entity(as[DeployContainer]) { deploy => 37 | handleRequest(service, deploy) 38 | } 39 | } 40 | } 41 | } 42 | 43 | object DeployController { 44 | def apply(service: ActorRef)(implicit dispatcher: ExecutionContext, 45 | timeout: Timeout): BaseController = { 46 | new DeployController(service) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/route/MasterController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.actor.ActorRef 19 | import akka.http.scaladsl.model.StatusCodes 20 | import akka.http.scaladsl.server.Route 21 | import akka.util.Timeout 22 | import akkeeper.api._ 23 | 24 | import scala.concurrent.ExecutionContext 25 | 26 | class MasterController(service: ActorRef)(implicit dispatcher: ExecutionContext, 27 | timeout: Timeout) extends BaseController { 28 | 29 | override val route: Route = 30 | pathPrefix("master") { 31 | path("terminate") { 32 | post { 33 | service ! TerminateMaster 34 | complete(StatusCodes.Accepted -> "") 35 | } 36 | } ~ 37 | path("heartbeat") { 38 | post { 39 | service ! Heartbeat 40 | complete(StatusCodes.OK -> "") 41 | } 42 | } 43 | } 44 | } 45 | 46 | object MasterController { 47 | def apply(service: ActorRef)(implicit dispatcher: ExecutionContext, 48 | timeout: Timeout): BaseController = { 49 | new MasterController(service) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/route/MonitoringController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.actor.ActorRef 19 | import akka.http.scaladsl.model.StatusCodes 20 | import akka.http.scaladsl.model.Uri.Path 21 | import akka.http.scaladsl.server.PathMatcher._ 22 | import akka.http.scaladsl.server.{PathMatcher1, Route} 23 | import akka.util.Timeout 24 | import akkeeper.api._ 25 | 26 | import scala.concurrent.ExecutionContext 27 | import scala.util.Try 28 | import MonitoringController._ 29 | 30 | class MonitoringController(service: ActorRef)(implicit dispatcher: ExecutionContext, 31 | timeout: Timeout) 32 | extends BaseController with MonitoringApiJsonProtocol { 33 | 34 | registerHandler[InstanceInfoResponse](StatusCodes.OK) 35 | registerHandler[InstancesList](StatusCodes.OK) 36 | registerHandler[InstanceNotFound](StatusCodes.NotFound) 37 | registerHandler[InstanceTerminated](StatusCodes.OK) 38 | 39 | override val route: Route = 40 | pathPrefix("instances") { 41 | path(InstanceIdMatcher) { instanceId => 42 | get { 43 | handleRequest(service, GetInstance(instanceId)) 44 | } ~ 45 | delete { 46 | handleRequest(service, TerminateInstance(instanceId)) 47 | } 48 | } ~ 49 | path(Segment) { _ => 50 | complete(StatusCodes.BadRequest -> "Invalid instance ID") 51 | } ~ 52 | (pathEnd | pathSingleSlash) { 53 | parameters('role.*, 'containerName.?, 'status.*) { (role, containerName, status) => 54 | val statuses = status.map(InstanceStatus.fromStringOption).flatten.toSet 55 | if (role.isEmpty && containerName.isEmpty && statuses.isEmpty) { 56 | handleRequest(service, GetInstances()) 57 | } else { 58 | handleRequest(service, GetInstancesBy(role.toSet, containerName, statuses)) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | object MonitoringController { 66 | def apply(service: ActorRef)(implicit dispatcher: ExecutionContext, 67 | timeout: Timeout): BaseController = { 68 | new MonitoringController(service) 69 | } 70 | 71 | private object InstanceIdMatcher extends PathMatcher1[InstanceId] { 72 | override def apply(v: Path): Matching[Tuple1[InstanceId]] = { 73 | if (!v.isEmpty) { 74 | val instanceId = Try(InstanceId.fromString(v.head.toString)) 75 | instanceId.map(id => Matched(Path.Empty, Tuple1(id))).getOrElse(Unmatched) 76 | } else { 77 | Unmatched 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/ContainerService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akka.actor._ 19 | import akkeeper.api._ 20 | import akkeeper.api.ContainerDefinition 21 | import akkeeper.common.config._ 22 | 23 | import scala.collection.mutable 24 | 25 | private[akkeeper] class ContainerService extends Actor with ActorLogging { 26 | 27 | private val containers: mutable.Map[String, ContainerDefinition] = mutable.Map.empty 28 | 29 | override def preStart(): Unit = { 30 | loadContainersFromConfig() 31 | log.info("Container service successfully initialized") 32 | super.preStart() 33 | } 34 | 35 | private def loadContainersFromConfig(): Unit = { 36 | val config = context.system.settings.config 37 | val configContainers = config.akkeeper.containers 38 | configContainers.foreach(c => containers.put(c.name, c)) 39 | } 40 | 41 | private def containerNotFound(requestId: RequestId, containerName: String): Any = { 42 | log.warning(s"Container with name $containerName was not found") 43 | ContainerNotFound(requestId, containerName) 44 | } 45 | 46 | private def getContainer(request: GetContainer): Any = { 47 | val requestId = request.requestId 48 | val containerName = request.name 49 | if (!containers.contains(containerName)) { 50 | containerNotFound(requestId, containerName) 51 | } else { 52 | ContainerGetResult(requestId, containers(containerName)) 53 | } 54 | } 55 | 56 | private def getContainers(request: GetContainers): Any = { 57 | ContainersList(request.requestId, containers.keys.toSeq) 58 | } 59 | 60 | override def receive: Receive = { 61 | case request: GetContainer => 62 | sender() ! getContainer(request) 63 | case request: GetContainers => 64 | sender() ! getContainers(request) 65 | case StopWithError(_) => 66 | log.error("Stopping the Container service because of external error") 67 | context.stop(self) 68 | } 69 | } 70 | 71 | object ContainerService extends RemoteServiceFactory { 72 | override val actorName = "containerService" 73 | 74 | private[akkeeper] def createLocal(factory: ActorRefFactory): ActorRef = { 75 | factory.actorOf(Props(classOf[ContainerService]), actorName) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/DeployService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akka.actor.{ActorRef, ActorRefFactory, Props} 19 | import akka.pattern.pipe 20 | import akkeeper.api._ 21 | import akkeeper.deploy._ 22 | import MonitoringService._ 23 | 24 | private[akkeeper] class DeployService(deployClient: DeployClient, 25 | containerService: ActorRef, 26 | monitoringService: ActorRef) extends RequestTrackingService { 27 | 28 | private implicit val dispatcher = context.dispatcher 29 | override protected val trackedMessages: Set[Class[_]] = Set(classOf[DeployContainer]) 30 | 31 | private var stopReason: Option[Throwable] = None 32 | 33 | private def deployInstances(request: DeployContainer, 34 | container: ContainerDefinition): SubmittedInstances = { 35 | val ids = (0 until request.quantity).map(_ => InstanceId(container.name)) 36 | val instanceInfos = ids.map(InstanceInfo.deploying) 37 | monitoringService ! InstancesUpdate(instanceInfos) 38 | 39 | val extendedContainer = container.copy( 40 | jvmArgs = request.jvmArgs.getOrElse(Seq.empty) ++ container.jvmArgs, 41 | jvmProperties = container.jvmProperties ++ request.properties.getOrElse(Map.empty)) 42 | 43 | val futures = deployClient.deploy(extendedContainer, ids) 44 | val logger = log 45 | futures.foreach(f => { 46 | f.map { 47 | case DeploySuccessful(id) => 48 | logger.debug(s"Instance $id deployed successfully") 49 | InstanceInfo.launching(id) 50 | case DeployFailed(id, e) => 51 | logger.error(e, s"Deployment of instance $id failed") 52 | InstanceInfo.deployFailed(id) 53 | }.pipeTo(monitoringService) 54 | }) 55 | SubmittedInstances(request.requestId, container.name, ids) 56 | } 57 | 58 | override def preStart(): Unit = { 59 | deployClient.start() 60 | log.info("Deploy service successfully initialized") 61 | super.preStart() 62 | } 63 | 64 | override def postStop(): Unit = { 65 | stopReason match { 66 | case Some(e) => deployClient.stopWithError(e) 67 | case None => deployClient.stop() 68 | } 69 | super.postStop() 70 | } 71 | 72 | override protected def serviceReceive: Receive = { 73 | case request: DeployContainer => 74 | // Before launching a new instance we should first 75 | // retrieve an information about the container. 76 | setOriginalSenderContext(request.requestId, request) 77 | containerService ! GetContainer(request.name, requestId = request.requestId) 78 | case ContainerGetResult(id, container) => 79 | // The information about the container was retrieved. 80 | // Now we can start the deployment process. 81 | val originalRequest = originalSenderContextAs[DeployContainer](id) 82 | val result = deployInstances(originalRequest, container) 83 | sendAndRemoveOriginalSender(result) 84 | case other: WithRequestId => 85 | // Some unexpected response from the container service (likely error). 86 | // Just send it as is to the original sender. 87 | sendAndRemoveOriginalSender(other) 88 | case StopWithError(e) => 89 | log.error("Stopping the Deploy service because of external error") 90 | stopReason = Some(e) 91 | context.stop(self) 92 | } 93 | } 94 | 95 | object DeployService extends RemoteServiceFactory { 96 | override val actorName = "deployService" 97 | 98 | private[akkeeper] def createLocal(factory: ActorRefFactory, 99 | deployClient: DeployClient, 100 | containerService: ActorRef, 101 | monitoringService: ActorRef): ActorRef = { 102 | factory.actorOf(Props(classOf[DeployService], deployClient, 103 | containerService, monitoringService), actorName) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/HeartbeatService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akka.actor.{Actor, ActorLogging, ActorRef, ActorRefFactory, Cancellable, Props} 19 | import akkeeper.api.{Heartbeat, TerminateMaster} 20 | import akkeeper.common.config._ 21 | 22 | import scala.concurrent.duration.FiniteDuration 23 | import HeartbeatService._ 24 | 25 | private[akkeeper] final class HeartbeatService extends Actor with ActorLogging { 26 | 27 | private implicit val dispatcher = context.dispatcher 28 | 29 | private val heartbeatConfig: HeartbeatConfig = context.system.settings.config.master.heartbeat 30 | private val timeoutTaskInterval: FiniteDuration = heartbeatConfig.timeout 31 | 32 | private var missedHeartbeatsCounter: Int = 0 33 | private var timeoutTaskCancellable: Option[Cancellable] = None 34 | 35 | override def preStart(): Unit = { 36 | scheduleHeartbeatTimeout() 37 | } 38 | 39 | override def postStop(): Unit = { 40 | timeoutTaskCancellable.foreach(_.cancel()) 41 | } 42 | 43 | override def receive: Receive = { 44 | case Heartbeat => onHeartbeat() 45 | case HeartbeatTimeout => onHeartbeatTimeout() 46 | } 47 | 48 | private def onHeartbeat(): Unit = { 49 | log.debug("Heartbeat received") 50 | timeoutTaskCancellable.foreach(_.cancel()) 51 | missedHeartbeatsCounter = 0 52 | scheduleHeartbeatTimeout() 53 | } 54 | 55 | private def onHeartbeatTimeout(): Unit = { 56 | missedHeartbeatsCounter += 1 57 | log.warning(s"Heartbeat timeout. $missedHeartbeatsCounter heartbeats have been missed so far") 58 | if (missedHeartbeatsCounter < heartbeatConfig.missedLimit) { 59 | scheduleHeartbeatTimeout() 60 | } else { 61 | log.error(s"The maximum number of missed heartbeats (${heartbeatConfig.missedLimit}) " + 62 | "has been exceeded. Terminating the master") 63 | context.parent ! TerminateMaster 64 | context.stop(self) 65 | } 66 | } 67 | 68 | private def scheduleHeartbeatTimeout(): Unit = { 69 | val task = context.system.scheduler.scheduleOnce(timeoutTaskInterval, self, HeartbeatTimeout) 70 | timeoutTaskCancellable = Some(task) 71 | } 72 | } 73 | 74 | object HeartbeatService { 75 | val HeartbeatServiceName = "heartbeatService" 76 | 77 | private case object HeartbeatTimeout 78 | 79 | private[akkeeper] def createLocal(factory: ActorRefFactory): ActorRef = { 80 | factory.actorOf(Props(classOf[HeartbeatService]), HeartbeatServiceName) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/InternalMessages.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akkeeper.api.InstanceId 19 | 20 | 21 | private[service] final case class StopWithError(reason: Throwable) 22 | private[service] final case class RefreshInstancesList(instances: Seq[InstanceId]) 23 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/MasterServiceException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akkeeper.common.AkkeeperException 19 | 20 | case class MasterServiceException(msg: String) extends AkkeeperException(msg) 21 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/MemberAutoDownService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import java.util.concurrent.atomic.AtomicInteger 19 | 20 | import akka.actor.{Actor, ActorLogging, ActorRef, ActorRefFactory, Props, Status} 21 | import akka.pattern.pipe 22 | import akka.cluster.{Cluster, Member, UniqueAddress} 23 | import akka.cluster.ClusterEvent.{InitialStateAsEvents, MemberRemoved, ReachableMember} 24 | import akkeeper.api.{InstanceId, InstanceInfo} 25 | import akkeeper.storage.{InstanceStorage, RecordNotFoundException} 26 | 27 | import scala.concurrent.duration._ 28 | import MemberAutoDownService._ 29 | 30 | /** 31 | * Monitors a status of the specified unreachable instance and automatically 32 | * excludes it from the cluster if the instance was deregistered from the storage. 33 | * This actor terminates itself when the instance is excluded from the cluster or 34 | * when it becomes reachable again. 35 | * 36 | * @param targetAddress the address of the target instance. 37 | * @param targetInstanceId the ID of the target instance. 38 | * @param instanceStorage the instance storage. 39 | * @param pollInterval the finite interval which indicates how often 40 | * the instance status should be checked. 41 | */ 42 | class MemberAutoDownService(targetAddress: UniqueAddress, 43 | targetInstanceId: InstanceId, 44 | instanceStorage: InstanceStorage, 45 | pollInterval: FiniteDuration) 46 | extends Actor with ActorLogging { 47 | 48 | private implicit val dispatcher = context.dispatcher 49 | private val cluster: Cluster = Cluster(context.system) 50 | 51 | override def preStart(): Unit = { 52 | cluster.subscribe(self, initialStateMode = InitialStateAsEvents, 53 | classOf[MemberRemoved], classOf[ReachableMember]) 54 | } 55 | 56 | override def receive: Receive = { 57 | pollCommandReceive orElse instanceStatusReceive orElse clusterEventReceive 58 | } 59 | 60 | private def clusterEventReceive: Receive = { 61 | case MemberRemoved(m, _) => 62 | onMemberStatusUpdate(m, s"The instance $targetInstanceId has been removed from the cluster") 63 | case ReachableMember(m) => 64 | onMemberStatusUpdate(m, s"The instance $targetInstanceId has become reachable again") 65 | } 66 | 67 | private def instanceStatusReceive: Receive = { 68 | case _: InstanceInfo => 69 | log.info(s"The instance $targetInstanceId seems to be alive despite being unreachable") 70 | schedulePoll 71 | case Status.Failure(RecordNotFoundException(_)) => 72 | log.warning(s"The instance $targetInstanceId was not found in the storage. " + 73 | "Excluding it from the cluster") 74 | cluster.down(targetAddress.address) 75 | cluster.unsubscribe(self) 76 | context.stop(self) 77 | case Status.Failure(e) => 78 | log.error(e, s"Failed to retrieve a status of the instance $targetInstanceId. Retrying...") 79 | schedulePoll 80 | } 81 | 82 | private def pollCommandReceive: Receive = { 83 | case PollInstanceStatus => 84 | instanceStorage.getInstance(targetInstanceId).pipeTo(self) 85 | } 86 | 87 | private def onMemberStatusUpdate(member: Member, logMsg: => String): Unit = { 88 | if (member.uniqueAddress == targetAddress) { 89 | log.info(logMsg) 90 | cluster.unsubscribe(self) 91 | context.stop(self) 92 | } 93 | } 94 | 95 | private def schedulePoll: Unit = { 96 | context.system.scheduler.scheduleOnce(pollInterval, self, PollInstanceStatus) 97 | } 98 | } 99 | 100 | object MemberAutoDownService { 101 | private[akkeeper] case object PollInstanceStatus 102 | 103 | private val DefaultPollInterval = 30 seconds 104 | 105 | private val serviceCounter: AtomicInteger = new AtomicInteger(0) 106 | 107 | private[akkeeper] def createLocal(factory: ActorRefFactory, 108 | targetAddress: UniqueAddress, 109 | targetInstanceId: InstanceId, 110 | instanceStorage: InstanceStorage, 111 | pollInterval: FiniteDuration = DefaultPollInterval): ActorRef = { 112 | val counter = serviceCounter.getAndIncrement() 113 | factory.actorOf(Props(classOf[MemberAutoDownService], targetAddress, 114 | targetInstanceId, instanceStorage, pollInterval), s"autoDown-$targetInstanceId-$counter") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/RemoteServiceFactory.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akka.actor.{ActorSelection, ActorSystem, RootActorPath} 19 | import akka.cluster.Cluster 20 | 21 | trait RemoteServiceFactory { 22 | def actorName: String 23 | 24 | def createRemote(system: ActorSystem): ActorSelection = { 25 | val cluster = Cluster(system) 26 | val akkeeperMaster = cluster.state.members 27 | .find(m => m.hasRole(MasterService.MasterServiceName)) 28 | .getOrElse(throw MasterServiceException("Failed to find a running Akkeeper Master instance")) 29 | val akkeeperMasterAddr = akkeeperMaster.address 30 | 31 | val basePath = RootActorPath(akkeeperMasterAddr) / "user" / MasterService.MasterServiceName 32 | val servicePath = 33 | if (actorName != MasterService.MasterServiceName) { 34 | basePath / actorName 35 | } else { 36 | basePath 37 | } 38 | system.actorSelection(servicePath) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/master/service/RequestTrackingService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akka.actor.{Actor, ActorLogging, ActorRef} 19 | import akkeeper.api.{RequestId, WithRequestId} 20 | 21 | import scala.collection.mutable 22 | 23 | private[service] case class SenderContext(sender: ActorRef, context: Option[Any]) 24 | 25 | private[service] trait RequestTrackingService extends Actor with ActorLogging { 26 | 27 | private val sendersById: mutable.Map[RequestId, SenderContext] = mutable.Map.empty 28 | private var currentServiceReceive: Option[Receive] = None 29 | 30 | private def getServiceReceive: Receive = { 31 | currentServiceReceive.getOrElse(serviceReceive) 32 | } 33 | 34 | private def trackedMessagesReceive: Receive = { 35 | case request: WithRequestId if trackedMessages.contains(request.getClass) => 36 | sendersById.put(request.requestId, SenderContext(sender(), None)) 37 | getServiceReceive(request) 38 | } 39 | 40 | protected def serviceReceive: Receive 41 | 42 | protected def trackedMessages: Set[Class[_]] = Set.empty 43 | 44 | protected def setOriginalSenderContext(id: RequestId, context: Any): Unit = { 45 | if (sendersById.contains(id)) { 46 | val record = sendersById(id) 47 | sendersById.put(id, record.copy(context = Some(context))) 48 | } else { 49 | throw MasterServiceException(s"Sender with ID $id was not found") 50 | } 51 | } 52 | 53 | protected def originalSenderContextAs[T](id: RequestId): T = { 54 | sendersById 55 | .get(id) 56 | .flatMap(_.context.map(_.asInstanceOf[T])) 57 | .getOrElse(throw MasterServiceException(s"No context provided for sender with ID $id")) 58 | } 59 | 60 | protected def originalSender(id: RequestId): ActorRef = { 61 | sendersById 62 | .get(id).map(_.sender) 63 | .getOrElse(throw MasterServiceException(s"Sender with ID $id was not found")) 64 | } 65 | 66 | protected def removeOriginalSender(id: RequestId): Unit = { 67 | sendersById.remove(id) 68 | } 69 | 70 | protected def sendAndRemoveOriginalSender[T <: WithRequestId](msg: T): Unit = { 71 | val recipient = originalSender(msg.requestId) 72 | recipient ! msg 73 | removeOriginalSender(msg.requestId) 74 | } 75 | 76 | protected def become(newState: Receive): Unit = { 77 | currentServiceReceive = Some(newState) 78 | context.become(trackedMessagesReceive orElse newState) 79 | } 80 | 81 | override def receive: Receive = { 82 | trackedMessagesReceive orElse serviceReceive 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/InstanceStorage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage 17 | 18 | import akkeeper.api._ 19 | import akkeeper.storage.zookeeper.ZookeeperClientConfig 20 | import akkeeper.storage.zookeeper.async.ZookeeperInstanceStorage 21 | 22 | import scala.concurrent.Future 23 | 24 | /** A persistent storage that stores information about existing instances. */ 25 | private[akkeeper] trait InstanceStorage extends Storage { 26 | 27 | /** Registers a new instance. Same instance can't be registered 28 | * more than once. The instance record must be removed automatically 29 | * once the client session is terminated. Session termination means 30 | * invocation of the [[Storage.stop()]] method. 31 | * 32 | * @param info the instance info. See [[InstanceInfo]]. 33 | * @return a container object with the registered instance ID. 34 | */ 35 | def registerInstance(info: InstanceInfo): Future[InstanceId] 36 | 37 | /** Retrieves the information about the instance by its ID. 38 | * 39 | * @param instanceId the ID of the instance. 40 | * @return a container object with the instance's information. 41 | * See [[InstanceInfo]]. 42 | */ 43 | def getInstance(instanceId: InstanceId): Future[InstanceInfo] 44 | 45 | /** Retrieves all instances that belong to the specified 46 | * container. 47 | * 48 | * @param containerName the name of the container. 49 | * @return a container object with the list of instance IDs. 50 | */ 51 | def getInstancesByContainer(containerName: String): Future[Seq[InstanceId]] 52 | 53 | /** Retrieves all existing instances. 54 | * 55 | * @return a container object with the list of instance IDs. 56 | */ 57 | def getInstances: Future[Seq[InstanceId]] 58 | } 59 | 60 | private[akkeeper] trait InstanceStorageFactory[T] extends (T => InstanceStorage) 61 | 62 | private[akkeeper] object InstanceStorageFactory { 63 | 64 | implicit object ZookeeperInstanceStorageFactory 65 | extends InstanceStorageFactory[ZookeeperClientConfig] { 66 | 67 | override def apply(config: ZookeeperClientConfig): InstanceStorage = { 68 | new ZookeeperInstanceStorage(config.child("instances")) 69 | } 70 | } 71 | 72 | def apply[T: InstanceStorageFactory](config: T): InstanceStorage = { 73 | implicitly[T](config) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/RecordAlreadyExistsException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage 17 | 18 | import akkeeper.common.AkkeeperException 19 | 20 | /** The exception that is thrown on creation attempt if the same record 21 | * already exists. 22 | */ 23 | case class RecordAlreadyExistsException(msg: String) extends AkkeeperException(msg: String) 24 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/RecordNotFoundException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage 17 | 18 | import akkeeper.common.AkkeeperException 19 | 20 | /** The exception that is thrown when the requested record is not found. */ 21 | case class RecordNotFoundException(msg: String) extends AkkeeperException(msg) 22 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/Storage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage 17 | 18 | /** A base interface for the persistent storage. */ 19 | private[akkeeper] trait Storage { 20 | 21 | /** Starts the storage's workflow. At this point a storage might 22 | * establish a client session or/and allocate necessary resources. 23 | */ 24 | def start(): Unit 25 | 26 | /** Stops the storage's workflow. All sessions must be terminated 27 | * at this point and all resources must be cleaned up. 28 | */ 29 | def stop(): Unit 30 | } 31 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/zookeeper/ZookeeperClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper 17 | 18 | import org.apache.curator.framework.{CuratorFramework, CuratorFrameworkFactory} 19 | import org.apache.curator.retry.ExponentialBackoffRetry 20 | 21 | private[zookeeper] abstract class ZookeeperClient(config: ZookeeperClientConfig) { 22 | protected val client: CuratorFramework = CuratorFrameworkFactory.builder() 23 | .namespace(config.namespace) 24 | .connectString(config.servers) 25 | .retryPolicy(new ExponentialBackoffRetry(config.connectionIntervalMs, config.maxRetries)) 26 | .canBeReadOnly(config.canBeReadOnly) 27 | .build() 28 | 29 | def start(): Unit = { 30 | client.start() 31 | } 32 | 33 | def stop(): Unit = { 34 | client.close() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/zookeeper/ZookeeperClientConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper 17 | 18 | import com.typesafe.config.Config 19 | 20 | private[akkeeper] case class ZookeeperClientConfig(servers: String, 21 | connectionIntervalMs: Int, 22 | maxRetries: Int, 23 | clientThreads: Option[Int] = None, 24 | namespace: String = "", 25 | canBeReadOnly: Boolean = true) { 26 | def child(childNamespace: String): ZookeeperClientConfig = { 27 | if (childNamespace.nonEmpty) { 28 | val newNameSpace = 29 | if (namespace.endsWith("/")) { 30 | namespace + childNamespace 31 | } else { 32 | namespace + "/" + childNamespace 33 | } 34 | this.copy(namespace = newNameSpace) 35 | } else { 36 | this 37 | } 38 | } 39 | } 40 | 41 | private[akkeeper] object ZookeeperClientConfig { 42 | def fromConfig(zkConfig: Config): ZookeeperClientConfig = { 43 | val servers = 44 | if (zkConfig.hasPath("servers") && zkConfig.getString("servers").nonEmpty) { 45 | zkConfig.getString("servers") 46 | } else { 47 | sys.env.getOrElse("ZK_QUORUM", "localhost:2181") 48 | } 49 | val clientThreads = 50 | if (zkConfig.hasPath("client-threads")) { 51 | Some(zkConfig.getInt("client-threads")) 52 | } else { 53 | None 54 | } 55 | ZookeeperClientConfig( 56 | servers = servers, 57 | connectionIntervalMs = zkConfig.getInt("connection-interval-ms"), 58 | maxRetries = zkConfig.getInt("max-retries"), 59 | clientThreads = clientThreads, 60 | namespace = zkConfig.getString("namespace"), 61 | canBeReadOnly = zkConfig.getBoolean("can-be-readonly") 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/zookeeper/ZookeeperException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper 17 | 18 | import akkeeper.common.AkkeeperException 19 | 20 | case class ZookeeperException(msg: String, returnCode: Int) extends AkkeeperException(msg) 21 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/zookeeper/async/AsyncZookeeperClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper.async 17 | 18 | import java.util.concurrent.Executors 19 | import akkeeper.storage._ 20 | import akkeeper.storage.zookeeper._ 21 | import AsyncZookeeperClient._ 22 | import org.apache.curator.framework.CuratorFramework 23 | import org.apache.curator.framework.api.{BackgroundCallback, CuratorEvent} 24 | import org.apache.zookeeper.CreateMode 25 | import org.apache.zookeeper.KeeperException.Code 26 | import org.apache.zookeeper.data.Stat 27 | 28 | import scala.collection.JavaConverters._ 29 | import scala.concurrent.{ExecutionContext, Future, Promise} 30 | import scala.util.Try 31 | 32 | private[zookeeper] class AsyncZookeeperClient(config: ZookeeperClientConfig, 33 | createMode: CreateMode) 34 | extends ZookeeperClient(config) { 35 | 36 | private val executor = Executors 37 | .newFixedThreadPool(config.clientThreads.getOrElse(DefaultClientThreads)) 38 | 39 | final def create(path: String, data: Array[Byte]): Future[String] = { 40 | val (callback, future) = callbackWithFuture(_.getPath) 41 | client.create() 42 | .creatingParentsIfNeeded 43 | .withMode(createMode) 44 | .inBackground(callback, executor) 45 | .forPath(normalizePath(path), data) 46 | future 47 | } 48 | 49 | final def create(path: String): Future[String] = { 50 | create(path, Array.empty) 51 | } 52 | 53 | final def update(path: String, data: Array[Byte]): Future[String] = { 54 | val (callback, future) = callbackWithFuture(_.getPath) 55 | client.setData().inBackground(callback, executor).forPath(normalizePath(path), data) 56 | future 57 | } 58 | 59 | final def get(path: String): Future[Array[Byte]] = { 60 | val (callback, future) = callbackWithFuture(_.getData) 61 | client.getData.inBackground(callback, executor).forPath(normalizePath(path)) 62 | future 63 | } 64 | 65 | final def delete(path: String): Future[String] = { 66 | val (callback, future) = callbackWithFuture(_.getPath) 67 | client.delete().inBackground(callback, executor).forPath(normalizePath(path)) 68 | future 69 | } 70 | 71 | final def exists(path: String): Future[Stat] = { 72 | val (callback, future) = callbackWithFuture(_.getStat) 73 | client.checkExists().inBackground(callback, executor).forPath(normalizePath(path)) 74 | future 75 | } 76 | 77 | final def children(path: String): Future[Seq[String]] = { 78 | val (callback, future) = callbackWithFuture(_.getChildren.asScala) 79 | client.getChildren.inBackground(callback, executor).forPath(normalizePath(path)) 80 | future 81 | } 82 | 83 | final def getExecutionContext: ExecutionContext = { 84 | ExecutionContext.fromExecutor(executor) 85 | } 86 | 87 | override def stop(): Unit = { 88 | executor.shutdown() 89 | super.stop() 90 | } 91 | } 92 | 93 | object AsyncZookeeperClient { 94 | val DefaultClientThreads = 5 95 | 96 | private object ResultCodeErrorExtractor { 97 | def unapply(code: Int): Option[Throwable] = { 98 | Code.get(code) match { 99 | case Code.OK => None 100 | case Code.NONODE => Some(RecordNotFoundException("ZK node was not found")) 101 | case Code.NODEEXISTS => Some(RecordAlreadyExistsException("ZK node already exists")) 102 | case other => 103 | Some(ZookeeperException(s"ZK operation failed (${other.toString})", other.intValue())) 104 | } 105 | } 106 | } 107 | 108 | private def asyncCallback[T](promise: Promise[T])(f: CuratorEvent => T): BackgroundCallback = { 109 | new BackgroundCallback { 110 | override def processResult(client: CuratorFramework, event: CuratorEvent): Unit = { 111 | event.getResultCode match { 112 | case ResultCodeErrorExtractor(error) => 113 | promise failure error 114 | case _ => 115 | promise complete Try(f(event)) 116 | } 117 | } 118 | } 119 | } 120 | 121 | private def callbackWithFuture[T](f: CuratorEvent => T): (BackgroundCallback, Future[T]) = { 122 | val promise = Promise[T]() 123 | val callback = asyncCallback(promise)(f) 124 | (callback, promise.future) 125 | } 126 | 127 | private def normalizePath(path: String): String = { 128 | if (path.startsWith("/")) path.trim else s"/$path".trim 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/zookeeper/async/BaseZookeeperStorage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper.async 17 | 18 | import akkeeper.storage.{RecordNotFoundException, Storage} 19 | import spray.json._ 20 | 21 | private[zookeeper] trait BaseZookeeperStorage extends Storage { 22 | 23 | protected def zookeeperClient: AsyncZookeeperClient 24 | 25 | override def start(): Unit = zookeeperClient.start() 26 | override def stop(): Unit = zookeeperClient.stop() 27 | 28 | protected def toBytes[T: JsonWriter](obj: T): Array[Byte] = { 29 | obj.toJson.compactPrint.getBytes("UTF-8") 30 | } 31 | 32 | protected def fromBytes[T: JsonReader](bytes: Array[Byte]): T = { 33 | new String(bytes, "UTF-8").parseJson.convertTo[T] 34 | } 35 | 36 | protected def notFoundToEmptySeq[T]: PartialFunction[Throwable, Seq[T]] = { 37 | case _: RecordNotFoundException => Seq.empty 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /akkeeper/src/main/scala/akkeeper/storage/zookeeper/async/ZookeeperInstanceStorage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper.async 17 | 18 | import akkeeper.api._ 19 | import akkeeper.api.InstanceInfoJsonProtocol._ 20 | import akkeeper.storage._ 21 | import akkeeper.storage.zookeeper.ZookeeperClientConfig 22 | import org.apache.zookeeper.CreateMode 23 | 24 | import scala.concurrent.Future 25 | import ZookeeperInstanceStorage._ 26 | import akkeeper.api.InstanceId 27 | 28 | private[akkeeper] class ZookeeperInstanceStorage(config: ZookeeperClientConfig) 29 | extends BaseZookeeperStorage with InstanceStorage { 30 | 31 | protected override val zookeeperClient = 32 | new AsyncZookeeperClient(config, CreateMode.EPHEMERAL) 33 | private implicit val executionContext = zookeeperClient.getExecutionContext 34 | 35 | override def registerInstance(status: InstanceInfo): Future[InstanceId] = { 36 | val instanceId = status.instanceId 37 | val path = instancePath(status) 38 | zookeeperClient 39 | .exists(path) 40 | .map(_ => throw RecordAlreadyExistsException(s"Instance $instanceId already exists")) 41 | .recoverWith { 42 | case _: RecordNotFoundException => 43 | zookeeperClient.create(path, toBytes(status)).map(pathToInstanceId) 44 | } 45 | } 46 | 47 | override def getInstance(instanceId: InstanceId): Future[InstanceInfo] = { 48 | zookeeperClient 49 | .get(instanceId.containerName + "/" + instanceId.toString) 50 | .map(fromBytes[InstanceInfo]) 51 | } 52 | 53 | override def getInstances: Future[Seq[InstanceId]] = { 54 | val instancesFuture = for { 55 | containers <- zookeeperClient.children("") 56 | } yield for { 57 | container <- containers 58 | } yield zookeeperClient.children(container) 59 | instancesFuture 60 | .flatMap(f => Future.sequence(f).map(_.flatten)) 61 | .map(_.map(pathToInstanceId)) 62 | .recover(notFoundToEmptySeq[InstanceId]) 63 | } 64 | 65 | override def getInstancesByContainer(containerName: String): Future[Seq[InstanceId]] = { 66 | zookeeperClient.children(containerName) 67 | .map(_.map(pathToInstanceId)) 68 | .recover(notFoundToEmptySeq[InstanceId]) 69 | } 70 | } 71 | 72 | private[akkeeper] object ZookeeperInstanceStorage { 73 | private def instancePath(status: InstanceInfo): String = { 74 | status.containerName + "/" + status.instanceId.toString 75 | } 76 | 77 | private def pathToInstanceId(path: String): InstanceId = { 78 | val split = path.split("/") 79 | val idString = if (split.size == 1) path else split.last 80 | InstanceId.fromString(idString) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /akkeeper/src/test/resources/application-container-test.conf: -------------------------------------------------------------------------------- 1 | akkeeper { 2 | containers = [ 3 | { 4 | name = "container1" 5 | actors = [ 6 | { 7 | name = "actor1" 8 | fqn = "com.test.Actor1" 9 | }, 10 | { 11 | name = "actor2" 12 | fqn = "com.test.Actor2" 13 | } 14 | ], 15 | cpus = 1 16 | memory = 1024 17 | jvm-args = [ "-Xmx2G" ] 18 | properties { 19 | property = "value" 20 | } 21 | }, 22 | { 23 | name = "container2" 24 | actors = [ 25 | { 26 | name = "actor3" 27 | fqn = "com.test.Actor3" 28 | } 29 | ], 30 | cpus = 2 31 | memory = 2048 32 | environment { 33 | envProperty = "value" 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/ActorTestUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper 17 | 18 | import akka.actor.ActorRef 19 | import akka.cluster.{Member, MemberStatus, UniqueAddress} 20 | import akka.pattern.gracefulStop 21 | 22 | import scala.concurrent.duration._ 23 | 24 | trait ActorTestUtils extends AwaitMixin { 25 | 26 | protected val gracefulStopTimeout: FiniteDuration = 6 seconds 27 | 28 | protected def gracefulActorStop(actor: ActorRef): Unit = { 29 | await(gracefulStop(actor, gracefulStopTimeout)) 30 | } 31 | 32 | protected def createTestMember(addr: UniqueAddress): Member = { 33 | createTestMember(addr, MemberStatus.Up) 34 | } 35 | 36 | protected def createTestMember(addr: UniqueAddress, status: MemberStatus): Member = { 37 | createTestMember(addr, status, Set.empty) 38 | } 39 | 40 | protected def createTestMember(addr: UniqueAddress, status: MemberStatus, roles: Set[String]): Member = { 41 | val ctr = classOf[Member].getDeclaredConstructor(classOf[UniqueAddress], classOf[Int], 42 | classOf[MemberStatus], classOf[Set[String]]) 43 | ctr.newInstance(addr, new Integer(1), status, roles + "dc-default") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/AwaitMixin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper 17 | 18 | import scala.concurrent.{Await, Awaitable} 19 | import scala.concurrent.duration._ 20 | 21 | trait AwaitMixin { 22 | 23 | protected implicit val awaitTimeout = 6 seconds 24 | 25 | protected def await[T](awaitable: Awaitable[T]) 26 | (implicit duration: Duration): T = { 27 | Await.result(awaitable, duration) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/container/service/TestUserActor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.container.service 17 | 18 | import akka.actor.Actor 19 | import TestUserActor._ 20 | 21 | class TestUserActor extends Actor { 22 | override def receive: Receive = { 23 | case TestPing => sender() ! TestPong 24 | case TestTerminate => context.stop(self) 25 | } 26 | } 27 | 28 | object TestUserActor { 29 | case object TestPing 30 | case object TestPong 31 | case object TestTerminate 32 | } 33 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/route/ContainerControllerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import java.util.concurrent.TimeUnit 19 | 20 | import akka.actor.ActorSystem 21 | import akka.http.scaladsl.model.StatusCodes 22 | import akka.testkit.{ImplicitSender, TestKit} 23 | import akka.util.Timeout 24 | import akkeeper.api._ 25 | import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} 26 | 27 | class ContainerControllerSpec(testSystem: ActorSystem) extends TestKit(testSystem) 28 | with FlatSpecLike with Matchers with ImplicitSender with RestTestUtils 29 | with DeployApiJsonProtocol with ContainerApiJsonProtocol with BeforeAndAfterAll { 30 | 31 | def this() = this(ActorSystem("ContainerControllerSpec")) 32 | 33 | override protected def afterAll(): Unit = { 34 | system.terminate() 35 | super.afterAll() 36 | } 37 | 38 | def createContainerDefinition(name: String): ContainerDefinition = { 39 | val memory = 1024 40 | val cpus = 1 41 | ContainerDefinition(name, cpus = cpus, memory = memory, actors = Seq.empty) 42 | } 43 | 44 | "Container Controller" should "return container by its ID" in { 45 | val controller = ContainerController(self) 46 | withHttpServer(controller.route) { restPort => 47 | val response = get[ContainerGetResult]("/containers/containerName", restPort) 48 | 49 | val request = expectMsgClass(classOf[GetContainer]) 50 | request.name shouldBe "containerName" 51 | val getResult = ContainerGetResult(request.requestId, 52 | createContainerDefinition("containerName")) 53 | lastSender ! getResult 54 | 55 | val (code, actualResult) = await(response) 56 | code shouldBe StatusCodes.OK.intValue 57 | actualResult shouldBe getResult 58 | } 59 | } 60 | 61 | it should "return 404 if the request container was not found" in { 62 | val controller = ContainerController(self) 63 | withHttpServer(controller.route) { restPort => 64 | val response = get[ContainerNotFound]("/containers/containerName", restPort) 65 | 66 | val request = expectMsgClass(classOf[GetContainer]) 67 | request.name shouldBe "containerName" 68 | val notFound = ContainerNotFound(request.requestId, "containerName") 69 | lastSender ! notFound 70 | 71 | val (code, actualResult) = await(response) 72 | code shouldBe StatusCodes.NotFound.intValue 73 | actualResult shouldBe notFound 74 | } 75 | } 76 | 77 | it should "return all available containers" in { 78 | val controller = ContainerController(self) 79 | withHttpServer(controller.route) { restPort => 80 | val response = get[ContainersList]("/containers/", restPort) 81 | 82 | val request = expectMsgClass(classOf[GetContainers]) 83 | val list = ContainersList(request.requestId, Seq("containerName")) 84 | lastSender ! list 85 | 86 | val (code, actualResult) = await(response) 87 | code shouldBe StatusCodes.OK.intValue 88 | actualResult shouldBe list 89 | } 90 | } 91 | 92 | it should "fail if unexpected error occurred" in { 93 | val timeoutMilliseconds = 100 94 | implicit val timeout = Timeout(timeoutMilliseconds, TimeUnit.MILLISECONDS) 95 | val controller = ContainerController(self) 96 | withHttpServer(controller.route) { restPort => 97 | val response = getRaw("/containers/", restPort) 98 | 99 | expectMsgClass(classOf[GetContainers]) 100 | 101 | val (code, _) = await(response) 102 | code shouldBe StatusCodes.InternalServerError.intValue 103 | } 104 | } 105 | 106 | it should "fail if the unexpected response arrives from the service" in { 107 | val controller = ContainerController(self) 108 | withHttpServer(controller.route) { restPort => 109 | val response = getRaw("/containers/", restPort) 110 | 111 | val request = expectMsgClass(classOf[GetContainers]) 112 | lastSender ! InstancesList(request.requestId, Seq.empty) 113 | 114 | val (code, _) = await(response) 115 | code shouldBe StatusCodes.InternalServerError.intValue 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/route/ControllerCompositeSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.actor.ActorSystem 19 | import akka.http.scaladsl.model.StatusCodes 20 | import akka.http.scaladsl.server.{Route, Directives} 21 | import akka.testkit.{ImplicitSender, TestKit} 22 | import org.scalatest.{BeforeAndAfterAll, Matchers, FlatSpecLike} 23 | 24 | class ControllerCompositeSpec(testSystem: ActorSystem) extends TestKit(testSystem) 25 | with FlatSpecLike with Matchers with ImplicitSender with RestTestUtils 26 | with BeforeAndAfterAll { 27 | 28 | def this() = this(ActorSystem("ControllerCompositeSpec")) 29 | 30 | override protected def afterAll(): Unit = { 31 | system.terminate() 32 | super.afterAll() 33 | } 34 | 35 | "Composite Controller" should "compose multiple controllers" in { 36 | val controller1 = ControllerCompositeSpec.createTestController("test1") 37 | val controller2 = ControllerCompositeSpec.createTestController("test2") 38 | 39 | def testRoute(name: String, restPort: Int): Unit = { 40 | val response = getRaw(s"/api/v1/$name", restPort) 41 | val (code, result) = await(response) 42 | code shouldBe StatusCodes.OK.intValue 43 | result shouldBe name 44 | } 45 | 46 | val controller = ControllerComposite("api/v1", Seq(controller1, controller2)) 47 | withHttpServer(controller.route) { restPort => 48 | testRoute("test1", restPort) 49 | testRoute("test2", restPort) 50 | } 51 | } 52 | } 53 | 54 | object ControllerCompositeSpec extends Directives { 55 | private def createTestController(name: String): BaseController = { 56 | new BaseController { 57 | override val route: Route = 58 | path(name) { 59 | get { 60 | complete(StatusCodes.OK -> name) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/route/DeployControllerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import java.util.concurrent.TimeUnit 19 | 20 | import akka.actor.ActorSystem 21 | import akka.http.scaladsl.model.StatusCodes 22 | import akka.testkit.{ImplicitSender, TestKit} 23 | import akka.util.Timeout 24 | import akkeeper.api._ 25 | import akkeeper.common.AkkeeperException 26 | import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} 27 | 28 | class DeployControllerSpec(testSystem: ActorSystem) extends TestKit(testSystem) 29 | with FlatSpecLike with Matchers with ImplicitSender with RestTestUtils 30 | with DeployApiJsonProtocol with ContainerApiJsonProtocol with BeforeAndAfterAll { 31 | 32 | def this() = this(ActorSystem("DeployControllerSpec")) 33 | 34 | override protected def afterAll(): Unit = { 35 | system.terminate() 36 | super.afterAll() 37 | } 38 | 39 | "Deploy Controller" should "handle deploy request properly" in { 40 | val controller = DeployController(self) 41 | withHttpServer(controller.route) { restPort => 42 | val request = DeployContainer("container", 1) 43 | val response = post[DeployContainer, SubmittedInstances](request, "/deploy", restPort) 44 | 45 | expectMsg(request) 46 | val submittedInstances = SubmittedInstances( 47 | requestId = request.requestId, 48 | containerName = request.name, 49 | instanceIds = Seq(InstanceId(request.name))) 50 | lastSender ! submittedInstances 51 | 52 | val (code, actualResult) = await(response) 53 | code shouldBe StatusCodes.Accepted.intValue 54 | actualResult shouldBe submittedInstances 55 | } 56 | } 57 | 58 | it should "handle invalid container properly" in { 59 | val controller = DeployController(self) 60 | withHttpServer(controller.route) { restPort => 61 | val request = DeployContainer("container", 1) 62 | val response = post[DeployContainer, ContainerNotFound](request, "/deploy", restPort) 63 | 64 | expectMsg(request) 65 | val notFound = ContainerNotFound(request.requestId, request.name) 66 | lastSender ! notFound 67 | 68 | val (code, actualResult) = await(response) 69 | code shouldBe StatusCodes.NotFound.intValue 70 | actualResult shouldBe notFound 71 | } 72 | } 73 | 74 | it should "handle deployment errors" in { 75 | val controller = DeployController(self) 76 | withHttpServer(controller.route) { restPort => 77 | val request = DeployContainer("container", 1) 78 | val response = postRaw(request, "/deploy", restPort) 79 | 80 | expectMsg(request) 81 | val error = OperationFailed(request.requestId, new AkkeeperException("fail")) 82 | lastSender ! error 83 | 84 | val (code, actualResult) = await(response) 85 | code shouldBe StatusCodes.InternalServerError.intValue 86 | actualResult should include("fail") 87 | actualResult should include(request.requestId.toString) 88 | } 89 | } 90 | 91 | it should "fail if unexpected error occurred" in { 92 | val timeoutMilliseconds = 100 93 | implicit val timeout = Timeout(timeoutMilliseconds, TimeUnit.MILLISECONDS) 94 | val controller = DeployController(self) 95 | withHttpServer(controller.route) { restPort => 96 | val request = DeployContainer("container", 1) 97 | val response = postRaw(request, "/deploy", restPort) 98 | 99 | expectMsg(request) 100 | val (code, _) = await(response) 101 | code shouldBe StatusCodes.InternalServerError.intValue 102 | } 103 | } 104 | 105 | it should "fail if the unexpected response arrives from the service" in { 106 | val controller = DeployController(self) 107 | withHttpServer(controller.route) { restPort => 108 | val request = DeployContainer("container", 1) 109 | val response = postRaw(request, "/deploy", restPort) 110 | 111 | expectMsg(request) 112 | lastSender ! InstancesList(request.requestId, Seq.empty) 113 | 114 | val (code, _) = await(response) 115 | code shouldBe StatusCodes.InternalServerError.intValue 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/route/MasterControllerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import akka.actor.ActorSystem 19 | import akka.http.scaladsl.model.{HttpMethods, HttpRequest, StatusCodes} 20 | import akka.testkit.{ImplicitSender, TestKit} 21 | import akkeeper.api.{Heartbeat, TerminateMaster} 22 | import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} 23 | 24 | class MasterControllerSpec(testSystem: ActorSystem) extends TestKit(testSystem) 25 | with FlatSpecLike with Matchers with ImplicitSender with RestTestUtils with BeforeAndAfterAll { 26 | 27 | def this() = this(ActorSystem("MasterControllerSpec")) 28 | 29 | override protected def afterAll(): Unit = { 30 | system.terminate() 31 | super.afterAll() 32 | } 33 | 34 | "Master Controller" should "send master termination message" in { 35 | val controller = MasterController(self) 36 | withHttpServer(controller.route) { restPort => 37 | val request = HttpRequest(uri = "/master/terminate") 38 | .withMethod(HttpMethods.POST) 39 | val response = sendRequest(request, restPort) 40 | 41 | expectMsg(TerminateMaster) 42 | 43 | val (code, actualResult) = await(response) 44 | code shouldBe StatusCodes.Accepted.intValue 45 | actualResult shouldBe empty 46 | } 47 | } 48 | 49 | it should "send heartbeat message" in { 50 | val controller = MasterController(self) 51 | withHttpServer(controller.route) { restPort => 52 | val request = HttpRequest(uri = "/master/heartbeat") 53 | .withMethod(HttpMethods.POST) 54 | val response = sendRequest(request, restPort) 55 | 56 | expectMsg(Heartbeat) 57 | 58 | val (code, actualResult) = await(response) 59 | code shouldBe StatusCodes.OK.intValue 60 | actualResult shouldBe empty 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/route/RestTestUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.route 17 | 18 | import java.net.ServerSocket 19 | 20 | import akka.http.scaladsl.Http 21 | import akka.http.scaladsl.model._ 22 | import akka.http.scaladsl.server.Route 23 | import akka.stream.ActorMaterializer 24 | import akka.stream.scaladsl.{Sink, Source} 25 | import akka.testkit.TestKit 26 | import akka.util.Timeout 27 | import akkeeper.AwaitMixin 28 | import scala.concurrent.Future 29 | import scala.concurrent.duration._ 30 | import spray.json._ 31 | 32 | trait RestTestUtils extends AwaitMixin { 33 | 34 | this: TestKit => 35 | 36 | implicit val materializer = ActorMaterializer() 37 | implicit val dispatcher = system.dispatcher 38 | implicit val timeout = Timeout(3 seconds) 39 | 40 | def allocatePort: Int = { 41 | val socket = new ServerSocket(0) 42 | val port = socket.getLocalPort 43 | socket.close() 44 | port 45 | } 46 | 47 | def withHttpServer[T](route: Route)(f: Int => T): T = { 48 | val port = allocatePort 49 | val bindFuture = Http().bindAndHandle(route, "localhost", port) 50 | val result = bindFuture.map(b => { 51 | val result = f(port) 52 | b.unbind() 53 | result 54 | }) 55 | await(result) 56 | } 57 | 58 | protected def sendRequest(request: HttpRequest, port: Int): Future[(Int, String)] = { 59 | val connectionFlow = Http().outgoingConnection("localhost", port) 60 | Source 61 | .single(request) 62 | .via(connectionFlow) 63 | .runWith(Sink.head) 64 | .flatMap(resp => resp.entity.toStrict(awaitTimeout).map(e => resp.status.intValue() -> e)) 65 | .map { 66 | case (statusCode, entity) => 67 | statusCode -> entity.getData().decodeString("UTF-8") 68 | } 69 | } 70 | 71 | private def deserialize[Resp: JsonReader](resp: Future[(Int, String)]): Future[(Int, Resp)] = { 72 | resp.map { 73 | case (statusCode, data) => 74 | statusCode -> data.parseJson.convertTo[Resp] 75 | } 76 | } 77 | 78 | def postRaw[Req: JsonWriter](req: Req, uri: String, port: Int): Future[(Int, String)] = { 79 | val jsonBody = req.toJson.prettyPrint 80 | val request = HttpRequest(uri = uri) 81 | .withMethod(HttpMethods.POST) 82 | .withEntity(HttpEntity(ContentTypes.`application/json`, jsonBody)) 83 | 84 | sendRequest(request, port) 85 | } 86 | 87 | def post[Req: JsonWriter, Resp: JsonReader](req: Req, uri: String, 88 | port: Int): Future[(Int, Resp)] = { 89 | deserialize(postRaw(req, uri, port)) 90 | } 91 | 92 | def patchRaw[Req: JsonWriter](req: Req, uri: String, port: Int): Future[(Int, String)] = { 93 | val jsonBody = req.toJson.prettyPrint 94 | val request = HttpRequest(uri = uri) 95 | .withMethod(HttpMethods.PATCH) 96 | .withEntity(HttpEntity(ContentTypes.`application/json`, jsonBody)) 97 | sendRequest(request, port) 98 | } 99 | 100 | def patch[Req: JsonWriter, Resp: JsonReader](req: Req, uri: String, 101 | port: Int): Future[(Int, Resp)] = { 102 | deserialize(patchRaw(req, uri, port)) 103 | } 104 | 105 | def getRaw(uri: String, port: Int): Future[(Int, String)] = { 106 | val request = HttpRequest(uri = uri) 107 | .withMethod(HttpMethods.GET) 108 | sendRequest(request, port) 109 | } 110 | 111 | def get[Resp: JsonReader](uri: String, port: Int): Future[(Int, Resp)] = { 112 | deserialize(getRaw(uri, port)) 113 | } 114 | 115 | def deleteRaw(uri: String, port: Int): Future[(Int, String)] = { 116 | val request = HttpRequest(uri = uri) 117 | .withMethod(HttpMethods.DELETE) 118 | sendRequest(request, port) 119 | } 120 | 121 | def delete[Resp: JsonReader](uri: String, port: Int): Future[(Int, Resp)] = { 122 | deserialize(deleteRaw(uri, port)) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/service/ContainerServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akka.actor.{ActorRef, ActorSystem, Props} 19 | import akka.testkit.{ImplicitSender, TestKit} 20 | import akkeeper.ActorTestUtils 21 | import akkeeper.api._ 22 | import akkeeper.common.AkkeeperException 23 | import com.typesafe.config.ConfigFactory 24 | import org.scalatest._ 25 | 26 | class ContainerServiceSpec(system: ActorSystem) extends TestKit(system) 27 | with FlatSpecLike with Matchers with ImplicitSender with ActorTestUtils with BeforeAndAfterAll { 28 | 29 | def this() = this(ActorSystem("ContainerServiceSpec", 30 | ConfigFactory.load("application-container-test.conf"))) 31 | 32 | override def afterAll(): Unit = { 33 | system.terminate() 34 | super.afterAll() 35 | } 36 | 37 | private def createContainerService: ActorRef = { 38 | childActorOf(Props(classOf[ContainerService]), ContainerService.actorName) 39 | } 40 | 41 | "A Container Service" should "return the list of available containers" in { 42 | val service = createContainerService 43 | 44 | val request = GetContainers() 45 | service ! request 46 | 47 | val response = expectMsgClass(classOf[ContainersList]) 48 | response.requestId shouldBe request.requestId 49 | response.containers should contain allOf("container1", "container2") 50 | 51 | gracefulActorStop(service) 52 | } 53 | 54 | it should "return a container definition" in { 55 | val service = createContainerService 56 | 57 | val request1 = GetContainer("container1") 58 | service ! request1 59 | 60 | val response1 = expectMsgClass(classOf[ContainerGetResult]) 61 | response1.requestId shouldBe request1.requestId 62 | val container1 = response1.container 63 | container1.name shouldBe "container1" 64 | container1.cpus shouldBe 1 65 | container1.memory shouldBe 1024 66 | container1.jvmArgs should contain ("-Xmx2G") 67 | container1.jvmProperties should contain ("property" -> "value") 68 | container1.environment shouldBe empty 69 | container1.actors.size shouldBe 2 70 | container1.actors should contain (ActorLaunchContext("actor1", "com.test.Actor1")) 71 | container1.actors should contain (ActorLaunchContext("actor2", "com.test.Actor2")) 72 | 73 | val request2 = GetContainer("container2") 74 | service ! request2 75 | 76 | val response2 = expectMsgClass(classOf[ContainerGetResult]) 77 | response2.requestId shouldBe request2.requestId 78 | val container2 = response2.container 79 | container2.name shouldBe "container2" 80 | container2.cpus shouldBe 2 81 | container2.memory shouldBe 2048 82 | container2.jvmArgs shouldBe empty 83 | container2.jvmProperties shouldBe empty 84 | container2.environment should contain ("envProperty" -> "value") 85 | container2.actors.size shouldBe 1 86 | container2.actors should contain (ActorLaunchContext("actor3", "com.test.Actor3")) 87 | 88 | gracefulActorStop(service) 89 | } 90 | 91 | it should "not find a container" in { 92 | val service = createContainerService 93 | 94 | val request = GetContainer("invalid") 95 | service ! request 96 | expectMsg(ContainerNotFound(request.requestId, "invalid")) 97 | 98 | gracefulActorStop(service) 99 | } 100 | 101 | it should "stop with an error" in { 102 | val service = createContainerService 103 | 104 | service ! StopWithError(new AkkeeperException("fail")) 105 | service ! GetContainer("container1") 106 | expectNoMessage() 107 | 108 | gracefulActorStop(service) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/service/HeartbeatServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2019 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import java.util.UUID 19 | 20 | import akka.actor.{ActorRef, ActorSystem, Props} 21 | import akka.testkit.{ImplicitSender, TestKit} 22 | import akkeeper.ActorTestUtils 23 | import akkeeper.api.{Heartbeat, TerminateMaster} 24 | import com.typesafe.config.{ConfigFactory, ConfigValueFactory} 25 | import org.scalamock.scalatest.MockFactory 26 | import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} 27 | 28 | import scala.concurrent.duration._ 29 | 30 | class HeartbeatServiceSpec(system: ActorSystem) extends TestKit(system) 31 | with FlatSpecLike with Matchers with ImplicitSender with MockFactory with ActorTestUtils 32 | with BeforeAndAfterAll { 33 | 34 | def this() = this(ActorSystem("HeartbeatServiceSpec", ConfigFactory.load() 35 | .withValue("akkeeper.master.heartbeat.timeout", ConfigValueFactory.fromAnyRef("1.2s")) 36 | .withValue("akkeeper.master.heartbeat.missed-limit", ConfigValueFactory.fromAnyRef("2")))) 37 | 38 | override def afterAll(): Unit = { 39 | system.terminate() 40 | super.afterAll() 41 | } 42 | 43 | private def createHeartbeatService(): ActorRef = { 44 | childActorOf(Props(classOf[HeartbeatService]), UUID.randomUUID().toString) 45 | } 46 | 47 | "Heartbeat Service" should "send termination message when heartbeat timeout occurs" in { 48 | val service = createHeartbeatService() 49 | expectMsg(3 seconds, TerminateMaster) 50 | 51 | gracefulActorStop(service) 52 | } 53 | 54 | it should "not terminate master if heartbeats are arriving as expected" in { 55 | val service = createHeartbeatService() 56 | val numOfHeartbeats = 4 57 | (0 until numOfHeartbeats).foreach { _ => 58 | service ! Heartbeat 59 | expectNoMessage(1 second) 60 | } 61 | 62 | gracefulActorStop(service) 63 | } 64 | 65 | it should "tolerate one missed heartbeat" in { 66 | val service = createHeartbeatService() 67 | service ! Heartbeat 68 | expectNoMessage(2 seconds) 69 | service ! Heartbeat 70 | expectNoMessage(2 seconds) 71 | 72 | gracefulActorStop(service) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/master/service/MemberAutoDownServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.master.service 17 | 18 | import akka.actor.{ActorRef, ActorSystem, Address, Props, Terminated} 19 | import akka.cluster.ClusterEvent.{MemberRemoved, ReachableMember} 20 | import akka.cluster.{MemberStatus, UniqueAddress} 21 | import akka.testkit.{ImplicitSender, TestKit} 22 | import akkeeper.ActorTestUtils 23 | import akkeeper.api._ 24 | import akkeeper.storage.{InstanceStorage, RecordNotFoundException} 25 | import org.scalamock.scalatest.MockFactory 26 | import org.scalatest.{BeforeAndAfterAll, FlatSpecLike, Matchers} 27 | 28 | import scala.concurrent.Future 29 | import scala.concurrent.duration._ 30 | 31 | class MemberAutoDownServiceSpec(system: ActorSystem) extends TestKit(system) 32 | with FlatSpecLike with Matchers with ImplicitSender with MockFactory with ActorTestUtils 33 | with BeforeAndAfterAll { 34 | 35 | def this() = this(ActorSystem("MemberAutoDownServiceSpec")) 36 | 37 | override def afterAll(): Unit = { 38 | system.terminate() 39 | super.afterAll() 40 | } 41 | 42 | private def createMemberAutdownService(targetAddress: UniqueAddress, 43 | targetInstanceId: InstanceId, 44 | instanceStorage: InstanceStorage, 45 | pollInterval: FiniteDuration = 30 seconds): ActorRef = { 46 | childActorOf(Props(classOf[MemberAutoDownService], targetAddress, 47 | targetInstanceId, instanceStorage, pollInterval), s"autoDown-$targetInstanceId") 48 | } 49 | 50 | "A Member Auto Down Service" should "exclude a dead instance from the cluster" in { 51 | val port = 12345 52 | val address = UniqueAddress(Address("akka.tcp", "MemberAutoDownServiceSpec", "localhost", port), 1L) 53 | val instanceId = InstanceId("container") 54 | 55 | val storage = mock[InstanceStorage] 56 | (storage.getInstance _).expects(instanceId).returns(Future failed RecordNotFoundException("")) 57 | 58 | val service = createMemberAutdownService(address, instanceId, storage) 59 | watch(service) 60 | service ! MemberAutoDownService.PollInstanceStatus 61 | 62 | expectMsgClass(classOf[Terminated]) 63 | } 64 | 65 | it should "periodically poll the instance status" in { 66 | val port = 12345 67 | val address = UniqueAddress(Address("akka.tcp", "MemberAutoDownServiceSpec", "localhost", port), 1L) 68 | val instanceId = InstanceId("container") 69 | val info = InstanceInfo(instanceId, InstanceUp, "", Set.empty, None, Set.empty) 70 | 71 | val storage = mock[InstanceStorage] 72 | (storage.getInstance _).expects(instanceId).returns(Future successful info).atLeastTwice() 73 | 74 | val service = createMemberAutdownService(address, instanceId, storage, 1 second) 75 | service ! MemberAutoDownService.PollInstanceStatus 76 | 77 | val timeout = 2000 78 | Thread.sleep(timeout) 79 | gracefulActorStop(service) 80 | } 81 | 82 | it should "stop when the target became reachable again" in { 83 | val port = 12345 84 | val address = UniqueAddress(Address("akka.tcp", "MemberAutoDownServiceSpec", "localhost", port), 1L) 85 | val member = createTestMember(address) 86 | val instanceId = InstanceId("container") 87 | val info = InstanceInfo(instanceId, InstanceUp, "", Set.empty, None, Set.empty) 88 | 89 | val storage = mock[InstanceStorage] 90 | (storage.getInstance _).expects(instanceId).returns(Future successful info) 91 | 92 | val service = createMemberAutdownService(address, instanceId, storage) 93 | watch(service) 94 | service ! MemberAutoDownService.PollInstanceStatus 95 | service ! ReachableMember(member) 96 | 97 | expectMsgClass(classOf[Terminated]) 98 | } 99 | 100 | it should "stop when the target left the cluster" in { 101 | val port = 12345 102 | val address = UniqueAddress(Address("akka.tcp", "MemberAutoDownServiceSpec", "localhost", port), 1L) 103 | val member = createTestMember(address, MemberStatus.Removed) 104 | val instanceId = InstanceId("container") 105 | val info = InstanceInfo(instanceId, InstanceUp, "", Set.empty, None, Set.empty) 106 | 107 | val storage = mock[InstanceStorage] 108 | (storage.getInstance _).expects(instanceId).returns(Future successful info) 109 | 110 | val service = createMemberAutdownService(address, instanceId, storage) 111 | watch(service) 112 | service ! MemberAutoDownService.PollInstanceStatus 113 | service ! MemberRemoved(member, MemberStatus.exiting) 114 | 115 | expectMsgClass(classOf[Terminated]) 116 | } 117 | 118 | it should "should retry on error" in { 119 | val port = 12345 120 | val address = UniqueAddress(Address("akka.tcp", "MemberAutoDownServiceSpec", "localhost", port), 1L) 121 | val instanceId = InstanceId("container") 122 | 123 | val storage = mock[InstanceStorage] 124 | (storage.getInstance _).expects(instanceId).returns(Future failed new Exception("")).atLeastTwice() 125 | 126 | val service = createMemberAutdownService(address, instanceId, storage, 1 second) 127 | service ! MemberAutoDownService.PollInstanceStatus 128 | 129 | val timeout = 2000 130 | Thread.sleep(timeout) 131 | gracefulActorStop(service) 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/storage/zookeeper/ZookeeperClientConfigSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper 17 | 18 | import com.typesafe.config.ConfigFactory 19 | import org.scalatest.{FlatSpec, Matchers} 20 | 21 | class ZookeeperClientConfigSpec extends FlatSpec with Matchers { 22 | 23 | val appConfig = ConfigFactory.parseString( 24 | """ 25 | |zookeeper { 26 | | servers = "localhost:2180" 27 | | connection-interval-ms = 3000 28 | | max-retries = 10 29 | | namespace = "akkeeper" 30 | | can-be-readonly = true 31 | | client-threads = 2 32 | |} 33 | """.stripMargin) 34 | 35 | "Zookeeper Client Config" should "derive properties from application config" in { 36 | val zkConfig = ZookeeperClientConfig.fromConfig(appConfig.getConfig("zookeeper")) 37 | zkConfig.servers shouldBe "localhost:2180" 38 | val interval = 3000 39 | zkConfig.connectionIntervalMs shouldBe interval 40 | val retries = 10 41 | zkConfig.maxRetries shouldBe retries 42 | zkConfig.namespace shouldBe "akkeeper" 43 | zkConfig.canBeReadOnly shouldBe true 44 | zkConfig.clientThreads shouldBe Some(2) 45 | } 46 | 47 | it should "produce child configurations" in { 48 | val rootConfig = ZookeeperClientConfig.fromConfig(appConfig.getConfig("zookeeper")) 49 | rootConfig.namespace shouldBe "akkeeper" 50 | 51 | rootConfig.child("") shouldBe rootConfig 52 | rootConfig.child("instance").namespace shouldBe "akkeeper/instance" 53 | rootConfig.copy(namespace = "akkeeper/") 54 | .child("instance").namespace shouldBe "akkeeper/instance" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/storage/zookeeper/async/AsyncZookeeperStorageSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper.async 17 | 18 | import java.util.UUID 19 | 20 | import akkeeper.AwaitMixin 21 | import akkeeper.storage._ 22 | import akkeeper.storage.zookeeper.ZookeeperClientConfig 23 | import org.apache.curator.test.{TestingServer => ZookeeperServer} 24 | import org.apache.zookeeper.CreateMode 25 | import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} 26 | 27 | class AsyncZookeeperStorageSpec extends FlatSpec 28 | with Matchers with BeforeAndAfterAll with AwaitMixin { 29 | 30 | private val zookeeper = new ZookeeperServer() 31 | private val connectionInterval = 1000 32 | private val clientConfig = ZookeeperClientConfig(zookeeper.getConnectString, 33 | connectionInterval, 1, None, "akkeeper") 34 | 35 | override protected def afterAll(): Unit = { 36 | zookeeper.stop() 37 | super.afterAll() 38 | } 39 | 40 | private def withStorage[T](f: AsyncZookeeperClient => T): T = { 41 | val storage = new AsyncZookeeperClient(clientConfig, CreateMode.PERSISTENT) 42 | storage.start() 43 | try { 44 | f(storage) 45 | } finally { 46 | storage.stop() 47 | } 48 | } 49 | 50 | "An Async Zookeeper storage" should "create a new node" in { 51 | withStorage { storage => 52 | val node = UUID.randomUUID().toString 53 | val payload = Array[Byte](1, 1, 2, 3) 54 | await(storage.create(node, payload)) 55 | 56 | val readResult = await(storage.get(node)) 57 | readResult shouldEqual payload 58 | } 59 | } 60 | 61 | it should "not create a node if it already exists" in { 62 | withStorage { storage => 63 | val node = UUID.randomUUID().toString 64 | val payload = Array[Byte](1, 1, 2, 3) 65 | await(storage.create(node, payload)) 66 | intercept[RecordAlreadyExistsException] { 67 | await(storage.create(node, payload)) 68 | } 69 | } 70 | } 71 | 72 | it should "update a node successfully" in { 73 | withStorage { storage => 74 | val node = UUID.randomUUID().toString 75 | await(storage.create(node)) 76 | await(storage.get(node)) shouldBe empty 77 | 78 | val payload = Array[Byte](1, 1, 2, 3) 79 | await(storage.update(node, payload)) 80 | await(storage.get(node)) shouldEqual payload 81 | } 82 | } 83 | 84 | it should "throw an error on update attempt if node doesn't exist" in { 85 | withStorage { storage => 86 | val node = UUID.randomUUID().toString 87 | val payload = Array[Byte](1, 1, 2, 3) 88 | intercept[RecordNotFoundException] { 89 | await(storage.update(node, payload)) 90 | } 91 | } 92 | } 93 | 94 | it should "delete a node successfully" in { 95 | withStorage { storage => 96 | val node = UUID.randomUUID().toString 97 | await(storage.create(node)) 98 | await(storage.delete(node)) 99 | intercept[RecordNotFoundException] { 100 | await(storage.get(node)) 101 | } 102 | } 103 | } 104 | 105 | it should "throw an error on delete attempt if a node doesn't exist" in { 106 | withStorage { storage => 107 | val node = UUID.randomUUID().toString 108 | intercept[RecordNotFoundException] { 109 | await(storage.delete(node)) 110 | } 111 | } 112 | } 113 | 114 | it should "throw an error on get attempt if a node deosn't exist" in { 115 | withStorage { storage => 116 | val node = UUID.randomUUID().toString 117 | intercept[RecordNotFoundException] { 118 | await(storage.get(node)) 119 | } 120 | } 121 | } 122 | 123 | it should "retrieve node children successfully" in { 124 | withStorage { storage => 125 | val node = UUID.randomUUID().toString 126 | await(storage.create(node)) 127 | await(storage.create(node + "/child1")) 128 | await(storage.create(node + "/child2")) 129 | val result = await(storage.children(node)).toSet 130 | val expected = Set("child1", "child2") 131 | result shouldEqual expected 132 | } 133 | } 134 | 135 | it should "retrieve empty list of node children successfully" in { 136 | withStorage { storage => 137 | val node = UUID.randomUUID().toString 138 | await(storage.create(node)) 139 | val result = await(storage.children(node)) 140 | result shouldBe empty 141 | } 142 | } 143 | 144 | it should "should check the node existence properly" in { 145 | withStorage { storage => 146 | val node = UUID.randomUUID().toString 147 | intercept[RecordNotFoundException] { 148 | await(storage.exists(node)) 149 | } 150 | await(storage.create(node)) 151 | await(storage.exists(node)) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/storage/zookeeper/async/ZookeeperBaseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper.async 17 | 18 | import akkeeper.storage.Storage 19 | import org.apache.curator.test.{TestingServer => ZookeeperServer} 20 | import org.scalatest.Suite 21 | 22 | trait ZookeeperBaseSpec { 23 | this: Suite => 24 | 25 | protected def withStorage[S <: Storage, T](storage: S, server: ZookeeperServer) 26 | (f: S => T): T = { 27 | storage.start() 28 | try { 29 | f(storage) 30 | } finally { 31 | storage.stop() 32 | server.stop() 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /akkeeper/src/test/scala/akkeeper/storage/zookeeper/async/ZookeeperInstanceStorageSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package akkeeper.storage.zookeeper.async 17 | 18 | import akka.actor.Address 19 | import akka.cluster.UniqueAddress 20 | import akkeeper.AwaitMixin 21 | import akkeeper.address._ 22 | import akkeeper.api._ 23 | import akkeeper.storage._ 24 | import akkeeper.storage.zookeeper.ZookeeperClientConfig 25 | import org.apache.curator.test.{TestingServer => ZookeeperServer} 26 | import org.scalatest.{FlatSpec, Matchers} 27 | 28 | class ZookeeperInstanceStorageSpec extends FlatSpec 29 | with Matchers with AwaitMixin with ZookeeperBaseSpec { 30 | 31 | private def withStorage[T](f: InstanceStorage => T): T = { 32 | val zookeeper = new ZookeeperServer() 33 | val connectionInterval = 1000 34 | val clientConfig = ZookeeperClientConfig(zookeeper.getConnectString, connectionInterval, 35 | 1, None, "akkeeper") 36 | val storage = new ZookeeperInstanceStorage(clientConfig) 37 | withStorage[InstanceStorage, T](storage, zookeeper)(f) 38 | } 39 | 40 | private def createInstanceStatus(container: String): InstanceInfo = { 41 | val id = InstanceId(container) 42 | val port = 2550 43 | InstanceInfo( 44 | instanceId = id, 45 | status = InstanceUp, 46 | containerName = container, 47 | roles = Set("testRole"), 48 | address = Some(UniqueAddress(Address("akka.tcp", "AkkaSystem", "localhost", port), 1L)), 49 | actors = Set("/user/actor1") 50 | ) 51 | } 52 | 53 | "A Zookeeper Instance Storage" should "return empty list of instances" in { 54 | withStorage { storage => 55 | val instances = await(storage.getInstances) 56 | instances shouldBe empty 57 | 58 | val instancesByContainer = await(storage.getInstancesByContainer("container")) 59 | instancesByContainer shouldBe empty 60 | } 61 | } 62 | 63 | it should "register a new instance" in { 64 | withStorage { storage => 65 | val container = "container1" 66 | val instance = createInstanceStatus(container) 67 | 68 | val instancesByContainer1 = await(storage.getInstancesByContainer("container1")) 69 | instancesByContainer1 shouldBe empty 70 | 71 | await(storage.registerInstance(instance)) shouldBe instance.instanceId 72 | val instancesByContainer2 = await(storage.getInstancesByContainer("container1")) 73 | instancesByContainer2.size shouldBe 1 74 | instancesByContainer2(0) shouldBe instance.instanceId 75 | 76 | await(storage.getInstances) should not be (empty) 77 | 78 | val instanceFromStorage = await(storage.getInstance(instance.instanceId)) 79 | instanceFromStorage shouldBe instance 80 | } 81 | } 82 | 83 | it should "throw an error on register attempt if instance already exists" in { 84 | withStorage { storage => 85 | val container = "container1" 86 | val instance = createInstanceStatus(container) 87 | 88 | await(storage.registerInstance(instance)) shouldBe instance.instanceId 89 | intercept[RecordAlreadyExistsException] { 90 | await(storage.registerInstance(instance)) 91 | } 92 | } 93 | } 94 | 95 | it should "fetch multiple instances from different containers" in { 96 | withStorage { storage => 97 | val container1 = "container1" 98 | val instance1 = createInstanceStatus(container1) 99 | 100 | val container2 = "container2" 101 | val instance2 = createInstanceStatus(container2) 102 | 103 | await(storage.registerInstance(instance1)) shouldBe instance1.instanceId 104 | await(storage.registerInstance(instance2)) shouldBe instance2.instanceId 105 | 106 | val instances = await(storage.getInstances) 107 | instances.size shouldBe 2 108 | 109 | instances should contain (instance1.instanceId) 110 | instances should contain (instance2.instanceId) 111 | } 112 | } 113 | 114 | it should "thrown an error if the instance with the given ID doesn't exist" in { 115 | withStorage { storage => 116 | val id = InstanceId("container") 117 | intercept[RecordNotFoundException] { 118 | await(storage.getInstance(id)) 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /bin/akkeeper-submit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2017-2018 Iaroslav Zeigerman 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | JAVA_BIN="java" 20 | if [[ "$JAVA_HOME" != "" ]]; then 21 | JAVA_BIN="$JAVA_HOME/bin/java" 22 | fi 23 | 24 | export HADOOP_CONF_DIR="${HADOOP_CONF_DIR:-$HADOOP_HOME/etc/hadoop}" 25 | export YARN_CONF_DIR="${YARN_CONF_DIR:-$HADOOP_HOME/etc/hadoop}" 26 | 27 | YARN_BIN="yarn" 28 | if [[ "$HADOOP_HOME" != "" ]]; then 29 | YARN_BIN="$HADOOP_HOME/bin/yarn" 30 | elif [[ "$HADOOP_PREFIX" != "" ]]; then 31 | YARN_BIN="$HADOOP_PREFIX/bin/yarn" 32 | fi 33 | 34 | AKKEEPER_HOME="${AKKEEPER_HOME:-$(dirname $(dirname $(realpath $0)))}" 35 | AKKEEPER_JARS="$AKKEEPER_HOME/lib" 36 | AKEEEPER_FAT_JAR="$AKKEEPER_JARS/$(ls $AKKEEPER_JARS | grep --color=never akkeeper-assembly)" 37 | AKKEEPER_CLASSPATH="$AKKEEPER_JARS/*:`$YARN_BIN classpath`" 38 | AKKEEPER_MAIN="akkeeper.launcher.LauncherMain" 39 | 40 | exec $JAVA_BIN -cp $AKKEEPER_CLASSPATH $AKKEEPER_MAIN --akkeeperJar $AKEEEPER_FAT_JAR "$@" 41 | -------------------------------------------------------------------------------- /docs/rest.md: -------------------------------------------------------------------------------- 1 | # Akkeeper REST API 2 | 3 | * [Deploy API](#deploy-api) 4 | * [Deploy instances](#deploy-instances) 5 | * [Monitoring API](#monitoring-api) 6 | * [Retrieve a list of all instances in a cluster](#retrieve-a-list-of-all-instances-in-a-cluster) 7 | * [Get all instances by specific role, container name or status](#get-all-instances-by-specific-role-container-name-or-status) 8 | * [Get a detailed info about the instance](#get-a-detailed-info-about-the-instance) 9 | * [Instance termination](#instance-termination) 10 | * [Container API](#container-api) 11 | * [Get a list of available containers](#get-a-list-of-available-containers) 12 | * [Get a detailed info about container](#get-a-detailed-info-about-container) 13 | * [Create a new container](#create-a-new-container) 14 | * [Update the existing container](#update-the-existing-container) 15 | * [Delete container](#delete-container) 16 | * [Master API](#master-api) 17 | * [Terminate master](#terminate-master) 18 | 19 | ## Deploy API 20 | ### Deploy instances 21 | 22 | **Command** 23 | ````bash 24 | curl -X POST -H "Content-Type: application/json" -d "@body.json" http://:5050/api/v1/deploy 25 | ```` 26 | **Request body** 27 | ```json 28 | { 29 | "name": "pingContainer", 30 | "quantity": 1 31 | } 32 | ``` 33 | **Response code** 34 | 35 | 202 - Accepted 36 | 37 | **Response body** 38 | ```json 39 | { 40 | "requestId": "477b324f-f620-45df-a0e6-bf7211d01fd2", 41 | "containerName": "pingContainer", 42 | "instanceIds": ["pingContainer-2aa1a500-4c6e-423c-bde1-d22295c7e1b6"] 43 | } 44 | ``` 45 | 46 | ## Monitoring API 47 | ### Retrieve a list of all instances in a cluster 48 | 49 | **Command** 50 | ```bash 51 | curl -X GET -H "Content-Type: application/json" http://:5050/api/v1/instances 52 | ``` 53 | **Request body** 54 | 55 | **Response code** 56 | 57 | 200 - OK 58 | 59 | **Response body** 60 | ```json 61 | { 62 | "requestId": "1fec516e-aeda-4859-b25c-bd641988c91c", 63 | "instanceIds": ["pingContainer-2aa1a500-4c6e-423c-bde1-d22295c7e1b6", "pingContainer-2e76ea01-a623-4aea-abfa-cf5e37c6c898"] 64 | } 65 | ``` 66 | 67 | ### Get all instances by specific role, container name or status 68 | 69 | **Command** 70 | ```bash 71 | curl -X GET -H "Content-Type: application/json" http://:5050/api/v1/instances?role=ping&containerName=pingContainer&status=up 72 | ``` 73 | **Request body** 74 | 75 | **Response code** 76 | 77 | 200 - OK 78 | 79 | **Response body** 80 | ```json 81 | { 82 | "requestId": "1fec516e-aeda-4859-b25c-bd641988c91c", 83 | "instanceIds": ["pingContainer-2aa1a500-4c6e-423c-bde1-d22295c7e1b6", "pingContainer-2e76ea01-a623-4aea-abfa-cf5e37c6c898"] 84 | } 85 | ``` 86 | 87 | ### Get a detailed info about the instance 88 | 89 | **Command** 90 | ```bash 91 | curl -X GET -H "Content-Type: application/json" http://:5050/api/v1/instances/pingContainer-2aa1a500-4c6e-423c-bde1-d22295c7e1b6 92 | ``` 93 | **Request body** 94 | 95 | **Response code** 96 | 97 | 200 - OK 98 | 99 | **Response body** 100 | ```json 101 | { 102 | "requestId": "0f292e02-d12c-4926-82f4-085e4cff8a69", 103 | "info": { 104 | "instanceId": "pingContainer-2aa1a500-4c6e-423c-bde1-d22295c7e1b6", 105 | "containerName": "pingContainer", 106 | "roles": ["ping"], 107 | "status": "UP", 108 | "actors": ["/user/akkeeperInstance/pingService"], 109 | "address": { 110 | "protocol": "akka.tcp", 111 | "system": "AkkeeperSystem", 112 | "host": "172.17.0.7", 113 | "port": 44874 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | ### Instance termination 120 | 121 | **Command** 122 | ```bash 123 | curl -X DELETE -H "Content-Type: application/json" http://:5050/api/v1/instances/pingContainer-2aa1a500-4c6e-423c-bde1-d22295c7e1b6 124 | ``` 125 | **Request body** 126 | 127 | **Response code** 128 | 129 | 200 - OK 130 | 131 | **Response body** 132 | ```json 133 | { 134 | "requestId": "382f7496-59b8-44fa-b877-35088faa1917", 135 | "instanceId": "pingContainer-2aa1a500-4c6e-423c-bde1-d22295c7e1b6" 136 | } 137 | ``` 138 | 139 | ## Container API 140 | ### Get a list of available containers 141 | 142 | **Command** 143 | ```bash 144 | curl -X GET -H "Content-Type: application/json" http://:5050/api/v1/containers 145 | ``` 146 | **Request body** 147 | 148 | **Response code** 149 | 150 | 200 - OK 151 | 152 | **Response body** 153 | ```json 154 | { 155 | "requestId": "5096a7db-3cf9-4696-89e1-e0a143cc4d25", 156 | "containers": ["pingContainer"] 157 | } 158 | ``` 159 | 160 | ### Get a detailed info about container 161 | 162 | **Command** 163 | ```bash 164 | curl -X GET -H "Content-Type: application/json" http://:5050/api/v1/containers/pingContainer 165 | ``` 166 | **Request body** 167 | 168 | **Response code** 169 | 170 | 200 - OK 171 | 172 | **Response body** 173 | ```json 174 | { 175 | "requestId": "f423e203-936b-45e1-8a21-6b9da7c4f1e3", 176 | "container": { 177 | "name": "pingContainer", 178 | "jvmArgs": ["-Xmx1G"], 179 | "jvmProperties": { 180 | "akka.cluster.roles.\"0\"": "ping", 181 | "ping-app.response-value": "Akkeeper" 182 | }, 183 | "environment": { 184 | 185 | }, 186 | "actors": [{ 187 | "name": "pingService", 188 | "fqn": "akkeeper.examples.PingActor" 189 | }], 190 | "cpus": 1, 191 | "memory": 1024 192 | } 193 | } 194 | ``` 195 | 196 | ## Master API 197 | ### Terminate master 198 | 199 | **Command** 200 | ```bash 201 | curl -X POST http://:5050/api/v1/master/terminate 202 | ``` 203 | **Request body** 204 | 205 | **Response code** 206 | 207 | 202 - Accepted 208 | 209 | **Response body** 210 | -------------------------------------------------------------------------------- /project/ReferenceMergeStrategy.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Iaroslav Zeigerman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.io.File 17 | 18 | import sbtassembly.MergeStrategy 19 | 20 | object ReferenceMergeStrategy extends MergeStrategy { 21 | override val name: String = "referenceConfMerge" 22 | 23 | override def apply(tempDir: File, path: String, files: Seq[File]): Either[String, Seq[(File, String)]] = { 24 | // Reverse the order of files to ensure that Akkeeper reference.conf goes last. 25 | val newFiles = files.reverse 26 | MergeStrategy.concat(tempDir, path, newFiles) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.0.0 2 | 3 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.7") 2 | 3 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") 4 | 5 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 6 | 7 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.5") 8 | 9 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.1") 10 | 11 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.6") 12 | 13 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0") 14 | -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Scalastyle standard configuration 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | /* 12 | * Copyright 2017-2018 Iaroslav Zeigerman 13 | * 14 | * Licensed under the Apache License, Version 2.0 (the "License"); 15 | * you may not use this file except in compliance with the License. 16 | * You may obtain a copy of the License at 17 | * 18 | * http://www.apache.org/licenses/LICENSE-2.0 19 | * 20 | * Unless required by applicable law or agreed to in writing, software 21 | * distributed under the License is distributed on an "AS IS" BASIS, 22 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | * See the License for the specific language governing permissions and 24 | * limitations under the License. 25 | */ 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | --------------------------------------------------------------------------------