├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.adoc ├── build.sbt ├── constructr-akka-testing ├── build.sbt └── src │ └── multi-jvm │ ├── resources │ └── log4j2.xml │ └── scala │ └── com │ └── lightbend │ └── constructr │ └── coordination │ └── zookeeper │ ├── DockerZookeeper.scala │ ├── MultiNodeZookeeperConstructrBaseSpec.scala │ └── MultiNodeZookeeperConstructrSpec.scala ├── constructr-coordination-zookeeper ├── build.sbt └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── com │ │ └── lightbend │ │ └── constructr │ │ └── coordination │ │ └── zookeeper │ │ ├── ZookeeperCoordination.scala │ │ └── ZookeeperNodes.scala │ └── test │ └── scala │ └── com │ └── lightbend │ └── constructr │ └── coordination │ └── zookeeper │ ├── DockerZookeeper.scala │ ├── ZookeeperCoordinationSpec.scala │ └── ZookeeperNodesSpec.scala ├── project ├── Build.scala ├── Dependencies.scala ├── build.properties └── plugins.sbt ├── shell-prompt.sbt └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | # sbt 2 | lib_managed 3 | project/project 4 | target 5 | 6 | # Worksheets (Eclipse or IntelliJ) 7 | *.sc 8 | 9 | # Eclipse 10 | .cache* 11 | .classpath 12 | .project 13 | .scala_dependencies 14 | .settings 15 | .target 16 | .worksheet 17 | 18 | # IntelliJ 19 | .idea 20 | 21 | # ENSIME 22 | .ensime 23 | .ensime_lucene 24 | 25 | # Mac 26 | .DS_Store 27 | 28 | # Akka Persistence 29 | journal 30 | snapshots 31 | 32 | # Log files 33 | *.log 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: scala 4 | 5 | jdk: 6 | - oraclejdk8 7 | 8 | services: 9 | - docker 10 | 11 | script: sbt run-tests 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Lightbend Project & Developer Guidelines 2 | 3 | These guidelines are meant to be a living document that should be changed and adapted as needed. We encourage changes that makes it easier to achieve our goals in an efficient way. 4 | 5 | These guidelines mainly applies to Lightbend’s “mature” projects - not necessarily to projects of the type ‘collection of scripts’ etc. 6 | 7 | ## General Workflow 8 | 9 | This is the process for committing code into master. There are of course exceptions to these rules, for example minor changes to comments and documentation, fixing a broken build etc. 10 | 11 | 1. Make sure you have signed the [Lightbend CLA](http://www.lightbend.com/contribute/cla), if not, sign it online. 12 | 2. Before starting to work on a feature or a fix, you have to make sure that: 13 | 1. There is a ticket for your work in the project's issue tracker. If not, create it first. 14 | 2. The ticket has been scheduled for the current milestone. 15 | 3. The ticket is estimated by the team. 16 | 4. The ticket have been discussed and prioritized by the team. 17 | 3. You should always perform your work in a Git feature branch. The branch should be given a descriptive name that explains its intent. Some teams also like adding the ticket number and/or the [GitHub](http://github.com) user ID to the branch name, these details is up to each of the individual teams. 18 | 4. When the feature or fix is completed you should open a [Pull Request](https://help.github.com/articles/using-pull-requests) on GitHub. 19 | 5. The Pull Request should be reviewed by other maintainers (as many as feasible/practical). Note that the maintainers can consist of outside contributors, both within and outside Lightbend. Outside contributors (for example from EPFL or independent committers) are encouraged to participate in the review process, it is not a closed process. 20 | 6. After the review you should fix the issues as needed (pushing a new commit for new review etc.), iterating until the reviewers give their thumbs up. 21 | 7. Once the code has passed review the Pull Request can be merged into the master branch. 22 | 23 | ## Pull Request Requirements 24 | 25 | For a Pull Request to be considered at all it has to meet these requirements: 26 | 27 | 1. Live up to the current code standard: 28 | - Not violate [DRY](http://programmer.97things.oreilly.com/wiki/index.php/Don%27t_Repeat_Yourself). 29 | - [Boy Scout Rule](http://programmer.97things.oreilly.com/wiki/index.php/The_Boy_Scout_Rule) needs to have been applied. 30 | 2. Regardless if the code introduces new features or fixes bugs or regressions, it must have comprehensive tests. 31 | 3. The code must be well documented in the Lightbend's standard documentation format (see the ‘Documentation’ section below). 32 | 4. Copyright: 33 | All Lightbend projects must include Lightbend copyright notices. Each project can choose between one of two approaches: 34 | 1. All source files in the project must have a Lightbend copyright notice in the file header. 35 | 2. The Notices file for the project includes the Lightbend copyright notice and no other files contain copyright notices. See http://www.apache.org/legal/src-headers.html for instructions for managing this approach for copyrights. 36 | 37 | Other guidelines to follow for copyright notices: 38 | - Use a form of ``Copyright (C) 2016 Lightbend Inc. ``, where the start year is when the project or file was first created and the end year is the last time the project or file was modified. 39 | - Never delete or change existing copyright notices, just add additional info. 40 | - Do not use ``@author`` tags since it does not encourage [Collective Code Ownership](http://www.extremeprogramming.org/rules/collective.html). However, each project should make sure that the contributors gets the credit they deserve—in a text file or page on the project website and in the release notes etc. 41 | 42 | If these requirements are not met then the code should **not** be merged into master, or even reviewed - regardless of how good or important it is. No exceptions. 43 | 44 | ## Continuous Integration 45 | 46 | Each project should be configured to use a continuous integration (CI) tool (i.e. a build server ala Jenkins). Lightbend has a Jenkins server farm that can be used. The CI tool should, on each push to master, build the **full** distribution and run **all** tests, and if something fails it should email out a notification with the failure report to the committer and the core team. The CI tool should also be used in conjunction with Lightbend’s Pull Request Validator (discussed below). 47 | 48 | ## Documentation 49 | 50 | All documentation should be generated using the sbt-site-plugin, *or* publish artifacts to a repository that can be consumed by the Lightbend stack. 51 | 52 | All documentation must abide by the following maxims: 53 | 54 | - Example code should be run as part of an automated test suite. 55 | - Version should be **programmatically** specifiable to the build. 56 | - Generation should be **completely automated** and available for scripting. 57 | - Artifacts that must be included in the Lightbend Stack should be published to a maven “documentation” repository as documentation artifacts. 58 | 59 | All documentation is preferred to be in Lightbend's standard documentation format [reStructuredText](http://doc.akka.io/docs/akka/snapshot/dev/documentation.html) compiled using Lightbend's customized [Sphinx](http://sphinx.pocoo.org/) based documentation generation system, which among other things allows all code in the documentation to be externalized into compiled files and imported into the documentation. 60 | 61 | For more info, or for a starting point for new projects, look at the [Lightbend Documentation Template project](https://github.com/typesafehub/doc-template) 62 | 63 | For larger projects that have invested a lot of time and resources into their current documentation and samples scheme (like for example Play), it is understandable that it will take some time to migrate to this new model. In these cases someone from the project needs to take the responsibility of manual QA and verifier for the documentation and samples. 64 | 65 | ## External Dependencies 66 | 67 | All the external runtime dependencies for the project, including transitive dependencies, must have an open source license that is equal to, or compatible with, [Apache 2](http://www.apache.org/licenses/LICENSE-2.0). 68 | 69 | This must be ensured by manually verifying the license for all the dependencies for the project: 70 | 71 | 1. Whenever a committer to the project changes a version of a dependency (including Scala) in the build file. 72 | 2. Whenever a committer to the project adds a new dependency. 73 | 3. Whenever a new release is cut (public or private for a customer). 74 | 75 | Which licenses that are compatible with Apache 2 are defined in [this doc](http://www.apache.org/legal/3party.html#category-a), where you can see that the licenses that are listed under ``Category A`` automatically compatible with Apache 2, while the ones listed under ``Category B`` needs additional action: 76 | > “Each license in this category requires some degree of [reciprocity](http://www.apache.org/legal/3party.html#define-reciprocal); therefore, additional action must be taken in order to minimize the chance that a user of an Apache product will create a derivative work of a reciprocally-licensed portion of an Apache product without being aware of the applicable requirements.” 77 | 78 | Each project must also create and maintain a list of all dependencies and their licenses, including all their transitive dependencies. This can be done in either in the documentation or in the build file next to each dependency. 79 | 80 | ## Work In Progress 81 | 82 | It is ok to work on a public feature branch in the GitHub repository. Something that can sometimes be useful for early feedback etc. If so then it is preferable to name the branch accordingly. This can be done by either prefix the name with ``wip-`` as in ‘Work In Progress’, or use hierarchical names like ``wip/..``, ``feature/..`` or ``topic/..``. Either way is fine as long as it is clear that it is work in progress and not ready for merge. This work can temporarily have a lower standard. However, to be merged into master it will have to go through the regular process outlined above, with Pull Request, review etc.. 83 | 84 | Also, to facilitate both well-formed commits and working together, the ``wip`` and ``feature``/``topic`` identifiers also have special meaning. Any branch labelled with ``wip`` is considered “git-unstable” and may be rebased and have its history rewritten. Any branch with ``feature``/``topic`` in the name is considered “stable” enough for others to depend on when a group is working on a feature. 85 | 86 | ## Creating Commits And Writing Commit Messages 87 | 88 | Follow these guidelines when creating public commits and writing commit messages. 89 | 90 | 1. If your work spans multiple local commits (for example; if you do safe point commits while working in a feature branch or work in a branch for long time doing merges/rebases etc.) then please do not commit it all but rewrite the history by squashing the commits into a single big commit which you write a good commit message for (like discussed in the following sections). For more info read this article: [Git Workflow](http://sandofsky.com/blog/git-workflow.html). Every commit should be able to be used in isolation, cherry picked etc. 91 | 2. First line should be a descriptive sentence what the commit is doing. It should be possible to fully understand what the commit does by just reading this single line. It is **not ok** to only list the ticket number, type "minor fix" or similar. Include reference to ticket number, prefixed with #, at the end of the first line. If the commit is a small fix, then you are done. If not, go to 3. 92 | 3. Following the single line description should be a blank line followed by an enumerated list with the details of the commit. 93 | 4. Add keywords for your commit (depending on the degree of automation we reach, the list may change over time): 94 | * ``Review by @gituser`` - if you want to notify someone on the team. The others can, and are encouraged to participate. 95 | * ``Fix/Fixing/Fixes/Close/Closing/Refs #ticket`` - if you want to mark the ticket as fixed in the issue tracker (Assembla understands this). 96 | * ``backport to _branch name_`` - if the fix needs to be cherry-picked to another branch (like 2.9.x, 2.10.x, etc) 97 | 98 | Example: 99 | 100 | Adding monadic API to Future. Fixes #2731 101 | 102 | * Details 1 103 | * Details 2 104 | * Details 3 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2016 Lightbend Inc. [http://www.lightbend.com] 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | [http://www.apache.org/licenses/LICENSE-2.0] 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. 16 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Lightbend Inc. -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = ConstructR-ZooKeeper 2 | 3 | image:https://travis-ci.org/typesafehub/constructr-zookeeper.svg?branch=master[Build Status,link=https://travis-ci.org/typesafehub/constructr-zookeeper] 4 | 5 | This library enables to use https://zookeeper.apache.org/[ZooKeeper] as cluster coordinator in a https://github.com/hseeberger/constructr[ConstructR] based node. 6 | 7 | https://github.com/hseeberger/constructr[ConstructR] aims at cluster bootstrapping (construction) by using a coordination service and provides etcd as the default one. By means of this library, you will be able to use https://zookeeper.apache.org/[ZooKeeper] as coordination service instead. 8 | 9 | == Installation 10 | 11 | SBT:: 12 | You will need to add the following dependency in your `build.sbt` in addition to the core ConstructR ones: 13 | + 14 | [source] 15 | ---- 16 | libraryDependencies += "com.lightbend.constructr" %% "constructr-coordination-zookeeper" % "0.4.0" 17 | ---- 18 | 19 | Gradle:: 20 | Add following to `build.gradle`. 21 | + 22 | .Scala 2.11 23 | ---- 24 | compile 'com.lightbend.constructr:constructr-coordination-zookeeper_2.11:0.4.0' 25 | ---- 26 | + 27 | .Scala 2.12 28 | ---- 29 | compile 'com.lightbend.constructr:constructr-coordination-zookeeper_2.12:0.4.0' 30 | ---- 31 | 32 | == Configuration 33 | 34 | Check https://github.com/hseeberger/constructr#coordination[this section] in ConstructR for general information about configuration. 35 | 36 | Check link:constructr-coordination-zookeeper/src/main/resources/reference.conf[reference.conf] for ZooKeeper related configuration. 37 | 38 | === Configuring ZK cluster nodes 39 | 40 | The default configuration tries to establish a connection to ZooKeeper on `localhost:2181`. 41 | 42 | Override the `constructr.coordination.nodes` configuration to specify another ZooKeeper node: 43 | 44 | [source] 45 | ---- 46 | constructr.coordination.nodes = ["10.10.10.10:2181"] 47 | ---- 48 | 49 | The format per node `ip:port`. 50 | 51 | You are also able to connect to a multi-node cluster by specifying multiple nodes, separated by a comma: 52 | 53 | [source] 54 | ---- 55 | constructr.coordination.nodes = ["10.10.10.10:2181", "10.10.10.11:2181", "10.10.10.12:2181"] 56 | ---- 57 | 58 | Additionally, comma separated connection string format is supported 59 | 60 | [source] 61 | ---- 62 | constructr.coordination.nodes = "10.10.10.10:2181,10.10.10.11:2181,10.10.10.12:2181" 63 | ---- 64 | 65 | == Testing 66 | 67 | Run tests: 68 | 69 | $ sbt test 70 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val constructrZookeeperRoot = project 2 | .copy(id = "constructr-zookeeper-root") 3 | .in(file(".")) 4 | .aggregate(constructrCoordinationZookeeper, constructrAkkaTesting) 5 | 6 | lazy val constructrCoordinationZookeeper = project 7 | .copy(id = "constructr-coordination-zookeeper") 8 | .in(file("constructr-coordination-zookeeper")) 9 | .enablePlugins(AutomateHeaderPlugin) 10 | 11 | lazy val constructrAkkaTesting = project 12 | .copy(id = "constructr-akka-testing") 13 | .in(file("constructr-akka-testing")) 14 | .enablePlugins(AutomateHeaderPlugin) 15 | .configs(MultiJvm) 16 | .dependsOn(constructrCoordinationZookeeper % "test->compile") 17 | 18 | name := "constructr-zookeeper-root" 19 | 20 | unmanagedSourceDirectories.in(Compile) := Vector.empty 21 | unmanagedSourceDirectories.in(Test) := Vector.empty 22 | 23 | // Executes the sbt sub projects sequentially and ensures that the shutdown of the first project has been occurred 24 | // before starting with the subsequent project. This is necessary due to starting and stopping Docker images during tests. 25 | parallelExecution in Global := false 26 | -------------------------------------------------------------------------------- /constructr-akka-testing/build.sbt: -------------------------------------------------------------------------------- 1 | name := "constructr-akka-testing" 2 | 3 | libraryDependencies ++= Vector( 4 | Library.constructr % "test", 5 | Library.akkaCluster % "test", 6 | Library.akkaMultiNodeTestkit % "test", 7 | Library.akkaTestkit % "test", 8 | Library.scalaTest % "test", 9 | Library.dockerTestKit % "test", 10 | Library.dockerTestKitImpl % "test" 11 | ) 12 | 13 | 14 | unmanagedSourceDirectories.in(MultiJvm) := Vector(scalaSource.in(MultiJvm).value) 15 | 16 | test.in(Test) := { test.in(MultiJvm).value; test.in(Test).value } 17 | 18 | inConfig(MultiJvm)(SbtScalariform.configScalariformSettings) 19 | 20 | AutomateHeaderPlugin.automateFor(MultiJvm) 21 | HeaderPlugin.settingsFor(MultiJvm) 22 | -------------------------------------------------------------------------------- /constructr-akka-testing/src/multi-jvm/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %date{HH:mm:ss} %-5level %logger{0} [%X{akkaSource}] - %msg%n 8 | 9 | 10 | 11 | 12 | %date{HH:mm:ss} %-5level %logger{0} [%X{akkaSource}] - %msg%n 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /constructr-akka-testing/src/multi-jvm/scala/com/lightbend/constructr/coordination/zookeeper/DockerZookeeper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination.zookeeper 18 | 19 | import com.whisk.docker.impl.dockerjava.DockerKitDockerJava 20 | import com.whisk.docker.{DockerContainer, DockerReadyChecker} 21 | 22 | trait DockerZookeeper extends DockerKitDockerJava { 23 | val DefaultZookeeperPort = 2181 24 | 25 | val zookeeperContainer: DockerContainer = DockerContainer("jplock/zookeeper:3.4.10") 26 | .withPorts(DefaultZookeeperPort -> Some(DefaultZookeeperPort)) 27 | .withReadyChecker(DockerReadyChecker.LogLineContains("binding to port")) 28 | 29 | abstract override def dockerContainers: List[DockerContainer] = zookeeperContainer :: super.dockerContainers 30 | } 31 | -------------------------------------------------------------------------------- /constructr-akka-testing/src/multi-jvm/scala/com/lightbend/constructr/coordination/zookeeper/MultiNodeZookeeperConstructrBaseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination.zookeeper 18 | 19 | import akka.actor.ActorDSL.{Act, actor} 20 | import akka.cluster.{Cluster, ClusterEvent} 21 | import akka.pattern.ask 22 | import akka.remote.testkit.{MultiNodeConfig, MultiNodeSpec} 23 | import akka.stream.ActorMaterializer 24 | import akka.testkit.TestDuration 25 | import akka.util.Timeout 26 | import com.lightbend.constructr.coordination.zookeeper.ZookeeperNodes.Nodes 27 | import com.typesafe.config.ConfigFactory 28 | import de.heikoseeberger.constructr.ConstructrExtension 29 | import de.heikoseeberger.constructr.coordination.Coordination 30 | import org.apache.curator.framework.CuratorFrameworkFactory 31 | import org.apache.curator.retry.RetryNTimes 32 | import org.scalatest.{BeforeAndAfterAll, FreeSpecLike, Matchers} 33 | 34 | import scala.concurrent.Await 35 | import scala.concurrent.duration._ 36 | 37 | object ConstructrMultiNodeConfig extends DockerZookeeper { 38 | val coordinationHost: String = { 39 | val dockerHostPattern = """tcp://(\S+):\d{1,5}""".r 40 | sys.env.get("DOCKER_HOST") 41 | .collect { case dockerHostPattern(address) => address } 42 | .getOrElse("127.0.0.1") 43 | } 44 | } 45 | 46 | class ConstructrMultiNodeConfig(zkPort: Int) extends MultiNodeConfig { 47 | 48 | import ConstructrMultiNodeConfig._ 49 | 50 | commonConfig(ConfigFactory.load()) 51 | for (n <- 1.to(5)) { 52 | val port = 2550 + n 53 | nodeConfig(role(port.toString))(ConfigFactory.parseString( 54 | s"""|akka.actor.provider = akka.cluster.ClusterActorRefProvider 55 | |akka.remote.netty.tcp.hostname = "127.0.0.1" 56 | |akka.remote.netty.tcp.port = $port 57 | |constructr.coordination.nodes = "$coordinationHost:$zkPort" 58 | |""".stripMargin 59 | )) 60 | } 61 | } 62 | 63 | abstract class MultiNodeZookeeperConstructrBaseSpec(coordinationPort: Int, clusterName: String) 64 | extends MultiNodeSpec(new ConstructrMultiNodeConfig(coordinationPort)) 65 | with FreeSpecLike with Matchers with BeforeAndAfterAll { 66 | 67 | import ConstructrMultiNodeConfig._ 68 | 69 | implicit val mat: ActorMaterializer = ActorMaterializer() 70 | 71 | private val zookeeperClient = CuratorFrameworkFactory.builder() 72 | .connectString(system.settings.config.getString(Nodes)) 73 | .retryPolicy(new RetryNTimes(0, 0)) 74 | .build() 75 | 76 | "Constructr should manage an Akka cluster" in { 77 | 78 | ConstructrExtension(system) 79 | val zookeeperCoordination = Coordination(clusterName, system) 80 | 81 | enterBarrier("coordination-started") 82 | 83 | val listener = actor(new Act { 84 | 85 | import ClusterEvent._ 86 | 87 | var isMember = false 88 | Cluster(context.system).subscribe(self, InitialStateAsEvents, classOf[MemberJoined], classOf[MemberUp]) 89 | become { 90 | case "isMember" => sender() ! isMember 91 | case MemberJoined(member) if member.address == Cluster(context.system).selfAddress => isMember = true 92 | case MemberUp(member) if member.address == Cluster(context.system).selfAddress => isMember = true 93 | } 94 | }) 95 | within(20.seconds.dilated) { 96 | awaitAssert { 97 | implicit val timeout: Timeout = Timeout(1.second.dilated) 98 | val isMember = Await.result((listener ? "isMember").mapTo[Boolean], 1.second.dilated) 99 | isMember shouldBe true 100 | } 101 | } 102 | 103 | enterBarrier("cluster-formed") 104 | 105 | within(5.seconds.dilated) { 106 | awaitAssert { 107 | val constructrNodes = Await.result( 108 | zookeeperCoordination.getNodes(), 109 | 1.second.dilated 110 | ) 111 | roles.to[Set].map(_.name.toInt) shouldEqual constructrNodes.flatMap(_.port) 112 | } 113 | } 114 | 115 | enterBarrier("done") 116 | } 117 | 118 | override def initialParticipants: Int = roles.size 119 | 120 | override protected def beforeAll(): Unit = multiNodeSpecBeforeAll 121 | 122 | override protected def atStartup(): Unit = { 123 | super.atStartup() 124 | runOn(roles.head) { 125 | startAllOrFail() 126 | } 127 | zookeeperClient.start() 128 | } 129 | 130 | override protected def afterAll(): Unit = multiNodeSpecAfterAll 131 | 132 | override protected def afterTermination(): Unit = { 133 | super.afterTermination() 134 | zookeeperClient.close() 135 | runOn(roles.head) { 136 | stopAllQuietly() 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /constructr-akka-testing/src/multi-jvm/scala/com/lightbend/constructr/coordination/zookeeper/MultiNodeZookeeperConstructrSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination.zookeeper 18 | 19 | class MultiNodeZookeeperConstructrSpecMultiJvmNode1 extends MultiNodeZookeeperConstructrSpec 20 | class MultiNodeZookeeperConstructrSpecMultiJvmNode2 extends MultiNodeZookeeperConstructrSpec 21 | class MultiNodeZookeeperConstructrSpecMultiJvmNode3 extends MultiNodeZookeeperConstructrSpec 22 | class MultiNodeZookeeperConstructrSpecMultiJvmNode4 extends MultiNodeZookeeperConstructrSpec 23 | class MultiNodeZookeeperConstructrSpecMultiJvmNode5 extends MultiNodeZookeeperConstructrSpec 24 | 25 | abstract class MultiNodeZookeeperConstructrSpec extends MultiNodeZookeeperConstructrBaseSpec( 26 | 2181, 27 | "MultiNodeZookeeperConstructrBaseSpec" 28 | ) 29 | -------------------------------------------------------------------------------- /constructr-coordination-zookeeper/build.sbt: -------------------------------------------------------------------------------- 1 | name := "constructr-coordination-zookeeper" 2 | 3 | libraryDependencies ++= Vector( 4 | Library.akkaActor, 5 | Library.constructrCoordination, 6 | Library.curatorFramework, 7 | Library.curatorRecipes, 8 | Library.akkaTestkit % "test", 9 | Library.scalaTest % "test", 10 | Library.dockerTestKit % "test", 11 | Library.dockerTestKitImpl % "test" 12 | ) 13 | -------------------------------------------------------------------------------- /constructr-coordination-zookeeper/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Lightbend Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | constructr { 16 | coordination { 17 | class-name = com.lightbend.constructr.coordination.zookeeper.ZookeeperCoordination 18 | nodes = ["localhost:2181"] 19 | zookeeper { 20 | rootpath = "/constructr" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /constructr-coordination-zookeeper/src/main/scala/com/lightbend/constructr/coordination/zookeeper/ZookeeperCoordination.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination 18 | package zookeeper 19 | 20 | import java.nio.charset.StandardCharsets.UTF_8 21 | import java.time.Instant 22 | import java.util.Base64 23 | 24 | import akka.Done 25 | import akka.actor.{ ActorSystem, Address, AddressFromURIString } 26 | import de.heikoseeberger.constructr.coordination.Coordination 27 | import org.apache.curator.framework.CuratorFrameworkFactory 28 | import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex 29 | import org.apache.curator.retry.RetryNTimes 30 | import org.apache.curator.utils.ZKPaths 31 | import org.apache.zookeeper.CreateMode 32 | import org.apache.zookeeper.KeeperException.NodeExistsException 33 | 34 | import scala.collection.JavaConverters._ 35 | import scala.concurrent.duration._ 36 | import scala.concurrent.{ Future, blocking } 37 | 38 | private object ZookeeperCoordination { 39 | 40 | object Converters { 41 | implicit class InstantOps(instant: Instant) { 42 | def encode: Array[Byte] = { 43 | val bytes = java.nio.ByteBuffer.allocate(java.lang.Long.BYTES).putLong(instant.toEpochMilli).array() 44 | Base64.getEncoder.encode(bytes) 45 | } 46 | 47 | def hasTimeLeft(): Boolean = 48 | !isOverdue() 49 | 50 | def isOverdue(): Boolean = 51 | Instant.now.isAfter(instant) 52 | 53 | def +(duration: Duration): Instant = 54 | instant.plusMillis(duration.toMillis) 55 | } 56 | 57 | implicit class ByteArrayOps(bytes: Array[Byte]) { 58 | def decodeInstant: Instant = { 59 | val decodedBytes = Base64.getDecoder.decode(bytes) 60 | Instant.ofEpochMilli(java.nio.ByteBuffer.wrap(decodedBytes).getLong) 61 | } 62 | } 63 | 64 | implicit class AddressOps(address: Address) { 65 | def encode: String = 66 | Base64.getUrlEncoder.encodeToString(address.toString.getBytes(UTF_8)) 67 | } 68 | 69 | implicit class StringOps(s: String) { 70 | def decodeNode: Address = 71 | AddressFromURIString(new String(Base64.getUrlDecoder.decode(s), UTF_8)) 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * A coordination service for ConstructR that uses Zookeeper as the distributed data store. 78 | * 79 | * The locking mechanism is using the [[InterProcessSemaphoreMutex]] lock from the Apache Curator library 80 | * in combination with an additional lock file to store the TTL. 81 | * 82 | * Zookeeper does not support the concept that keys can expiry based on a TTL. 83 | * Therefore, this implementation is using [[Instant]] to represent a TTL. 84 | * The instant value is stored inside the key as a data object. 85 | * It is stored in Zookeeper as milliseconds, converted to a byte array and then encoded as a Base64 string. 86 | * The TTL in milliseconds represents the time elapsed since 1970-01-01T00:00:00 UTC. 87 | * Because TTL value is always converted into the UTC time zone, it can be safely used across different time zones. 88 | */ 89 | final class ZookeeperCoordination(clusterName: String, system: ActorSystem) extends Coordination with ZookeeperNodes { 90 | 91 | import ZookeeperCoordination.Converters._ 92 | 93 | private implicit val ec = system.dispatcher 94 | 95 | private val RootPath = 96 | system.settings.config.getString("constructr.coordination.zookeeper.rootpath") 97 | 98 | private val BasePath = s"$RootPath/$clusterName" 99 | private val NodesPath = s"$BasePath/nodes" 100 | private val BaseLockPath = s"$BasePath/locks" 101 | private val SharedLockPath = s"$BaseLockPath/shared" 102 | private val NodesLockKey = s"$BaseLockPath/nodes-lock" 103 | 104 | private val client = 105 | CuratorFrameworkFactory.builder() 106 | .connectString(nodesConnectionString(system)) 107 | .retryPolicy(new RetryNTimes(0, 0)) 108 | .build() 109 | 110 | run() 111 | private val lock = init() 112 | 113 | private def run(): Unit = { 114 | def shutdown(): Unit = { 115 | system.log.info("Zookeeper client closes connection to nodes [{}]..", nodesConnectionString(system)) 116 | client.close() 117 | } 118 | 119 | system.log.info("Zookeeper client tries to establish a connection to nodes [{}]..", nodesConnectionString(system)) 120 | client.start() 121 | client.blockUntilConnected() 122 | sys.addShutdownHook(shutdown()) 123 | } 124 | 125 | private def init(): InterProcessSemaphoreMutex = { 126 | ZKPaths.mkdirs(client.getZookeeperClient.getZooKeeper, NodesPath) 127 | ZKPaths.mkdirs(client.getZookeeperClient.getZooKeeper, BaseLockPath) 128 | new InterProcessSemaphoreMutex(client, SharedLockPath) 129 | } 130 | 131 | override def getNodes(): Future[Set[Address]] = 132 | blockingFuture { 133 | nodes.flatMap { node => 134 | val nodePath = s"$NodesPath/$node" 135 | val deadline = client.getData.forPath(nodePath).decodeInstant 136 | if (deadline.hasTimeLeft()) { 137 | Some(node.decodeNode) 138 | } else { 139 | client.delete().forPath(nodePath) 140 | None 141 | } 142 | } 143 | } 144 | 145 | override def lock(self: Address, ttl: FiniteDuration): Future[Boolean] = { 146 | def readLock(): Option[Instant] = 147 | Option(client.checkExists().forPath(NodesLockKey)).map { _ => 148 | client.getData.forPath(NodesLockKey).decodeInstant 149 | } 150 | 151 | def writeLock(expiredLockExist: Boolean): Boolean = { 152 | try { 153 | lock.acquire() 154 | if (lock.isAcquiredInThisProcess) { 155 | if (expiredLockExist) 156 | client.delete().forPath(NodesLockKey) 157 | try { 158 | client.create().forPath(NodesLockKey, (Instant.now + ttl).encode) 159 | true 160 | } catch { 161 | case e: NodeExistsException => 162 | // In the meantime another process has created the write lock. 163 | // We know that this write lock is active by another process 164 | // and therefore return false. 165 | false 166 | } 167 | } else { 168 | false 169 | } 170 | } finally { 171 | lock.release() 172 | } 173 | } 174 | 175 | blockingFuture { 176 | readLock() match { 177 | case Some(deadline) if deadline.hasTimeLeft() => false 178 | case Some(deadline) => writeLock(expiredLockExist = true) 179 | case None => writeLock(expiredLockExist = false) 180 | } 181 | } 182 | } 183 | 184 | override def addSelf(self: Address, ttl: FiniteDuration): Future[Done] = { 185 | blockingFuture { 186 | val nodePath = s"$NodesPath/${self.encode}" 187 | Option(client.checkExists().forPath(nodePath)) 188 | .foreach(_ => client.delete().forPath(nodePath)) 189 | 190 | client.create().withMode(CreateMode.EPHEMERAL).forPath(nodePath, (Instant.now + ttl).encode) 191 | Done 192 | } 193 | } 194 | 195 | override def refresh(self: Address, ttl: FiniteDuration): Future[Done] = 196 | blockingFuture { 197 | nodes.foreach { node => 198 | val nodePath = s"$NodesPath/$node" 199 | if (node.decodeNode == self) 200 | client.setData().forPath(nodePath, (Instant.now + ttl).encode) 201 | else if (client.getData.forPath(nodePath).decodeInstant.isOverdue()) 202 | client.delete().forPath(nodePath) 203 | } 204 | Done 205 | } 206 | 207 | private def nodes: Set[String] = 208 | client 209 | .getChildren 210 | .forPath(NodesPath) 211 | .asScala 212 | .toSet 213 | 214 | private def blockingFuture[T](f: => T): Future[T] = 215 | Future(blocking(f)) 216 | } 217 | -------------------------------------------------------------------------------- /constructr-coordination-zookeeper/src/main/scala/com/lightbend/constructr/coordination/zookeeper/ZookeeperNodes.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination.zookeeper 18 | 19 | import akka.actor.ActorSystem 20 | import com.lightbend.constructr.coordination.zookeeper.ZookeeperNodes.Nodes 21 | import com.typesafe.config.ConfigException.WrongType 22 | 23 | import scala.collection.JavaConverters.iterableAsScalaIterableConverter 24 | import scala.util.Try 25 | 26 | object ZookeeperNodes { 27 | val Nodes: String = "constructr.coordination.nodes" 28 | } 29 | 30 | /** 31 | * Helper for extracting Zookeeper nodes configuration from {@link akka.actor.ActorSystem ActorSystem} settings. 32 | * 33 | * First, tries to get comma-saparated list of nodes from `String` settings, 34 | * if not found then falls back to parsing `List` of strings. 35 | */ 36 | trait ZookeeperNodes { 37 | def nodesConnectionString(system: ActorSystem): String = { 38 | Try( 39 | system.settings.config.getString(Nodes) 40 | ).recover { 41 | case ex: WrongType => system.settings.config.getStringList(Nodes).asScala.mkString(",") 42 | }.get 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /constructr-coordination-zookeeper/src/test/scala/com/lightbend/constructr/coordination/zookeeper/DockerZookeeper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination.zookeeper 18 | 19 | import com.whisk.docker.impl.dockerjava.DockerKitDockerJava 20 | import com.whisk.docker.{ DockerContainer, DockerReadyChecker } 21 | 22 | trait DockerZookeeper extends DockerKitDockerJava { 23 | val DefaultZookeeperPort = 2181 24 | 25 | val zookeeperContainer: DockerContainer = DockerContainer("jplock/zookeeper:3.4.10") 26 | .withPorts(DefaultZookeeperPort -> Some(DefaultZookeeperPort)) 27 | .withReadyChecker(DockerReadyChecker.LogLineContains("binding to port")) 28 | 29 | abstract override def dockerContainers: List[DockerContainer] = zookeeperContainer :: super.dockerContainers 30 | } 31 | -------------------------------------------------------------------------------- /constructr-coordination-zookeeper/src/test/scala/com/lightbend/constructr/coordination/zookeeper/ZookeeperCoordinationSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination.zookeeper 18 | 19 | import akka.Done 20 | import akka.actor.{ ActorSystem, AddressFromURIString } 21 | import akka.testkit.TestProbe 22 | import com.typesafe.config.ConfigFactory 23 | import com.whisk.docker.scalatest.DockerTestKit 24 | import de.heikoseeberger.constructr.coordination.Coordination 25 | import org.scalatest.{ Matchers, WordSpec } 26 | 27 | import scala.concurrent.duration._ 28 | import scala.concurrent.{ Await, Awaitable } 29 | import scala.util.Random 30 | 31 | object ZookeeperCoordinationSpec { 32 | private val address1 = AddressFromURIString("akka.tcp://default@a:2552") 33 | private val address2 = AddressFromURIString("akka.tcp://default@b:2552") 34 | 35 | private val coordinationHost = { 36 | val dockerHostPattern = """tcp://(\S+):\d{1,5}""".r 37 | sys.env.get("DOCKER_HOST") 38 | .collect { case dockerHostPattern(address) => address } 39 | .getOrElse("127.0.0.1") 40 | } 41 | } 42 | 43 | class ZookeeperCoordinationSpec extends WordSpec with Matchers with DockerZookeeper with DockerTestKit { 44 | 45 | import ZookeeperCoordinationSpec._ 46 | 47 | private implicit val system: ActorSystem = { 48 | val config = ConfigFactory.parseString( 49 | s""" 50 | |constructr.coordination.nodes = ["$coordinationHost:2181"] 51 | """.stripMargin 52 | ).withFallback(ConfigFactory.load()) 53 | ActorSystem("default", config) 54 | } 55 | 56 | "ZookeeperCoordination" should { 57 | "correctly interact with zookeeper" in { 58 | val coordination: Coordination = Coordination(randomString(), system) 59 | 60 | resultOf(coordination.getNodes()) shouldBe 'empty 61 | 62 | resultOf(coordination.lock(address1, 10.seconds)) shouldBe true 63 | resultOf(coordination.lock(address2, 10.seconds)) shouldBe false 64 | 65 | resultOf(coordination.addSelf(address1, 10.seconds)) shouldBe Done 66 | resultOf(coordination.getNodes()) shouldBe Set(address1) 67 | 68 | resultOf(coordination.refresh(address1, 1.second)) shouldBe Done 69 | resultOf(coordination.getNodes()) shouldBe Set(address1) 70 | 71 | val probe = TestProbe() 72 | import probe._ 73 | within(5.seconds) { // 2 seconds should be enough, but who knows hows ... 74 | awaitAssert { 75 | resultOf(coordination.getNodes()) shouldBe 'empty 76 | } 77 | } 78 | } 79 | } 80 | 81 | override def afterAll() = { 82 | Await.ready(system.terminate(), Duration.Inf) 83 | super.afterAll() 84 | } 85 | 86 | private def resultOf[A](awaitable: Awaitable[A], max: FiniteDuration = 3.seconds): A = Await.result(awaitable, max) 87 | 88 | private def randomString() = math.abs(Random.nextInt).toString 89 | } 90 | -------------------------------------------------------------------------------- /constructr-coordination-zookeeper/src/test/scala/com/lightbend/constructr/coordination/zookeeper/ZookeeperNodesSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Lightbend Inc. 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 | 17 | package com.lightbend.constructr.coordination.zookeeper 18 | 19 | import akka.actor.ActorSystem 20 | import com.lightbend.constructr.coordination.zookeeper.ZookeeperNodes.Nodes 21 | import com.typesafe.config.ConfigFactory 22 | import org.scalatest.{ Matchers, WordSpec } 23 | 24 | class ZookeeperNodesSpec extends WordSpec with Matchers with ZookeeperNodes { 25 | 26 | "ZookeeperNodes" should { 27 | "should extract nodes from a list" in { 28 | val config = ConfigFactory.parseString( 29 | s""" 30 | |constructr.coordination.nodes = ["host1:2181", "host2:2181"] 31 | """.stripMargin 32 | ) 33 | val actorSystem = ActorSystem("default", config) 34 | 35 | config.getStringList(Nodes).size() shouldBe 2 36 | config.getStringList(Nodes) should contain("host1:2181") 37 | config.getStringList(Nodes) should contain("host2:2181") 38 | 39 | nodesConnectionString(actorSystem) shouldBe "host1:2181,host2:2181" 40 | } 41 | 42 | "should extract ZK nodes from a string" in { 43 | val config = ConfigFactory.parseString( 44 | s""" 45 | |constructr.coordination.nodes = "host1:2181,host2:2181" 46 | """.stripMargin 47 | ) 48 | val actorSystem = ActorSystem("default", config) 49 | 50 | config.getString(Nodes) shouldBe "host1:2181,host2:2181" 51 | nodesConnectionString(actorSystem) shouldBe "host1:2181,host2:2181" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import sbt.plugins.JvmPlugin 3 | import sbt.Keys._ 4 | import com.typesafe.sbt.SbtScalariform 5 | import de.heikoseeberger.sbtheader.HeaderPlugin 6 | import de.heikoseeberger.sbtheader.license.Apache2_0 7 | import sbtrelease.ReleasePlugin.autoImport._ 8 | import com.typesafe.sbt.SbtPgp.autoImport._ 9 | import PgpKeys._ 10 | import xerial.sbt.Sonatype.autoImport._ 11 | import ReleaseTransformations._ 12 | 13 | import scalariform.formatter.preferences.{AlignSingleLineCaseStatements, DoubleIndentClassDeclaration} 14 | 15 | object Build extends AutoPlugin { 16 | 17 | override def requires = JvmPlugin && HeaderPlugin && SbtScalariform 18 | 19 | override def trigger = allRequirements 20 | 21 | override def projectSettings = Vector( 22 | // Core settings 23 | organization := "com.lightbend.constructr", 24 | scalaVersion := Version.scala212, 25 | crossScalaVersions := Vector(Version.scala212, Version.scala211), 26 | scalacOptions ++= Vector( 27 | "-unchecked", 28 | "-deprecation", 29 | "-language:_", 30 | "-target:jvm-1.8", 31 | "-encoding", "UTF-8", 32 | "-Ywarn-unused-import" 33 | ), 34 | licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0")), 35 | homepage := Some(url("https://github.com/typesafehub/constructr-zookeeper")), 36 | developers := List( 37 | Developer("lightbend", "Lightbend Library Contributors", "", url("https://github.com/typesafehub/constructr-zookeeper/graphs/contributors")) 38 | ), 39 | scmInfo := Some(ScmInfo(url("https://github.com/typesafehub/constructr-zookeeper"), "git@github.com:typesafehub/constructr-zookeeper.git")), 40 | unmanagedSourceDirectories.in(Compile) := Vector(scalaSource.in(Compile).value), 41 | unmanagedSourceDirectories.in(Test) := Vector(scalaSource.in(Test).value), 42 | 43 | 44 | // Scalariform settings 45 | SbtScalariform.autoImport.scalariformPreferences := SbtScalariform.autoImport.scalariformPreferences.value 46 | .setPreference(AlignSingleLineCaseStatements, true) 47 | .setPreference(AlignSingleLineCaseStatements.MaxArrowIndent, 100) 48 | .setPreference(DoubleIndentClassDeclaration, true), 49 | 50 | // Header settings 51 | HeaderPlugin.autoImport.headers := Map( 52 | "scala" -> Apache2_0("2016", "Lightbend Inc. "), 53 | "conf" -> Apache2_0("2016", "Lightbend Inc. ", "#") 54 | ), 55 | 56 | // Sonatype settings 57 | sonatypeProfileName := "com.lightbend", 58 | 59 | // Release settings 60 | releaseCrossBuild := true, 61 | releaseProcess := Seq[ReleaseStep]( 62 | checkSnapshotDependencies, 63 | inquireVersions, 64 | runClean, 65 | releaseStepCommand("run-tests"), 66 | setReleaseVersion, 67 | commitReleaseVersion, 68 | tagRelease, 69 | ReleaseStep(action = Command.process("publishSigned", _), enableCrossBuild = true), 70 | setNextVersion, 71 | commitNextVersion, 72 | ReleaseStep(action = Command.process("sonatypeReleaseAll", _), enableCrossBuild = true), 73 | pushChanges 74 | ) 75 | 76 | // FIXME 77 | // For some reason, +test results in tests failing due to bincompat issues, explicitly 78 | // issuing clean seems to work around it for now 79 | ) ++ addCommandAlias("run-tests", s";++${Version.scala211};clean;test;++${Version.scala212};clean;test") 80 | } 81 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Version { 4 | val akka = "2.5.1" 5 | val constructr = "0.18.0" 6 | val curator = "2.12.0" 7 | val scala211 = "2.11.11" 8 | val scala212 = "2.12.4" 9 | val scalaTest = "3.0.1" 10 | val dockerItScala = "0.9.5" 11 | } 12 | 13 | object Library { 14 | val akkaActor = "com.typesafe.akka" %% "akka-actor" % Version.akka 15 | val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % Version.akka 16 | val akkaMultiNodeTestkit = "com.typesafe.akka" %% "akka-multi-node-testkit" % Version.akka 17 | val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % Version.akka 18 | val constructr = "de.heikoseeberger" %% "constructr" % Version.constructr 19 | val constructrCoordination = "de.heikoseeberger" %% "constructr-coordination" % Version.constructr 20 | val curatorFramework = "org.apache.curator" % "curator-framework" % Version.curator 21 | val curatorRecipes = "org.apache.curator" % "curator-recipes" % Version.curator 22 | val scalaTest = "org.scalatest" %% "scalatest" % Version.scalaTest 23 | val dockerTestKit = "com.whisk" %% "docker-testkit-scalatest" % Version.dockerItScala 24 | val dockerTestKitImpl = "com.whisk" %% "docker-testkit-impl-docker-java" % Version.dockerItScala 25 | } 26 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.13 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.7.0") 2 | addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.3.11") 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 4 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3") 5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1") 6 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0") 7 | -------------------------------------------------------------------------------- /shell-prompt.sbt: -------------------------------------------------------------------------------- 1 | shellPrompt.in(ThisBuild) := (state => s"[${Project.extract(state).currentRef.project}]> ") 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.4.2-SNAPSHOT" 2 | --------------------------------------------------------------------------------